Coverage for src/lilbee/app/settings_map.py: 100%

46 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Shared settings map for interactive configuration.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field, replace 

6from enum import StrEnum 

7 

8from lilbee.app.themes import DARK_THEMES 

9from lilbee.core.config.model import CLEARABLE_MODEL_FIELDS 

10from lilbee.core.config.schema import field_value_set 

11 

12 

13class RenderStyle(StrEnum): 

14 """How a setting is displayed in /settings.""" 

15 

16 COMPACT = "compact" 

17 FULL = "full" 

18 MULTILINE = "multiline" 

19 

20 

21class SettingGroup(StrEnum): 

22 """Logical bucket names rendered by ``/settings`` and ``settings_list``.""" 

23 

24 MODELS = "Models" 

25 GENERATION = "Generation" 

26 RETRIEVAL = "Retrieval" 

27 INGEST = "Ingest" 

28 OCR_TUNING = "OCR-Tuning" 

29 WIKI = "Wiki" 

30 MEMORY = "Memory" 

31 CRAWLING = "Crawling" 

32 LOCAL_SERVERS = "Local-Servers" 

33 API_KEYS = "API-Keys" 

34 SYSTEM = "System" 

35 DISPLAY = "Display" 

36 

37 

38@dataclass(frozen=True) 

39class SettingDef: 

40 """Metadata for an interactive setting. 

41 

42 ``writable`` is a TUI rendering hint: fields marked ``writable=False`` 

43 (the model role slots) get a dedicated picker rather than an inline 

44 editor, and the ``/set`` slash command refuses them, except that it 

45 sets or clears a model role that can be off. The actual 

46 write contract for HTTP / MCP / programmatic surfaces lives in 

47 ``config_meta.WRITABLE_CONFIG_FIELDS`` + ``MODEL_ROLE_FIELDS`` and 

48 is enforced by ``app.settings.apply_settings_update``. 

49 

50 ``choices`` is the closed value set a picker renders. It is read off 

51 the Config field's own type, so only a field whose value set lives 

52 outside that type declares one. 

53 

54 ``hidden`` keeps the setting out of the TUI settings screen while 

55 leaving it reachable via ``lilbee set`` and the ``LILBEE_*`` env 

56 var: use it for transport/server knobs that aren't relevant to a 

57 typical TUI session. 

58 

59 ``advanced`` keeps the setting shown, but folded into a collapsed 

60 section at the bottom of its tab: use it for a setting a normal 

61 user never needs to get good results (engine internals, timeouts, 

62 batch sizes, thresholds for internal heuristics, debug knobs). 

63 """ 

64 

65 type: type 

66 nullable: bool 

67 group: SettingGroup 

68 writable: bool = True 

69 render: RenderStyle = field(default=RenderStyle.COMPACT) 

70 help_text: str = "" 

71 choices: tuple[str, ...] | None = None 

72 hidden: bool = False 

73 advanced: bool = False 

74 # List editors validate each line as a regex only when this is set; flag-style 

75 # lists (e.g. crawl_browser_extra_args) would be wrongly rejected otherwise. 

76 validate_regex: bool = False 

77 # Credentials: the TUI masks the editor so the value is never on screen in 

78 # plain text, including while it is being pasted. 

79 secret: bool = False 

80 

81 

82CHUNK_LIMIT_KEY = "max_chunks_per_file" 

83 

