ltcai 5.5.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 (44) hide show
  1. package/README.md +43 -24
  2. package/docs/CHANGELOG.md +69 -0
  3. package/frontend/openapi.json +716 -3
  4. package/frontend/src/api/client.ts +119 -2
  5. package/frontend/src/api/openapi.ts +621 -4
  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 +28 -3
  12. package/frontend/src/routes.ts +2 -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 +162 -0
  17. package/latticeai/app_factory.py +235 -456
  18. package/latticeai/core/marketplace.py +1 -1
  19. package/latticeai/core/workspace_os.py +86 -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 +271 -0
  31. package/latticeai/services/run_executor.py +33 -0
  32. package/latticeai/services/triggers.py +30 -1
  33. package/package.json +1 -1
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +5 -5
  38. package/static/app/assets/index-D2zafMYb.js +16 -0
  39. package/static/app/assets/index-D2zafMYb.js.map +1 -0
  40. package/static/app/assets/index-xRn29gI8.css +2 -0
  41. package/static/app/index.html +2 -2
  42. package/static/app/assets/index-C7vzwUjU.js +0 -16
  43. package/static/app/assets/index-C7vzwUjU.js.map +0 -1
  44. package/static/app/assets/index-HN4f2wbe.css +0 -2
@@ -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
- 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
361
  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,123 +1337,55 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1480
1337
  agent_registry=AGENT_REGISTRY,
1481
1338
  )
1482
1339
 
1483
- # ── v4 Trigger system (T7d): interval + brain-event workflow triggers.
1484
- from latticeai.services.triggers import TRIGGER_HOOK_NAME, TriggerService
1485
-
1486
- TRIGGER_SERVICE = TriggerService(
1340
+ _automation_runtime = build_automation_runtime(
1487
1341
  store=WORKSPACE_OS,
1488
- run_workflow=lambda wf_id, inputs: PLATFORM.run_workflow_by_id(
1489
- wf_id, None, None, with_agent=False, inputs=inputs,
1490
- ),
1342
+ platform=PLATFORM,
1491
1343
  data_dir=DATA_DIR,
1492
- )
1493
- # Idempotent hook registration: ingestion post_tool events fan into triggers.
1494
- _trigger_hook_id = next(
1495
- (h.get("id") for h in HOOKS_REGISTRY._state.get("custom", [])
1496
- if h.get("name") == TRIGGER_HOOK_NAME),
1497
- None,
1498
- )
1499
- if _trigger_hook_id is None:
1500
- _trigger_hook_id = HOOKS_REGISTRY.register(
1501
- name=TRIGGER_HOOK_NAME,
1502
- kind="post_tool",
1503
- description="Fires brain_event workflow triggers when knowledge enters the brain.",
1504
- )["id"]
1505
- HOOKS_REGISTRY.register_hook(_trigger_hook_id, TRIGGER_SERVICE.hook_runner())
1506
-
1507
- # Single AgentRuntime boundary over the orchestrator + run store.
1508
- # (lattice_brain/runtime.agent_runtime.AgentRuntime — see runtime/__init__.py for full dep graph + entry mapping)
1509
- AGENT_RUNTIME = AgentRuntime(
1510
- store=WORKSPACE_OS,
1511
- orchestrator_factory=PLATFORM.build_orchestrator,
1512
1344
  workspace_graph=_workspace_graph,
1513
1345
  append_audit_event=append_audit_event,
1514
1346
  hooks=HOOKS_REGISTRY,
1515
1347
  )
1516
- RUN_EXECUTOR = RunExecutor(
1517
- store=WORKSPACE_OS,
1518
- agent_runtime=AGENT_RUNTIME,
1519
- build_workflow_runners=PLATFORM.build_workflow_runners,
1520
- workspace_graph=_workspace_graph,
1521
- append_audit_event=append_audit_event,
1522
- hooks=HOOKS_REGISTRY,
1523
- )
1524
- 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)
1525
1353
  app.state.run_executor = RUN_EXECUTOR
1526
1354
  app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
1527
1355
  TRIGGER_SERVICE.start()
1528
1356
 
1529
1357
  # ── Hooks dispatch: bind real built-in runners ───────────────────────────────
