ltcai 5.6.0 → 6.0.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 (40) hide show
  1. package/README.md +42 -25
  2. package/docs/CHANGELOG.md +38 -0
  3. package/frontend/openapi.json +39 -0
  4. package/frontend/src/api/client.ts +104 -23
  5. package/frontend/src/api/openapi.ts +48 -0
  6. package/frontend/src/components/FirstRunGuide.tsx +3 -3
  7. package/frontend/src/features/review/ReviewCard.tsx +91 -0
  8. package/frontend/src/features/review/ReviewInbox.tsx +112 -0
  9. package/frontend/src/features/review/reviewHelpers.ts +69 -0
  10. package/frontend/src/i18n.ts +8 -8
  11. package/frontend/src/pages/Act.tsx +5 -177
  12. package/frontend/src/routes.ts +1 -0
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/runtime/multi_agent.py +1 -1
  15. package/latticeai/__init__.py +1 -1
  16. package/latticeai/api/review_queue.py +7 -3
  17. package/latticeai/app_factory.py +224 -473
  18. package/latticeai/core/marketplace.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/runtime/app_context_runtime.py +13 -0
  21. package/latticeai/runtime/automation_runtime.py +64 -0
  22. package/latticeai/runtime/bootstrap.py +48 -0
  23. package/latticeai/runtime/context_runtime.py +43 -0
  24. package/latticeai/runtime/hooks_runtime.py +77 -0
  25. package/latticeai/runtime/lifespan_runtime.py +138 -0
  26. package/latticeai/runtime/persistence_runtime.py +87 -0
  27. package/latticeai/runtime/platform_services_runtime.py +39 -0
  28. package/latticeai/runtime/router_registration.py +570 -0
  29. package/latticeai/runtime/web_runtime.py +65 -0
  30. package/latticeai/services/review_queue.py +20 -4
  31. package/package.json +1 -1
  32. package/src-tauri/Cargo.lock +1 -1
  33. package/src-tauri/Cargo.toml +1 -1
  34. package/src-tauri/tauri.conf.json +1 -1
  35. package/static/app/asset-manifest.json +3 -3
  36. package/static/app/assets/index-D2zafMYb.js +16 -0
  37. package/static/app/assets/index-D2zafMYb.js.map +1 -0
  38. package/static/app/index.html +1 -1
  39. package/static/app/assets/index-xMFu94cX.js +0 -16
  40. package/static/app/assets/index-xMFu94cX.js.map +0 -1
@@ -17,9 +17,34 @@ from __future__ import annotations
17
17
  import threading
18
18
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
19
19
 
20
+ from latticeai.runtime.automation_runtime import build_automation_runtime
21
+ from latticeai.runtime.app_context_runtime import build_app_context
22
+ from latticeai.runtime.bootstrap import build_session_runtime
20
23
  from latticeai.runtime.brain_runtime import build_brain_runtime
21
24
  from latticeai.runtime.config_runtime import build_config_runtime
25
+ from latticeai.runtime.context_runtime import build_context_runtime
26
+ from latticeai.runtime.hooks_runtime import (
27
+ bind_builtin_hook_runners,
28
+ bind_trigger_hook_runner,
29
+ build_hooks_runtime,
30
+ )
31
+ from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
32
+ from latticeai.runtime.platform_services_runtime import (
33
+ build_brain_network,
34
+ build_model_service,
35
+ )
36
+ from latticeai.runtime.persistence_runtime import build_persistence_runtime
37
+ from latticeai.runtime.router_registration import (
38
+ build_auth_admin_security_router_bundle,
39
+ build_static_routes_bundle,
40
+ register_health_and_model_routers,
41
+ register_foundation_routers,
42
+ register_interaction_routers,
43
+ register_platform_feature_routers,
44
+ register_review_and_brain_tail_routers,
45
+ )
22
46
  from latticeai.runtime.security_runtime import build_security_runtime
47
+ from latticeai.runtime.web_runtime import build_web_runtime
23
48
 
24
49
  if TYPE_CHECKING: # imports for annotations only — keep module import light
25
50
  from fastapi import FastAPI
@@ -34,7 +59,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
34
59
  deliberately *inside* this function so that importing the module performs
35
60
  no GPU init, no singleton construction, and no filesystem writes.
