Coverage for src/lilbee/core/config/defaults.py: 100%

56 statements  

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

1"""Default values and constants for :mod:`lilbee.core.config`. 

2 

3Holds frozen literal data: the config file name, directory ignore lists, the 

4NER label allow-list, LanceDB table names, the default HTTP timeout and context 

5size, the crawl URL exclusion patterns (grouped per category), the default RAG 

6and general system prompts, and the CORS allow-origin regex. 

7""" 

8 

9from __future__ import annotations 

10 

11from typing import Any 

12from urllib.parse import quote 

13 

14from lilbee.core.config import mediawiki_namespaces as wiki_namespaces 

15 

16# Upper bounds for the numbers lilbee passes to the crawl engine, shared by every entry point. 

17MAX_CRAWL_DEPTH = 100 

18MAX_CRAWL_PAGES = 10_000_000 

19MAX_CRAWL_WAIT_SECONDS = 3600.0 

20MAX_CRAWL_CONCURRENCY = 256 

21 

22DEFAULT_IGNORE_DIRS = frozenset( 

23 { 

24 "node_modules", 

25 "__pycache__", 

26 "venv", 

27 "build", 

28 "dist", 

29 "target", 

30 "vendor", 

31 "_build", 

32 "coverage", 

33 "htmlcov", 

34 } 

35) 

36 

37# spaCy NER labels that map onto something wiki-shaped. Excludes 

38# QUANTITY / ORDINAL / CARDINAL / DATE / TIME / MONEY / PERCENT / 

39# LANGUAGE / LAW because pages for "42" or "2021" are never useful, and 

40# NORP (nationalities / political / religious groups) because its surfaces 

41# are adjectival (Saturnian, American) and make poor page subjects; opt 

42# back in via the config override. FAC (buildings / airports) stays: 

43# corpora routinely surface them as wiki-worthy topics. 

44DEFAULT_ALLOWED_NER_LABELS = frozenset( 

45 {"PERSON", "ORG", "GPE", "LOC", "EVENT", "WORK_OF_ART", "PRODUCT", "FAC"} 

46) 

47 

48# Timeout for backend catalog / management HTTP calls. 

49DEFAULT_HTTP_TIMEOUT = 30.0 

50 

51# Safe default + cap for chat-mode n_ctx; full 128K+ training contexts OOM laptops. 

52DEFAULT_NUM_CTX = 8192 

53 

54# Idle minutes before the engine unloads its weights; also read by the engine reaper. 

55ENGINE_IDLE_TTL_MINUTES_DEFAULT = 5 

56CONFIG_FILE_NAME = "config.toml" 

57ENV_PREFIX = "LILBEE_" 

58SKIP_TOML_ENV = "LILBEE_SKIP_TOML_CONFIG" 

59 

60CHUNKS_TABLE = "chunks" 

61SOURCES_TABLE = "_sources" 

62CITATIONS_TABLE = "_citations" 

63MEMORIES_TABLE = "_memories" 

64# Which file each crawled page owns; kept, like memories, through a rebuild. 

65CRAWL_PAGES_TABLE = "_crawl_pages" 

66META_TABLE = "_meta" 

67PAGE_TEXTS_TABLE = "_page_texts" 

68CONCEPT_NODES_TABLE = "concept_nodes" 

69CONCEPT_EDGES_TABLE = "concept_edges" 

70CHUNK_CONCEPTS_TABLE = "chunk_concepts" 

71ENTITIES_TABLE = "entities" 

72ENTITY_SCHEMA_TABLE = "_entity_schema" 

73# Per-(subject, source) wiki mention evidence. The wiki stub index is a 

74# corpus-wide aggregate over this table, so a subject named below the floor in 

75# each separately-synced source still crosses it once its rows are all present. 

76WIKI_MENTIONS_TABLE = "_wiki_mentions" 

77 

78# Default URL-exclusion regexes for recursive crawls. Grouped by source 

79# CMS / category. User overrides come from LILBEE_CRAWL_EXCLUDE_PATTERNS 

80# (newline-separated) or config.toml. 

81 

82# WordPress scaffolding: admin UIs, APIs, RPC, numeric permalinks, Elementor. 

83_WP_EXCLUDE: tuple[str, ...] = ( 

84 r"/wp-admin/", 

85 r"/wp-login(\.php)?", 

86 r"/wp-json/", 

87 r"/xmlrpc\.php", 

88 r"/wp-cron\.php", 

89 r"/wp-includes/", 

90 r"/wp-content/uploads/", 

91 r"\?p=\d+", 

92 r"\?page_id=\d+", 

93 r"\?cat=\d+", 

94 r"/elementor-\d+", 

95 r"\?elementor_library", 

96) 