84SETTINGS_MAP: dict[str, SettingDef] = { 

85 "chat_model": SettingDef( 

86 str, 

87 nullable="chat_model" in CLEARABLE_MODEL_FIELDS, 

88 writable=False, 

89 group=SettingGroup.MODELS, 

90 help_text="LLM used for chat generation (vision and reranking are separate slots)", 

91 ), 

92 "vision_model": SettingDef( 

93 str, 

94 nullable="vision_model" in CLEARABLE_MODEL_FIELDS, 

95 writable=False, 

96 group=SettingGroup.MODELS, 

97 help_text=( 

98 "Picks the OCR engine: a set vision model reads scanned pages, an empty one " 

99 "(including an empty LILBEE_VISION_MODEL) leaves them to Tesseract. " 

100 "The ocr setting decides whether pages are read" 

101 ), 

102 ), 

103 "ocr": SettingDef( 

104 str, 

105 nullable=False, 

106 group=SettingGroup.INGEST, 

107 help_text=( 

108 "Scanned pages: auto reads each page without usable text and keeps the text of " 

109 "every other page; all reads every page of every file on every future ingest; " 

110 "off skips them. vision_model picks the engine" 

111 ), 

112 ), 

113 "ocr_timeout": SettingDef( 

114 float, 

115 nullable=False, 

116 group=SettingGroup.OCR_TUNING, 

117 help_text="Per-page timeout in seconds for vision OCR (0 = no limit)", 

118 advanced=True, 

119 ), 

120 "vision_load_budget_s": SettingDef( 

121 float, 

122 nullable=False, 

123 group=SettingGroup.OCR_TUNING, 

124 help_text=( 

125 "Wall-clock seconds reserved for the vision worker to load the" 

126 " model. Total PDF-OCR budget = load_budget + ocr_timeout * pages." 

127 ), 

128 advanced=True, 

129 ), 

130 "vision_ocr_max_tokens": SettingDef( 

131 int, 

132 nullable=False, 

133 group=SettingGroup.OCR_TUNING, 

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 advanced=True, 

139 ), 

140 "vision_ocr_concurrency": SettingDef( 

141 int, 

142 nullable=False, 

143 group=SettingGroup.OCR_TUNING, 

144 help_text="Pages OCR'd concurrently per vision server; each slot adds KV cache memory", 

145 advanced=True, 

146 ), 

147 "extraction_timeout": SettingDef( 

148 int, 

149 nullable=False, 

150 group=SettingGroup.INGEST, 

151 help_text=( 

152 "Wall-clock seconds one file gets to extract before ingest gives up" 

153 " on it (0 = no limit)" 

154 ), 

155 advanced=True, 

156 ), 

157 "ingest_workers": SettingDef( 

158 int, 

159 nullable=False, 

160 group=SettingGroup.INGEST, 

161 help_text="Workers for discovering and hashing files (0 = auto, all available cores)", 

162 advanced=True, 

163 ), 

164 "ingest_processes": SettingDef( 

165 int, 

166 nullable=False, 

167 group=SettingGroup.INGEST, 

168 help_text=( 

169 "Ingest worker processes, one GPU each (0 = auto, one per card). Used" 

170 " once the corpus is big enough to pay for them; 1 keeps ingest in this" 

171 " process" 

172 ), 

173 advanced=True, 

174 ), 

175 "mcp_tool_threads": SettingDef( 

176 int, 

177 nullable=False, 

178 group=SettingGroup.LOCAL_SERVERS, 

179 help_text=( 

180 "Threads for synchronous MCP tool handlers; the ceiling on how many agents" 

181 " one daemon serves before retrieval calls queue" 

182 ), 

183 advanced=True, 

184 ), 

185 "auto_sync": SettingDef( 

186 bool, 

187 nullable=False, 

188 group=SettingGroup.INGEST, 

189 help_text="Run a sync before `lilbee ask` (disable on large static corpora)", 

190 ), 

191 "entity_extraction": SettingDef( 

192 bool, 

193 nullable=False, 

194 group=SettingGroup.INGEST, 

195 help_text="Extract typed entities automatically at sync (schema induced on first run)", 

196 ), 

197 "semantic_chunking": SettingDef( 

198 bool, 

199 nullable=False, 

200 group=SettingGroup.INGEST, 

201 help_text=( 

202 "Opt-in topic-aware chunker (default off; may fragment numbered procedures) " 

203 "(changes invalidate the index)" 

204 ), 

205 ), 

206 "topic_threshold": SettingDef( 

207 float, 

208 nullable=False, 

209 group=SettingGroup.INGEST, 

210 help_text=( 

211 "Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking " 

212 "is on (changes invalidate the index)" 

213 ), 

214 advanced=True, 

215 ), 

216 "token_sizing": SettingDef( 

217 bool, 

218 nullable=False, 

219 group=SettingGroup.INGEST, 

220 help_text=( 

221 "Size chunks by real embedder tokens, not chars; on by itself when the " 

222 "embedder's window is below the character budget (changes invalidate the index)" 

223 ), 

224 ), 

225 "table_extraction": SettingDef( 

226 bool, 

227 nullable=False, 

228 group=SettingGroup.INGEST, 

229 help_text="Index each extracted table as its own chunk (changes invalidate the index)", 

230 ), 

231 "layout_detection": SettingDef( 

232 bool, 

233 nullable=False, 

234 group=SettingGroup.INGEST, 

235 help_text=( 

236 "Layout-aware PDF extraction: reading order plus header/footer " 

237 "stripping (changes invalidate the index)" 

238 ), 

239 ), 

240 "table_model": SettingDef( 

241 str, 

242 nullable=False, 

243 group=SettingGroup.INGEST, 

244 help_text=( 

245 "Table structure model used when layout detection is on: slanet_auto " 

246 "(docling-parity default), other slanet variants, tatr, or disabled " 

247 "(changes invalidate the index)" 

248 ), 

249 advanced=True, 

250 ), 

251 "batch_extraction": SettingDef( 

252 bool, 

253 nullable=False, 

254 group=SettingGroup.INGEST, 

255 help_text="Coalesce concurrent extractions into one xberg batch call", 

256 ), 

257 "batch_extraction_size": SettingDef( 

258 int, 

259 nullable=False, 

260 group=SettingGroup.INGEST, 

261 help_text="Max files per extract_batch call when batch extraction is on", 

262 advanced=True, 

263 ), 

264 "analyze_max_files": SettingDef( 

265 int, 

266 nullable=False, 

267 group=SettingGroup.INGEST, 

268 help_text=( 

269 "Most documents lilbee analyze reads; more are sampled evenly down to this many." 

270 " Code, image and archive files are counted, not read. Raise it to cover more" 

271 " documents for a slower run. The default is not yet measured on large corpora" 

272 ), 

273 ), 

274 "extraction_threads": SettingDef( 

275 int, 

276 nullable=False, 

277 group=SettingGroup.INGEST, 

278 help_text=( 

279 "Threads xberg uses for PDF rendering, OCR and layout models, and the" 

280 " most Tesseract OCR sessions that run at once (0 = auto, half the" 

281 " available cores). Takes full effect after a restart." 

282 ), 

283 advanced=True, 

284 ), 

285 "embedding_model": SettingDef( 

286 str, 

287 nullable="embedding_model" in CLEARABLE_MODEL_FIELDS, 

288 writable=False, 

289 group=SettingGroup.MODELS, 

290 help_text="Model used to embed document chunks", 

291 ), 

292 "reranker_model": SettingDef( 

293 str, 

294 nullable="reranker_model" in CLEARABLE_MODEL_FIELDS, 

295 writable=False, 

296 group=SettingGroup.MODELS, 

297 help_text="Cross-encoder model for result reranking (empty = off)", 

298 ), 

299 "reranker_type": SettingDef( 

300 str, 

301 nullable=False, 

302 group=SettingGroup.MODELS, 

303 help_text=( 

304 "Reranker serving mode: auto (detect cross-encoder vs LLM by model), " 

305 "cross_encoder, or llm" 

306 ), 

307 advanced=True, 

308 ), 

309 "reranker_prompt": SettingDef( 

310 str, 

311 nullable=False, 

312 group=SettingGroup.MODELS, 

313 help_text="Relevance prompt for LLM rerankers (blank uses the built-in template)", 

314 ), 

315 "include_uncensored": SettingDef( 

316 bool, 

317 nullable=False, 

318 group=SettingGroup.MODELS, 

319 help_text=( 

320 "Recommend safety-stripped models in Picks and Discover" 

321 " (browse and search always list them)" 

322 ), 

323 ), 

324 "temperature": SettingDef( 

325 float, 

326 nullable=True, 

327 group=SettingGroup.GENERATION, 

328 help_text="Sampling temperature (higher = more creative)", 

329 ), 

330 "top_p": SettingDef( 

331 float, 

332 nullable=True, 

333 group=SettingGroup.GENERATION, 

334 help_text="Nucleus sampling cutoff probability", 

335 ), 

336 "top_k_sampling": SettingDef( 

337 int, 

338 nullable=True, 

339 group=SettingGroup.GENERATION, 

340 help_text="Top-K sampling: number of tokens to consider", 

341 ), 

342 "repeat_penalty": SettingDef( 

343 float, 

344 nullable=True, 

345 group=SettingGroup.GENERATION, 

346 help_text="Penalty for repeating tokens", 

347 ), 

348 "num_ctx": SettingDef( 

349 int, 

350 nullable=True, 

351 group=SettingGroup.GENERATION, 

352 help_text=( 

353 "Context window size in tokens. Leave empty to size automatically " 

354 "(aims for chat_n_ctx_target, ceiling at num_ctx_max or training_ctx)." 

355 ), 

356 ), 

357 "num_ctx_max": SettingDef( 

358 int, 

359 nullable=True, 

360 group=SettingGroup.GENERATION, 

361 help_text=( 

362 "Explicit ceiling for the dynamic context picker. Leave empty to " 

363 "use the model's training_ctx from GGUF metadata as the only " 

364 "ceiling. Set to cap below training_ctx (saves KV memory)." 

365 ), 

366 advanced=True, 

367 ), 

368 "chat_n_ctx_target": SettingDef( 

369 int, 

370 nullable=False, 

371 group=SettingGroup.GENERATION, 

372 help_text=( 

373 "Working context the dynamic picker aims for. Fits a RAG turn " 

374 "with reasoning headroom; raise for long-document chat." 

375 ), 

376 advanced=True, 

377 ), 

378 "flash_attention": SettingDef( 

379 bool, 

380 nullable=True, 

381 group=SettingGroup.GENERATION, 

382 help_text=( 

383 "Flash attention. Empty (auto) enables it; disable for backends or " 

384 "models where it misbehaves. Resolves the V-cache padding warning " 

385 "on models with uneven per-layer V dims. On Vulkan and SYCL lilbee " 

386 "sends auto, not on, even when this is true." 

387 ), 

388 advanced=True, 

389 ), 

390 "kv_cache_type": SettingDef( 

391 str, 

392 nullable=False, 

393 group=SettingGroup.GENERATION, 

394 help_text=( 

395 "KV cache element type for both K and V. q8_0 / q4_0 halve or quarter " 

396 "cache memory and apply only where lilbee sends flash attention on. On " 

397 "Vulkan and SYCL lilbee sends auto, so the cache is f16 there; it is " 

398 "also f16 when flash attention is off." 

399 ), 

400 advanced=True, 

401 ), 

402 "n_gpu_layers": SettingDef( 

403 int, 

404 nullable=True, 

405 group=SettingGroup.GENERATION, 

406 help_text=( 

407 "Layers to offload to GPU. Empty = all (recommended), 0 = CPU only, " 

408 "positive int = partial offload for tight VRAM." 

409 ), 

410 advanced=True, 

411 ), 

412 "cpu_moe": SettingDef( 

413 bool, 

414 nullable=False, 

415 group=SettingGroup.GENERATION, 

416 help_text=( 

417 "Keep a mixture-of-experts model's expert weights in system memory so " 

418 "it fits a smaller GPU. No effect on dense models." 

419 ), 

420 advanced=True, 

421 ), 

422 "n_cpu_moe": SettingDef( 

423 int, 

424 nullable=True, 

425 group=SettingGroup.GENERATION, 

426 help_text=( 

427 "Offload only the first N layers' experts to system memory. Takes " 

428 "precedence over the offload-everything setting; smaller N stays faster." 

429 ), 

430 advanced=True, 

431 ), 

432 "fast_model_downloads": SettingDef( 

433 bool, 

434 nullable=False, 

435 group=SettingGroup.GENERATION, 

436 help_text=( 

437 "Warning: Hugging Face states this mode uses all available bandwidth " 

438 "and CPU cores, and buffers far more of the download in memory. " 

439 "Faster on a fast connection, at the cost of everything else running " 

440 "on the machine. Leave it off unless the machine can spare that. " 

441 "Requires a restart to take effect." 

442 ), 

443 advanced=True, 

444 ), 

445 "gpu_devices": SettingDef( 

446 str, 

447 nullable=True, 

448 group=SettingGroup.GENERATION, 

449 help_text=( 

450 "Restrict llama.cpp to specific GPU indexes on dual-GPU machines " 

451 "(e.g. NVIDIA dGPU + integrated). Comma-separated, like '0' or '0,1'. " 

452 "Applies to Vulkan, CUDA, and ROCm. Requires a restart to take effect." 

453 ), 

454 advanced=True, 

455 ), 

456 "seed": SettingDef( 

457 int, 

458 nullable=True, 

459 group=SettingGroup.GENERATION, 

460 help_text=( 

461 "Random seed for reproducible output. On a local engine, the search steps that " 

462 "call the model (query expansion, hypothetical answers, history rewrite) use it " 

463 "too. A hosted provider can ignore it" 

464 ), 

465 advanced=True, 

466 ), 

467 "rag_system_prompt": SettingDef( 

468 str, 

469 nullable=False, 

470 render=RenderStyle.MULTILINE, 

471 group=SettingGroup.GENERATION, 

472 help_text="System prompt sent when answering with retrieved context", 

473 ), 

474 "general_system_prompt": SettingDef( 

475 str, 

476 nullable=False, 

477 render=RenderStyle.MULTILINE, 

478 group=SettingGroup.GENERATION, 

479 help_text="System prompt sent when there are no documents to ground the answer", 

480 ), 

481 "chat_compaction": SettingDef( 

482 bool, 

483 nullable=False, 

484 group=SettingGroup.GENERATION, 

485 help_text=( 

486 "Off (default): when a chat outgrows the model's context window the oldest " 

487 "turns are dropped. They stay on screen but the model stops seeing them, and " 

488 "the context chip by the prompt shows the window filling. Costs nothing. " 

489 "On: those turns are condensed into a short summary the model keeps reading, " 

490 "so it still knows roughly what was said. That costs one extra model call each " 

491 "time it fires, pausing the reply for a few seconds on a GPU and considerably " 

492 "longer on a CPU-only machine. Worth turning on if your hardware is quick." 

493 ), 

494 ), 

495 "sessions_enabled": SettingDef( 

496 bool, 

497 nullable=False, 

498 group=SettingGroup.GENERATION, 

499 help_text=( 

500 "On (default): conversations are saved automatically, and you can list, " 

501 "resume, rename, and delete them from the Sessions drawer (ctrl+o), the " 

502 "Sessions tab, and the /sessions command. Off: nothing is written to disk, " 

503 "the ctrl+o binding leaves the footer, and opening the Sessions view shows a " 

504 "notice that sessions are turned off. Turn it off if you would rather your " 

505 "chats not persist. Covers the TUI, the HTTP server, and the CLI; agent " 

506 "sessions have their own setting." 

507 ), 

508 ), 

509 "mcp_sessions_enabled": SettingDef( 

510 bool, 

511 nullable=False, 

512 group=SettingGroup.GENERATION, 

513 help_text=( 

514 "Off (default): the session tools are not offered over MCP, and a connected " 

515 "agent cannot create or read agent sessions. On: an agent can keep its own " 

516 "saved conversations, separate from yours. Most agent hosts already track " 

517 "their own history, and the tools cost context on every request, so this " 

518 "stays off unless you want an agent owning conversations." 

519 ), 

520 ), 

521 "mcp_profiles_enabled": SettingDef( 

522 bool, 

523 nullable=False, 

524 group=SettingGroup.GENERATION, 

525 help_text=( 

526 "Off (default): the profile tools are not offered over MCP. On: a connected " 

527 "agent can list, compare, apply, save and manage profiles. The tools cost " 

528 "context on every request, so this stays off unless you want an agent tuning " 

529 "a project's settings through profiles." 

530 ), 

531 ), 

532 "chat_mode": SettingDef( 

533 str, 

534 nullable=False, 

535 group=SettingGroup.GENERATION, 

536 help_text="search runs every chat turn through document retrieval; chat skips it", 

537 ), 

538 "top_k": SettingDef( 

539 int, 

540 nullable=False, 

541 group=SettingGroup.RETRIEVAL, 

542 help_text="Number of chunks returned by search", 

543 ), 

544 "rerank_candidates": SettingDef( 

545 int, 

546 nullable=False, 

547 group=SettingGroup.RETRIEVAL, 

548 help_text="Candidate pool size for reranking", 

549 advanced=True, 

550 ), 

551 "rerank_blend": SettingDef( 

552 bool, 

553 nullable=False, 

554 group=SettingGroup.RETRIEVAL, 

555 help_text="Blend reranker scores with retrieval fusion (off = pure reranker order)", 

556 ), 

557 "rerank_min_score": SettingDef( 

558 float, 

559 nullable=True, 

560 group=SettingGroup.RETRIEVAL, 

561 help_text="Drop candidates whose raw reranker score is below this (unset = off)", 

562 advanced=True, 

563 ), 

564 "show_reasoning": SettingDef( 

565 bool, 

566 nullable=False, 

567 group=SettingGroup.DISPLAY, 

568 help_text="Show model reasoning/thinking tokens in output", 

569 ), 

570 "completions_reasoning": SettingDef( 

571 str, 

572 nullable=False, 

573 group=SettingGroup.GENERATION, 

574 help_text=( 

575 "How /v1/chat/completions presents thinking: separate " 

576 "reasoning_content field, inline thinking as plain content text, " 

577 "or off (ask the model not to think)" 

578 ), 

579 advanced=True, 

580 ), 

581 "messages_reasoning": SettingDef( 

582 str, 

583 nullable=False, 

584 group=SettingGroup.GENERATION, 

585 help_text=( 

586 "How /v1/messages presents thinking: separate thinking block, " 

587 "inline thinking as plain answer text, or off (ask the model not " 

588 "to think)" 

589 ), 

590 advanced=True, 

591 ), 

592 "lilbee_name": SettingDef( 

593 str, 

594 nullable=False, 

595 group=SettingGroup.DISPLAY, 

596 help_text=( 

597 "Human-readable label for this lilbee, shown in the status bar. " 

598 "Empty falls back to 'global' for the platform default dir or " 

599 "to the project path (~-substituted and left-truncated)." 

600 ), 

601 ), 

602 "show_lilbee_path": SettingDef( 

603 bool, 

604 nullable=False, 

605 group=SettingGroup.DISPLAY, 

606 help_text=( 

607 "Show the full absolute path in the status bar: expands 'global' " 

608 "to its on-disk path and skips ~ substitution / truncation." 

609 ), 

610 ), 

611 "theme": SettingDef( 

612 str, 

613 nullable=False, 

614 group=SettingGroup.DISPLAY, 

615 help_text="TUI color theme. Cycle with Ctrl+T; the active theme persists across sessions.", 

616 choices=tuple(DARK_THEMES), 

617 ), 

618 "wiki": SettingDef( 

619 bool, 

620 nullable=False, 

621 group=SettingGroup.WIKI, 

622 help_text=( 

623 "Enable the wiki layer (cited concept and entity pages). " 

624 "GPU-heavy: a build spends one LLM call per source document, " 

625 "so a large library takes hours. Enabling this generates nothing " 

626 "on its own: you wikify explicitly, or turn on wiki_auto_update" 

627 ), 

628 ), 

629 "wiki_auto_update": SettingDef( 

630 bool, 

631 nullable=False, 

632 group=SettingGroup.WIKI, 

633 help_text="Regenerate touched wiki pages after each sync (off: wikify explicitly)", 

634 ), 

635 "wiki_dir": SettingDef( 

636 str, 

637 nullable=False, 

638 writable=False, 

639 group=SettingGroup.WIKI, 

640 help_text=( 

641 "Directory under data_root where wiki pages live (set via env / config.toml only)" 

642 ), 

643 ), 

644 "wiki_prune_raw": SettingDef( 

645 bool, 

646 nullable=False, 

647 group=SettingGroup.WIKI, 

648 help_text="Delete raw chunks after summarizing into the wiki", 

649 ), 

650 "wiki_embedding_faithfulness_threshold": SettingDef( 

651 float, 

652 nullable=False, 

653 group=SettingGroup.WIKI, 

654 help_text=( 

655 "Minimum cosine similarity (0-1) between a generated page and " 

656 "the mean of its source chunk vectors before publishing. " 

657 "Pages below the threshold route to drafts/." 

658 ), 

659 advanced=True, 

660 ), 

661 "wiki_stale_citation_threshold": SettingDef( 

662 float, 

663 nullable=False, 

664 group=SettingGroup.WIKI, 

665 help_text="Fraction of stale citations before a page is flagged by wiki prune", 

666 advanced=True, 

667 ), 

668 "wiki_drift_threshold": SettingDef( 

669 float, 

670 nullable=False, 

671 group=SettingGroup.WIKI, 

672 help_text="Max fraction of changed lines before regeneration requires review", 

673 advanced=True, 

674 ), 

675 "wiki_clusterer": SettingDef( 

676 str, 

677 nullable=False, 

678 group=SettingGroup.WIKI, 

679 help_text="Synthesis clusterer backend (embedding or concepts)", 

680 advanced=True, 

681 ), 

682 "wiki_entity_mode": SettingDef( 

683 str, 

684 nullable=False, 

685 group=SettingGroup.WIKI, 

686 help_text=( 

687 "Entity extraction strategy. ner_entities (typed spaCy NER) is the " 

688 "only implemented mode; the other values fall back to it with a warning" 

689 ), 

690 advanced=True, 

691 ), 

692 "wiki_entity_min_mentions": SettingDef( 

693 int, 

694 nullable=False, 

695 group=SettingGroup.WIKI, 

696 help_text="Minimum chunk mentions before an entity or concept gets its own page", 

697 advanced=True, 

698 ), 

699 "wiki_stub_max_chunk_refs": SettingDef( 

700 int, 

701 nullable=False, 

702 group=SettingGroup.WIKI, 

703 help_text=( 

704 "How many source chunks a page kept for lazy generation draws on. " 

705 "Caps the browse index's size; already more than one page's context " 

706 "budget admits, so raising it rarely changes what a page says" 

707 ), 

708 advanced=True, 

709 ), 

710 "wiki_ingest_update_cap": SettingDef( 

711 int, 

712 nullable=False, 

713 group=SettingGroup.WIKI, 

714 help_text=( 

715 "Touched-page cap for auto-update after sync. " 

716 "Beyond this count, run `lilbee wiki update` manually." 

717 ), 

718 advanced=True, 

719 ), 

720 "wiki_synthesis_prompt": SettingDef( 

721 str, 

722 nullable=False, 

723 render=RenderStyle.FULL, 

724 group=SettingGroup.WIKI, 

725 help_text=( 

726 "Prompt for cross-source synthesis pages. " 

727 "Must keep {topic}, {source_list}, and {chunks_text}." 

728 ), 

729 ), 

730 "wiki_entity_page_prompt": SettingDef( 

731 str, 

732 nullable=False, 

733 render=RenderStyle.FULL, 

734 group=SettingGroup.WIKI, 

735 help_text=( 

736 "Prompt for a single page generated on demand from one subject's " 

737 "chunks across every source naming it. " 

738 "Must keep {topic}, {source_list}, and {chunks_text}." 

739 ), 

740 ), 

741 "wiki_entity_batch_prompt": SettingDef( 

742 str, 

743 nullable=False, 

744 render=RenderStyle.FULL, 

745 group=SettingGroup.WIKI, 

746 help_text=( 

747 "Prompt for the per-source batched call. " 

748 "Must keep {source}, {entity_list}, {chunks_text}, and {concept_instruction}." 

749 ), 

750 ), 

751 "wiki_extract_concepts": SettingDef( 

752 bool, 

753 nullable=False, 

754 group=SettingGroup.WIKI, 

755 help_text=( 

756 "Whether the per-source batched call asks the LLM to curate concept pages " 

757 "alongside the pre-extracted entity list." 

758 ), 

759 advanced=True, 

760 ), 

761 "wiki_batch_min_chunks": SettingDef( 

762 int, 

763 nullable=False, 

764 group=SettingGroup.WIKI, 

765 help_text=( 

766 "Minimum chunks a source must contribute before its batched call includes " 

767 "concept curation. Sources below the floor skip the concept-curation " 

768 "instruction; sources with zero entities AND below the floor are skipped entirely." 

769 ), 

770 advanced=True, 

771 ), 

772 "wiki_clusterer_k": SettingDef( 

773 int, 

774 nullable=False, 

775 group=SettingGroup.WIKI, 

776 help_text="Mutual-kNN neighborhood size for the clusterer (0 = auto)", 

777 advanced=True, 

778 ), 

779 "memory_enabled": SettingDef( 

780 bool, 

781 nullable=False, 

782 group=SettingGroup.MEMORY, 

783 help_text="Master switch for long-term chat memory (off by default)", 

784 ), 

785 "memory_auto_extract": SettingDef( 

786 bool, 

787 nullable=False, 

788 group=SettingGroup.MEMORY, 

789 help_text="Auto-save durable facts and preferences from each TUI turn (needs memory on)", 

790 ), 

791 "memory_top_k": SettingDef( 

792 int, 

793 nullable=False, 

794 group=SettingGroup.MEMORY, 

795 help_text="Maximum facts recalled into context per turn", 

796 ), 

797 "memory_max_distance": SettingDef( 

798 float, 

799 nullable=False, 

800 group=SettingGroup.MEMORY, 

801 help_text="Recall cutoff distance, 0.0-1.0 (lower is stricter)", 

802 advanced=True, 

803 ), 

804 "memory_token_budget": SettingDef( 

805 int, 

806 nullable=False, 

807 group=SettingGroup.MEMORY, 

808 help_text="Token cap on the recalled-memory block added to the prompt", 

809 advanced=True, 

810 ), 

811 "memory_max_per_owner": SettingDef( 

812 int, 

813 nullable=False, 

814 group=SettingGroup.MEMORY, 

815 help_text="Soft cap before the oldest memories are evicted", 

816 hidden=True, 

817 ), 

818 "memory_dedup_distance": SettingDef( 

819 float, 

820 nullable=False, 

821 group=SettingGroup.MEMORY, 

822 help_text="Near-duplicate distance below which a new memory updates the old", 

823 hidden=True, 

824 ), 

825 "crawl_max_depth": SettingDef( 

826 int, 

827 nullable=True, 

828 group=SettingGroup.CRAWLING, 

829 help_text="Optional recursion-depth cap, 0 to 100 (blank = no cap; per-crawl values win)", 

830 ), 

831 "crawl_render_mode": SettingDef( 

832 str, 

833 nullable=False, 

834 group=SettingGroup.CRAWLING, 

835 help_text=( 

836 "How crawls fetch pages. http = lightweight, no browser (default, best " 

837 "for static and server-rendered sites). browser = Chromium with " 

838 "JavaScript enabled for client-rendered sites, at much higher memory cost." 

839 ), 

840 ), 

841 "crawl_browser_extra_args": SettingDef( 

842 list, 

843 nullable=False, 

844 group=SettingGroup.CRAWLING, 

845 help_text=( 

846 "Browser mode: extra Chromium launch flags, one per line. " 

847 "Defaults trim shared-memory and GPU use." 

848 ), 

849 advanced=True, 

850 ), 

851 "crawl_browser_extra_wait_ms": SettingDef( 

852 int, 

853 nullable=False, 

854 group=SettingGroup.CRAWLING, 

855 help_text=( 

856 "Browser mode: milliseconds to wait after a page has loaded and stopped changing, " 

857 "for sites that draw their text later. Raise it when a page's text is missing; " 

858 "0 = no extra wait, at most 600000 (ten minutes). http mode ignores it." 

859 ), 

860 ), 

861 "crawl_login_host": SettingDef( 

862 str, 

863 nullable=False, 

864 group=SettingGroup.CRAWLING, 

865 help_text=( 

866 "The one host that gets the stored crawl login, as a bare host name such as " 

867 "wiki.example.com, with no scheme or path. The match ignores the " 

868 "scheme and the port of the crawled URL. A crawl of any other host sends " 

869 "no stored login. " 

870 "Blank = no stored login" 

871 ), 

872 ), 

873 "crawl_authorization": SettingDef( 

874 str, 

875 nullable=False, 

876 group=SettingGroup.CRAWLING, 

877 secret=True, 

878 help_text=( 

879 "Authorization header value sent to the crawl login host. Use it or a " 

880 "username, not both. Stored in plain text in config.toml" 

881 ), 

882 ), 

883 "crawl_username": SettingDef( 

884 str, 

885 nullable=False, 

886 group=SettingGroup.CRAWLING, 

887 help_text="Basic-auth username sent to the crawl login host. Blank = none", 

888 ), 

889 "crawl_password": SettingDef( 

890 str, 

891 nullable=False, 

892 group=SettingGroup.CRAWLING, 

893 secret=True, 

894 help_text=( 

895 "Basic-auth password sent with the crawl login username. Stored in plain " 

896 "text in config.toml" 

897 ), 

898 ), 

899 "crawl_cookie": SettingDef( 

900 str, 

901 nullable=False, 

902 group=SettingGroup.CRAWLING, 

903 secret=True, 

904 help_text=( 

905 "Cookie header value sent to the crawl login host. Stored in plain text in config.toml" 

906 ), 

907 ), 

908 "crawl_user_agent": SettingDef( 

909 str, 

910 nullable=False, 

911 group=SettingGroup.CRAWLING, 

912 help_text=( 

913 "User agent of every crawl request, for a site that answers some clients " 

914 "differently. Blank = no override: browser mode sends Chrome's own, " 

915 "http mode sends crawlberg's, and the sitemap lookup sends its HTTP " 

916 "client's. A Chrome user agent also sets the matching " 

917 "client hints in browser mode." 

918 ), 

919 ), 

920 "crawl_respect_robots_txt": SettingDef( 

921 bool, 

922 nullable=False, 

923 group=SettingGroup.CRAWLING, 

924 help_text=( 

925 "Skip the pages a site's robots.txt disallows, and every page of a site " 

926 "whose robots.txt cannot be read (a server error, a rate limit or no answer). Turn off " 

927 "to crawl a site you own or have permission to crawl in full." 

928 ), 

929 ), 

930 "crawl_max_pages": SettingDef( 

931 int, 

932 nullable=True, 

933 group=SettingGroup.CRAWLING, 

934 help_text="Optional global cap on total pages per crawl, 1 to 10000000 (blank = no cap).", 

935 ), 

936 "crawl_safety_max_pages": SettingDef( 

937 int, 

938 nullable=False, 

939 group=SettingGroup.CRAWLING, 

940 help_text="Default page bound for an unbounded crawl, so a hostile site cannot " 

941 "exhaust the disk, 1 to 10000000. An explicit max-pages overrides it; raise this " 

942 "to crawl larger sites unbounded.", 

943 advanced=True, 

944 ), 

945 "crawl_timeout": SettingDef( 

946 int, 

947 nullable=False, 

948 group=SettingGroup.CRAWLING, 

949 help_text="Per-page fetch timeout in seconds, 1 to 3600", 

950 advanced=True, 

951 ), 

952 "crawl_sync_interval": SettingDef( 

953 int, 

954 nullable=False, 

955 group=SettingGroup.CRAWLING, 

956 help_text="Seconds between periodic re-syncs during a crawl (0 = sync only at end)", 

957 ), 

958 "crawl_mean_delay": SettingDef( 

959 float, 

960 nullable=False, 

961 group=SettingGroup.CRAWLING, 

962 help_text="Seconds each concurrent request waits between requests to a site, 0 to 3600", 

963 advanced=True, 

964 ), 

965 "crawl_max_delay_range": SettingDef( 

966 float, 

967 nullable=False, 

968 group=SettingGroup.CRAWLING, 

969 help_text="Random jitter (seconds) added on top of mean delay, 0 to 3600", 

970 advanced=True, 

971 ), 

972 "crawl_concurrent_requests": SettingDef( 

973 int, 

974 nullable=False, 

975 group=SettingGroup.CRAWLING, 

976 help_text="Concurrent in-flight URLs within one crawl, 1 to 256", 

977 advanced=True, 

978 ), 

979 "crawl_retry_on_rate_limit": SettingDef( 

980 bool, 

981 nullable=False, 

982 group=SettingGroup.CRAWLING, 

983 help_text="Retry a page that answers HTTP 429 or 503. Off: no retries", 

984 ), 

985 "crawl_retry_base_delay_min": SettingDef( 

986 float, 

987 nullable=False, 

988 group=SettingGroup.CRAWLING, 

989 help_text=( 

990 "Base delay range, low end (seconds, 0 to 3600). The first retry waits halfway" 

991 " between the low and high end, and each later retry waits twice as long" 

992 ), 

993 advanced=True, 

994 ), 

995 "crawl_retry_base_delay_max": SettingDef( 

996 float, 

997 nullable=False, 

998 group=SettingGroup.CRAWLING, 

999 help_text=( 

1000 "Base delay range, high end (seconds, 0 to 3600). The first retry waits halfway" 

1001 " between the low and high end, and each later retry waits twice as long" 

1002 ), 

1003 advanced=True, 

1004 ), 

1005 "crawl_retry_max_backoff": SettingDef( 

1006 float, 

1007 nullable=False, 

1008 group=SettingGroup.CRAWLING, 

1009 help_text="Upper bound on any single backoff wait (seconds, 0 to 3600)", 

1010 advanced=True, 

1011 ), 

1012 "crawl_retry_max_attempts": SettingDef( 

1013 int, 

1014 nullable=False, 

1015 group=SettingGroup.CRAWLING, 

1016 help_text="Retries per page. A crawl makes at most 20, whatever the value", 

1017 advanced=True, 

1018 ), 

1019 "crawl_exclude_patterns": SettingDef( 

1020 list, 

1021 nullable=False, 

1022 group=SettingGroup.CRAWLING, 

1023 validate_regex=True, 

1024 help_text=( 

1025 "Regex patterns that skip URLs at link-discovery time during " 

1026 "recursive crawls. One per line." 

1027 ), 

1028 ), 

1029 "openrouter_api_key": SettingDef( 

1030 str, 

1031 nullable=False, 

1032 group=SettingGroup.API_KEYS, 

1033 secret=True, 

1034 help_text="OpenRouter API key (enables frontier models in chat picker)", 

1035 ), 

1036 "gemini_api_key": SettingDef( 

1037 str, 

1038 nullable=False, 

1039 group=SettingGroup.API_KEYS, 

1040 secret=True, 

1041 help_text="Google Gemini API key (enables frontier models in chat picker)", 

1042 ), 

1043 "anthropic_api_key": SettingDef( 

1044 str, 

1045 nullable=False, 

1046 group=SettingGroup.API_KEYS, 

1047 secret=True, 

1048 help_text="Anthropic API key (enables frontier models in chat picker)", 

1049 ), 

1050 "openai_api_key": SettingDef( 

1051 str, 

1052 nullable=False, 

1053 group=SettingGroup.API_KEYS, 

1054 secret=True, 

1055 help_text="OpenAI API key (enables frontier models in chat picker)", 

1056 ), 

1057 "mistral_api_key": SettingDef( 

1058 str, 

1059 nullable=False, 

1060 group=SettingGroup.API_KEYS, 

1061 secret=True, 

1062 help_text="Mistral API key (enables frontier models in chat picker)", 

1063 ), 

1064 "deepseek_api_key": SettingDef( 

1065 str, 

1066 nullable=False, 

1067 group=SettingGroup.API_KEYS, 

1068 secret=True, 

1069 help_text="DeepSeek API key (enables frontier models in chat picker)", 

1070 ), 

1071 "llm_api_key": SettingDef( 

1072 str, 

1073 nullable=False, 

1074 group=SettingGroup.API_KEYS, 

1075 secret=True, 

1076 help_text=( 

1077 "API key for the remote OpenAI-compatible endpoint and local model servers " 

1078 "such as LM Studio. Ollama and hosted models never get this key; hosted " 

1079 "models use their own provider key" 

1080 ), 

1081 ), 

1082 "hf_token": SettingDef( 

1083 str, 

1084 nullable=False, 

1085 group=SettingGroup.SYSTEM, 

1086 secret=True, 

1087 help_text=( 

1088 "HuggingFace access token. Avoids the unauthenticated download " 

1089 "rate limit and unlocks gated repos. Stored in plain text in " 

1090 "config.toml. Env vars (LILBEE_HF_TOKEN, HF_TOKEN) override." 

1091 ), 

1092 ), 

1093 "chunk_size": SettingDef( 

1094 int, 

1095 nullable=False, 

1096 group=SettingGroup.INGEST, 

1097 help_text="Document chunk size in tokens (changes invalidate the index)", 

1098 ), 

1099 "chunk_overlap": SettingDef( 

1100 int, 

1101 nullable=False, 

1102 group=SettingGroup.INGEST, 

1103 help_text="Tokens of overlap between adjacent chunks (preserves context across boundaries)", 

1104 ), 

1105 CHUNK_LIMIT_KEY: SettingDef( 

1106 int, 

1107 nullable=False, 

1108 group=SettingGroup.INGEST, 

1109 help_text=( 

1110 "Most chunks one file can add to the index; a file over the limit is skipped, " 

1111 "not embedded (0 = no limit). Raise it for a long document such as a " 

1112 "thousand-page manual at a small chunk_size, then retry skipped files" 

1113 ), 

1114 ), 

1115 "ocr_language": SettingDef( 

1116 list, 

1117 nullable=False, 

1118 group=SettingGroup.INGEST, 

1119 help_text="Tesseract OCR languages when no vision model is set; '+'-join, e.g. eng+deu", 

1120 ), 

1121 "ocr_strategy": SettingDef( 

1122 str, 

1123 nullable=False, 

1124 group=SettingGroup.OCR_TUNING, 

1125 help_text=( 

1126 "PDF pages to OCR: auto (each page without usable text: its text layer is" 

1127 " missing, garbled or under 32 non-blank characters) or scanned_pages (also every" 

1128 " page that looks like a scan, e.g. a scanned page with a hidden text layer)" 

1129 ), 

1130 advanced=True, 

1131 ), 

1132 "ocr_scan_confidence": SettingDef( 

1133 float, 

1134 nullable=False, 

1135 group=SettingGroup.OCR_TUNING, 

1136 help_text=( 

1137 "How sure xberg must be that a page is a scan before it OCRs it (0-1)." 

1138 " Applies only when ocr_strategy is scanned_pages. Lower it to 0.5 to" 

1139 " also OCR slides with a full-page background image" 

1140 ), 

1141 advanced=True, 

1142 ), 

1143 "force_ocr_pages": SettingDef( 

1144 list, 

1145 nullable=False, 

1146 group=SettingGroup.OCR_TUNING, 

1147 help_text=( 

1148 "PDF page numbers that lilbee OCRs in every PDF, comma-separated (e.g. 1,3)" 

1149 " or one per line" 

1150 ), 

1151 advanced=True, 

1152 ), 

1153 "worker_pool_eager_start": SettingDef( 

1154 bool, 

1155 nullable=False, 

1156 group=SettingGroup.INGEST, 

1157 help_text=( 

1158 "Spawn every configured role server at TUI startup instead of on first use. " 

1159 "Trades cold-start time per role for first-call latency" 

1160 ), 

1161 advanced=True, 

1162 ), 

1163 "keep_engine_warm": SettingDef( 

1164 bool, 

1165 nullable=False, 

1166 group=SettingGroup.SYSTEM, 

1167 help_text=( 

1168 "Keep models loaded while lilbee runs and the engine up for warm launches; " 

1169 "after every lilbee process has exited, models unload after the idle time. " 

1170 "Off stops the engine on last exit unless another lilbee asked to keep it. " 

1171 "A change applies at the next engine start" 

1172 ), 

1173 ), 

1174 "engine_idle_ttl_minutes": SettingDef( 

1175 int, 

1176 nullable=False, 

1177 group=SettingGroup.SYSTEM, 

1178 help_text=( 

1179 "Idle minutes before the engine unloads its weights; with keep_engine_warm, " 

1180 "counted from the last lilbee exit; 0 keeps them loaded. " 

1181 "A change applies at the next engine start" 

1182 ), 

1183 advanced=True, 

1184 ), 

1185 "agent_mcp_enabled": SettingDef( 

1186 bool, 

1187 nullable=False, 

1188 group=SettingGroup.SYSTEM, 

1189 help_text=( 

1190 "Register lilbee's MCP search tool into agent launchers (opencode, hermes). " 

1191 "Disable to bring your own MCP servers; lilbee stays the model provider" 

1192 ), 

1193 ), 

1194 "max_tokens": SettingDef( 

1195 int, 

1196 nullable=True, 

1197 group=SettingGroup.GENERATION, 

1198 help_text="Hard cap on generated tokens per response (blank = the model's context window)", 

1199 ), 

1200 "max_reasoning_chars": SettingDef( 

1201 int, 

1202 nullable=False, 

1203 group=SettingGroup.GENERATION, 

1204 help_text=( 

1205 "Maximum reasoning characters before lilbee forces the model to answer " 

1206 "(0 = unlimited; per-model overrides apply on top)" 

1207 ), 

1208 advanced=True, 

1209 ), 

1210 "model_keep_alive": SettingDef( 

1211 int, 

1212 nullable=False, 

1213 group=SettingGroup.GENERATION, 

1214 help_text="Seconds the loaded model stays warm between calls (0 = unload immediately)", 

1215 advanced=True, 

1216 ), 

1217 "gpu_memory_fraction": SettingDef( 

1218 float, 

1219 nullable=False, 

1220 group=SettingGroup.GENERATION, 

1221 help_text="Fraction of GPU memory the model is allowed to claim (0.1-1.0)", 

1222 advanced=True, 

1223 ), 

1224 "usable_vram_fraction": SettingDef( 

1225 float, 

1226 nullable=False, 

1227 group=SettingGroup.GENERATION, 

1228 help_text=( 

1229 "Share of a GPU placement may fill, leaving room for fragmentation and driver " 

1230 "overhead (0.5-1.0). Raise it if a model that should fit is being refused; " 

1231 "lower it if loads fail near the top of the card." 

1232 ), 

1233 advanced=True, 

1234 ), 

1235 "system_memory_reserve_gb": SettingDef( 

1236 float, 

1237 nullable=False, 

1238 group=SettingGroup.GENERATION, 

1239 help_text=( 

1240 "RAM held back for the OS in GiB when serving from system memory (no discrete " 

1241 "GPU). Capped at a quarter of total RAM either way." 

1242 ), 

1243 advanced=True, 

1244 ), 

1245 "embed_replicas": SettingDef( 

1246 int, 

1247 nullable=False, 

1248 group=SettingGroup.GENERATION, 

1249 help_text="Embedding servers in parallel (0 = auto, one per GPU; positive pins the count)", 

1250 advanced=True, 

1251 ), 

1252 "vision_replicas": SettingDef( 

1253 int, 

1254 nullable=False, 

1255 group=SettingGroup.OCR_TUNING, 

1256 help_text="Vision OCR servers in parallel (0 = auto, one per GPU; positive pins the count)", 

1257 advanced=True, 

1258 ), 

1259 "candidate_multiplier": SettingDef( 

1260 int, 

1261 nullable=False, 

1262 group=SettingGroup.RETRIEVAL, 

1263 help_text="Candidate-pool multiplier over top_k before reranking", 

1264 advanced=True, 

1265 ), 

1266 "title_search": SettingDef( 

1267 bool, 

1268 nullable=False, 

1269 group=SettingGroup.RETRIEVAL, 

1270 help_text="Match queries against document titles as a third hybrid-search arm", 

1271 ), 

1272 "title_search_weight": SettingDef( 

1273 float, 

1274 nullable=False, 

1275 group=SettingGroup.RETRIEVAL, 

1276 help_text="Title arm weight in rank fusion (1.0 = equal voice with the other arms)", 

1277 advanced=True, 

1278 ), 

1279 "lexical_fusion_weight": SettingDef( 

1280 float, 

1281 nullable=False, 

1282 group=SettingGroup.RETRIEVAL, 

1283 help_text="BM25 arm weight in fusion (1.0 = equal to vector; lower to favor dense)", 

1284 advanced=True, 

1285 ), 

1286 "adaptive_fusion": SettingDef( 

1287 bool, 

1288 nullable=False, 

1289 group=SettingGroup.RETRIEVAL, 

1290 help_text="Scale the BM25 weight per query by vector-arm confidence, not a fixed value", 

1291 ), 

1292 "adaptive_fusion_margin": SettingDef( 

1293 float, 

1294 nullable=False, 

1295 group=SettingGroup.RETRIEVAL, 

1296 help_text="Vector-similarity margin at which adaptive fusion fully silences the BM25 arm", 

1297 advanced=True, 

1298 ), 

1299 "filter_structural_chunks": SettingDef( 

1300 bool, 

1301 nullable=False, 

1302 group=SettingGroup.RETRIEVAL, 

1303 help_text="Drop tables-of-contents and classification-banner cover pages from results", 

1304 ), 

1305 "fts_language": SettingDef( 

1306 str, 

1307 nullable=False, 

1308 group=SettingGroup.RETRIEVAL, 

1309 help_text="Stemmer/stop-word language for BM25 indexes (rebuild to apply)", 

1310 ), 

1311 "embed_titles": SettingDef( 

1312 bool, 

1313 nullable=False, 

1314 group=SettingGroup.RETRIEVAL, 

1315 help_text="Prefix document titles to chunk embeddings (rebuild to apply)", 

1316 ), 

1317 "contextual_enrichment": SettingDef( 

1318 bool, 

1319 nullable=False, 

1320 group=SettingGroup.RETRIEVAL, 

1321 help_text="LLM context sentence per chunk embedding (slow ingest; rebuild to apply)", 

1322 ), 

1323 "history_rewrite": SettingDef( 

1324 bool, 

1325 nullable=False, 

1326 group=SettingGroup.RETRIEVAL, 

1327 help_text=( 

1328 "Rewrite a follow-up that refers to earlier turns into a standalone " 

1329 "retrieval query (one extra chat-model call on those turns)" 

1330 ), 

1331 ), 

1332 "intent_routing": SettingDef( 

1333 bool, 

1334 nullable=False, 

1335 group=SettingGroup.RETRIEVAL, 

1336 help_text="Route document-name lookups to exact retrieval, count questions to a scan", 

1337 ), 

1338 "intent_llm": SettingDef( 

1339 bool, 

1340 nullable=False, 

1341 group=SettingGroup.RETRIEVAL, 

1342 help_text=( 

1343 "Classify count questions with the chat model when the fast patterns " 

1344 "miss (covers phrasing variants and other languages; adds one short " 

1345 "LLM call to those turns)" 

1346 ), 

1347 ), 

1348 "ann_index_threshold": SettingDef( 

1349 int, 

1350 nullable=False, 

1351 group=SettingGroup.RETRIEVAL, 

1352 help_text="Chunk count to start building an ANN vector index (0 = always flat search)", 

1353 advanced=True, 

1354 ), 

1355 "max_distance": SettingDef( 

1356 float, 

1357 nullable=False, 

1358 group=SettingGroup.RETRIEVAL, 

1359 help_text="Maximum vector distance for retrieval matches (lower = stricter)", 

1360 ), 

1361 "min_relevance_score": SettingDef( 

1362 float, 

1363 nullable=False, 

1364 group=SettingGroup.RETRIEVAL, 

1365 help_text="Minimum RRF relevance score for hybrid search results (0.0 = no filter)", 

1366 advanced=True, 

1367 ), 

1368 "max_context_sources": SettingDef( 

1369 int, 

1370 nullable=False, 

1371 group=SettingGroup.RETRIEVAL, 

1372 help_text="Maximum unique sources contributing chunks to a single answer", 

1373 ), 

1374 "neighbor_expansion": SettingDef( 

1375 int, 

1376 nullable=False, 

1377 group=SettingGroup.RETRIEVAL, 

1378 help_text="Adjacent chunks merged into each retrieved passage per side (0 = off)", 

1379 advanced=True, 

1380 ), 

1381 "diversity_max_per_source": SettingDef( 

1382 int, 

1383 nullable=False, 

1384 group=SettingGroup.RETRIEVAL, 

1385 help_text=( 

1386 "Maximum chunks accepted from any one source (caps source dominance). " 

1387 "A query that names one document is not capped." 

1388 ), 

1389 advanced=True, 

1390 ), 

1391 "mmr_lambda": SettingDef( 

1392 float, 

1393 nullable=False, 

1394 group=SettingGroup.RETRIEVAL, 

1395 help_text=( 

1396 "MMR lambda balancing relevance vs diversity (0 = max diversity, 1 = max relevance)" 

1397 ), 

1398 advanced=True, 

1399 ), 

1400 "temporal_filtering": SettingDef( 

1401 bool, 

1402 nullable=False, 

1403 group=SettingGroup.RETRIEVAL, 

1404 help_text="Detect temporal queries and bias retrieval toward recent chunks", 

1405 ), 

1406 "hyde": SettingDef( 

1407 bool, 

1408 nullable=False, 

1409 group=SettingGroup.RETRIEVAL, 

1410 help_text="Use HyDE (hypothetical answer expansion) to broaden retrieval", 

1411 ), 

1412 "hyde_weight": SettingDef( 

1413 float, 

1414 nullable=False, 

1415 group=SettingGroup.RETRIEVAL, 

1416 help_text="Weight on the HyDE-generated query vector when blending with the original", 

1417 advanced=True, 

1418 ), 

1419 "query_expansion_count": SettingDef( 

1420 int, 

1421 nullable=False, 

1422 group=SettingGroup.RETRIEVAL, 

1423 help_text="Number of paraphrase expansions per query (0 disables expansion)", 

1424 advanced=True, 

1425 ), 

1426 "expansion_similarity_threshold": SettingDef( 

1427 float, 

1428 nullable=False, 

1429 group=SettingGroup.RETRIEVAL, 

1430 help_text="Minimum cosine similarity an expansion must keep with the original query", 

1431 advanced=True, 

1432 ), 

1433 "expansion_short_query_tokens": SettingDef( 

1434 int, 

1435 nullable=False, 

1436 group=SettingGroup.RETRIEVAL, 

1437 help_text="Queries at or below this token count skip expansion (saves a model call)", 

1438 advanced=True, 

1439 ), 

1440 "expansion_guardrails": SettingDef( 

1441 bool, 

1442 nullable=False, 

1443 group=SettingGroup.RETRIEVAL, 

1444 help_text="Drop expansions that diverge from the original intent", 

1445 advanced=True, 

1446 ), 

1447 "adaptive_threshold": SettingDef( 

1448 bool, 

1449 nullable=False, 

1450 group=SettingGroup.RETRIEVAL, 

1451 help_text="Widen the distance cutoff when too few results pass (vector-only fallback path)", 

1452 ), 

1453 "adaptive_threshold_step": SettingDef( 

1454 float, 

1455 nullable=False, 

1456 group=SettingGroup.RETRIEVAL, 

1457 help_text="Step size for adaptive relevance-score relaxation when initial recall is empty", 

1458 advanced=True, 

1459 ), 

1460 "concept_graph": SettingDef( 

1461 bool, 

1462 nullable=False, 

1463 group=SettingGroup.RETRIEVAL, 

1464 help_text="Boost retrieval scores for chunks that share concepts with the query", 

1465 ), 

1466 "concept_boost_weight": SettingDef( 

1467 float, 

1468 nullable=False, 

1469 group=SettingGroup.RETRIEVAL, 

1470 help_text="Maximum boost (0-1) the concept graph can add to a chunk's relevance", 

1471 advanced=True, 

1472 ), 

1473 "concept_max_per_chunk": SettingDef( 

1474 int, 

1475 nullable=False, 

1476 group=SettingGroup.RETRIEVAL, 

1477 help_text="Maximum concept tags stored per chunk (caps graph density)", 

1478 advanced=True, 

1479 ), 

1480 "documents_dir": SettingDef( 

1481 str, 

1482 nullable=False, 

1483 group=SettingGroup.SYSTEM, 

1484 help_text="Local documents root that lilbee sync ingests (blank = data_root/documents)", 

1485 ), 

1486 "vault_base": SettingDef( 

1487 str, 

1488 nullable=True, 

1489 group=SettingGroup.SYSTEM, 

1490 help_text="Markdown vault root; results carry a vault-relative path (blank = none)", 

1491 ), 

1492 "sse_heartbeat_interval": SettingDef( 

1493 float, 

1494 nullable=False, 

1495 group=SettingGroup.SYSTEM, 

1496 help_text="Seconds between SSE keep-alive frames sent to idle HTTP stream clients", 

1497 hidden=True, 

1498 ), 

1499 "llm_provider": SettingDef( 

1500 str, 

1501 nullable=False, 

1502 group=SettingGroup.API_KEYS, 

1503 help_text=( 

1504 "Inference provider: auto (default, runs models locally on llama-server) " 

1505 "or remote (external OpenAI-compatible endpoint)" 

1506 ), 

1507 ), 

1508 "ollama_base_url": SettingDef( 

1509 str, 

1510 nullable=False, 

1511 group=SettingGroup.LOCAL_SERVERS, 

1512 help_text="Ollama server URL (blank uses http://localhost:11434)", 

1513 ), 

1514 "lm_studio_base_url": SettingDef( 

1515 str, 

1516 nullable=False, 

1517 group=SettingGroup.LOCAL_SERVERS, 

1518 help_text="LM Studio server URL (blank uses http://localhost:1234/v1)", 

1519 ), 

1520 "llama_server_path": SettingDef( 

1521 str, 

1522 nullable=False, 

1523 group=SettingGroup.API_KEYS, 

1524 help_text="Path to a llama-server binary (empty: bundled wheel or PATH)", 

1525 advanced=True, 

1526 ), 

1527 "wiki_summary_max_tokens": SettingDef( 

1528 int, 

1529 nullable=False, 

1530 group=SettingGroup.WIKI, 

1531 help_text="Maximum tokens generated per wiki page", 

1532 advanced=True, 

1533 ), 

1534 "wiki_temperature": SettingDef( 

1535 float, 

1536 nullable=False, 

1537 group=SettingGroup.WIKI, 

1538 help_text="Temperature used for wiki page synthesis (low = stay close to sources)", 

1539 advanced=True, 

1540 ), 

1541} 

1542 

1543 

1544def _fill_value_sets(settings: dict[str, SettingDef]) -> None: 

1545 """Give every setting the closed value set Config declares for it. 

1546 

1547 An explicit ``choices`` wins, for a field whose value set lives outside 

1548 its type (``theme``) and for a deliberate narrowing. 

1549 """ 

1550 for key, definition in settings.items(): 

1551 if definition.choices is not None: 

1552 continue 

1553 declared = field_value_set(key) 

1554 if declared is not None: 

1555 settings[key] = replace(definition, choices=declared) 

1556 

1557 

1558_fill_value_sets(SETTINGS_MAP)