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

26 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-04-29 19:16 +0000

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

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from enum import StrEnum 

7 

8from pydantic_core import PydanticUndefined 

9 

10from lilbee.config import ClustererBackend, KvCacheType, WikiEntityMode, cfg 

11 

12 

13class RenderStyle(StrEnum): 

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

15 

16 COMPACT = "compact" 

17 FULL = "full" 

18 LIST_COLLAPSED = "list_collapsed" 

19 

20 

21@dataclass(frozen=True) 

22class SettingDef: 

23 """Metadata for an interactive setting.""" 

24 

25 type: type 

26 nullable: bool 

27 writable: bool = True 

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

29 group: str = "General" 

30 help_text: str = "" 

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

32 

33 

34def get_default(key: str) -> object: 

35 """Return the cfg default for a setting key.""" 

36 field_info = type(cfg).model_fields[key] 

37 if field_info.default_factory is not None: 

38 return field_info.default_factory() # type: ignore[call-arg] 

39 if field_info.default is PydanticUndefined: 

40 return None 

41 return field_info.default 

42 

43 

44SETTINGS_MAP: dict[str, SettingDef] = { 

45 "chat_model": SettingDef( 

46 str, 

47 nullable=False, 

48 writable=False, 

49 group="Models", 

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

51 ), 

52 "vision_model": SettingDef( 

53 str, 

54 nullable=True, 

55 writable=False, 

56 group="Models", 

57 help_text="Vision model for scanned PDF OCR (empty = disabled; Tesseract only)", 

58 ), 

59 "enable_ocr": SettingDef( 

60 bool, 

61 nullable=True, 

62 group="Ingest", 

63 help_text="Vision OCR for scanned PDFs (empty = auto-detect from vision_model)", 

64 ), 

65 "ocr_timeout": SettingDef( 

66 float, 

67 nullable=False, 

68 group="Ingest", 

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

70 ), 

71 "semantic_chunking": SettingDef( 

72 bool, 

73 nullable=False, 

74 group="Ingest", 

75 help_text="Opt-in topic-aware chunker (default off; may fragment numbered procedures)", 

76 ), 

77 "topic_threshold": SettingDef( 

78 float, 

79 nullable=False, 

80 group="Ingest", 

81 help_text="Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on", 

82 ), 

83 "embedding_model": SettingDef( 

84 str, 

85 nullable=False, 

86 writable=False, 

87 group="Models", 

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

89 ), 

90 "reranker_model": SettingDef( 

91 str, 

92 nullable=True, 

93 writable=False, 

94 group="Models", 

95 help_text="Cross-encoder model for result reranking", 

96 ), 

97 "temperature": SettingDef( 

98 float, 

99 nullable=True, 

100 group="Generation", 

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

102 ), 

103 "top_p": SettingDef( 

104 float, 

105 nullable=True, 

106 group="Generation", 

107 help_text="Nucleus sampling cutoff probability", 

108 ), 

109 "top_k_sampling": SettingDef( 

110 int, 

111 nullable=True, 

112 group="Generation", 

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

114 ), 

115 "repeat_penalty": SettingDef( 

116 float, 

117 nullable=True, 

118 group="Generation", 

119 help_text="Penalty for repeating tokens", 

120 ), 

121 "num_ctx": SettingDef( 

122 int, 

123 nullable=True, 

124 group="Generation", 

125 help_text=( 

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

127 "to the host's available memory (capped at num_ctx_max)." 

128 ), 

129 ), 

130 "num_ctx_max": SettingDef( 

131 int, 

132 nullable=False, 

133 group="Generation", 

134 help_text=( 

135 "Upper bound for the dynamic context picker when num_ctx is unset. " 

136 "Higher allows more retrieval context on hosts with spare memory." 

137 ), 

138 ), 

139 "flash_attention": SettingDef( 

140 bool, 

141 nullable=True, 

142 group="Generation", 

143 help_text=( 

144 "Flash attention. Empty (auto) tries it on with a fallback for older " 

145 "llama-cpp-python builds; resolves the V-cache padding warning on " 

146 "models with uneven per-layer V dims." 

147 ), 

148 ), 

149 "kv_cache_type": SettingDef( 

150 str, 

151 nullable=False, 

152 group="Generation", 

153 help_text=( 

154 "KV cache element type. q8_0 / q4_0 halve or quarter cache memory " 

155 "but require flash attention to be enabled." 

156 ), 

157 choices=tuple(t.value for t in KvCacheType), 

158 ), 

159 "n_gpu_layers": SettingDef( 

160 int, 

161 nullable=True, 

162 group="Generation", 

163 help_text=( 

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

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

166 ), 

167 ), 

168 "seed": SettingDef( 

169 int, 

170 nullable=True, 

171 group="Generation", 

172 help_text="Random seed for reproducible output", 

173 ), 

174 "system_prompt": SettingDef( 

175 str, 

176 nullable=False, 

177 render=RenderStyle.FULL, 

178 group="Generation", 

179 help_text="System prompt sent before every conversation", 

180 ), 

181 "top_k": SettingDef( 

182 int, 

183 nullable=False, 

184 group="Retrieval", 

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

186 ), 

187 "rerank_candidates": SettingDef( 

188 int, 

189 nullable=False, 

190 group="Retrieval", 

191 help_text="Candidate pool size for reranking", 

192 ), 

193 "show_reasoning": SettingDef( 

194 bool, 

195 nullable=False, 

196 group="Display", 

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

198 ), 

199 "theme": SettingDef( 

200 str, 

201 nullable=False, 

202 group="Display", 

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

204 ), 

205 "wiki": SettingDef( 

206 bool, 

207 nullable=False, 

208 group="Wiki", 

209 help_text="Enable the wiki layer (synthesis pages with citations)", 

210 ), 

211 "wiki_dir": SettingDef( 

212 str, 

213 nullable=False, 

214 group="Wiki", 

215 help_text="Directory under data_root where wiki pages are stored", 

216 ), 

217 "wiki_prune_raw": SettingDef( 

218 bool, 

219 nullable=False, 

220 group="Wiki", 

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

222 ), 

223 "wiki_embedding_faithfulness_threshold": SettingDef( 

224 float, 

225 nullable=False, 

226 group="Wiki", 

227 help_text=( 

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

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

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

231 ), 

232 ), 

233 "wiki_stale_citation_threshold": SettingDef( 

234 float, 

235 nullable=False, 

236 group="Wiki", 

237 help_text="Fraction of stale citations that triggers page regeneration", 

238 ), 

239 "wiki_drift_threshold": SettingDef( 

240 float, 

241 nullable=False, 

242 group="Wiki", 

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

244 ), 

245 "wiki_clusterer": SettingDef( 

246 str, 

247 nullable=False, 

248 group="Wiki", 

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

250 choices=tuple(b.value for b in ClustererBackend), 

251 ), 

252 "wiki_entity_mode": SettingDef( 

253 str, 

254 nullable=False, 

255 group="Wiki", 

256 help_text=( 

257 "Entity extraction strategy " 

258 "(ner_entities = default, typed NER entities; " 

259 "plus_llm_types = NER + LLM-proposed schema; " 

260 "llm_tagged = LLM tags every chunk)" 

261 ), 

262 choices=tuple(m.value for m in WikiEntityMode), 

263 ), 

264 "wiki_entity_min_mentions": SettingDef( 

265 int, 

266 nullable=False, 

267 group="Wiki", 

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

269 ), 

270 "wiki_concept_max_chunks_per_page": SettingDef( 

271 int, 

272 nullable=False, 

273 group="Wiki", 

274 help_text="Maximum chunks passed into each concept or entity page generation call", 

275 ), 

276 "wiki_related_max": SettingDef( 

277 int, 

278 nullable=False, 

279 group="Wiki", 

280 help_text="Maximum related concepts listed in the `## Related` section of each page", 

281 ), 

282 "wiki_ingest_update_cap": SettingDef( 

283 int, 

284 nullable=False, 

285 group="Wiki", 

286 help_text=( 

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

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

289 ), 

290 ), 

291 "wiki_summary_prompt": SettingDef( 

292 str, 

293 nullable=False, 

294 render=RenderStyle.FULL, 

295 group="Wiki", 

296 help_text=( 

297 "Prompt for per-source summary pages. " 

298 "Must keep the {source_name} and {chunks_text} placeholders." 

299 ), 

300 ), 

301 "wiki_synthesis_prompt": SettingDef( 

302 str, 

303 nullable=False, 

304 render=RenderStyle.FULL, 

305 group="Wiki", 

306 help_text=( 

307 "Prompt for cross-source synthesis pages. " 

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

309 ), 

310 ), 

311 "wiki_entity_batch_prompt": SettingDef( 

312 str, 

313 nullable=False, 

314 render=RenderStyle.FULL, 

315 group="Wiki", 

316 help_text=( 

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

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

319 ), 

320 ), 

321 "wiki_extract_concepts": SettingDef( 

322 bool, 

323 nullable=False, 

324 group="Wiki", 

325 help_text=( 

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

327 "alongside the pre-extracted entity list." 

328 ), 

329 ), 

330 "wiki_batch_min_chunks": SettingDef( 

331 int, 

332 nullable=False, 

333 group="Wiki", 

334 help_text=( 

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

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

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

338 ), 

339 ), 

340 "wiki_clusterer_k": SettingDef( 

341 int, 

342 nullable=False, 

343 group="Wiki", 

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

345 ), 

346 "crawl_max_depth": SettingDef( 

347 int, 

348 nullable=True, 

349 group="Crawling", 

350 help_text="Optional recursion-depth cap (blank = no cap; per-crawl values win)", 

351 ), 

352 "crawl_max_pages": SettingDef( 

353 int, 

354 nullable=True, 

355 group="Crawling", 

356 help_text="Optional global cap on total pages per crawl (blank = no cap).", 

357 ), 

358 "crawl_timeout": SettingDef( 

359 int, 

360 nullable=False, 

361 group="Crawling", 

362 help_text="Per-page fetch timeout in seconds", 

363 ), 

364 "crawl_sync_interval": SettingDef( 

365 int, 

366 nullable=False, 

367 group="Crawling", 

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

369 ), 

370 "crawl_mean_delay": SettingDef( 

371 float, 

372 nullable=False, 

373 group="Crawling", 

374 help_text="Seconds between in-flight requests within a single crawl", 

375 ), 

376 "crawl_max_delay_range": SettingDef( 

377 float, 

378 nullable=False, 

379 group="Crawling", 

380 help_text="Random jitter (seconds) added on top of mean delay", 

381 ), 

382 "crawl_concurrent_requests": SettingDef( 

383 int, 

384 nullable=False, 

385 group="Crawling", 

386 help_text="Concurrent in-flight URLs within one crawl", 

387 ), 

388 "crawl_retry_on_rate_limit": SettingDef( 

389 bool, 

390 nullable=False, 

391 group="Crawling", 

392 help_text="Enable per-domain backoff and retries on HTTP 429/503", 

393 ), 

394 "crawl_retry_base_delay_min": SettingDef( 

395 float, 

396 nullable=False, 

397 group="Crawling", 

398 help_text="Minimum base-delay (seconds) on rate-limit responses", 

399 ), 

400 "crawl_retry_base_delay_max": SettingDef( 

401 float, 

402 nullable=False, 

403 group="Crawling", 

404 help_text="Maximum base-delay (seconds) on rate-limit responses", 

405 ), 

406 "crawl_retry_max_backoff": SettingDef( 

407 float, 

408 nullable=False, 

409 group="Crawling", 

410 help_text="Upper bound on any single backoff wait (seconds)", 

411 ), 

412 "crawl_retry_max_attempts": SettingDef( 

413 int, 

414 nullable=False, 

415 group="Crawling", 

416 help_text="Retry count per URL when a rate-limit code comes back", 

417 ), 

418 "crawl_exclude_patterns": SettingDef( 

419 list, 

420 nullable=False, 

421 group="Crawling", 

422 render=RenderStyle.LIST_COLLAPSED, 

423 help_text=( 

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

425 "recursive crawls. One per line." 

426 ), 

427 ), 

428 "openai_api_key": SettingDef( 

429 str, 

430 nullable=False, 

431 group="API-Keys", 

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

433 ), 

434 "anthropic_api_key": SettingDef( 

435 str, 

436 nullable=False, 

437 group="API-Keys", 

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

439 ), 

440 "gemini_api_key": SettingDef( 

441 str, 

442 nullable=False, 

443 group="API-Keys", 

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

445 ), 

446}