1530
- # The registry lists built-in hooks; binding a runner here makes them *execute*
1531
- # real platform behaviour when fired (not a placeholder). Runners take a
1532
- # HookContext and may mutate its payload, return a status dict, or block.
1533
- # Bind a real runner to every built-in hook so none is a silent no-op.
1534
- register_builtin_hook_runners(
1535
- HOOKS_REGISTRY,
1358
+ bind_builtin_hook_runners(
1359
+ registry=HOOKS_REGISTRY,
1536
1360
  append_audit_event=append_audit_event,
1537
1361
  get_tool_permission=get_tool_permission,
1538
1362
  classify_sensitive_message=classify_sensitive_message,
1539
1363
  )
1540
1364
 
1541
- app.include_router(create_plugins_router(
1542
- registry=PLUGIN_REGISTRY,
1365
+ register_platform_feature_routers(
1366
+ app,
1367
+ create_plugins_router=create_plugins_router,
1368
+ plugin_registry=PLUGIN_REGISTRY,
1543
1369
  require_user=require_user,
1544
1370
  require_admin=require_admin,
1545
1371
  append_audit_event=append_audit_event,
1546
- register_skill=PLATFORM.register_plugin_skill,
1547
- plugin_runners_factory=lambda: PLATFORM.plugin_capability_runners(None, None),
1372
+ platform=PLATFORM,
1548
1373
  ui_file_response=ui_file_response,
1549
1374
  static_dir=STATIC_DIR,
1550
- ))
1551
-
1552
- app.include_router(create_workflow_designer_router(
1375
+ create_workflow_designer_router=create_workflow_designer_router,
1553
1376
  store=WORKSPACE_OS,
1554
- require_user=require_user,
1555
1377
  get_current_user=get_current_user,
1556
- gate_read=PLATFORM.gate_read,
1557
- gate_write=PLATFORM.gate_write,
1558
1378
  workspace_graph=_workspace_graph,
1559
- build_runners=PLATFORM.build_workflow_runners,
1560
- append_audit_event=append_audit_event,
1561
- ui_file_response=ui_file_response,
1562
- static_dir=STATIC_DIR,
1563
1379
  hooks=HOOKS_REGISTRY,
1564
1380
  run_executor=RUN_EXECUTOR,
1565
1381
  trigger_service=TRIGGER_SERVICE,
1566
- ))
1567
-
1568
- app.include_router(create_agents_router(
1569
- store=WORKSPACE_OS,
1570
- orchestrator_factory=PLATFORM.build_orchestrator,
1571
- require_user=require_user,
1572
- get_current_user=get_current_user,
1573
- gate_read=PLATFORM.gate_read,
1574
- gate_write=PLATFORM.gate_write,
1575
- workspace_graph=_workspace_graph,
1576
- append_audit_event=append_audit_event,
1577
- ui_file_response=ui_file_response,
1578
- static_dir=STATIC_DIR,
1382
+ create_agents_router=create_agents_router,
1579
1383
  agent_runtime=AGENT_RUNTIME,
1580
- run_executor=RUN_EXECUTOR,
1581
- ))
1582
-
1583
- app.include_router(create_marketplace_router(
1584
- store=WORKSPACE_OS,
1585
- catalog=TEMPLATE_CATALOG,
1586
- require_user=require_user,
1587
- gate_read=PLATFORM.gate_read,
1588
- gate_write=PLATFORM.gate_write,
1589
- workspace_graph=_workspace_graph,
1590
- ))
1591
-
1592
- app.include_router(create_realtime_router(
1593
- bus=REALTIME_BUS,
1594
- require_user=require_user,
1595
- get_current_user=get_current_user,
1596
- allowed_scopes=PLATFORM.allowed_scopes,
1597
- ui_file_response=ui_file_response,
1598
- static_dir=STATIC_DIR,
1599
- ))
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
+ )
1600
1389
 
1601
1390
 
1602
1391
  # ── Health & Info ──────────────────────────────────────────────────────────────
@@ -1605,26 +1394,23 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1605
1394
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1606
1395
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1607
1396
  # Heavier engine mutation endpoints remain below in server_app.
1608
- MODEL_SERVICE = ModelService(
1397
+ MODEL_SERVICE = build_model_service(
1609
1398
  model_router=router,
1610
1399
  runtime_features=runtime_features,
1611
1400
  is_public=IS_PUBLIC_MODE,
1612
1401
  )
1613
- app.include_router(create_health_router(
1402
+ register_health_and_model_routers(
1403
+ app,
1404
+ create_health_router=create_health_router,
1614
1405
  model_service=MODEL_SERVICE,
1615
1406
  engine_status=engine_status,
1616
1407
  get_current_user=get_current_user,
1617
1408
  require_auth=REQUIRE_AUTH,
1618
1409
  app_version=APP_VERSION,
1619
1410
  app_mode=APP_MODE,
1620
- ))
1621
-
1622
-
1623
- # ── Model / Engine router (latticeai.api.models, v1.3.0) ─────────────────────
1624
- app.include_router(create_models_router(
1411
+ create_models_router=create_models_router,
1625
1412
  model_router=router,
1626
1413
  require_user=require_user,
1627
- get_current_user=get_current_user,
1628
1414
  load_users=load_users,
1629
1415
  get_user_role=get_user_role,
1630
1416
  install_engine=install_engine,
@@ -1636,7 +1422,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1636
1422
  sse_event=sse_event,
1637
1423
  ensure_ollama_server=ensure_ollama_server,
1638
1424
  local_binary=local_binary,
1639
- engine_status=engine_status,
1640
1425
  filter_lower_family_versions=filter_lower_family_versions,
1641
1426
  list_compat_profiles=_list_compat_profiles,
1642
1427
  set_user_api_key=set_user_api_key,
@@ -1645,14 +1430,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1645
1430
  cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
1646
1431
  is_public_mode=IS_PUBLIC_MODE,
1647
1432
  allow_local_models=ALLOW_LOCAL_MODELS,
1648
- require_auth=REQUIRE_AUTH,
1649
- ))
1433
+ )
1650
1434
 
1651
1435
 
1652
1436
  # ── Chat / Completion ──────────────────────────────────────────────────────────
1653
1437
 
1654
- app.include_router(create_chat_router(context))
1655
-
1656
1438
  def _embedding_info() -> dict:
1657
1439
  from latticeai.core.embedding_providers import PROVIDER_TYPES, embedding_provider_profiles
1658
1440
  info = EMBEDDER.as_dict()
@@ -1669,20 +1451,21 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1669
1451
  return None
1670
1452
  return PLATFORM.allowed_scopes(user)
1671
1453
 
1672
- app.include_router(create_search_router(
1673
- 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,
1674
1460
  allowed_workspaces_for=_allowed_workspaces_for,
1675
1461
  require_user=require_user,
1676
1462
  embedding_info=_embedding_info,
1677
- ))
1678
-
1679
- app.include_router(create_tools_router(
1463
+ create_tools_router=create_tools_router,
1680
1464
  ingestion_pipeline=INGESTION_PIPELINE,
1681
1465
  config=CONFIG,
1682
1466
  data_dir=DATA_DIR,
1683
1467
  static_dir=STATIC_DIR,
1684
1468
  model_router=router,
1685
- require_user=require_user,
1686
1469
  require_admin=require_admin,
1687
1470
  get_current_user=get_current_user,
1688
1471
  clear_history=clear_history,
@@ -1700,53 +1483,49 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1700
1483
  install_mcp=install_mcp,
1701
1484
  mcp_public_item=mcp_public_item,
1702
1485
  hooks=HOOKS_REGISTRY,
1703
- ))
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
+ )
1704
1493
 
1705
- app.include_router(create_hooks_router(
1706
- registry=HOOKS_REGISTRY,
1707
- require_user=require_user,
1708
- append_audit_event=append_audit_event,
1709
- ))
1494
+ from latticeai.api.review_queue import create_review_queue_router
1710
1495
 
1711
- app.include_router(create_agent_registry_router(
1712
- registry=AGENT_REGISTRY,
1713
- require_user=require_user,
1714
- append_audit_event=append_audit_event,
1715
- ))
1496
+ def _run_review_item(item, *, user_email, scope):
1497
+ """run_now: re-execute the suggestion's source workflow (preview/regenerate)."""
1498
+ wf_id = (item.get("payload") or {}).get("workflow_id") or (item.get("provenance") or {}).get("workflow_id")
1499
+ if not wf_id:
1500
+ raise HTTPException(status_code=409, detail="review item has no workflow to run")
1501
+ return PLATFORM.run_workflow_by_id(
1502
+ wf_id, user_email, scope, with_agent=False,
1503
+ inputs={"__review_item__": item.get("id")},
1504
+ )
1716
1505
 
1717
- app.include_router(create_memory_router(
1718
- service=MEMORY_SERVICE,
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,
1719
1510
  require_user=require_user,
1720
- get_current_user=get_current_user,
1721
1511
  gate_read=PLATFORM.gate_read,
1722
1512
  gate_write=PLATFORM.gate_write,
1513
+ run_review_item=_run_review_item,
1723
1514
  append_audit_event=append_audit_event,
1724
- ))
1725
-
1726
- app.include_router(create_browser_router(
1727
- pipeline=INGESTION_PIPELINE,
1728
- require_user=require_user,
1729
- ))
1730
-
1731
- app.include_router(create_portability_router(
1732
- service=KG_PORTABILITY,
1733
- 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,
1734
1519
  require_admin=require_admin,
1735
- ))
1736
-
1737
- BRAIN_NETWORK = BrainNetwork(
1738
- identity=DEVICE_IDENTITY,
1739
- portability=KG_PORTABILITY,
1520
+ build_brain_network=build_brain_network,
1521
+ device_identity=DEVICE_IDENTITY,
1740
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,
1741
1528
  )
1742
- app.include_router(create_network_router(
1743
- network=BRAIN_NETWORK,
1744
- identity=DEVICE_IDENTITY,
1745
- require_user=require_user,
1746
- ))
1747
-
1748
- app.include_router(create_garden_router(gardener=gardener, require_user=require_user))
1749
- app.include_router(create_setup_router(model_router=router, require_user=require_user))
1750
1529
 
1751
1530
  # ── Entry Point ────────────────────────────────────────────────────────────────
1752
1531