36
61
  """
37
- import asyncio
38
62
  import hashlib
39
63
  import json
40
64
  import logging
@@ -45,7 +69,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
45
69
  import subprocess
46
70
  import sys
47
71
  import time
48
- from contextlib import asynccontextmanager
49
72
  from pathlib import Path
50
73
 
51
74
  try:
@@ -56,14 +79,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
56
79
  print(f"⚠️ MLX Metal context unavailable: {e}")
57
80
  mx = None
58
81
  import uvicorn
59
- from fastapi import FastAPI, HTTPException, Request
60
- from fastapi.middleware.cors import CORSMiddleware
61
- from fastapi.staticfiles import StaticFiles
82
+ from fastapi import HTTPException, Request
62
83
  from pydantic import BaseModel
63
84
 
64
85
  from latticeai.models.router import LLMRouter, normalize_branding
65
86
  from lattice_brain._kg_common import set_llm_router
66
- from local_knowledge_api import LocalKnowledgeWatcher
67
87
  from latticeai.core.security import (
68
88
  hash_password,
69
89
  verify_password,
@@ -74,7 +94,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
74
94
  check_ip_rate_limit as _check_ip_rate_limit,
75
95
  enforce_rate_limit as _enforce_rate_limit,
76
96
  )
77
- from latticeai.core.sessions import SessionStore as _SessionStore
78
97
  from latticeai.core.audit import (
79
98
  get_audit_log as _get_audit_log,
80
99
  append_audit_event as _append_audit_event,
@@ -88,13 +107,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
88
107
  from latticeai.core.model_compat import list_cached_profiles as _list_compat_profiles
89
108
  from latticeai.core.workspace_os import (
90
109
  WORKSPACE_OS_VERSION,
91
- WorkspaceOSStore,
92
110
  remove_skill_directory,
93
111
  )
94
112
  from latticeai.core.enterprise import (
95
113
  capability_registry,
96
114
  )
97
- from latticeai.core.invitations import InvitationStore
98
115
  from latticeai.core.policy import normalize_role, policy_matrix, require_capability
99
116
  from latticeai.core.users import (
100
117
  ensure_user_identity,
@@ -104,13 +121,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
104
121
  save_users_file,
105
122
  user_id_for_email as _user_id_for_email,
106
123
  )
107
- from latticeai.services.app_context import AppContext
108
- from latticeai.services.workspace_service import WorkspaceService
109
- from latticeai.services.model_service import ModelService
110
124
  from latticeai.services.chat_service import ChatService
111
- from latticeai.services.search_service import SearchService
112
125
  from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
113
- from lattice_brain.runtime.agent_runtime import AgentRuntime
114
126
  from latticeai.services.model_runtime import (
115
127
  CLOUD_VERIFY_TTL_SECONDS,
116
128
  ENGINE_MODEL_CATALOG,
@@ -133,11 +145,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
133
145
  from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
134
146
  from latticeai.api.health import create_health_router
135
147
  # ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
136
- from latticeai.core.plugins import PluginRegistry
137
- from latticeai.core.realtime import RealtimeBus
138
- from latticeai.core.marketplace import TemplateCatalog
139
148
  from latticeai.services.platform_runtime import PlatformRuntime
140
- from latticeai.services.run_executor import RunExecutor
141
149
  from latticeai.api.plugins import create_plugins_router
142
150
  from latticeai.api.workflow_designer import create_workflow_designer_router
143
151
  from latticeai.api.agents import create_agents_router
@@ -152,23 +160,14 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
152
160
  from latticeai.api.garden import create_garden_router
153
161
  from latticeai.api.setup import create_setup_router
154
162
  from latticeai.api.hooks import create_hooks_router
155
- from lattice_brain.runtime.hooks import HooksRegistry
156
- from latticeai.core.builtin_hooks import register_builtin_hook_runners
157
163
  from latticeai.core.product_hardening import build_product_hardening_status
158
164
  from latticeai.api.agent_registry import create_agent_registry_router
159
- from latticeai.core.agent_registry import AgentRegistry
160
165
  from latticeai.api.memory import create_memory_router
161
166
  from latticeai.api.browser import create_browser_router
162
167
  from latticeai.api.portability import create_portability_router
163
- from latticeai.services.memory_service import MemoryService
164
- from lattice_brain.ingestion import IngestionItem, IngestionPipeline
168
+ from lattice_brain.ingestion import IngestionItem
165
169
  from lattice_brain.storage import storage_from_env
166
- from lattice_brain.context import ContextAssembler
167
- from lattice_brain.memory import BrainMemory
168
- from lattice_brain.identity import DeviceIdentity
169
- from lattice_brain.network import BrainNetwork
170
170
  from latticeai.api.network import create_network_router
171
- from lattice_brain.portability import KGPortabilityService
172
171
  # The aliased names below look unused but are part of the legacy
173
172
  # ``server_app`` attribute surface: every local is exported via
174
173
  # ``dict(locals())`` and reached through ``server_app.__getattr__``
@@ -281,10 +280,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
281
280
  return True
282
281
  return False
283
282
 
284
- # ── Session store — delegated to latticeai.core.sessions ──────────────────────
285
- _SESSION_TTL = 60 * 60 * 24
286
- _session_store = _SessionStore()
287
-
283
+ # ── Session store — delegated to latticeai.runtime.bootstrap ──────────────────
288
284
  def _check_rate_limit(ip: str, action: str, max_calls: int, window_secs: float) -> None:
289
285
  _check_ip_rate_limit(ip, action, max_calls=max_calls, window_secs=window_secs)
290
286
 
@@ -294,17 +290,15 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
294
290
  def user_id_for_email(email: Optional[str]) -> Optional[str]:
295
291
  return _user_id_for_email(load_users(), email)
296
292
 
297
- def create_session(email: str) -> str:
298
- return _session_store.create(user_id_for_email(email) or email, email=email)
299
-
300
- def get_session_email(token: str) -> Optional[str]:
301
- return _session_store.get_email(token)
302
-
303
- def get_session_user_id(token: str) -> Optional[str]:
304
- return _session_store.get_subject(token)
305
-
306
- def invalidate_session(token: str) -> None:
307
- _session_store.invalidate(token)
293
+ # Session token lifecycle (store + create/get/invalidate closures) lives in
294
+ # the bootstrap seam; user_id_for_email is injected as the subject resolver.
295
+ _session_runtime = build_session_runtime(user_id_resolver=user_id_for_email)
296
+ _SESSION_TTL = _session_runtime["_SESSION_TTL"]
297
+ _session_store = _session_runtime["_session_store"]
298
+ create_session = _session_runtime["create_session"]
299
+ get_session_email = _session_runtime["get_session_email"]
300
+ get_session_user_id = _session_runtime["get_session_user_id"]
301
+ invalidate_session = _session_runtime["invalidate_session"]
308
302
 
309
303
  # ── User Management Logic ──────────────────────────────────────────────────
310
304
  BASE_DIR = Path(__file__).resolve().parent.parent
@@ -360,54 +354,40 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
360
354
  # file is left untouched on disk as the import source.
361
355
  CONVERSATIONS = _brain_runtime["CONVERSATIONS"]
362
356
  # Hooks registry is constructed here (ahead of the watcher) so folder-watch
363
- # reindexes can fire the pre_index/post_index lifecycle hooks.
364
- HOOKS_REGISTRY = HooksRegistry(DATA_DIR / "hooks.json")
365
- LOCAL_KG_WATCHER = LocalKnowledgeWatcher(lambda: KNOWLEDGE_GRAPH, hooks=HOOKS_REGISTRY) if ENABLE_GRAPH else None
366
- # ── v2 Realtime bus: constructed first so the store can fan every timeline
367
- # event into the realtime feed via a single additive sink (no per-call wiring).
368
- REALTIME_BUS = RealtimeBus()
369
- WORKSPACE_OS = WorkspaceOSStore(DATA_DIR, event_sink=REALTIME_BUS)
370
- # Service layer (latticeai.services) wraps the store with scope/permission
371
- # guardrails; routers and the app assembly share this single instance.
372
- WORKSPACE_SERVICE = WorkspaceService(WORKSPACE_OS, resolve_user_id=user_id_for_email)
373
- INVITATION_STORE = InvitationStore(DATA_DIR / "invitations.json")
374
- # ── v2 Plugin SDK registry (extends skills; discovers plugins/<id>/plugin.json)
375
- PLUGINS_DIR = Path(os.getenv("LATTICEAI_PLUGINS_DIR") or (BASE_DIR / "plugins"))
376
- PLUGIN_REGISTRY = PluginRegistry(PLUGINS_DIR, store=WORKSPACE_OS)
377
- TEMPLATE_CATALOG = TemplateCatalog()
378
- # ── v3.2 platform registries: lifecycle hooks + agent registry, persisted under
379
- # DATA_DIR so the /app Hooks and Agent Registry views read/write real state.
380
- # (HOOKS_REGISTRY is constructed earlier, before the local-knowledge watcher.)
381
- AGENT_REGISTRY = AgentRegistry(DATA_DIR / "agent_registry.json")
382
- # Unified long-term memory platform fronting workspace memories, agent
383
- # snapshots, conversation history, and the KG graph/vector index.
384
- MEMORY_SERVICE = MemoryService(
385
- store=WORKSPACE_OS,
357
+ # reindexes can fire the pre_index/post_index lifecycle hooks. The registry
358
+ # + watcher pair is assembled behind the hooks_runtime seam.
359
+ _hooks_runtime = build_hooks_runtime(
386
360
  data_dir=DATA_DIR,
387
- knowledge_graph=KNOWLEDGE_GRAPH,
388
361
  enable_graph=ENABLE_GRAPH,
389
- history_file=HISTORY_FILE,
390
- conversation_store=CONVERSATIONS,
391
- )
392
- # ── v3.6.0 unified ingestion pipeline: the single write-side seam into the
393
- # Knowledge Graph. Every new source (web URL, browser tab, …) flows through this
394
- # so pre_tool/post_tool hooks fire on ingestion and provenance is captured
395
- # uniformly. Existing direct ingest callers keep working; new paths converge here.
396
- INGESTION_PIPELINE = IngestionPipeline(
397
- KNOWLEDGE_GRAPH,
398
- hooks=HOOKS_REGISTRY,
399
- enable_graph=ENABLE_GRAPH,
400
- audit=lambda action, detail, user: append_audit_event(action, user_email=user, **detail),
362
+ knowledge_graph_getter=lambda: KNOWLEDGE_GRAPH,
401
363
  )
402
- # ── v3.6.0 Knowledge Graph portability: local export / import / backup / restore.
403
- # The graph is the user's durable asset, so it must be portable with no cloud.
404
- DEVICE_IDENTITY = DeviceIdentity(DATA_DIR)
405
- KG_PORTABILITY = KGPortabilityService(
406
- knowledge_graph=KNOWLEDGE_GRAPH,
364
+ HOOKS_REGISTRY = _hooks_runtime["HOOKS_REGISTRY"]
365
+ LOCAL_KG_WATCHER = _hooks_runtime["LOCAL_KG_WATCHER"]
366
+ # ── Persistence/service graph: workspace store, realtime feed, plugin/memory
367
+ # registries, ingestion pipeline, device identity, and portability services.
368
+ _persistence_runtime = build_persistence_runtime(
407
369
  data_dir=DATA_DIR,
370
+ base_dir=BASE_DIR,
408
371
  enable_graph=ENABLE_GRAPH,
409
- device_identity=DEVICE_IDENTITY,
372
+ knowledge_graph=KNOWLEDGE_GRAPH,
373
+ hooks_registry=HOOKS_REGISTRY,
374
+ history_file=HISTORY_FILE,
375
+ conversations=CONVERSATIONS,
376
+ user_id_for_email=user_id_for_email,
377
+ audit=lambda action, detail, user: append_audit_event(action, user_email=user, **detail),
410
378
  )
379
+ REALTIME_BUS = _persistence_runtime["REALTIME_BUS"]
380
+ WORKSPACE_OS = _persistence_runtime["WORKSPACE_OS"]
381
+ WORKSPACE_SERVICE = _persistence_runtime["WORKSPACE_SERVICE"]
382
+ INVITATION_STORE = _persistence_runtime["INVITATION_STORE"]
383
+ PLUGINS_DIR = _persistence_runtime["PLUGINS_DIR"]
384
+ PLUGIN_REGISTRY = _persistence_runtime["PLUGIN_REGISTRY"]
385
+ TEMPLATE_CATALOG = _persistence_runtime["TEMPLATE_CATALOG"]
386
+ AGENT_REGISTRY = _persistence_runtime["AGENT_REGISTRY"]
387
+ MEMORY_SERVICE = _persistence_runtime["MEMORY_SERVICE"]
388
+ INGESTION_PIPELINE = _persistence_runtime["INGESTION_PIPELINE"]
389
+ DEVICE_IDENTITY = _persistence_runtime["DEVICE_IDENTITY"]
390
+ KG_PORTABILITY = _persistence_runtime["KG_PORTABILITY"]
411
391
 
412
392
  def _require_graph():
413
393
  if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
@@ -1071,134 +1051,38 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1071
1051
  except Exception as exc:
1072
1052
  logging.warning("garden vault import skipped: %s", exc)
1073
1053
 
1074
- async def autoload_default_model() -> None:
1075
- if not AUTOLOAD_MODELS:
1076
- print("⏭️ Model autoload disabled by LATTICEAI_AUTOLOAD_MODELS=false.")
1077
- return
1078
-
1079
- if IS_PUBLIC_MODE:
1080
- model_id = PUBLIC_MODEL
1081
- provider = model_id.split(":", 1)[0] if ":" in model_id else "openai"
1082
- env_by_provider = {
1083
- "openai": "OPENAI_API_KEY",
1084
- "openrouter": "OPENROUTER_API_KEY",
1085
- "groq": "GROQ_API_KEY",
1086
- "together": "TOGETHER_API_KEY",
1087
- "ollama": "OLLAMA_API_KEY",
1088
- }
1089
- required_env = env_by_provider.get(provider)
1090
- if required_env and not os.getenv(required_env) and provider != "ollama":
1091
- print(f"🌐 Public mode ready. Set {required_env} to autoload {model_id}.")
1092
- return
1093
- print(f"🌐 Public mode autoload: {model_id}")
1094
- try:
1095
- msg = await router.load_model(model_id)
1096
- print(f"✅ {msg}")
1097
- except Exception as e:
1098
- print(f"⚠️ Public model autoload failed: {e}")
1099
- return
1100
-
1101
- if not ALLOW_LOCAL_MODELS:
1102
- print("⏭️ Local model autoload skipped because LATTICEAI_ALLOW_LOCAL_MODELS=false.")
1103
- return
1104
-
1105
- print("⏳ Auto-loading local model stack:")
1106
- print(f" - Target: {LOCAL_MODEL}")
1107
- if LOCAL_DRAFT_MODEL:
1108
- print(f" - Draft: {LOCAL_DRAFT_MODEL}")
1109
- else:
1110
- print(" - Draft: disabled (set LATTICEAI_LOCAL_DRAFT_MODEL to enable)")
1111
- try:
1112
- await router.load_model(LOCAL_MODEL, draft_model_id=LOCAL_DRAFT_MODEL or None)
1113
- except Exception as e:
1114
- print(f"⚠️ Local model autoload failed: {e}")
1115
-
1116
- async def unload_idle_models_loop() -> None:
1117
- if MODEL_IDLE_UNLOAD_SECONDS <= 0:
1118
- print("⏭️ Model idle unload disabled.")
1119
- return
1120
- while True:
1121
- await asyncio.sleep(min(60, MODEL_IDLE_UNLOAD_SECONDS))
1122
- try:
1123
- unloaded = router.unload_idle_models(MODEL_IDLE_UNLOAD_SECONDS)
1124
- if unloaded:
1125
- print(f"🧹 Idle model unload: {', '.join(unloaded)}")
1126
- except Exception as e:
1127
- logging.warning("Idle model unload failed: %s", e)
1128
-
1129
- def _spawn(coro, *, name: str):
1130
- """Fire-and-forget asyncio task that logs exceptions instead of swallowing them."""
1131
- task = asyncio.create_task(coro, name=name)
1132
- def _on_done(t: asyncio.Task) -> None:
1133
- if t.cancelled():
1134
- return
1135
- exc = t.exception()
1136
- if exc is not None:
1137
- logging.warning("background task '%s' failed: %s", name, exc)
1138
- task.add_done_callback(_on_done)
1139
- return task
1140
-
1141
-
1142
- @asynccontextmanager
1143
- async def lifespan(app: FastAPI):
1144
- try:
1145
- print(f"🧭 Lattice AI mode: {APP_MODE}")
1146
- if ENABLE_TELEGRAM:
1147
- from telegram_bot import run_bot
1148
- _spawn(run_bot(), name="telegram_bot")
1149
- print("🚀 Telegram Bot Bridge activated!")
1150
- else:
1151
- print("⏭️ Telegram Bot Bridge disabled for this mode.")
1152
- _spawn(unload_idle_models_loop(), name="unload_idle_models")
1153
- _spawn(autoload_default_model(), name="autoload_default_model")
1154
- if LOCAL_KG_WATCHER:
1155
- restored = LOCAL_KG_WATCHER.restore_enabled_sources()
1156
- if restored.get("restored"):
1157
- print(f"🕸️ Local knowledge watchers restored: {restored['restored']}")
1158
- except Exception as e:
1159
- print(f"⚠️ Startup sequence failed: {e}")
1160
- try:
1161
- yield
1162
- finally:
1163
- if LOCAL_KG_WATCHER:
1164
- LOCAL_KG_WATCHER.stop_all()
1165
- router.unload_all()
1166
- for proc in LOCAL_SERVER_PROCESSES.values():
1167
- try:
1168
- if proc.poll() is None:
1169
- proc.terminate()
1170
- proc.wait(timeout=5)
1171
- except Exception:
1172
- pass
1173
-
1174
- app = FastAPI(title=f"Lattice AI Server ({APP_MODE})", version=APP_VERSION, lifespan=lifespan)
1175
-
1176
- CORS_ALLOWED_ORIGINS = [
1177
- f"http://localhost:{DEFAULT_PORT}",
1178
- f"http://127.0.0.1:{DEFAULT_PORT}",
1179
- *CORS_EXTRA_ORIGINS,
1180
- ]
1181
- if CORS_ALLOW_NETWORK:
1182
- CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS + [
1183
- f"http://{DEFAULT_HOST}:{DEFAULT_PORT}",
1184
- f"https://{DEFAULT_HOST}:{DEFAULT_PORT}",
1185
- ]
1186
-
1187
- app.add_middleware(
1188
- CORSMiddleware,
1189
- allow_origins=CORS_ALLOWED_ORIGINS,
1190
- allow_methods=["*"],
1191
- allow_headers=["*"],
1192
- allow_credentials=True,
1054
+ _lifespan_runtime = build_lifespan_runtime(
1055
+ app_mode=APP_MODE,
1056
+ enable_telegram=ENABLE_TELEGRAM,
1057
+ autoload_models=AUTOLOAD_MODELS,
1058
+ is_public_mode=IS_PUBLIC_MODE,
1059
+ public_model=PUBLIC_MODEL,
1060
+ allow_local_models=ALLOW_LOCAL_MODELS,
1061
+ local_model=LOCAL_MODEL,
1062
+ local_draft_model=LOCAL_DRAFT_MODEL,
1063
+ model_idle_unload_seconds=MODEL_IDLE_UNLOAD_SECONDS,
1064
+ model_router=router,
1065
+ local_kg_watcher=LOCAL_KG_WATCHER,
1066
+ local_server_processes=LOCAL_SERVER_PROCESSES,
1067
+ logger=logging,
1193
1068
  )
1069
+ autoload_default_model = _lifespan_runtime["autoload_default_model"]
1070
+ unload_idle_models_loop = _lifespan_runtime["unload_idle_models_loop"]
1071
+ _spawn = _lifespan_runtime["_spawn"]
1072
+ lifespan = _lifespan_runtime["lifespan"]
1194
1073
 
1195
- # UI 파일이 담길 static 폴더 연결
1196
- STATIC_DIR.mkdir(parents=True, exist_ok=True)
1197
- app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
1198
- # PWA icons served at /icons/*
1199
- _ICONS_DIR = STATIC_DIR / "icons"
1200
- if _ICONS_DIR.exists():
1201
- app.mount("/icons", StaticFiles(directory=str(_ICONS_DIR)), name="icons")
1074
+ _web_runtime = build_web_runtime(
1075
+ app_mode=APP_MODE,
1076
+ app_version=APP_VERSION,
1077
+ lifespan=lifespan,
1078
+ default_host=DEFAULT_HOST,
1079
+ default_port=DEFAULT_PORT,
1080
+ cors_extra_origins=CORS_EXTRA_ORIGINS,
1081
+ cors_allow_network=CORS_ALLOW_NETWORK,
1082
+ static_dir=STATIC_DIR,
1083
+ )
1084
+ app = _web_runtime["app"]
1085
+ CORS_ALLOWED_ORIGINS = _web_runtime["CORS_ALLOWED_ORIGINS"]
1202
1086
  ensure_agent_root()
1203
1087
 
1204
1088
  OPEN_REGISTRATION = CONFIG.open_registration
@@ -1227,7 +1111,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1227
1111
  get_current_user=get_current_user,
1228
1112
  get_user_api_key=get_user_api_key,
1229
1113
  )
1230
- STATIC_ROUTES = create_static_routes_router(
1114
+ _static_routes_bundle = build_static_routes_bundle(
1115
+ create_static_routes_router=create_static_routes_router,
1231
1116
  static_dir=STATIC_DIR,
1232
1117
  invite_gate_enabled=INVITE_GATE_ENABLED,
1233
1118
  invite_code=INVITE_CODE,
@@ -1235,102 +1120,70 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1235
1120
  model_router=router,
1236
1121
  require_user=require_user,
1237
1122
  )
1238
- ui_file_response = STATIC_ROUTES.ui_file_response
1239
- local_sysinfo = STATIC_ROUTES.local_sysinfo
1240
- app.include_router(STATIC_ROUTES.router)
1123
+ STATIC_ROUTES = _static_routes_bundle["STATIC_ROUTES"]
1124
+ ui_file_response = _static_routes_bundle["ui_file_response"]
1125
+ local_sysinfo = _static_routes_bundle["local_sysinfo"]
1241
1126
 
1242
1127
  # ── Auth & Admin routers (latticeai.api) ─────────────────────────────────────
1243
- app.include_router(create_auth_router(
1244
- load_users=load_users, save_users=save_users,
1245
- hash_password=hash_password, verify_and_migrate=verify_and_migrate_password,
1246
- create_session=create_session, get_session_email=get_session_email,
1247
- invalidate_session=invalidate_session, extract_bearer_token=_extract_bearer_token,
1248
- get_user_role=get_user_role, require_user=require_user,
1249
- check_ip_rate_limit=_check_rate_limit, client_ip=_client_ip,
1250
- get_sso_settings=get_sso_settings, get_sso_discovery=_get_sso_discovery,
1128
+ _foundation_router_bundle = build_auth_admin_security_router_bundle(
1129
+ create_auth_router=create_auth_router,
1130
+ load_users=load_users,
1131
+ save_users=save_users,
1132
+ hash_password=hash_password,
1133
+ verify_and_migrate_password=verify_and_migrate_password,
1134
+ create_session=create_session,
1135
+ get_session_email=get_session_email,
1136
+ invalidate_session=invalidate_session,
1137
+ extract_bearer_token=_extract_bearer_token,
1138
+ get_user_role=get_user_role,
1139
+ require_user=require_user,
1140
+ check_ip_rate_limit=_check_rate_limit,
1141
+ client_ip=_client_ip,
1142
+ get_sso_settings=get_sso_settings,
1143
+ get_sso_discovery=_get_sso_discovery,
1251
1144
  public_sso_config=public_sso_config,
1252
- open_registration=OPEN_REGISTRATION, session_ttl=_SESSION_TTL,
1145
+ open_registration=OPEN_REGISTRATION,
1146
+ session_ttl=_SESSION_TTL,
1253
1147
  require_auth=REQUIRE_AUTH,
1254
1148
  ensure_identity=ensure_user_identity,
1255
- ))
1256
-
1257
- def _graph_stats_safe():
1258
- try:
1259
- return KNOWLEDGE_GRAPH.stats() if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else {"disabled": True}
1260
- except Exception as e:
1261
- return {"error": str(e)}
1262
-
1263
- def _product_hardening_status():
1264
- return build_product_hardening_status(
1265
- config=CONFIG,
1266
- portability=KG_PORTABILITY,
1267
- device_identity=DEVICE_IDENTITY,
1268
- )
1269
-
1270
- app.include_router(create_admin_router(
1271
- require_admin=require_admin, require_user=require_user,
1272
- load_users=load_users, save_users=save_users,
1273
- get_user_role=get_user_role, get_history=get_history,
1274
- get_audit_log=get_audit_log,
1275
- public_user=public_user, load_vpc_config=load_vpc_config,
1149
+ create_admin_router=create_admin_router,
1150
+ require_admin=require_admin,
1151
+ get_history=get_history,
1152
+ get_audit_log=_get_audit_log,
1153
+ audit_file=AUDIT_FILE,
1154
+ public_user=public_user,
1155
+ load_vpc_config=load_vpc_config,
1276
1156
  save_vpc_config=save_vpc_config,
1277
1157
  build_admin_audit_report=build_admin_audit_report,
1278
1158
  build_sensitivity_report=build_sensitivity_report,
1279
1159
  append_audit_event=append_audit_event,
1280
- public_sso_config=public_sso_config, save_sso_config=save_sso_config,
1281
- get_graph_stats=_graph_stats_safe, enable_graph=ENABLE_GRAPH,
1282
- invite_code=INVITE_CODE, invite_gate_enabled=INVITE_GATE_ENABLED,
1160
+ save_sso_config=save_sso_config,
1161
+ knowledge_graph=KNOWLEDGE_GRAPH,
1162
+ enable_graph=ENABLE_GRAPH,
1163
+ logger=logging,
1164
+ invite_code=INVITE_CODE,
1165
+ invite_gate_enabled=INVITE_GATE_ENABLED,
1283
1166
  default_port=DEFAULT_PORT,
1284
1167
  policy_matrix=policy_matrix,
1285
- product_hardening_status=_product_hardening_status,
1286
- ))
1287
-
1288
- app.include_router(create_invitations_router(
1168
+ build_product_hardening_status=build_product_hardening_status,
1169
+ config=CONFIG,
1170
+ kg_portability=KG_PORTABILITY,
1171
+ device_identity=DEVICE_IDENTITY,
1172
+ create_invitations_router=create_invitations_router,
1289
1173
  invitation_store=INVITATION_STORE,
1290
1174
  workspace_service=WORKSPACE_SERVICE,
1291
- require_admin=require_admin,
1292
- require_user=require_user,
1293
1175
  user_id_for_email=user_id_for_email,
1294
- append_audit_event=append_audit_event,
1295
- ))
1296
-
1297
- # ── Security & Audit Command Center (피드백 #5) ──────────────────────────────
1298
- def _security_audit_events_safe() -> List[Dict]:
1299
- try:
1300
- return _get_audit_log(AUDIT_FILE)
1301
- except Exception as e:
1302
- logging.warning("security audit events load failed: %s", e)
1303
- return []
1304
-
1305
- def _security_list_uploaded_files() -> List[Dict]:
1306
- """Audit log에서 document_upload 이벤트를 가공해서 file 목록으로 노출."""
1307
- files: List[Dict] = []
1308
- for idx, e in enumerate(_security_audit_events_safe()):
1309
- if e.get("event_type") != "document_upload":
1310
- continue
1311
- files.append({
1312
- "file_id": str(e.get("filename") or idx),
1313
- "filename": e.get("filename"),
1314
- "user_email": e.get("user_email"),
1315
- "user_nickname": e.get("user_nickname"),
1316
- "uploaded_at": e.get("timestamp"),
1317
- "ext": e.get("ext"),
1318
- "bytes": e.get("bytes"),
1319
- "sensitivity": e.get("sensitivity") or "none",
1320
- "sensitive_labels": e.get("sensitive_labels") or [],
1321
- "content_preview": e.get("content_preview"),
1322
- })
1323
- return files
1324
-
1325
- app.include_router(_create_security_router(
1326
- require_admin=require_admin,
1327
- get_history=get_history,
1328
- get_audit_events=_security_audit_events_safe,
1176
+ create_security_router=_create_security_router,
1329
1177
  classify_sensitive_message=classify_sensitive_message,
1330
- build_sensitivity_report=build_sensitivity_report,
1331
- list_uploaded_files=_security_list_uploaded_files,
1332
- append_audit_event=append_audit_event,
1333
- ))
1178
+ )
1179
+ auth_router = _foundation_router_bundle["auth_router"]
1180
+ admin_router = _foundation_router_bundle["admin_router"]
1181
+ invitations_router = _foundation_router_bundle["invitations_router"]
1182
+ security_router = _foundation_router_bundle["security_router"]
1183
+ _graph_stats_safe = _foundation_router_bundle["_graph_stats_safe"]
1184
+ _product_hardening_status = _foundation_router_bundle["_product_hardening_status"]
1185
+ _security_audit_events_safe = _foundation_router_bundle["_security_audit_events_safe"]
1186
+ _security_list_uploaded_files = _foundation_router_bundle["_security_list_uploaded_files"]
1334
1187
 
1335
1188
  # ── Static UI/status routes moved to latticeai.api.static_routes ──
1336
1189
 
@@ -1365,22 +1218,18 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1365
1218
  return KNOWLEDGE_GRAPH if (ENABLE_GRAPH and KNOWLEDGE_GRAPH) else None
1366
1219
 
1367
1220
 
1368
- SEARCH_SERVICE = SearchService(graph_store=_workspace_graph())
1369
-
1370
- # ── v4 Context System: one budgeted, provenance-carrying assembly over the
1371
- # product's own retrieval stack (memories + hybrid search + garden notes).
1372
- BRAIN_MEMORY = BrainMemory(INGESTION_PIPELINE)
1373
- def _scoped_hybrid_search(q, user_email=None, **kw):
1374
- allowed = None
1375
- if REQUIRE_AUTH and user_email:
1376
- allowed = PLATFORM.allowed_scopes(user_email)
1377
- return SEARCH_SERVICE.hybrid_search(q, allowed_workspaces=allowed, **kw)
1378
-
1379
- CONTEXT_ASSEMBLER = ContextAssembler(
1380
- memory_recall=MEMORY_SERVICE.recall,
1381
- hybrid_search=_scoped_hybrid_search,
1382
- notes_context=gardener.get_relevant_context,
1221
+ _context_runtime = build_context_runtime(
1222
+ graph_store=_workspace_graph(),
1223
+ ingestion_pipeline=INGESTION_PIPELINE,
1224
+ memory_service=MEMORY_SERVICE,
1225
+ gardener=gardener,
1226
+ require_auth=REQUIRE_AUTH,
1227
+ allowed_scopes_for_user=lambda user_email: PLATFORM.allowed_scopes(user_email),
1383
1228
  )
1229
+ SEARCH_SERVICE = _context_runtime["SEARCH_SERVICE"]
1230
+ BRAIN_MEMORY = _context_runtime["BRAIN_MEMORY"]
1231
+ CONTEXT_ASSEMBLER = _context_runtime["CONTEXT_ASSEMBLER"]
1232
+ _scoped_hybrid_search = _context_runtime["_scoped_hybrid_search"]
1384
1233
 
1385
1234
 
1386
1235
  # ── Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy.
@@ -1395,7 +1244,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1395
1244
 
1396
1245
  # ── Typed dependency context (latticeai.services.app_context) ────────────────
1397
1246
  # One context object replaces the historical 25-30-kwarg router wiring.
1398
- context = AppContext(
1247
+ context = build_app_context(
1399
1248
  config=CONFIG,
1400
1249
  data_dir=DATA_DIR,
1401
1250
  static_dir=STATIC_DIR,
@@ -1449,8 +1298,16 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1449
1298
  )
1450
1299
  app.state.context = context
1451
1300
 
1452
- # ── Workspace OS + Organization router (latticeai.api.workspace, v1.2.0) ──────
1453
- app.include_router(create_workspace_router(context))
1301
+ register_foundation_routers(
1302
+ app,
1303
+ static_router=STATIC_ROUTES.router,
1304
+ auth_router=auth_router,
1305
+ admin_router=admin_router,
1306
+ invitations_router=invitations_router,
1307
+ security_router=security_router,
1308
+ create_workspace_router=create_workspace_router,
1309
+ context=context,
1310
+ )
1454
1311
 
1455
1312
 
1456
1313
  # ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
@@ -1480,130 +1337,55 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1480
1337
  agent_registry=AGENT_REGISTRY,
1481
1338
  )
1482
1339
 
1483
- # ── Brain review queue (5.6.0): the suggestion inbox automation drops into.
1484
- from latticeai.services.review_queue import ReviewQueueService
1485
-
1486
- REVIEW_QUEUE = ReviewQueueService(store=WORKSPACE_OS)
1487
-
1488
- # ── v4 Trigger system (T7d): interval + brain-event workflow triggers.
1489
- from latticeai.services.triggers import TRIGGER_HOOK_NAME, TriggerService
1490
-
1491
- TRIGGER_SERVICE = TriggerService(
1340
+ _automation_runtime = build_automation_runtime(
1492
1341
  store=WORKSPACE_OS,
1493
- run_workflow=lambda wf_id, inputs: PLATFORM.run_workflow_by_id(
1494
- wf_id, None, None, with_agent=False, inputs=inputs,
1495
- ),
1342
+ platform=PLATFORM,
1496
1343
  data_dir=DATA_DIR,
1497
- review_sink=REVIEW_QUEUE,
1498
- )
1499
- # Idempotent hook registration: ingestion post_tool events fan into triggers.
1500
- _trigger_hook_id = next(
1501
- (h.get("id") for h in HOOKS_REGISTRY._state.get("custom", [])
1502
- if h.get("name") == TRIGGER_HOOK_NAME),
1503
- None,
1504
- )
1505
- if _trigger_hook_id is None:
1506
- _trigger_hook_id = HOOKS_REGISTRY.register(
1507
- name=TRIGGER_HOOK_NAME,
1508
- kind="post_tool",
1509
- description="Fires brain_event workflow triggers when knowledge enters the brain.",
1510
- )["id"]
1511
- HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
1512
-
1513
- # Single AgentRuntime boundary over the orchestrator + run store.
1514
- # (lattice_brain/runtime.agent_runtime.AgentRuntime — see runtime/__init__.py for full dep graph + entry mapping)
1515
- AGENT_RUNTIME = AgentRuntime(
1516
- store=WORKSPACE_OS,
1517
- orchestrator_factory=PLATFORM.build_orchestrator,
1518
1344
  workspace_graph=_workspace_graph,
1519
1345
  append_audit_event=append_audit_event,
1520
1346
  hooks=HOOKS_REGISTRY,
1521
1347
  )
1522
- RUN_EXECUTOR = RunExecutor(
1523
- store=WORKSPACE_OS,
1524
- agent_runtime=AGENT_RUNTIME,
1525
- build_workflow_runners=PLATFORM.build_workflow_runners,
1526
- workspace_graph=_workspace_graph,
1527
- append_audit_event=append_audit_event,
1528
- hooks=HOOKS_REGISTRY,
1529
- review_sink=REVIEW_QUEUE,
1530
- )
1531
- AGENT_RUNTIME.attach_executor(RUN_EXECUTOR)
1348
+ REVIEW_QUEUE = _automation_runtime["REVIEW_QUEUE"]
1349
+ TRIGGER_SERVICE = _automation_runtime["TRIGGER_SERVICE"]
1350
+ AGENT_RUNTIME = _automation_runtime["AGENT_RUNTIME"]
1351
+ RUN_EXECUTOR = _automation_runtime["RUN_EXECUTOR"]
1352
+ bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
1532
1353
  app.state.run_executor = RUN_EXECUTOR
1533
1354
  app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
1534
1355
  TRIGGER_SERVICE.start()
1535
1356
 
1536
1357
  # ── Hooks dispatch: bind real built-in runners ───────────────────────────────
1537
- # The registry lists built-in hooks; binding a runner here makes them *execute*
1538
- # real platform behaviour when fired (not a placeholder). Runners take a
1539
- # HookContext and may mutate its payload, return a status dict, or block.
1540
- # Bind a real runner to every built-in hook so none is a silent no-op.
1541
- register_builtin_hook_runners(
1542
- HOOKS_REGISTRY,
1358
+ bind_builtin_hook_runners(
1359
+ registry=HOOKS_REGISTRY,
1543
1360
  append_audit_event=append_audit_event,
1544
1361
  get_tool_permission=get_tool_permission,
1545
1362
  classify_sensitive_message=classify_sensitive_message,
1546
1363
  )
1547
1364
 
1548
- app.include_router(create_plugins_router(
1549
- registry=PLUGIN_REGISTRY,
1365
+ register_platform_feature_routers(
1366
+ app,
1367
+ create_plugins_router=create_plugins_router,
1368
+ plugin_registry=PLUGIN_REGISTRY,
1550
1369
  require_user=require_user,
1551
1370
  require_admin=require_admin,
1552
1371
  append_audit_event=append_audit_event,
1553
- register_skill=PLATFORM.register_plugin_skill,
1554
- plugin_runners_factory=lambda: PLATFORM.plugin_capability_runners(None, None),
1372
+ platform=PLATFORM,
1555
1373
  ui_file_response=ui_file_response,
1556
1374
  static_dir=STATIC_DIR,
1557
- ))
1558
-
1559
- app.include_router(create_workflow_designer_router(
1375
+ create_workflow_designer_router=create_workflow_designer_router,
1560
1376
  store=WORKSPACE_OS,
1561
- require_user=require_user,
1562
1377
  get_current_user=get_current_user,
1563
- gate_read=PLATFORM.gate_read,
1564
- gate_write=PLATFORM.gate_write,
1565
1378
  workspace_graph=_workspace_graph,
1566
- build_runners=PLATFORM.build_workflow_runners,
1567
- append_audit_event=append_audit_event,
1568
- ui_file_response=ui_file_response,
1569
- static_dir=STATIC_DIR,
1570
1379
  hooks=HOOKS_REGISTRY,
1571
1380
  run_executor=RUN_EXECUTOR,
1572
1381
  trigger_service=TRIGGER_SERVICE,
1573
- ))
1574
-
1575
- app.include_router(create_agents_router(
1576
- store=WORKSPACE_OS,
1577
- orchestrator_factory=PLATFORM.build_orchestrator,
1578
- require_user=require_user,
1579
- get_current_user=get_current_user,
1580
- gate_read=PLATFORM.gate_read,
1581
- gate_write=PLATFORM.gate_write,
1582
- workspace_graph=_workspace_graph,
1583
- append_audit_event=append_audit_event,
1584
- ui_file_response=ui_file_response,
1585
- static_dir=STATIC_DIR,
1382
+ create_agents_router=create_agents_router,
1586
1383
  agent_runtime=AGENT_RUNTIME,
1587
- run_executor=RUN_EXECUTOR,
1588
- ))
1589
-
1590
- app.include_router(create_marketplace_router(
1591
- store=WORKSPACE_OS,
1592
- catalog=TEMPLATE_CATALOG,
1593
- require_user=require_user,
1594
- gate_read=PLATFORM.gate_read,
1595
- gate_write=PLATFORM.gate_write,
1596
- workspace_graph=_workspace_graph,
1597
- ))
1598
-
1599
- app.include_router(create_realtime_router(
1600
- bus=REALTIME_BUS,
1601
- require_user=require_user,
1602
- get_current_user=get_current_user,
1603
- allowed_scopes=PLATFORM.allowed_scopes,
1604
- ui_file_response=ui_file_response,
1605
- static_dir=STATIC_DIR,
1606
- ))
1384
+ create_marketplace_router=create_marketplace_router,
1385
+ template_catalog=TEMPLATE_CATALOG,
1386
+ create_realtime_router=create_realtime_router,
1387
+ realtime_bus=REALTIME_BUS,
1388
+ )
1607
1389
 
1608
1390
 
1609
1391
  # ── Health & Info ──────────────────────────────────────────────────────────────
@@ -1612,26 +1394,23 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1612
1394
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1613
1395
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1614
1396
  # Heavier engine mutation endpoints remain below in server_app.
1615
- MODEL_SERVICE = ModelService(
1397
+ MODEL_SERVICE = build_model_service(
1616
1398
  model_router=router,
1617
1399
  runtime_features=runtime_features,
1618
1400
  is_public=IS_PUBLIC_MODE,
1619
1401
  )
1620
- app.include_router(create_health_router(
1402
+ register_health_and_model_routers(
1403
+ app,
1404
+ create_health_router=create_health_router,
1621
1405
  model_service=MODEL_SERVICE,
1622
1406
  engine_status=engine_status,
1623
1407
  get_current_user=get_current_user,
1624
1408
  require_auth=REQUIRE_AUTH,
1625
1409
  app_version=APP_VERSION,
1626
1410
  app_mode=APP_MODE,
1627
- ))
1628
-
1629
-
1630
- # ── Model / Engine router (latticeai.api.models, v1.3.0) ─────────────────────
1631
- app.include_router(create_models_router(
1411
+ create_models_router=create_models_router,
1632
1412
  model_router=router,
1633
1413
  require_user=require_user,
1634
- get_current_user=get_current_user,
1635
1414
  load_users=load_users,
1636
1415
  get_user_role=get_user_role,
1637
1416
  install_engine=install_engine,
@@ -1643,7 +1422,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1643
1422
  sse_event=sse_event,
1644
1423
  ensure_ollama_server=ensure_ollama_server,
1645
1424
  local_binary=local_binary,
1646
- engine_status=engine_status,
1647
1425
  filter_lower_family_versions=filter_lower_family_versions,
1648
1426
  list_compat_profiles=_list_compat_profiles,
1649
1427
  set_user_api_key=set_user_api_key,
@@ -1652,14 +1430,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1652
1430
  cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
1653
1431
  is_public_mode=IS_PUBLIC_MODE,
1654
1432
  allow_local_models=ALLOW_LOCAL_MODELS,
1655
- require_auth=REQUIRE_AUTH,
1656
- ))
1433
+ )
1657
1434
 
1658
1435
 
1659
1436
  # ── Chat / Completion ──────────────────────────────────────────────────────────
1660
1437
 
1661
- app.include_router(create_chat_router(context))
1662
-
1663
1438
  def _embedding_info() -> dict:
1664
1439
  from latticeai.core.embedding_providers import PROVIDER_TYPES, embedding_provider_profiles
1665
1440
  info = EMBEDDER.as_dict()
@@ -1676,20 +1451,21 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1676
1451
  return None
1677
1452
  return PLATFORM.allowed_scopes(user)
1678
1453
 
1679
- app.include_router(create_search_router(
1680
- service=SEARCH_SERVICE,
1454
+ register_interaction_routers(
1455
+ app,
1456
+ create_chat_router=create_chat_router,
1457
+ context=context,
1458
+ create_search_router=create_search_router,
1459
+ search_service=SEARCH_SERVICE,
1681
1460
  allowed_workspaces_for=_allowed_workspaces_for,
1682
1461
  require_user=require_user,
1683
1462
  embedding_info=_embedding_info,
1684
- ))
1685
-
1686
- app.include_router(create_tools_router(
1463
+ create_tools_router=create_tools_router,
1687
1464
  ingestion_pipeline=INGESTION_PIPELINE,
1688
1465
  config=CONFIG,
1689
1466
  data_dir=DATA_DIR,
1690
1467
  static_dir=STATIC_DIR,
1691
1468
  model_router=router,
1692
- require_user=require_user,
1693
1469
  require_admin=require_admin,
1694
1470
  get_current_user=get_current_user,
1695
1471
  clear_history=clear_history,
@@ -1707,28 +1483,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1707
1483
  install_mcp=install_mcp,
1708
1484
  mcp_public_item=mcp_public_item,
1709
1485
  hooks=HOOKS_REGISTRY,
1710
- ))
1711
-
1712
- app.include_router(create_hooks_router(
1713
- registry=HOOKS_REGISTRY,
1714
- require_user=require_user,
1715
- append_audit_event=append_audit_event,
1716
- ))
1717
-
1718
- app.include_router(create_agent_registry_router(
1719
- registry=AGENT_REGISTRY,
1720
- require_user=require_user,
1721
- append_audit_event=append_audit_event,
1722
- ))
1723
-
1724
- app.include_router(create_memory_router(
1725
- service=MEMORY_SERVICE,
1726
- require_user=require_user,
1727
- get_current_user=get_current_user,
1728
- gate_read=PLATFORM.gate_read,
1729
- gate_write=PLATFORM.gate_write,
1730
- append_audit_event=append_audit_event,
1731
- ))
1486
+ create_hooks_router=create_hooks_router,
1487
+ create_agent_registry_router=create_agent_registry_router,
1488
+ agent_registry=AGENT_REGISTRY,
1489
+ create_memory_router=create_memory_router,
1490
+ memory_service=MEMORY_SERVICE,
1491
+ platform=PLATFORM,
1492
+ )
1732
1493
 
1733
1494
  from latticeai.api.review_queue import create_review_queue_router
1734
1495
 
@@ -1742,39 +1503,29 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1742
1503
  inputs={"__review_item__": item.get("id")},
1743
1504
  )
1744
1505
 
1745
- app.include_router(create_review_queue_router(
1746
- service=REVIEW_QUEUE,
1506
+ BRAIN_NETWORK = register_review_and_brain_tail_routers(
1507
+ app,
1508
+ create_review_queue_router=create_review_queue_router,
1509
+ review_queue=REVIEW_QUEUE,
1747
1510
  require_user=require_user,
1748
1511
  gate_read=PLATFORM.gate_read,
1749
1512
  gate_write=PLATFORM.gate_write,
1750
1513
  run_review_item=_run_review_item,
1751
1514
  append_audit_event=append_audit_event,
1752
- ))
1753
-
1754
- app.include_router(create_browser_router(
1755
- pipeline=INGESTION_PIPELINE,
1756
- require_user=require_user,
1757
- ))
1758
-
1759
- app.include_router(create_portability_router(
1760
- service=KG_PORTABILITY,
1761
- require_user=require_user,
1515
+ create_browser_router=create_browser_router,
1516
+ ingestion_pipeline=INGESTION_PIPELINE,
1517
+ create_portability_router=create_portability_router,
1518
+ kg_portability=KG_PORTABILITY,
1762
1519
  require_admin=require_admin,
1763
- ))
1764
-
1765
- BRAIN_NETWORK = BrainNetwork(
1766
- identity=DEVICE_IDENTITY,
1767
- portability=KG_PORTABILITY,
1520
+ build_brain_network=build_brain_network,
1521
+ device_identity=DEVICE_IDENTITY,
1768
1522
  data_dir=DATA_DIR,
1523
+ create_network_router=create_network_router,
1524
+ create_garden_router=create_garden_router,
1525
+ gardener=gardener,
1526
+ create_setup_router=create_setup_router,
1527
+ model_router=router,
1769
1528
  )
1770
- app.include_router(create_network_router(
1771
- network=BRAIN_NETWORK,
1772
- identity=DEVICE_IDENTITY,
1773
- require_user=require_user,
1774
- ))
1775
-
1776
- app.include_router(create_garden_router(gardener=gardener, require_user=require_user))
1777
- app.include_router(create_setup_router(model_router=router, require_user=require_user))
1778
1529
 
1779
1530
  # ── Entry Point ────────────────────────────────────────────────────────────────
1780
1531