97 

98# Pagination and archive permalinks (WP + other CMSes share this shape). 

99_ARCHIVE_EXCLUDE: tuple[str, ...] = ( 

100 r"/page/\d+/?$", 

101 r"\?paged?=\d+", 

102 r"/20\d{2}(/\d{2}(/\d{2})?)?/?$", 

103 r"/tag/", 

104 r"/category/", 

105 r"/author/", 

106 r"/archives?/?$", 

107 r"/comment-page-\d+", 

108) 

109 

110# Syndication feeds (content-duplicated in HTML pages). 

111_FEED_EXCLUDE: tuple[str, ...] = ( 

112 r"/feed/?$", 

113 r"/feed/atom/?$", 

114 r"/feed/rdf/?$", 

115 r"/comments/feed/?$", 

116 r"/rss/?$", 

117) 

118 

119# Duplicate views of the same canonical page (AMP, print, preview). 

120_DUPLICATE_VIEW_EXCLUDE: tuple[str, ...] = ( 

121 r"/amp/?$", 

122 r"\?amp=", 

123 r"\?print=", 

124 r"/print/?$", 

125 r"\?preview=", 

126) 

127 

128# WP attachment URLs (point at media, not content pages). 

129_ATTACHMENT_EXCLUDE: tuple[str, ...] = ( 

130 r"/attachment/", 

131 r"\?attachment_id=", 

132) 

133 

134# Regexes against the whole URL, not globs. A generic name matches only as the 

135# whole first path segment of a site: /cart and /cart/items, but neither 

136# /cartography nor /docs/payments/cart. 

137_SITE_ROOT = r"^https?://[^/?#]+" 

138_PATH_BOUNDARY = r"(?:/|\?|#|$)" 

139 

140 

141def _first_segments(*paths: str) -> tuple[str, ...]: 

142 """Anchor each path so it matches only the whole first path segment of a site.""" 

143 return tuple(_SITE_ROOT + path + _PATH_BOUNDARY for path in paths) 

144 

145 

146# Auth and account flows (generic across CMSes and e-commerce platforms). 

147_AUTH_EXCLUDE: tuple[str, ...] = _first_segments( 

148 r"/login", 

149 r"/logout", 

150 r"/register", 

151 r"/signup", 

152 r"/signin", 

153 r"/account", 

154 r"/profile", 

155 r"/password-reset", 

156 r"/forgot-password", 

157) 

158_AUTH_EXCLUDE = (*_AUTH_EXCLUDE, r"/my-account/") 

159 

160# E-commerce transactional flows (cart / checkout / compare / etc.). 

161_ECOMMERCE_EXCLUDE: tuple[str, ...] = _first_segments( 

162 r"/cart", 

163 r"/checkout", 

164 r"/wishlist", 

165 r"/orders?", 

166 r"/compare", 

167) 

168_ECOMMERCE_EXCLUDE = ( 

169 *_ECOMMERCE_EXCLUDE, 

170 r"/products\.json", 

171 r"/collections/.+/products/.+\?page=", 

172) 

173 

174# Site-meta URLs and non-HTML resources; skipped before fetch. 

175_META_EXCLUDE: tuple[str, ...] = ( 

176 r"/sitemap[^/]*\.xml", 

177 r"/robots\.txt", 

178 r"/humans\.txt", 

179 r"/favicon\.ico", 

180 r"/\.well-known/", 

181 r"\.(jpe?g|png|gif|webp|avif|svg|ico|pdf|docx?|xlsx?|pptx?|zip|tar|gz|mp3|mp4|webm|ogg|ttf|woff2?|css|js|map|json|xml)(\?.*)?$", 

182) 

183 

184 

185def _namespace_pattern(names: tuple[str, ...]) -> str: 

186 """The pattern for a wiki address in one of the namespaces *names*, in any language. 

187 

188 crawlberg matches a pattern against the address with every character outside ASCII 

189 percent-encoded as UTF-8, so the pattern holds each name in that form. 

190 """ 

191 return rf"/wiki/(?:{'|'.join(dict.fromkeys(quote(name) for name in names))}):" 

192 

193 

194# Mediawiki/Wikipedia navlinks that dominate BFS before the article body. A wiki shows each 

