ltcai 6.0.0 β†’ 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +33 -35
  2. package/docs/CHANGELOG.md +68 -0
  3. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
  4. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
  5. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
  6. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
  7. package/frontend/src/App.tsx +3 -1281
  8. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  9. package/frontend/src/components/ProductFlow.tsx +32 -662
  10. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  12. package/frontend/src/features/brain/BrainHome.tsx +999 -0
  13. package/frontend/src/features/brain/brainData.ts +98 -0
  14. package/frontend/src/features/brain/graphLayout.ts +26 -0
  15. package/frontend/src/features/brain/types.ts +44 -0
  16. package/frontend/src/features/review/ReviewCard.tsx +15 -10
  17. package/frontend/src/i18n.ts +208 -0
  18. package/frontend/src/styles.css +240 -0
  19. package/lattice_brain/__init__.py +1 -1
  20. package/lattice_brain/runtime/multi_agent.py +1 -1
  21. package/latticeai/__init__.py +1 -1
  22. package/latticeai/api/chat.py +52 -33
  23. package/latticeai/api/tools.py +50 -23
  24. package/latticeai/app_factory.py +65 -47
  25. package/latticeai/cli/__init__.py +1 -0
  26. package/latticeai/cli/entrypoint.py +283 -0
  27. package/latticeai/cli/runtime.py +37 -0
  28. package/latticeai/core/marketplace.py +1 -1
  29. package/latticeai/core/workspace_os.py +1 -1
  30. package/latticeai/integrations/__init__.py +0 -0
  31. package/latticeai/integrations/telegram_bot.py +1009 -0
  32. package/latticeai/runtime/lifespan_runtime.py +1 -1
  33. package/latticeai/runtime/platform_runtime_wiring.py +87 -0
  34. package/latticeai/runtime/router_registration.py +49 -30
  35. package/latticeai/services/app_context.py +1 -0
  36. package/latticeai/services/p_reinforce.py +258 -0
  37. package/latticeai/services/router_context.py +52 -0
  38. package/latticeai/services/tool_dispatch.py +82 -25
  39. package/ltcai_cli.py +7 -305
  40. package/p_reinforce.py +4 -255
  41. package/package.json +2 -1
  42. package/scripts/release_smoke.py +133 -0
  43. package/scripts/wheel_smoke.py +4 -0
  44. package/src-tauri/Cargo.lock +1 -1
  45. package/src-tauri/Cargo.toml +1 -1
  46. package/src-tauri/tauri.conf.json +1 -1
  47. package/static/app/asset-manifest.json +5 -5
  48. package/static/app/assets/{index-xRn29gI8.css β†’ index-B2-1Gm0q.css} +1 -1
  49. package/static/app/assets/index-D91Rz5--.js +16 -0
  50. package/static/app/assets/index-D91Rz5--.js.map +1 -0
  51. package/static/app/index.html +2 -2
  52. package/telegram_bot.py +9 -1008
  53. package/static/app/assets/index-D2zafMYb.js +0 -16
  54. package/static/app/assets/index-D2zafMYb.js.map +0 -1
