Coverage for src/lilbee/app/settings_map.py: 100%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-08 09:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-08 09:20 +0000
1"""Shared settings map for interactive configuration."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from enum import StrEnum
8from pydantic_core import PydanticUndefined
10from lilbee.app.themes import DARK_THEMES
11from lilbee.core.config import cfg
12from lilbee.core.config.enums import (
13 ChatMode,
14 ClustererBackend,
15 CrawlRenderMode,
16 KvCacheType,
17 LlmProvider,
18 ReasoningMode,
19 RerankerType,
20 TableModel,
21 WikiEntityMode,
22)
23from lilbee.core.config.model import FTS_LANGUAGES
26class RenderStyle(StrEnum):
27 """How a setting is displayed in /settings."""
29 COMPACT = "compact"
30 FULL = "full"
31 MULTILINE = "multiline"
34class SettingGroup(StrEnum):
35 """Logical bucket names rendered by ``/settings`` and ``settings_list``."""
37 MODELS = "Models"
38 GENERATION = "Generation"
39 RETRIEVAL = "Retrieval"
40 INGEST = "Ingest"
41 WIKI = "Wiki"
42 MEMORY = "Memory"
43 CRAWLING = "Crawling"
44 LOCAL_SERVERS = "Local-Servers"
45 API_KEYS = "API-Keys"
46 SYSTEM = "System"
47 DISPLAY = "Display"
48 GENERAL = "General"
51@dataclass(frozen=True)
52class SettingDef:
53 """Metadata for an interactive setting.
55 ``writable`` is a TUI rendering hint: fields marked ``writable=False``
56 (the model role slots) get a dedicated picker rather than an inline
57 editor, and the ``/set`` slash command refuses them. The actual
58 write contract for HTTP / MCP / programmatic surfaces lives in
59 ``config_meta.WRITABLE_CONFIG_FIELDS`` + ``MODEL_ROLE_FIELDS`` and
60 is enforced by ``app.settings.apply_settings_update``.
62 ``hidden`` keeps the setting out of the TUI settings screen while
63 leaving it reachable via ``lilbee set`` and the ``LILBEE_*`` env
64 var: use it for transport/server knobs that aren't relevant to a
65 typical TUI session.
66 """
68 type: type
69 nullable: bool
70 writable: bool = True
71 render: RenderStyle = field(default=RenderStyle.COMPACT)
72 group: SettingGroup = SettingGroup.GENERAL
73 help_text: str = ""
74 choices: tuple[str, ...] | None = None
75 hidden: bool = False
76 # List editors validate each line as a regex only when this is set; flag-style
77 # lists (e.g. crawl_browser_extra_args) would be wrongly rejected otherwise.
78 validate_regex: bool = False
79 # Credentials: the TUI masks the editor so the value is never on screen in
80 # plain text, including while it is being pasted.
81 secret: bool = False
84def get_default(key: str) -> object:
85 """Return the cfg default for a setting key."""
86 field_info = type(cfg).model_fields[key]
87 if field_info.default_factory is not None:
88 return field_info.default_factory() # type: ignore[call-arg]
89 if field_info.default is PydanticUndefined:
90 return None
91 return field_info.default
94SETTINGS_MAP: dict[str, SettingDef] = {
95 "chat_model": SettingDef(
96 str,
97 nullable=False,
98 writable=False,
99 group=SettingGroup.MODELS,
100 help_text="LLM used for chat generation (vision and reranking are separate slots)",
101 ),
102 "vision_model": SettingDef(
103 str,
104 nullable=True,
105 writable=False,
106 group=SettingGroup.MODELS,
107 help_text="Vision model for scanned PDF OCR (empty = disabled; Tesseract only)",
108 ),
109 "enable_ocr": SettingDef(
110 bool,
111 nullable=True,
112 group=SettingGroup.INGEST,
113 help_text="Vision OCR for scanned PDFs (empty = auto-detect from vision_model)",
114 ),
115 "ocr_timeout": SettingDef(
116 float,
117 nullable=False,
118 group=SettingGroup.INGEST,
119 help_text="Per-page timeout in seconds for vision OCR (0 = no limit)",
120 ),
121 "vision_load_budget_s": SettingDef(
122 float,
123 nullable=False,
124 group=SettingGroup.INGEST,
125 help_text=(
126 "Wall-clock seconds reserved for the vision worker to load the"
127 " model. Total PDF-OCR budget = load_budget + ocr_timeout * pages."
128 ),
129 ),
130 "vision_ocr_max_tokens": SettingDef(
131 int,
132 nullable=False,
133 group=SettingGroup.INGEST,
134 help_text=(
135 "Hard cap on tokens generated per OCR page (bounds runaway repetition"
136 " loops); raising it lengthens page generation, so give ocr_timeout headroom"
137 ),
138 ),
139 "vision_ocr_concurrency": SettingDef(
140 int,
141 nullable=False,
142 group=SettingGroup.INGEST,
143 help_text="Pages OCR'd concurrently per vision server; each slot adds KV cache memory",
144 ),
145 "extraction_timeout": SettingDef(
146 int,
147 nullable=False,
148 group=SettingGroup.INGEST,
149 help_text=(
150 "Wall-clock seconds one file gets to extract before ingest gives up"
151 " on it (0 = no limit)"
152 ),
153 ),
154 "ingest_workers": SettingDef(
155 int,
156 nullable=False,
157 group=SettingGroup.INGEST,
158 help_text="Workers for discovering and hashing files (0 = auto, all available cores)",
159 ),
160 "ingest_processes": SettingDef(
161 int,
162 nullable=False,
163 group=SettingGroup.INGEST,
164 help_text=(
165 "Ingest worker processes, one GPU each (0 = auto, one per card). Used"
166 " once the corpus is big enough to pay for them; 1 keeps ingest in this"
167 " process"
168 ),
169 ),
170 "mcp_tool_threads": SettingDef(
171 int,
172 nullable=False,
173 group=SettingGroup.LOCAL_SERVERS,
174 help_text=(
175 "Threads for synchronous MCP tool handlers; the ceiling on how many agents"
176 " one daemon serves before retrieval calls queue"
177 ),
178 ),
179 "crawl_convert_workers": SettingDef(
180 int,
181 nullable=False,
182 group=SettingGroup.CRAWLING,
183 help_text=(
184 "Crawled pages converted to markdown on worker threads at once, so a crawl"
185 " does not block request handling; 0 converts on the event loop"
186 ),
187 ),
188 "auto_sync": SettingDef(
189 bool,
190 nullable=False,
191 group=SettingGroup.INGEST,
192 help_text="Run a sync before `lilbee ask` (disable on large static corpora)",
193 ),
194 "entity_extraction": SettingDef(
195 bool,
196 nullable=False,
197 group=SettingGroup.INGEST,
198 help_text="Extract typed entities automatically at sync (schema induced on first run)",
199 ),
200 "semantic_chunking": SettingDef(
201 bool,
202 nullable=False,
203 group=SettingGroup.INGEST,
204 help_text="Opt-in topic-aware chunker (default off; may fragment numbered procedures)",
205 ),
206 "topic_threshold": SettingDef(
207 float,
208 nullable=False,
209 group=SettingGroup.INGEST,
210 help_text="Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on",
211 ),
212 "token_sizing": SettingDef(
213 bool,
214 nullable=False,
215 group=SettingGroup.INGEST,
216 help_text="Size chunks by real embedder tokens, not chars (changes invalidate the index)",
217 ),
218 "table_extraction": SettingDef(
219 bool,
220 nullable=False,
221 group=SettingGroup.INGEST,
222 help_text="Index each extracted table as its own chunk (changes invalidate the index)",
223 ),
224 "layout_detection": SettingDef(
225 bool,
226 nullable=False,
227 group=SettingGroup.INGEST,
228 help_text=(
229 "Layout-aware PDF extraction: reading order plus header/footer "
230 "stripping (changes invalidate the index)"
231 ),
232 ),
233 "table_model": SettingDef(
234 str,
235 nullable=False,
236 group=SettingGroup.INGEST,
237 choices=tuple(m.value for m in TableModel),
238 help_text=(
239 "Table structure model used when layout detection is on: slanet_auto "
240 "(docling-parity default), other slanet variants, tatr, or disabled "
241 "(changes invalidate the index)"
242 ),
243 ),
244 "batch_extraction": SettingDef(
245 bool,
246 nullable=False,
247 group=SettingGroup.INGEST,
248 help_text="Coalesce concurrent extractions into one xberg batch call",
249 ),
250 "batch_extraction_size": SettingDef(
251 int,
252 nullable=False,
253 group=SettingGroup.INGEST,
254 help_text="Max files per extract_batch call when batch extraction is on",
255 ),
256 "embedding_model": SettingDef(
257 str,
258 nullable=False,
259 writable=False,
260 group=SettingGroup.MODELS,
261 help_text="Model used to embed document chunks",
262 ),
263 "reranker_model": SettingDef(
264 str,
265 nullable=True,
266 writable=False,
267 group=SettingGroup.MODELS,
268 help_text="Cross-encoder model for result reranking",
269 ),
270 "reranker_type": SettingDef(
271 str,
272 nullable=False,
273 group=SettingGroup.MODELS,
274 choices=tuple(t.value for t in RerankerType),
275 help_text=(
276 "Reranker serving mode: auto (detect cross-encoder vs LLM by model), "
277 "cross_encoder, or llm"
278 ),
279 ),
280 "reranker_prompt": SettingDef(
281 str,
282 nullable=False,
283 group=SettingGroup.MODELS,
284 help_text="Relevance prompt for LLM rerankers (blank uses the built-in template)",
285 ),
286 "temperature": SettingDef(
287 float,
288 nullable=True,
289 group=SettingGroup.GENERATION,
290 help_text="Sampling temperature (higher = more creative)",
291 ),
292 "top_p": SettingDef(
293 float,
294 nullable=True,
295 group=SettingGroup.GENERATION,
296 help_text="Nucleus sampling cutoff probability",
297 ),
298 "top_k_sampling": SettingDef(
299 int,
300 nullable=True,
301 group=SettingGroup.GENERATION,
302 help_text="Top-K sampling: number of tokens to consider",
303 ),
304 "repeat_penalty": SettingDef(
305 float,
306 nullable=True,
307 group=SettingGroup.GENERATION,
308 help_text="Penalty for repeating tokens",
309 ),
310 "num_ctx": SettingDef(
311 int,
312 nullable=True,
313 group=SettingGroup.GENERATION,
314 help_text=(
315 "Context window size in tokens. Leave empty to size automatically "
316 "(aims for chat_n_ctx_target, ceiling at num_ctx_max or training_ctx)."
317 ),
318 ),
319 "num_ctx_max": SettingDef(
320 int,
321 nullable=True,
322 group=SettingGroup.GENERATION,
323 help_text=(
324 "Explicit ceiling for the dynamic context picker. Leave empty to "
325 "use the model's training_ctx from GGUF metadata as the only "
326 "ceiling. Set to cap below training_ctx (saves KV memory)."
327 ),
328 ),
329 "chat_n_ctx_target": SettingDef(
330 int,
331 nullable=False,
332 group=SettingGroup.GENERATION,
333 help_text=(
334 "Working context the dynamic picker aims for. Fits a RAG turn "
335 "with reasoning headroom; raise for long-document chat."
336 ),
337 ),
338 "flash_attention": SettingDef(
339 bool,
340 nullable=True,
341 group=SettingGroup.GENERATION,
342 help_text=(
343 "Flash attention. Empty (auto) enables it; disable for backends or "
344 "models where it misbehaves. Resolves the V-cache padding warning "
345 "on models with uneven per-layer V dims."
346 ),
347 ),
348 "kv_cache_type": SettingDef(
349 str,
350 nullable=False,
351 group=SettingGroup.GENERATION,
352 help_text=(
353 "KV cache element type. q8_0 / q4_0 halve or quarter cache memory "
354 "but require flash attention to be enabled."
355 ),
356 choices=tuple(t.value for t in KvCacheType),
357 ),
358 "n_gpu_layers": SettingDef(
359 int,
360 nullable=True,
361 group=SettingGroup.GENERATION,
362 help_text=(
363 "Layers to offload to GPU. Empty = all (recommended), 0 = CPU only, "
364 "positive int = partial offload for tight VRAM."
365 ),
366 ),
367 "cpu_moe": SettingDef(
368 bool,
369 nullable=False,
370 group=SettingGroup.GENERATION,
371 help_text=(
372 "Keep a mixture-of-experts model's expert weights in system memory so "
373 "it fits a smaller GPU. No effect on dense models."
374 ),
375 ),
376 "n_cpu_moe": SettingDef(
377 int,
378 nullable=True,
379 group=SettingGroup.GENERATION,
380 help_text=(
381 "Offload only the first N layers' experts to system memory. Takes "
382 "precedence over the offload-everything setting; smaller N stays faster."
383 ),
384 ),
385 "fast_model_downloads": SettingDef(
386 bool,
387 nullable=False,
388 group=SettingGroup.GENERATION,
389 help_text=(
390 "Warning: Hugging Face states this mode uses all available bandwidth "
391 "and CPU cores, and buffers far more of the download in memory. "
392 "Faster on a fast connection, at the cost of everything else running "
393 "on the machine. Leave it off unless the machine can spare that. "
394 "Requires a restart to take effect."
395 ),
396 ),
397 "gpu_devices": SettingDef(
398 str,
399 nullable=True,
400 group=SettingGroup.GENERATION,
401 help_text=(
402 "Restrict llama.cpp to specific GPU indexes on dual-GPU machines "
403 "(e.g. NVIDIA dGPU + integrated). Comma-separated, like '0' or '0,1'. "
404 "Applies to Vulkan, CUDA, and ROCm. Requires a restart to take effect."
405 ),
406 ),
407 "main_gpu": SettingDef(
408 int,
409 nullable=True,
410 group=SettingGroup.GENERATION,
411 help_text=(
412 "Primary GPU index for llama.cpp when multiple devices are visible. "
413 "Empty = let llama.cpp pick (index 0). Set this together with "
414 "gpu_devices to pin inference to a specific card. Requires a restart "
415 "to take effect."
416 ),
417 ),
418 "seed": SettingDef(
419 int,
420 nullable=True,
421 group=SettingGroup.GENERATION,
422 help_text="Random seed for reproducible output",
423 ),
424 "rag_system_prompt": SettingDef(
425 str,
426 nullable=False,
427 render=RenderStyle.MULTILINE,
428 group=SettingGroup.GENERATION,
429 help_text="System prompt sent when answering with retrieved context",
430 ),
431 "general_system_prompt": SettingDef(
432 str,
433 nullable=False,
434 render=RenderStyle.MULTILINE,
435 group=SettingGroup.GENERATION,
436 help_text="System prompt sent when there are no documents to ground the answer",
437 ),
438 "chat_compaction": SettingDef(
439 bool,
440 nullable=False,
441 group=SettingGroup.GENERATION,
442 help_text=(
443 "Off (default): when a chat outgrows the model's context window the oldest "
444 "turns are dropped. They stay on screen but the model stops seeing them, and "
445 "the context chip by the prompt shows the window filling. Costs nothing. "
446 "On: those turns are condensed into a short summary the model keeps reading, "
447 "so it still knows roughly what was said. That costs one extra model call each "
448 "time it fires, pausing the reply for a few seconds on a GPU and considerably "
449 "longer on a CPU-only machine. Worth turning on if your hardware is quick."
450 ),
451 ),
452 "sessions_enabled": SettingDef(
453 bool,
454 nullable=False,
455 group=SettingGroup.GENERATION,
456 help_text=(
457 "On (default): conversations are saved automatically, and you can list, "
458 "resume, rename, and delete them from the Sessions drawer (ctrl+o), the "
459 "Sessions tab, and the /sessions command. Off: nothing is written to disk, "
460 "the ctrl+o binding leaves the footer, and opening the Sessions view shows a "
461 "notice that sessions are turned off. Turn it off if you would rather your "
462 "chats not persist. Covers the TUI, the HTTP server, and the CLI; agent "
463 "sessions have their own setting."
464 ),
465 ),
466 "mcp_sessions_enabled": SettingDef(
467 bool,
468 nullable=False,
469 group=SettingGroup.GENERATION,
470 help_text=(
471 "Off (default): the session tools are not offered over MCP, and a connected "
472 "agent cannot create or read agent sessions. On: an agent can keep its own "
473 "saved conversations, separate from yours. Most agent hosts already track "
474 "their own history, and the tools cost context on every request, so this "
475 "stays off unless you want an agent owning conversations."
476 ),
477 ),
478 "chat_mode": SettingDef(
479 str,
480 nullable=False,
481 group=SettingGroup.GENERATION,
482 choices=tuple(m.value for m in ChatMode),
483 help_text="search runs every chat turn through document retrieval; chat skips it",
484 ),
485 "top_k": SettingDef(
486 int,
487 nullable=False,
488 group=SettingGroup.RETRIEVAL,
489 help_text="Number of chunks returned by search",
490 ),
491 "rerank_candidates": SettingDef(
492 int,
493 nullable=False,
494 group=SettingGroup.RETRIEVAL,
495 help_text="Candidate pool size for reranking",
496 ),
497 "rerank_blend": SettingDef(
498 bool,
499 nullable=False,
500 group=SettingGroup.RETRIEVAL,
501 help_text="Blend reranker scores with retrieval fusion (off = pure reranker order)",
502 ),
503 "rerank_min_score": SettingDef(
504 float,
505 nullable=True,
506 group=SettingGroup.RETRIEVAL,
507 help_text="Drop candidates whose raw reranker score is below this (unset = off)",
508 ),
509 "show_reasoning": SettingDef(
510 bool,
511 nullable=False,
512 group=SettingGroup.DISPLAY,
513 help_text="Show model reasoning/thinking tokens in output",
514 ),
515 "completions_reasoning": SettingDef(
516 str,
517 nullable=False,
518 group=SettingGroup.GENERATION,
519 help_text=(
520 "How /v1/chat/completions presents thinking: separate "
521 "reasoning_content field, inline thinking as plain content text, "
522 "or off (ask the model not to think)"
523 ),
524 choices=tuple(m.value for m in ReasoningMode),
525 ),
526 "messages_reasoning": SettingDef(
527 str,
528 nullable=False,
529 group=SettingGroup.GENERATION,
530 help_text=(
531 "How /v1/messages presents thinking: separate thinking block, "
532 "inline thinking as plain answer text, or off (ask the model not "
533 "to think)"
534 ),
535 choices=tuple(m.value for m in ReasoningMode),
536 ),
537 "lilbee_name": SettingDef(
538 str,
539 nullable=False,
540 group=SettingGroup.DISPLAY,
541 help_text=(
542 "Human-readable label for this lilbee, shown in the status bar. "
543 "Empty falls back to 'global' for the platform default dir or "
544 "to the project path (~-substituted and left-truncated)."
545 ),
546 ),
547 "show_lilbee_path": SettingDef(
548 bool,
549 nullable=False,
550 group=SettingGroup.DISPLAY,
551 help_text=(
552 "Show the full absolute path in the status bar: expands 'global' "
553 "to its on-disk path and skips ~ substitution / truncation."
554 ),
555 ),
556 "theme": SettingDef(
557 str,
558 nullable=False,
559 group=SettingGroup.DISPLAY,
560 help_text="TUI color theme. Cycle with Ctrl+T; the active theme persists across sessions.",
561 choices=tuple(DARK_THEMES),
562 ),
563 "wiki": SettingDef(
564 bool,
565 nullable=False,
566 group=SettingGroup.WIKI,
567 help_text=(
568 "Enable the wiki layer (cited concept and entity pages). "
569 "GPU-heavy: a build spends one LLM call per source document, "
570 "so a large library takes hours. Enabling this generates nothing "
571 "on its own: you wikify explicitly, or turn on wiki_auto_update"
572 ),
573 ),
574 "wiki_auto_update": SettingDef(
575 bool,
576 nullable=False,
577 group=SettingGroup.WIKI,
578 help_text="Regenerate touched wiki pages after each sync (off: wikify explicitly)",
579 ),
580 "wiki_dir": SettingDef(
581 str,
582 nullable=False,
583 writable=False,
584 group=SettingGroup.WIKI,
585 help_text=(
586 "Directory under data_root where wiki pages live (set via env / config.toml only)"
587 ),
588 ),
589 "wiki_prune_raw": SettingDef(
590 bool,
591 nullable=False,
592 group=SettingGroup.WIKI,
593 help_text="Delete raw chunks after summarizing into the wiki",
594 ),
595 "wiki_embedding_faithfulness_threshold": SettingDef(
596 float,
597 nullable=False,
598 group=SettingGroup.WIKI,
599 help_text=(
600 "Minimum cosine similarity (0-1) between a generated page and "
601 "the mean of its source chunk vectors before publishing. "
602 "Pages below the threshold route to drafts/."
603 ),
604 ),
605 "wiki_stale_citation_threshold": SettingDef(
606 float,
607 nullable=False,
608 group=SettingGroup.WIKI,
609 help_text="Fraction of stale citations before a page is flagged by wiki prune",
610 ),
611 "wiki_drift_threshold": SettingDef(
612 float,
613 nullable=False,
614 group=SettingGroup.WIKI,
615 help_text="Max fraction of changed lines before regeneration requires review",
616 ),
617 "wiki_clusterer": SettingDef(
618 str,
619 nullable=False,
620 group=SettingGroup.WIKI,
621 help_text="Synthesis clusterer backend (embedding or concepts)",
622 choices=tuple(b.value for b in ClustererBackend),
623 ),
624 "wiki_entity_mode": SettingDef(
625 str,
626 nullable=False,
627 group=SettingGroup.WIKI,
628 help_text=(
629 "Entity extraction strategy. ner_entities (typed spaCy NER) is the "
630 "only implemented mode; the other values fall back to it with a warning"
631 ),
632 choices=tuple(m.value for m in WikiEntityMode),
633 ),
634 "wiki_entity_min_mentions": SettingDef(
635 int,
636 nullable=False,
637 group=SettingGroup.WIKI,
638 help_text="Minimum chunk mentions before an entity or concept gets its own page",
639 ),
640 "wiki_stub_max_chunk_refs": SettingDef(
641 int,
642 nullable=False,
643 group=SettingGroup.WIKI,
644 help_text=(
645 "How many source chunks a page kept for lazy generation draws on. "
646 "Caps the browse index's size; already more than one page's context "
647 "budget admits, so raising it rarely changes what a page says"
648 ),
649 ),
650 "wiki_ingest_update_cap": SettingDef(
651 int,
652 nullable=False,
653 group=SettingGroup.WIKI,
654 help_text=(
655 "Touched-page cap for auto-update after sync. "
656 "Beyond this count, run `lilbee wiki update` manually."
657 ),
658 ),
659 "wiki_synthesis_prompt": SettingDef(
660 str,
661 nullable=False,
662 render=RenderStyle.FULL,
663 group=SettingGroup.WIKI,
664 help_text=(
665 "Prompt for cross-source synthesis pages. "
666 "Must keep {topic}, {source_list}, and {chunks_text}."
667 ),
668 ),
669 "wiki_entity_page_prompt": SettingDef(
670 str,
671 nullable=False,
672 render=RenderStyle.FULL,
673 group=SettingGroup.WIKI,
674 help_text=(
675 "Prompt for a single page generated on demand from one subject's "
676 "chunks across every source naming it. "
677 "Must keep {topic}, {source_list}, and {chunks_text}."
678 ),
679 ),
680 "wiki_entity_batch_prompt": SettingDef(
681 str,
682 nullable=False,
683 render=RenderStyle.FULL,
684 group=SettingGroup.WIKI,
685 help_text=(
686 "Prompt for the per-source batched call. "
687 "Must keep {source}, {entity_list}, {chunks_text}, and {concept_instruction}."
688 ),
689 ),
690 "wiki_extract_concepts": SettingDef(
691 bool,
692 nullable=False,
693 group=SettingGroup.WIKI,
694 help_text=(
695 "Whether the per-source batched call asks the LLM to curate concept pages "
696 "alongside the pre-extracted entity list."
697 ),
698 ),
699 "wiki_batch_min_chunks": SettingDef(
700 int,
701 nullable=False,
702 group=SettingGroup.WIKI,
703 help_text=(
704 "Minimum chunks a source must contribute before its batched call includes "
705 "concept curation. Sources below the floor skip the concept-curation "
706 "instruction; sources with zero entities AND below the floor are skipped entirely."
707 ),
708 ),
709 "wiki_clusterer_k": SettingDef(
710 int,
711 nullable=False,
712 group=SettingGroup.WIKI,
713 help_text="Mutual-kNN neighborhood size for the clusterer (0 = auto)",
714 ),
715 "memory_enabled": SettingDef(
716 bool,
717 nullable=False,
718 group=SettingGroup.MEMORY,
719 help_text="Master switch for long-term chat memory (off by default)",
720 ),
721 "memory_auto_extract": SettingDef(
722 bool,
723 nullable=False,
724 group=SettingGroup.MEMORY,
725 help_text="Auto-save durable facts and preferences from each TUI turn (needs memory on)",
726 ),
727 "memory_top_k": SettingDef(
728 int,
729 nullable=False,
730 group=SettingGroup.MEMORY,
731 help_text="Maximum facts recalled into context per turn",
732 ),
733 "memory_max_distance": SettingDef(
734 float,
735 nullable=False,
736 group=SettingGroup.MEMORY,
737 help_text="Recall cutoff distance, 0.0-1.0 (lower is stricter)",
738 ),
739 "memory_token_budget": SettingDef(
740 int,
741 nullable=False,
742 group=SettingGroup.MEMORY,
743 help_text="Token cap on the recalled-memory block added to the prompt",
744 ),
745 "memory_max_per_owner": SettingDef(
746 int,
747 nullable=False,
748 group=SettingGroup.MEMORY,
749 help_text="Soft cap before the oldest memories are evicted",
750 hidden=True,
751 ),
752 "memory_dedup_distance": SettingDef(
753 float,
754 nullable=False,
755 group=SettingGroup.MEMORY,
756 help_text="Near-duplicate distance below which a new memory updates the old",
757 hidden=True,
758 ),
759 "crawl_max_depth": SettingDef(
760 int,
761 nullable=True,
762 group=SettingGroup.CRAWLING,
763 help_text="Optional recursion-depth cap (blank = no cap; per-crawl values win)",
764 ),
765 "crawl_render_mode": SettingDef(
766 str,
767 nullable=False,
768 group=SettingGroup.CRAWLING,
769 help_text=(
770 "How crawls fetch pages. http = lightweight, no browser (default, best "
771 "for static and server-rendered sites). browser = Chromium with "
772 "JavaScript enabled for client-rendered sites, at much higher memory cost."
773 ),
774 choices=tuple(m.value for m in CrawlRenderMode),
775 ),
776 "crawl_browser_recycle_pages": SettingDef(
777 int,
778 nullable=False,
779 group=SettingGroup.CRAWLING,
780 help_text=(
781 "Browser mode: recycle the Chromium process every N pages to cap memory "
782 "growth on long crawls (0 = never recycle)."
783 ),
784 ),
785 "crawl_browser_extra_args": SettingDef(
786 list,
787 nullable=False,
788 group=SettingGroup.CRAWLING,
789 help_text=(
790 "Browser mode: extra Chromium launch flags, one per line. "
791 "Defaults trim shared-memory and GPU use."
792 ),
793 ),
794 "crawl_max_pages": SettingDef(
795 int,
796 nullable=True,
797 group=SettingGroup.CRAWLING,
798 help_text="Optional global cap on total pages per crawl (blank = no cap).",
799 ),
800 "crawl_safety_max_pages": SettingDef(
801 int,
802 nullable=False,
803 group=SettingGroup.CRAWLING,
804 help_text="Default page bound for an unbounded crawl, so a hostile site cannot "
805 "exhaust the disk. An explicit max-pages overrides it; raise this to crawl "
806 "larger sites unbounded.",
807 ),
808 "crawl_timeout": SettingDef(
809 int,
810 nullable=False,
811 group=SettingGroup.CRAWLING,
812 help_text="Per-page fetch timeout in seconds",
813 ),
814 "crawl_sync_interval": SettingDef(
815 int,
816 nullable=False,
817 group=SettingGroup.CRAWLING,
818 help_text="Seconds between periodic re-syncs during a crawl (0 = sync only at end)",
819 ),
820 "crawl_mean_delay": SettingDef(
821 float,
822 nullable=False,
823 group=SettingGroup.CRAWLING,
824 help_text="Seconds between in-flight requests within a single crawl",
825 ),
826 "crawl_max_delay_range": SettingDef(
827 float,
828 nullable=False,
829 group=SettingGroup.CRAWLING,
830 help_text="Random jitter (seconds) added on top of mean delay",
831 ),
832 "crawl_concurrent_requests": SettingDef(
833 int,
834 nullable=False,
835 group=SettingGroup.CRAWLING,
836 help_text="Concurrent in-flight URLs within one crawl",
837 ),
838 "crawl_retry_on_rate_limit": SettingDef(
839 bool,
840 nullable=False,
841 group=SettingGroup.CRAWLING,
842 help_text="Enable per-domain backoff and retries on HTTP 429/503",
843 ),
844 "crawl_retry_base_delay_min": SettingDef(
845 float,
846 nullable=False,
847 group=SettingGroup.CRAWLING,
848 help_text="Minimum base-delay (seconds) on rate-limit responses",
849 ),
850 "crawl_retry_base_delay_max": SettingDef(
851 float,
852 nullable=False,
853 group=SettingGroup.CRAWLING,
854 help_text="Maximum base-delay (seconds) on rate-limit responses",
855 ),
856 "crawl_retry_max_backoff": SettingDef(
857 float,
858 nullable=False,
859 group=SettingGroup.CRAWLING,
860 help_text="Upper bound on any single backoff wait (seconds)",
861 ),
862 "crawl_retry_max_attempts": SettingDef(
863 int,
864 nullable=False,
865 group=SettingGroup.CRAWLING,
866 help_text="Retry count per URL when a rate-limit code comes back",
867 ),
868 "crawl_exclude_patterns": SettingDef(
869 list,
870 nullable=False,
871 group=SettingGroup.CRAWLING,
872 validate_regex=True,
873 help_text=(
874 "Regex patterns that skip URLs at link-discovery time during "
875 "recursive crawls. One per line."
876 ),
877 ),
878 "openrouter_api_key": SettingDef(
879 str,
880 nullable=False,
881 group=SettingGroup.API_KEYS,
882 secret=True,
883 help_text="OpenRouter API key (enables frontier models in chat picker)",
884 ),
885 "gemini_api_key": SettingDef(
886 str,
887 nullable=False,
888 group=SettingGroup.API_KEYS,
889 secret=True,
890 help_text="Google Gemini API key (enables frontier models in chat picker)",
891 ),
892 "anthropic_api_key": SettingDef(
893 str,
894 nullable=False,
895 group=SettingGroup.API_KEYS,
896 secret=True,
897 help_text="Anthropic API key (enables frontier models in chat picker)",
898 ),
899 "openai_api_key": SettingDef(
900 str,
901 nullable=False,
902 group=SettingGroup.API_KEYS,
903 secret=True,
904 help_text="OpenAI API key (enables frontier models in chat picker)",
905 ),
906 "mistral_api_key": SettingDef(
907 str,
908 nullable=False,
909 group=SettingGroup.API_KEYS,
910 secret=True,
911 help_text="Mistral API key (enables frontier models in chat picker)",
912 ),
913 "deepseek_api_key": SettingDef(
914 str,
915 nullable=False,
916 group=SettingGroup.API_KEYS,
917 secret=True,
918 help_text="DeepSeek API key (enables frontier models in chat picker)",
919 ),
920 "llm_api_key": SettingDef(
921 str,
922 nullable=False,
923 group=SettingGroup.API_KEYS,
924 secret=True,
925 help_text="API key for the remote OpenAI-compatible endpoint (llm_provider = remote)",
926 ),
927 "hf_token": SettingDef(
928 str,
929 nullable=False,
930 group=SettingGroup.SYSTEM,
931 secret=True,
932 help_text=(
933 "HuggingFace access token. Avoids the unauthenticated download "
934 "rate limit and unlocks gated repos. Stored in plain text in "
935 "config.toml. Env vars (LILBEE_HF_TOKEN, HF_TOKEN) override."
936 ),
937 ),
938 "chunk_size": SettingDef(
939 int,
940 nullable=False,
941 group=SettingGroup.INGEST,
942 help_text="Document chunk size in tokens (changes invalidate the index)",
943 ),
944 "chunk_overlap": SettingDef(
945 int,
946 nullable=False,
947 group=SettingGroup.INGEST,
948 help_text="Tokens of overlap between adjacent chunks (preserves context across boundaries)",
949 ),
950 "max_chunks_per_file": SettingDef(
951 int,
952 nullable=False,
953 group=SettingGroup.INGEST,
954 help_text=(
955 "Most chunks one file can add to the index; a file over the limit is skipped, "
956 "not embedded (0 = no limit). Raise it for a long document such as a "
957 "thousand-page manual at a small chunk_size, then retry skipped files"
958 ),
959 ),
960 "tesseract_timeout": SettingDef(
961 float,
962 nullable=False,
963 group=SettingGroup.INGEST,
964 help_text="Per-page Tesseract timeout in seconds (used when no vision model is set)",
965 ),
966 "ocr_language": SettingDef(
967 list,
968 nullable=False,
969 group=SettingGroup.INGEST,
970 help_text="Tesseract OCR languages when no vision model is set; '+'-join, e.g. eng+deu",
971 ),
972 "worker_pool_eager_start": SettingDef(
973 bool,
974 nullable=False,
975 group=SettingGroup.INGEST,
976 help_text=(
977 "Spawn every configured role server at TUI startup instead of on first use. "
978 "Trades cold-start time per role for first-call latency"
979 ),
980 ),
981 "keep_engine_warm": SettingDef(
982 bool,
983 nullable=False,
984 group=SettingGroup.SYSTEM,
985 help_text=(
986 "Let the engine outlive lilbee for warm launches; off stops it on last "
987 "exit unless another lilbee sharing the engine asked to keep it"
988 ),
989 ),
990 "engine_idle_ttl_minutes": SettingDef(
991 int,
992 nullable=False,
993 group=SettingGroup.SYSTEM,
994 help_text="Idle minutes before the engine unloads its weights; 0 keeps them loaded",
995 ),
996 "agent_mcp_enabled": SettingDef(
997 bool,
998 nullable=False,
999 group=SettingGroup.SYSTEM,
1000 help_text=(
1001 "Register lilbee's MCP search tool into agent launchers (opencode, hermes). "
1002 "Disable to bring your own MCP servers; lilbee stays the model provider"
1003 ),
1004 ),
1005 "max_tokens": SettingDef(
1006 int,
1007 nullable=True,
1008 group=SettingGroup.GENERATION,
1009 help_text="Hard cap on generated tokens per response (blank = no cap)",
1010 ),
1011 "max_reasoning_chars": SettingDef(
1012 int,
1013 nullable=False,
1014 group=SettingGroup.GENERATION,
1015 help_text=(
1016 "Maximum reasoning characters before lilbee forces the model to answer "
1017 "(0 = unlimited; per-model overrides apply on top)"
1018 ),
1019 ),
1020 "model_keep_alive": SettingDef(
1021 int,
1022 nullable=False,
1023 group=SettingGroup.GENERATION,
1024 help_text="Seconds the loaded model stays warm between calls (0 = unload immediately)",
1025 ),
1026 "gpu_memory_fraction": SettingDef(
1027 float,
1028 nullable=False,
1029 group=SettingGroup.GENERATION,
1030 help_text="Fraction of GPU memory the model is allowed to claim (0.1-1.0)",
1031 ),
1032 "usable_vram_fraction": SettingDef(
1033 float,
1034 nullable=False,
1035 group=SettingGroup.GENERATION,
1036 help_text=(
1037 "Share of a GPU placement may fill, leaving room for fragmentation and driver "
1038 "overhead (0.5-1.0). Raise it if a model that should fit is being refused; "
1039 "lower it if loads fail near the top of the card."
1040 ),
1041 ),
1042 "system_memory_reserve_gb": SettingDef(
1043 float,
1044 nullable=False,
1045 group=SettingGroup.GENERATION,
1046 help_text=(
1047 "RAM held back for the OS in GiB when serving from system memory (no discrete "
1048 "GPU). Capped at a quarter of total RAM either way."
1049 ),
1050 ),
1051 "embed_replicas": SettingDef(
1052 int,
1053 nullable=False,
1054 group=SettingGroup.GENERATION,
1055 help_text="Embedding servers in parallel (0 = auto, one per GPU; positive pins the count)",
1056 ),
1057 "vision_replicas": SettingDef(
1058 int,
1059 nullable=False,
1060 group=SettingGroup.GENERATION,
1061 help_text="Vision OCR servers in parallel (0 = auto, one per GPU; positive pins the count)",
1062 ),
1063 "candidate_multiplier": SettingDef(
1064 int,
1065 nullable=False,
1066 group=SettingGroup.RETRIEVAL,
1067 help_text="Candidate-pool multiplier over top_k before reranking",
1068 ),
1069 "title_search": SettingDef(
1070 bool,
1071 nullable=False,
1072 group=SettingGroup.RETRIEVAL,
1073 help_text="Match queries against document titles as a third hybrid-search arm",
1074 ),
1075 "title_search_weight": SettingDef(
1076 float,
1077 nullable=False,
1078 group=SettingGroup.RETRIEVAL,
1079 help_text="Title arm weight in rank fusion (1.0 = equal voice with the other arms)",
1080 ),
1081 "lexical_fusion_weight": SettingDef(
1082 float,
1083 nullable=False,
1084 group=SettingGroup.RETRIEVAL,
1085 help_text="BM25 arm weight in fusion (1.0 = equal to vector; lower to favor dense)",
1086 ),
1087 "adaptive_fusion": SettingDef(
1088 bool,
1089 nullable=False,
1090 group=SettingGroup.RETRIEVAL,
1091 help_text="Scale the BM25 weight per query by vector-arm confidence, not a fixed value",
1092 ),
1093 "adaptive_fusion_margin": SettingDef(
1094 float,
1095 nullable=False,
1096 group=SettingGroup.RETRIEVAL,
1097 help_text="Vector-similarity margin at which adaptive fusion fully silences the BM25 arm",
1098 ),
1099 "filter_structural_chunks": SettingDef(
1100 bool,
1101 nullable=False,
1102 group=SettingGroup.RETRIEVAL,
1103 help_text="Drop tables-of-contents and classification-banner cover pages from results",
1104 ),
1105 "fts_language": SettingDef(
1106 str,
1107 nullable=False,
1108 group=SettingGroup.RETRIEVAL,
1109 choices=tuple(sorted(FTS_LANGUAGES)),
1110 help_text="Stemmer/stop-word language for BM25 indexes (rebuild to apply)",
1111 ),
1112 "embed_titles": SettingDef(
1113 bool,
1114 nullable=False,
1115 group=SettingGroup.RETRIEVAL,
1116 help_text="Prefix document titles to chunk embeddings (rebuild to apply)",
1117 ),
1118 "contextual_enrichment": SettingDef(
1119 bool,
1120 nullable=False,
1121 group=SettingGroup.RETRIEVAL,
1122 help_text="LLM context sentence per chunk embedding (slow ingest; rebuild to apply)",
1123 ),
1124 "history_rewrite": SettingDef(
1125 bool,
1126 nullable=False,
1127 group=SettingGroup.RETRIEVAL,
1128 help_text=(
1129 "Rewrite a follow-up that refers to earlier turns into a standalone "
1130 "retrieval query (one extra chat-model call on those turns)"
1131 ),
1132 ),
1133 "intent_routing": SettingDef(
1134 bool,
1135 nullable=False,
1136 group=SettingGroup.RETRIEVAL,
1137 help_text="Route document-name lookups to exact retrieval, count questions to a scan",
1138 ),
1139 "intent_llm": SettingDef(
1140 bool,
1141 nullable=False,
1142 group=SettingGroup.RETRIEVAL,
1143 help_text=(
1144 "Classify count questions with the chat model when the fast patterns "
1145 "miss (covers phrasing variants and other languages; adds one short "
1146 "LLM call to those turns)"
1147 ),
1148 ),
1149 "ann_index_threshold": SettingDef(
1150 int,
1151 nullable=False,
1152 group=SettingGroup.RETRIEVAL,
1153 help_text="Chunk count to start building an ANN vector index (0 = always flat search)",
1154 ),
1155 "max_distance": SettingDef(
1156 float,
1157 nullable=False,
1158 group=SettingGroup.RETRIEVAL,
1159 help_text="Maximum vector distance for retrieval matches (lower = stricter)",
1160 ),
1161 "min_relevance_score": SettingDef(
1162 float,
1163 nullable=False,
1164 group=SettingGroup.RETRIEVAL,
1165 help_text="Minimum RRF relevance score for hybrid search results (0.0 = no filter)",
1166 ),
1167 "max_context_sources": SettingDef(
1168 int,
1169 nullable=False,
1170 group=SettingGroup.RETRIEVAL,
1171 help_text="Maximum unique sources contributing chunks to a single answer",
1172 ),
1173 "neighbor_expansion": SettingDef(
1174 int,
1175 nullable=False,
1176 group=SettingGroup.RETRIEVAL,
1177 help_text="Adjacent chunks merged into each retrieved passage per side (0 = off)",
1178 ),
1179 "diversity_max_per_source": SettingDef(
1180 int,
1181 nullable=False,
1182 group=SettingGroup.RETRIEVAL,
1183 help_text="Maximum chunks accepted from any one source (caps source dominance)",
1184 ),
1185 "mmr_lambda": SettingDef(
1186 float,
1187 nullable=False,
1188 group=SettingGroup.RETRIEVAL,
1189 help_text=(
1190 "MMR lambda balancing relevance vs diversity (0 = max diversity, 1 = max relevance)"
1191 ),
1192 ),
1193 "temporal_filtering": SettingDef(
1194 bool,
1195 nullable=False,
1196 group=SettingGroup.RETRIEVAL,
1197 help_text="Detect temporal queries and bias retrieval toward recent chunks",
1198 ),
1199 "hyde": SettingDef(
1200 bool,
1201 nullable=False,
1202 group=SettingGroup.RETRIEVAL,
1203 help_text="Use HyDE (hypothetical answer expansion) to broaden retrieval",
1204 ),
1205 "hyde_weight": SettingDef(
1206 float,
1207 nullable=False,
1208 group=SettingGroup.RETRIEVAL,
1209 help_text="Weight on the HyDE-generated query vector when blending with the original",
1210 ),
1211 "query_expansion_count": SettingDef(
1212 int,
1213 nullable=False,
1214 group=SettingGroup.RETRIEVAL,
1215 help_text="Number of paraphrase expansions per query (0 disables expansion)",
1216 ),
1217 "expansion_similarity_threshold": SettingDef(
1218 float,
1219 nullable=False,
1220 group=SettingGroup.RETRIEVAL,
1221 help_text="Minimum cosine similarity an expansion must keep with the original query",
1222 ),
1223 "expansion_short_query_tokens": SettingDef(
1224 int,
1225 nullable=False,
1226 group=SettingGroup.RETRIEVAL,
1227 help_text="Queries at or below this token count skip expansion (saves a model call)",
1228 ),
1229 "expansion_guardrails": SettingDef(
1230 bool,
1231 nullable=False,
1232 group=SettingGroup.RETRIEVAL,
1233 help_text="Drop expansions that diverge from the original intent",
1234 ),
1235 "adaptive_threshold": SettingDef(
1236 bool,
1237 nullable=False,
1238 group=SettingGroup.RETRIEVAL,
1239 help_text="Widen the distance cutoff when too few results pass (vector-only fallback path)",
1240 ),
1241 "adaptive_threshold_step": SettingDef(
1242 float,
1243 nullable=False,
1244 group=SettingGroup.RETRIEVAL,
1245 help_text="Step size for adaptive relevance-score relaxation when initial recall is empty",
1246 ),
1247 "concept_graph": SettingDef(
1248 bool,
1249 nullable=False,
1250 group=SettingGroup.RETRIEVAL,
1251 help_text="Boost retrieval scores for chunks that share concepts with the query",
1252 ),
1253 "concept_boost_weight": SettingDef(
1254 float,
1255 nullable=False,
1256 group=SettingGroup.RETRIEVAL,
1257 help_text="Maximum boost (0-1) the concept graph can add to a chunk's relevance",
1258 ),
1259 "concept_max_per_chunk": SettingDef(
1260 int,
1261 nullable=False,
1262 group=SettingGroup.RETRIEVAL,
1263 help_text="Maximum concept tags stored per chunk (caps graph density)",
1264 ),
1265 "documents_dir": SettingDef(
1266 str,
1267 nullable=False,
1268 group=SettingGroup.SYSTEM,
1269 help_text="Local documents root that lilbee sync ingests (blank = data_root/documents)",
1270 ),
1271 "vault_base": SettingDef(
1272 str,
1273 nullable=True,
1274 group=SettingGroup.SYSTEM,
1275 help_text="Markdown vault root; results carry a vault-relative path (blank = none)",
1276 ),
1277 "sse_heartbeat_interval": SettingDef(
1278 float,
1279 nullable=False,
1280 group=SettingGroup.SYSTEM,
1281 help_text="Seconds between SSE keep-alive frames sent to idle HTTP stream clients",
1282 hidden=True,
1283 ),
1284 "llm_provider": SettingDef(
1285 str,
1286 nullable=False,
1287 group=SettingGroup.API_KEYS,
1288 choices=tuple(p.value for p in LlmProvider),
1289 help_text=(
1290 "Inference provider: auto (default, runs models locally on llama-server) "
1291 "or remote (external OpenAI-compatible endpoint)"
1292 ),
1293 ),
1294 "ollama_base_url": SettingDef(
1295 str,
1296 nullable=False,
1297 group=SettingGroup.LOCAL_SERVERS,
1298 help_text="Ollama server URL (blank uses http://localhost:11434)",
1299 ),
1300 "lm_studio_base_url": SettingDef(
1301 str,
1302 nullable=False,
1303 group=SettingGroup.LOCAL_SERVERS,
1304 help_text="LM Studio server URL (blank uses http://localhost:1234/v1)",
1305 ),
1306 "llama_server_path": SettingDef(
1307 str,
1308 nullable=False,
1309 group=SettingGroup.API_KEYS,
1310 help_text="Path to a llama-server binary (empty: bundled wheel or PATH)",
1311 ),
1312 "wiki_summary_max_tokens": SettingDef(
1313 int,
1314 nullable=False,
1315 group=SettingGroup.WIKI,
1316 help_text="Maximum tokens generated per wiki page",
1317 ),
1318 "wiki_temperature": SettingDef(
1319 float,
1320 nullable=False,
1321 group=SettingGroup.WIKI,
1322 help_text="Temperature used for wiki page synthesis (low = stay close to sources)",
1323 ),
1324}