195# namespace under its own language, so each pattern lists the names of the largest Wikipedias. 

196# A pattern needs the namespace name before the first colon: an article title that holds a 

197# colon, such as Re:Zero or Mission:_Impossible, is content and is crawled. 

198# Help: is a content namespace and is crawled. 

199_MEDIAWIKI_EXCLUDE: tuple[str, ...] = ( 

200 r"/wiki/Main_Page$", 

201 _namespace_pattern(wiki_namespaces.PROJECT), 

202 _namespace_pattern(wiki_namespaces.PORTAL), 

203 _namespace_pattern(wiki_namespaces.SPECIAL), 

204 _namespace_pattern(wiki_namespaces.CATEGORY), 

205 _namespace_pattern(wiki_namespaces.TEMPLATE), 

206 _namespace_pattern(wiki_namespaces.TEMPLATE_TALK), 

207 _namespace_pattern(wiki_namespaces.TALK), 

208 _namespace_pattern(wiki_namespaces.FILE), 

209 _namespace_pattern(wiki_namespaces.FILE_TALK), 

210 _namespace_pattern(wiki_namespaces.USER), 

211 _namespace_pattern(wiki_namespaces.USER_TALK), 

212 r"/w/index\.php", 

213) 

214 

215 

216def exclude_pattern_lines(value: Any) -> Any: 

217 """The patterns in *value*: a string is one pattern per line, any other value passes through. 

218 

219 Regex uses commas (``{2,4}``) and pipes (alternation), so a newline is the only separator. 

220 """ 

221 if isinstance(value, str): 

222 return [line.strip() for line in value.splitlines() if line.strip()] 

223 return value 

224 

225 

226DEFAULT_CRAWL_EXCLUDE_PATTERNS: tuple[str, ...] = ( 

227 *_WP_EXCLUDE, 

228 *_ARCHIVE_EXCLUDE, 

229 *_FEED_EXCLUDE, 

230 *_DUPLICATE_VIEW_EXCLUDE, 

231 *_ATTACHMENT_EXCLUDE, 

232 *_AUTH_EXCLUDE, 

233 *_ECOMMERCE_EXCLUDE, 

234 *_META_EXCLUDE, 

235 *_MEDIAWIKI_EXCLUDE, 

236) 

237 

238 

239DEFAULT_RAG_SYSTEM_PROMPT = ( 

240 "You are a precise assistant answering from the user's own documents. " 

241 "Ground every claim in the numbered context passages and nothing else; if " 

242 "they don't cover the question, say so plainly instead of guessing or " 

243 "answering from general knowledge. Synthesize across passages rather than " 

244 "leaning on one, and if they disagree, note the conflict. Cite inline by " 

245 "placing the passage number in brackets right after the claim it supports " 

246 "(e.g. [1] or [2][5]), and cite only passages you actually used. Do not " 

247 "write a Sources, References, or Bibliography list at the end; the app adds " 

248 "the real source list for you. Prefer exact values, names, and short quotes " 

249 "from the context over paraphrase. Handle any material: prose, notes, " 

250 "tables, transcripts, or code; for code, prefer a working example. When " 

251 "asked how to do something, lay the answer out as ordered steps; if the " 

252 "context covers the procedure only partially, give the steps it contains " 

253 "and name what's missing rather than glossing over the gap. Match the " 

254 "answer's length to the question: exhaustive requests deserve every " 

255 "relevant detail the context offers." 

256) 

257 

258DEFAULT_GENERAL_SYSTEM_PROMPT = ( 

259 "You are a helpful, direct assistant. Answer the user's question from " 

260 "general knowledge. Keep responses concise unless asked to elaborate. " 

261 "For code, prefer working examples over abstract explanations." 

262) 

263 

264# CORS allow-origin regex: Obsidian (desktop + iOS) and localhost loopback. 

265# Mutating endpoints still require auth regardless of origin. 

266DEFAULT_CORS_ORIGIN_REGEX = ( 

267 r"^(app://obsidian\.md" 

268 r"|capacitor://localhost" 

269 r"|https?://localhost(:\d+)?" 

270 r"|https?://127\.0\.0\.1(:\d+)?" 

271 r"|https?://\[::1\](:\d+)?)$" 

272) 

273 

274 

275def env_var_name(field: str) -> str: 

276 """The ``LILBEE_<FIELD>`` environment variable that sets a config field.""" 

277 return f"{ENV_PREFIX}{field.upper()}"