@@ -102,7 +102,7 @@ def build_lifespan_runtime(
102
102
  try:
103
103
  print(f"🧭 Lattice AI mode: {app_mode}")
104
104
  if enable_telegram:
105
- from telegram_bot import run_bot
105
+ from latticeai.integrations.telegram_bot import run_bot
106
106
 
107
107
  spawn(run_bot(), name="telegram_bot")
108
108
  print("πŸš€ Telegram Bot Bridge activated!")
@@ -0,0 +1,87 @@
1
+ """Platform and automation wiring for the application factory.
2
+
3
+ This module owns the cross-subsystem runtime assembly that used to live inline
4
+ inside ``app_factory._build``. Keeping it here reduces the app factory's global
5
+ surface without changing router order or the legacy exported runtime names.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable, Dict
11
+
12
+ from latticeai.runtime.automation_runtime import build_automation_runtime
13
+ from latticeai.services.platform_runtime import PlatformRuntime
14
+
15
+
16
+ def build_platform_automation_runtime(
17
+ *,
18
+ model_router: Any,
19
+ workspace_store: Any,
20
+ workspace_service: Any,
21
+ plugin_registry: Any,
22
+ get_current_user: Callable[..., Any],
23
+ workspace_graph: Callable[[], Any],
24
+ workspace_scope_from_request: Callable[..., Any],
25
+ get_tool_permission: Callable[..., Any],
26
+ hooks: Any,
27
+ agent_registry: Any,
28
+ data_dir: Any,
29
+ append_audit_event: Callable[..., Any],
30
+ ) -> Dict[str, Any]:
31
+ """Build platform services, automation services, and hook bindings.
32
+
33
+ The returned names intentionally match the historical local variables in
34
+ ``app_factory._build`` so ``dict(locals())`` continues to expose the legacy
35
+ ``server_app`` compatibility surface.
36
+ """
37
+
38
+ def _llm_generate_sync(
39
+ message: str,
40
+ context: str = "",
41
+ max_tokens: int = 1024,
42
+ temperature: float = 0.1,
43
+ ) -> str:
44
+ # Synchronous model bridge for the orchestrator's role runner. Safe
45
+ # because the agents run endpoint executes start() in a worker thread
46
+ # (asyncio.to_thread), where no event loop is running.
47
+ import asyncio as _asyncio
48
+
49
+ return str(_asyncio.run(model_router.generate(
50
+ message,
51
+ context=context,
52
+ max_tokens=max_tokens,
53
+ temperature=temperature,
54
+ )))
55
+
56
+ platform = PlatformRuntime(
57
+ store=workspace_store,
58
+ workspace_service=workspace_service,
59
+ plugin_registry=plugin_registry,
60
+ get_current_user=get_current_user,
61
+ workspace_graph=workspace_graph,
62
+ workspace_scope_from_request=workspace_scope_from_request,
63
+ get_tool_permission=get_tool_permission,
64
+ hooks=hooks,
65
+ llm_generate=_llm_generate_sync,
66
+ llm_available=lambda: bool(getattr(model_router, "current_model_id", None)),
67
+ agent_registry=agent_registry,
68
+ )
69
+
70
+ automation_runtime = build_automation_runtime(
71
+ store=workspace_store,
72
+ platform=platform,
73
+ data_dir=data_dir,
74
+ workspace_graph=workspace_graph,
75
+ append_audit_event=append_audit_event,
76
+ hooks=hooks,
77
+ )
78
+
79
+ return {
80
+ "_llm_generate_sync": _llm_generate_sync,
81
+ "PLATFORM": platform,
82
+ "_automation_runtime": automation_runtime,
83
+ "REVIEW_QUEUE": automation_runtime["REVIEW_QUEUE"],
84
+ "TRIGGER_SERVICE": automation_runtime["TRIGGER_SERVICE"],
85
+ "AGENT_RUNTIME": automation_runtime["AGENT_RUNTIME"],
86
+ "RUN_EXECUTOR": automation_runtime["RUN_EXECUTOR"],
87
+ }
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Any
12
12
 
13
+ from latticeai.services.router_context import InteractionRouterContext
14
+
13
15
 
14
16
  def build_static_routes_bundle(
15
17
  *,
@@ -412,45 +414,61 @@ def register_health_and_model_routers(
412
414
  def register_interaction_routers(
413
415
  app: Any,
414
416
  *,
417
+ interaction_context: InteractionRouterContext | None = None,
415
418
  create_chat_router: Any,
416
- context: Any,
419
+ context: Any = None,
417
420
  create_search_router: Any,
418
- search_service: Any,
419
- allowed_workspaces_for: Any,
420
- require_user: Any,
421
- embedding_info: Any,
421
+ search_service: Any = None,
422
+ allowed_workspaces_for: Any = None,
423
+ require_user: Any = None,
424
+ embedding_info: Any = None,
422
425
  create_tools_router: Any,
423
- ingestion_pipeline: Any,
424
- config: Any,
425
- data_dir: Any,
426
- static_dir: Any,
427
- model_router: Any,
428
- require_admin: Any,
429
- get_current_user: Any,
430
- clear_history: Any,
431
- append_audit_event: Any,
432
- enforce_rate_limit: Any,
433
- bytes_match_extension: Any,
434
- classify_sensitive_message: Any,
435
- save_to_history: Any,
436
- enable_graph: bool,
437
- knowledge_graph: Any,
438
- require_graph: Any,
439
- local_kg_watcher: Any,
440
- load_mcp_installs: Any,
441
- recommend_mcps: Any,
442
- install_mcp: Any,
443
- mcp_public_item: Any,
444
- hooks: Any,
426
+ ingestion_pipeline: Any = None,
427
+ config: Any = None,
428
+ data_dir: Any = None,
429
+ static_dir: Any = None,
430
+ model_router: Any = None,
431
+ require_admin: Any = None,
432
+ get_current_user: Any = None,
433
+ clear_history: Any = None,
434
+ append_audit_event: Any = None,
435
+ enforce_rate_limit: Any = None,
436
+ bytes_match_extension: Any = None,
437
+ classify_sensitive_message: Any = None,
438
+ save_to_history: Any = None,
439
+ enable_graph: bool | None = None,
440
+ knowledge_graph: Any = None,
441
+ require_graph: Any = None,
442
+ local_kg_watcher: Any = None,
443
+ load_mcp_installs: Any = None,
444
+ recommend_mcps: Any = None,
445
+ install_mcp: Any = None,
446
+ mcp_public_item: Any = None,
447
+ hooks: Any = None,
445
448
  create_hooks_router: Any,
446
449
  create_agent_registry_router: Any,
447
- agent_registry: Any,
450
+ agent_registry: Any = None,
448
451
  create_memory_router: Any,
449
- memory_service: Any,
450
- platform: Any,
452
+ memory_service: Any = None,
453
+ platform: Any = None,
451
454
  ) -> tuple[Any, ...]:
452
455
  """Register chat/search/tools/hooks/registry/memory routes in order."""
453
456
 
457
+ tool_context = None
458
+ if interaction_context is not None:
459
+ context = interaction_context.chat_context
460
+ search_service = interaction_context.search_service
461
+ allowed_workspaces_for = interaction_context.allowed_workspaces_for
462
+ require_user = interaction_context.require_user
463
+ embedding_info = interaction_context.embedding_info
464
+ tool_context = interaction_context.tool_context
465
+ get_current_user = tool_context.get_current_user
466
+ append_audit_event = tool_context.append_audit_event
467
+ hooks = interaction_context.hooks
468
+ agent_registry = interaction_context.agent_registry
469
+ memory_service = interaction_context.memory_service
470
+ platform = interaction_context.platform
471
+
454
472
  return register_routers(
455
473
  app,
456
474
  create_chat_router(context),
@@ -461,6 +479,7 @@ def register_interaction_routers(
461
479
  embedding_info=embedding_info,
462
480
  ),
463
481
  create_tools_router(
482
+ tool_context=tool_context,
464
483
  ingestion_pipeline=ingestion_pipeline,
465
484
  config=config,
466
485
  data_dir=data_dir,
@@ -34,6 +34,7 @@ class AppContext:
34
34
  chat_service: Any = None
35
35
  context_assembler: Any = None
36
36
  brain_memory: Any = None
37
+ chat_agent_runtime: Any = None
37
38
  gardener: Any = None
38
39
  hooks: Any = None
39
40
  realtime_bus: Any = None
@@ -0,0 +1,258 @@
1
+ """
2
+ P-Reinforce Knowledge Gardener β€” notes capture with a brain-backed memory.
3
+
4
+ v4 (T4.3 garden absorption): the markdown vault is no longer a second brain.
5
+ The vault stays as the user-owned, Obsidian-compatible *mirror* (capability
6
+ preserved), but the Knowledge Graph is authoritative: notes created through
7
+ the API are ingested through the unified pipeline (provenance + hooks), the
8
+ existing vault is imported idempotently, and chat context comes from brain
9
+ queries instead of an O(n) vault scan per message.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import re
15
+ import shutil
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ BRAIN_DIR = Path(
21
+ os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
22
+ or os.getenv("LATTICEAI_BRAIN_DIR")
23
+ or Path.home() / ".ltcai-brain"
24
+ )
25
+
26
+ STRUCTURE = {
27
+ "10_Wiki": "κ²€μ¦λœ 지식, κ°œλ… μ„€λͺ…, 레퍼런슀",
28
+ "00_Raw": "μ •μ œλ˜μ§€ μ•Šμ€ μ›μ‹œ 데이터, 아이디어 λ©”λͺ¨",
29
+ "20_Skills": "μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ½”λ“œ μŠ€λ‹ˆνŽ«, ν”„λ‘¬ν”„νŠΈ, μ›Œν¬ν”Œλ‘œ",
30
+ "30_Projects": "ν”„λ‘œμ νŠΈλ³„ μ»¨ν…μŠ€νŠΈ, μ§„ν–‰ 상황",
31
+ "40_Log": "λ‚ μ§œλ³„ μž‘μ—… 둜그",
32
+ }
33
+
34
+
35
+ class PReinforceGardener:
36
+ def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
37
+ self._pipeline = ingestion_pipeline
38
+ self._kg = knowledge_graph
39
+ self._ensure_structure()
40
+
41
+ def _ensure_structure(self):
42
+ for folder in STRUCTURE:
43
+ (BRAIN_DIR / folder).mkdir(parents=True, exist_ok=True)
44
+ # 인덱슀 파일
45
+ index_path = BRAIN_DIR / "INDEX.md"
46
+ if not index_path.exists():
47
+ index_path.write_text(self._render_index())
48
+
49
+ def _render_index(self) -> str:
50
+ lines = ["# 🧠 Lattice AI Brain β€” P-Reinforce Index\n"]
51
+ lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
52
+ lines.append("\nThis folder is an Obsidian-compatible Markdown vault.\n")
53
+ lines.append("\nThe Knowledge Graph is the authoritative store; this vault is the\nuser-owned markdown mirror of garden notes.\n")
54
+ for folder, desc in STRUCTURE.items():
55
+ lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
56
+ lines.append("## Connector Status\n")
57
+ lines.append(f"- OCR engine: `{'tesseract' if shutil.which('tesseract') else 'not installed'}`\n")
58
+ return "\n".join(lines)
59
+
60
+ # ── Classify ──────────────────────────────────────────────────────────────
61
+
62
+ def _classify(self, text: str) -> str:
63
+ """κ°„λ‹¨ν•œ κ·œμΉ™ 기반 λΆ„λ₯˜ (LLM 없이도 λ™μž‘)"""
64
+ text_lower = text.lower()
65
+
66
+ code_signals = ["def ", "class ", "import ", "```", "function ", "const ", "let ", "var "]
67
+ if any(s in text for s in code_signals):
68
+ return "20_Skills"
69
+
70
+ wiki_signals = ["κ°œλ…", "원리", "μ΄λž€", "what is", "how does", "definition", "explanation"]
71
+ if any(s in text_lower for s in wiki_signals):
72
+ return "10_Wiki"
73
+
74
+ project_signals = ["project", "ν”„λ‘œμ νŠΈ", "todo", "task", "μž‘μ—…", "κΈ°λŠ₯", "feature"]
75
+ if any(s in text_lower for s in project_signals):
76
+ return "30_Projects"
77
+
78
+ return "00_Raw"
79
+
80
+ # ── File Naming ───────────────────────────────────────────────────────────
81
+
82
+ def _make_filename(self, text: str, folder: str) -> str:
83
+ # 첫 쀄을 제λͺ©μœΌλ‘œ
84
+ first_line = text.strip().split("\n")[0][:60]
85
+ # 파일λͺ… μ•ˆμ „ν•˜κ²Œ
86
+ safe = re.sub(r"[^\w\s-]", "", first_line).strip()
87
+ safe = re.sub(r"\s+", "_", safe)
88
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
89
+ return f"{timestamp}_{safe or 'note'}.md"
90
+
91
+ # ── Process ───────────────────────────────────────────────────────────────
92
+
93
+ async def process(self, raw_data: str, category: Optional[str] = None) -> dict:
94
+ folder = category if category in STRUCTURE else self._classify(raw_data)
95
+ filename = self._make_filename(raw_data, folder)
96
+ filepath = BRAIN_DIR / folder / filename
97
+
98
+ # λ§ˆν¬λ‹€μš΄ 미러 (μ‚¬μš©μž μ†Œμœ  Obsidian ν˜Έν™˜ μ•„ν‹°νŒ©νŠΈ)
99
+ content = self._wrap_markdown(raw_data, folder)
100
+ filepath.write_text(content, encoding="utf-8")
101
+
102
+ # 였늘 λ‘œκ·Έμ—λ„ 기둝
103
+ self._append_log(raw_data[:200], folder, filename)
104
+
105
+ result = {
106
+ "status": "saved",
107
+ "folder": folder,
108
+ "filename": filename,
109
+ "path": str(filepath),
110
+ "classified_as": folder,
111
+ "description": STRUCTURE[folder],
112
+ }
113
+ # λ‘λ‡Œ(Knowledge Graph)κ°€ 정식 μ €μž₯μ†Œ: 톡합 μˆ˜μ§‘ νŒŒμ΄ν”„λΌμΈμœΌλ‘œ ingest.
114
+ result.update(self._ingest_note(raw_data, source_uri=str(filepath), folder=folder))
115
+ return result
116
+
117
+ def _ingest_note(self, text: str, *, source_uri: str, folder: str, title: Optional[str] = None) -> dict:
118
+ if self._pipeline is None:
119
+ return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
120
+ try:
121
+ from lattice_brain.ingestion import IngestionItem
122
+
123
+ ingest = self._pipeline.ingest(
124
+ IngestionItem(
125
+ source_type="note",
126
+ title=title or text.strip().split("\n")[0][:80],
127
+ text=text,
128
+ source_uri=source_uri,
129
+ metadata={"garden_folder": folder, "pipeline": "p-reinforce"},
130
+ )
131
+ )
132
+ if ingest.status != "ok":
133
+ return {"graph": ingest.status, "graph_detail": ingest.detail}
134
+ return {
135
+ "graph": "ok",
136
+ "graph_node_id": ingest.node_id,
137
+ "provenance_id": ingest.provenance_id,
138
+ "duplicate": ingest.duplicate,
139
+ }
140
+ except Exception as exc:
141
+ logging.warning("garden note ingest failed: %s", exc)
142
+ return {"graph": "failed", "graph_detail": str(exc)}
143
+
144
+ def import_vault(self) -> dict:
145
+ """Idempotent import of every existing vault note into the brain.
146
+
147
+ Content-hash dedup in the store makes re-runs safe; vault files are
148
+ never modified or deleted. INDEX.md and the daily logs are skipped.
149
+ """
150
+ if self._pipeline is None:
151
+ return {"status": "unavailable", "imported": 0}
152
+ imported = duplicates = failed = 0
153
+ for file_path in sorted(BRAIN_DIR.rglob("*.md")):
154
+ if file_path.name == "INDEX.md" or "40_Log" in file_path.parts:
155
+ continue
156
+ try:
157
+ text = file_path.read_text(encoding="utf-8")
158
+ except Exception:
159
+ failed += 1
160
+ continue
161
+ folder = file_path.parent.name if file_path.parent != BRAIN_DIR else "00_Raw"
162
+ outcome = self._ingest_note(
163
+ text, source_uri=str(file_path), folder=folder, title=file_path.stem
164
+ )
165
+ if outcome.get("graph") == "ok":
166
+ if outcome.get("duplicate"):
167
+ duplicates += 1
168
+ else:
169
+ imported += 1
170
+ else:
171
+ failed += 1
172
+ if imported:
173
+ logging.info("garden: imported %d vault notes into the brain (%d already known)", imported, duplicates)
174
+ return {"status": "ok", "imported": imported, "duplicates": duplicates, "failed": failed}
175
+
176
+ def _wrap_markdown(self, raw: str, folder: str) -> str:
177
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
178
+ first_line = raw.strip().split("\n")[0][:80]
179
+ lines = [
180
+ f"# {first_line}",
181
+ f"\n> πŸ“ `{folder}` | πŸ• {now} | Lattice AI MLX\n",
182
+ "---\n",
183
+ raw,
184
+ "\n\n---",
185
+ "*Auto-organized by P-Reinforce Gardener*",
186
+ ]
187
+ return "\n".join(lines)
188
+
189
+ def _append_log(self, preview: str, folder: str, filename: str):
190
+ today = datetime.now().strftime("%Y-%m-%d")
191
+ log_path = BRAIN_DIR / "40_Log" / f"{today}.md"
192
+ entry = f"\n- [{datetime.now().strftime('%H:%M')}] β†’ `{folder}/{filename}`\n > {preview[:100]}\n"
193
+ with open(log_path, "a", encoding="utf-8") as f:
194
+ if log_path.stat().st_size == 0 if log_path.exists() else True:
195
+ f.write(f"# πŸ“… Log β€” {today}\n")
196
+ f.write(entry)
197
+
198
+ # ── Tree ──────────────────────────────────────────────────────────────────
199
+
200
+ def get_tree(self) -> dict:
201
+ """지식 정원 파일트리 (λ§ˆν¬λ‹€μš΄ 미러 κΈ°μ€€)."""
202
+ folders = []
203
+ for folder, desc in STRUCTURE.items():
204
+ folder_path = BRAIN_DIR / folder
205
+ files = []
206
+ if folder_path.exists():
207
+ for file_path in sorted(folder_path.glob("*.md")):
208
+ try:
209
+ stat = file_path.stat()
210
+ files.append({
211
+ "name": file_path.name,
212
+ "size_bytes": stat.st_size,
213
+ "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
214
+ })
215
+ except OSError:
216
+ continue
217
+ folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
218
+ return {"root": str(BRAIN_DIR), "folders": folders}
219
+
220
+ def get_relevant_context(self, query: str, limit: int = 3) -> str:
221
+ """질문과 κ΄€λ ¨λœ 정원 λ…ΈνŠΈλ₯Ό λ‘λ‡Œμ—μ„œ 검색해 μ»¨ν…μŠ€νŠΈλ‘œ λ°˜ν™˜.
222
+
223
+ v4: μ±„νŒ…λ§ˆλ‹€ vault 전체λ₯Ό rglob ν•˜λ˜ O(n) μŠ€μΊ”μ„ 브레인 κ²€μƒ‰μœΌλ‘œ
224
+ λŒ€μ²΄. κ·Έλž˜ν”„κ°€ μ—†μœΌλ©΄(λΉ„ν™œμ„±) κΈ°μ‘΄ 파일 μŠ€μΊ”μœΌλ‘œ μ •μ§ν•˜κ²Œ 폴백.
225
+ """
226
+ if self._kg is not None:
227
+ try:
228
+ matches = self._kg.search(query, max(limit * 4, 8)).get("matches", [])
229
+ results = []
230
+ for match in matches:
231
+ meta = match.get("metadata") or {}
232
+ if not (meta.get("garden_folder") or meta.get("pipeline") == "p-reinforce"):
233
+ continue
234
+ title = match.get("title") or "note"
235
+ body = match.get("summary") or ""
236
+ results.append(f"--- Document: {title} ---\n{body[:800]}")
237
+ if len(results) >= limit:
238
+ break
239
+ return "\n\n".join(results)
240
+ except Exception as exc:
241
+ logging.debug("garden brain context failed, falling back to vault scan: %s", exc)
242
+ return self._scan_vault_context(query, limit)
243
+
244
+ def _scan_vault_context(self, query: str, limit: int = 3) -> str:
245
+ results = []
246
+ for file_path in BRAIN_DIR.rglob("*.md"):
247
+ if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
248
+ continue
249
+ try:
250
+ content = file_path.read_text(encoding="utf-8")
251
+ keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
252
+ if any(k.lower() in content.lower() for k in keywords):
253
+ results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
254
+ if len(results) >= limit:
255
+ break
256
+ except Exception:
257
+ continue
258
+ return "\n\n".join(results)
@@ -0,0 +1,52 @@
1
+ """Typed router assembly contexts for app-factory decomposition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ToolRouterContext:
12
+ """Runtime dependencies for direct tool, upload, MCP, and KG routes."""
13
+
14
+ config: Any
15
+ ingestion_pipeline: Any
16
+ data_dir: Path
17
+ static_dir: Path
18
+ model_router: Any
19
+ require_user: Any
20
+ require_admin: Any
21
+ get_current_user: Any
22
+ clear_history: Any
23
+ append_audit_event: Any
24
+ enforce_rate_limit: Any
25
+ bytes_match_extension: Any
26
+ classify_sensitive_message: Any
27
+ save_to_history: Any
28
+ enable_graph: bool
29
+ knowledge_graph: Any
30
+ require_graph: Any
31
+ local_kg_watcher: Any
32
+ load_mcp_installs: Any
33
+ recommend_mcps: Any
34
+ install_mcp: Any
35
+ mcp_public_item: Any
36
+ hooks: Any = None
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class InteractionRouterContext:
41
+ """Runtime dependencies for chat/search/tools/hooks/memory route assembly."""
42
+
43
+ chat_context: Any
44
+ search_service: Any
45
+ allowed_workspaces_for: Any
46
+ require_user: Any
47
+ embedding_info: Any
48
+ tool_context: ToolRouterContext
49
+ hooks: Any
50
+ agent_registry: Any
51
+ memory_service: Any
52
+ platform: Any