ltcai 8.9.0 → 9.1.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -27,25 +27,40 @@ def build_automation_runtime(
27
27
  from latticeai.services.triggers import TriggerService
28
28
 
29
29
  review_queue = ReviewQueueService(store=store)
30
+
31
+ def _run_triggered_workflow(workflow_id: str, inputs: Dict[str, Any]) -> Dict[str, Any]:
32
+ trigger = (inputs or {}).get("__trigger__") or {}
33
+ return platform.run_workflow_by_id(
34
+ workflow_id,
35
+ trigger.get("user_email"),
36
+ trigger.get("workspace_id"),
37
+ with_agent=True,
38
+ inputs=inputs,
39
+ )
40
+
30
41
  trigger_service = TriggerService(
31
42
  store=store,
32
- run_workflow=lambda wf_id, inputs: platform.run_workflow_by_id(
33
- wf_id,
34
- None,
35
- None,
36
- with_agent=False,
37
- inputs=inputs,
38
- ),
43
+ run_workflow=_run_triggered_workflow,
39
44
  data_dir=data_dir,
40
45
  review_sink=review_queue,
41
46
  tz_name=tz_name,
42
47
  )
48
+
49
+ def _memory_ingest(**kwargs):
50
+ try:
51
+ # Delegate to store's upsert (enriches memories + KG automatically via internal graph.ingest_event)
52
+ return store.upsert_memory(**kwargs)
53
+ except Exception:
54
+ return None
55
+
43
56
  agent_runtime = AgentRuntime(
44
57
  store=store,
45
58
  orchestrator_factory=platform.build_orchestrator,
46
59
  workspace_graph=workspace_graph,
47
60
  append_audit_event=append_audit_event,
48
61
  hooks=hooks,
62
+ memory_ingest=_memory_ingest,
63
+ review_sink=review_queue,
49
64
  )
50
65
  run_executor = RunExecutor(
51
66
  store=store,
@@ -2,7 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Dict
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from latticeai.runtime.stages import RuntimeStage
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class BrainRuntime(RuntimeStage):
13
+ BRAIN_CORE: Any
14
+ KNOWLEDGE_GRAPH: Any
15
+ CONVERSATIONS: Any
6
16
 
7
17
 
8
18
  def build_brain_runtime(
@@ -12,7 +22,7 @@ def build_brain_runtime(
12
22
  enable_graph: bool,
13
23
  embedder: Any,
14
24
  storage_engine: Any,
15
- ) -> Dict[str, Any]:
25
+ ) -> BrainRuntime:
16
26
  """Construct Brain Core storage/conversation primitives behind one seam."""
17
27
 
18
28
  from lattice_brain import BrainCore, ConversationStore
@@ -33,9 +43,11 @@ def build_brain_runtime(
33
43
  else ConversationStore(data_dir / "knowledge_graph.sqlite")
34
44
  )
35
45
  conversations.import_legacy_json(history_file)
36
- return {
37
- "BRAIN_CORE": brain_core,
38
- "KNOWLEDGE_GRAPH": knowledge_graph,
39
- "CONVERSATIONS": conversations,
40
- }
46
+ return BrainRuntime(
47
+ BRAIN_CORE=brain_core,
48
+ KNOWLEDGE_GRAPH=knowledge_graph,
49
+ CONVERSATIONS=conversations,
50
+ )
51
+
41
52
 
53
+ __all__ = ["BrainRuntime", "build_brain_runtime"]
@@ -56,6 +56,7 @@ def build_interaction_contexts(
56
56
  install_mcp: Any,
57
57
  mcp_public_item: Any,
58
58
  hooks: Any,
59
+ workspace_service: Any,
59
60
  chat_context: Any,
60
61
  search_service: Any,
61
62
  allowed_workspaces_for: Any,
@@ -89,6 +90,7 @@ def build_interaction_contexts(
89
90
  install_mcp=install_mcp,
90
91
  mcp_public_item=mcp_public_item,
91
92
  hooks=hooks,
93
+ workspace_service=workspace_service,
92
94
  allowed_workspaces_for=allowed_workspaces_for,
93
95
  )
94
96
  interaction_router_context = InteractionRouterContext(
@@ -2,39 +2,71 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Dict, Optional
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Optional
7
+
8
+ from latticeai.runtime.stages import RuntimeStage
6
9
 
7
10
  if TYPE_CHECKING:
8
11
  from latticeai.core.config import Config
9
12
 
10
13
 
11
- def build_config_runtime(config: "Optional[Config]" = None) -> Dict[str, Any]:
14
+ @dataclass(frozen=True)
15
+ class ConfigRuntime(RuntimeStage):
16
+ CONFIG: Any
17
+ APP_MODE: str
18
+ IS_PUBLIC_MODE: bool
19
+ DEFAULT_HOST: str
20
+ DEFAULT_PORT: int
21
+ NETWORK_EXPOSED: bool
22
+ ENABLE_TELEGRAM: bool
23
+ ENABLE_GRAPH: bool
24
+ AUTOLOAD_MODELS: bool
25
+ MODEL_IDLE_UNLOAD_SECONDS: int
26
+ ALLOW_LOCAL_MODELS: bool
27
+ REQUIRE_AUTH: bool
28
+ ALLOW_PLAINTEXT_API_KEYS: bool
29
+ CORS_ALLOW_NETWORK: bool
30
+ CORS_EXTRA_ORIGINS: Any
31
+ PUBLIC_MODEL: str
32
+ LOCAL_MODEL: str
33
+ LOCAL_DRAFT_MODEL: str
34
+ TIMEZONE: str
35
+ MAX_LOCAL_MODELS: int
36
+ ALLOW_MODEL_DOWNLOADS: bool
37
+ MODEL_DOWNLOAD_TIMEOUT: int
38
+
39
+
40
+ def build_config_runtime(config: "Optional[Config]" = None) -> ConfigRuntime:
12
41
  """Build app configuration values without importing model/runtime code."""
13
42
 
14
43
  from latticeai.core.config import Config
15
44
 
16
45
  cfg = config if config is not None else Config.from_env()
17
- return {
18
- "CONFIG": cfg,
19
- "APP_MODE": cfg.app_mode,
20
- "IS_PUBLIC_MODE": cfg.is_public,
21
- "DEFAULT_HOST": cfg.host,
22
- "DEFAULT_PORT": cfg.port,
23
- "NETWORK_EXPOSED": cfg.network_exposed,
24
- "ENABLE_TELEGRAM": cfg.enable_telegram,
25
- "ENABLE_GRAPH": cfg.enable_graph,
26
- "AUTOLOAD_MODELS": cfg.autoload_models,
27
- "MODEL_IDLE_UNLOAD_SECONDS": cfg.model_idle_unload_seconds,
28
- "ALLOW_LOCAL_MODELS": cfg.allow_local_models,
29
- "REQUIRE_AUTH": cfg.require_auth,
30
- "ALLOW_PLAINTEXT_API_KEYS": cfg.allow_plaintext_api_keys,
31
- "CORS_ALLOW_NETWORK": cfg.cors_allow_network,
32
- "CORS_EXTRA_ORIGINS": cfg.cors_extra_origins,
33
- "PUBLIC_MODEL": cfg.public_model,
34
- "LOCAL_MODEL": cfg.local_model,
35
- "LOCAL_DRAFT_MODEL": cfg.local_draft_model,
36
- "TIMEZONE": cfg.timezone,
37
- "MAX_LOCAL_MODELS": cfg.max_local_models,
38
- "ALLOW_MODEL_DOWNLOADS": cfg.allow_model_downloads,
39
- "MODEL_DOWNLOAD_TIMEOUT": cfg.model_download_timeout,
40
- }
46
+ return ConfigRuntime(
47
+ CONFIG=cfg,
48
+ APP_MODE=cfg.app_mode,
49
+ IS_PUBLIC_MODE=cfg.is_public,
50
+ DEFAULT_HOST=cfg.host,
51
+ DEFAULT_PORT=cfg.port,
52
+ NETWORK_EXPOSED=cfg.network_exposed,
53
+ ENABLE_TELEGRAM=cfg.enable_telegram,
54
+ ENABLE_GRAPH=cfg.enable_graph,
55
+ AUTOLOAD_MODELS=cfg.autoload_models,
56
+ MODEL_IDLE_UNLOAD_SECONDS=cfg.model_idle_unload_seconds,
57
+ ALLOW_LOCAL_MODELS=cfg.allow_local_models,
58
+ REQUIRE_AUTH=cfg.require_auth,
59
+ ALLOW_PLAINTEXT_API_KEYS=cfg.allow_plaintext_api_keys,
60
+ CORS_ALLOW_NETWORK=cfg.cors_allow_network,
61
+ CORS_EXTRA_ORIGINS=cfg.cors_extra_origins,
62
+ PUBLIC_MODEL=cfg.public_model,
63
+ LOCAL_MODEL=cfg.local_model,
64
+ LOCAL_DRAFT_MODEL=cfg.local_draft_model,
65
+ TIMEZONE=cfg.timezone,
66
+ MAX_LOCAL_MODELS=cfg.max_local_models,
67
+ ALLOW_MODEL_DOWNLOADS=cfg.allow_model_downloads,
68
+ MODEL_DOWNLOAD_TIMEOUT=cfg.model_download_timeout,
69
+ )
70
+
71
+
72
+ __all__ = ["ConfigRuntime", "build_config_runtime"]
@@ -23,16 +23,28 @@ def build_context_runtime(
23
23
  search_service = SearchService(graph_store=graph_store)
24
24
  brain_memory = BrainMemory(ingestion_pipeline)
25
25
 
26
- def scoped_hybrid_search(q, user_email=None, **kw):
26
+ def scoped_hybrid_search(q, user_email=None, workspace_id=None, **kw):
27
27
  allowed = None
28
- if require_auth and user_email:
29
- allowed = allowed_scopes_for_user(user_email)
28
+ if require_auth:
29
+ if workspace_id is not None:
30
+ allowed = {workspace_id}
31
+ elif user_email:
32
+ allowed = allowed_scopes_for_user(user_email)
30
33
  return search_service.hybrid_search(q, allowed_workspaces=allowed, **kw)
31
34
 
35
+ def scoped_notes_context(q, user_email=None, workspace_id=None, **kw):
36
+ allowed = None
37
+ if require_auth:
38
+ if workspace_id is not None:
39
+ allowed = {workspace_id}
40
+ elif user_email:
41
+ allowed = allowed_scopes_for_user(user_email)
42
+ return gardener.get_relevant_context(q, allowed_workspaces=allowed, **kw)
43
+
32
44
  context_assembler = ContextAssembler(
33
45
  memory_recall=memory_service.recall,
34
46
  hybrid_search=scoped_hybrid_search,
35
- notes_context=gardener.get_relevant_context,
47
+ notes_context=scoped_notes_context,
36
48
  )
37
49
 
38
50
  return {
@@ -0,0 +1,163 @@
1
+ """Conversation-history query/clear seam extracted from the app factory.
2
+
3
+ The read and clear helpers (scope resolution, ``get_history``, conversation
4
+ grouping, and the clear operations) used to be defined inline in
5
+ ``app_factory._build``. They are behaviour-preserving closures over the durable
6
+ conversation store and the workspace service; moving them here keeps the factory
7
+ a wiring path. Names are returned unchanged for the legacy ``server_app``
8
+ compatibility namespace.
9
+
10
+ Note: ``save_to_history`` (the *write* path) stays in the factory — it is bound
11
+ up with redaction, audit, and the ingestion pipeline that are assembled around
12
+ it — so only the query/clear side moves here.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ def build_history_query_runtime(
22
+ *,
23
+ conversations: Any,
24
+ workspace_service: Any,
25
+ require_auth: bool,
26
+ logging: Any,
27
+ ) -> Dict[str, Any]:
28
+ """Return the history scope/query/clear helpers as a name → callable dict."""
29
+
30
+ def _history_allowed_workspaces_for(user_email: Optional[str]):
31
+ if not require_auth or not user_email:
32
+ return None
33
+ try:
34
+ return set(workspace_service.readable_workspaces(user_email))
35
+ except Exception as exc:
36
+ logging.warning("history workspace scope resolution failed for %s: %s", user_email, exc)
37
+ return set()
38
+
39
+ def _history_include_legacy_global(user_email: Optional[str]) -> bool:
40
+ return not require_auth or not user_email
41
+
42
+ def get_history(
43
+ user_email: Optional[str] = None,
44
+ allowed_workspaces=None,
45
+ include_legacy_global: Optional[bool] = None,
46
+ ):
47
+ try:
48
+ if allowed_workspaces is None and user_email:
49
+ allowed_workspaces = _history_allowed_workspaces_for(user_email)
50
+ if include_legacy_global is None:
51
+ include_legacy_global = _history_include_legacy_global(user_email)
52
+ return conversations.history(
53
+ user_email=user_email,
54
+ allowed_workspaces=allowed_workspaces,
55
+ include_legacy_global=include_legacy_global,
56
+ )
57
+ except Exception as e:
58
+ logging.warning("get_history failed: %s", e)
59
+ return []
60
+
61
+ def conversation_title(item: Dict) -> str:
62
+ content = str(item.get("content") or "").strip()
63
+ content = re.sub(r"\s+", " ", content)
64
+ return content[:48] or "새 대화"
65
+
66
+ def group_history_conversations(history: Optional[List[Dict]] = None) -> List[Dict]:
67
+ history = history if history is not None else get_history()
68
+ grouped: Dict[str, Dict] = {}
69
+ order: List[str] = []
70
+
71
+ for index, item in enumerate(history):
72
+ conv_id = item.get("conversation_id")
73
+ if not conv_id:
74
+ conv_id = "legacy-previous-history"
75
+
76
+ if conv_id not in grouped:
77
+ grouped[conv_id] = {
78
+ "id": conv_id,
79
+ "title": "이전 대화 기록" if conv_id == "legacy-previous-history" else conversation_title(item),
80
+ "created_at": item.get("timestamp"),
81
+ "updated_at": item.get("timestamp"),
82
+ "message_count": 0,
83
+ "last_message": "",
84
+ "source": item.get("source"),
85
+ }
86
+ order.append(conv_id)
87
+
88
+ conv = grouped[conv_id]
89
+ conv["message_count"] += 1
90
+ conv["updated_at"] = item.get("timestamp") or conv.get("updated_at")
91
+ conv["last_message"] = conversation_title(item)
92
+ if conv_id != "legacy-previous-history" and item.get("role") == "user" and (not conv.get("title") or conv["title"] == "새 대화"):
93
+ conv["title"] = conversation_title(item)
94
+
95
+ return sorted((grouped[key] for key in order), key=lambda item: item.get("updated_at") or "", reverse=True)
96
+
97
+ def get_conversation_messages(
98
+ conversation_id: str,
99
+ *,
100
+ user_email: Optional[str] = None,
101
+ allowed_workspaces=None,
102
+ include_legacy_global: Optional[bool] = None,
103
+ ) -> List[Dict]:
104
+ history = get_history(
105
+ user_email=user_email,
106
+ allowed_workspaces=allowed_workspaces,
107
+ include_legacy_global=include_legacy_global,
108
+ )
109
+ if conversation_id == "legacy-previous-history":
110
+ return [item for item in history if not item.get("conversation_id")]
111
+ return [item for item in history if item.get("conversation_id") == conversation_id]
112
+
113
+ def clear_history(
114
+ keep_last: int = 0,
115
+ *,
116
+ user_email: Optional[str] = None,
117
+ allowed_workspaces=None,
118
+ include_legacy_global: Optional[bool] = None,
119
+ ) -> Dict:
120
+ if allowed_workspaces is None and user_email:
121
+ allowed_workspaces = _history_allowed_workspaces_for(user_email)
122
+ if include_legacy_global is None:
123
+ include_legacy_global = _history_include_legacy_global(user_email)
124
+ return conversations.clear_all(
125
+ keep_last=keep_last,
126
+ user_email=user_email,
127
+ allowed_workspaces=allowed_workspaces,
128
+ include_legacy_global=include_legacy_global,
129
+ )
130
+
131
+ def clear_conversation(
132
+ conversation_id: str,
133
+ started_at: Optional[str] = None,
134
+ *,
135
+ user_email: Optional[str] = None,
136
+ allowed_workspaces=None,
137
+ include_legacy_global: Optional[bool] = None,
138
+ ) -> Dict:
139
+ if allowed_workspaces is None and user_email:
140
+ allowed_workspaces = _history_allowed_workspaces_for(user_email)
141
+ if include_legacy_global is None:
142
+ include_legacy_global = _history_include_legacy_global(user_email)
143
+ return conversations.clear_conversation(
144
+ conversation_id,
145
+ started_at=started_at,
146
+ user_email=user_email,
147
+ allowed_workspaces=allowed_workspaces,
148
+ include_legacy_global=include_legacy_global,
149
+ )
150
+
151
+ return {
152
+ "_history_allowed_workspaces_for": _history_allowed_workspaces_for,
153
+ "_history_include_legacy_global": _history_include_legacy_global,
154
+ "get_history": get_history,
155
+ "conversation_title": conversation_title,
156
+ "group_history_conversations": group_history_conversations,
157
+ "get_conversation_messages": get_conversation_messages,
158
+ "clear_history": clear_history,
159
+ "clear_conversation": clear_conversation,
160
+ }
161
+
162
+
163
+ __all__ = ["build_history_query_runtime"]
@@ -21,7 +21,7 @@ def build_hooks_runtime(
21
21
  """Construct the hooks registry and local-knowledge watcher behind one seam."""
22
22
 
23
23
  from lattice_brain.runtime.hooks import HooksRegistry
24
- from local_knowledge_api import LocalKnowledgeWatcher
24
+ from latticeai.services.local_knowledge import LocalKnowledgeWatcher
25
25
 
26
26
  hooks_registry = HooksRegistry(data_dir / "hooks.json")
27
27
  local_kg_watcher = (
@@ -2,14 +2,28 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from dataclasses import dataclass
5
6
  from typing import Any
6
7
 
7
8
  from latticeai.runtime.platform_services_runtime import build_model_service
8
9
 
9
10
 
10
- def configure_model_runtime_from_context(**kwargs: Any) -> None:
11
- configure_model_runtime = kwargs.pop("configure_model_runtime")
12
- configure_model_runtime(**kwargs)
11
+ @dataclass(frozen=True)
12
+ class ModelRuntime:
13
+ """Typed model-provider stage registered with the HTTP application."""
14
+
15
+ router: Any
16
+ runtime_service: Any
17
+ service: Any
18
+ runtime_features: Any
19
+ is_public: bool
20
+
21
+
22
+ def configure_model_runtime_from_context(**kwargs: Any) -> Any:
23
+ """Build and return an isolated model runtime from app-owned values."""
24
+
25
+ build_model_runtime = kwargs.pop("build_model_runtime")
26
+ return build_model_runtime(**kwargs)
13
27
 
14
28
 
15
29
  def register_model_runtime_routers(
@@ -19,10 +33,11 @@ def register_model_runtime_routers(
19
33
  create_models_router: Any,
20
34
  register_health_and_model_routers: Any,
21
35
  model_router: Any,
36
+ runtime_service: Any,
22
37
  runtime_features: Any,
23
38
  is_public_mode: bool,
24
39
  **kwargs: Any,
25
- ) -> Any:
40
+ ) -> ModelRuntime:
26
41
  model_service = build_model_service(
27
42
  model_router=model_router,
28
43
  runtime_features=runtime_features,
@@ -37,4 +52,17 @@ def register_model_runtime_routers(
37
52
  is_public_mode=is_public_mode,
38
53
  **kwargs,
39
54
  )
40
- return model_service
55
+ return ModelRuntime(
56
+ router=model_router,
57
+ runtime_service=runtime_service,
58
+ service=model_service,
59
+ runtime_features=runtime_features,
60
+ is_public=is_public_mode,
61
+ )
62
+
63
+
64
+ __all__ = [
65
+ "ModelRuntime",
66
+ "configure_model_runtime_from_context",
67
+ "register_model_runtime_routers",
68
+ ]
@@ -0,0 +1,163 @@
1
+ """Explicit runtime exports for the legacy :mod:`server_app` facade.
2
+
3
+ The composition root must never export ``locals()``. Every compatibility name
4
+ is selected here and every typed assembly stage remains available through the
5
+ ``RuntimeBundle`` without leaking construction scratch state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Dict, Mapping
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RuntimeBundle:
16
+ """Typed application assembly result and its five explicit stages."""
17
+
18
+ app: Any
19
+ CONFIG: Any
20
+ KNOWLEDGE_GRAPH: Any
21
+ INGESTION_PIPELINE: Any
22
+ AGENT_RUNTIME: Any
23
+ HOOKS_REGISTRY: Any
24
+ REVIEW_QUEUE: Any
25
+ AGENT_REGISTRY: Any
26
+ model_router: Any
27
+ build_runtime: Any
28
+ get_shared_runtime: Any
29
+ create_app: Any
30
+ config_runtime: Any
31
+ security_runtime: Any
32
+ brain_runtime: Any
33
+ model_runtime: Any
34
+ router_bundle: Any
35
+
36
+ def as_legacy_dict(self) -> Dict[str, Any]:
37
+ return {
38
+ name: getattr(self, name)
39
+ for name in RUNTIME_BUNDLE_EXPORTS
40
+ }
41
+
42
+ @property
43
+ def stages(self) -> Dict[str, Any]:
44
+ return {
45
+ "config": self.config_runtime,
46
+ "security": self.security_runtime,
47
+ "brain": self.brain_runtime,
48
+ "models": self.model_runtime,
49
+ "routers": self.router_bundle,
50
+ }
51
+
52
+
53
+ RUNTIME_BUNDLE_EXPORTS = frozenset(
54
+ {
55
+ "app",
56
+ "CONFIG",
57
+ "KNOWLEDGE_GRAPH",
58
+ "INGESTION_PIPELINE",
59
+ "AGENT_RUNTIME",
60
+ "HOOKS_REGISTRY",
61
+ "REVIEW_QUEUE",
62
+ "AGENT_REGISTRY",
63
+ "model_router",
64
+ "build_runtime",
65
+ "get_shared_runtime",
66
+ "create_app",
67
+ }
68
+ )
69
+
70
+ LEGACY_UNDERSCORE_EXPORTS = {
71
+ "_LOCAL_WRITE_BLOCKED_PREFIXES",
72
+ "_RATE_LIMIT_ENABLED",
73
+ "_SESSION_TTL",
74
+ "_TOOL_CATALOG_BRIEF",
75
+ "_TOOL_GOVERNANCE_DEFAULT",
76
+ "_agent_risk",
77
+ "_allowed_workspaces_for",
78
+ "_build_admin_audit_report",
79
+ "_build_sensitivity_report",
80
+ "_bytes_match_extension",
81
+ "_check_ip_rate_limit",
82
+ "_check_rate_limit",
83
+ "_check_tool_role",
84
+ "_classify_sensitive_message",
85
+ "_client_ip",
86
+ "_create_security_router",
87
+ "_embedding_info",
88
+ "_fetch_skills_marketplace",
89
+ "_get_audit_log",
90
+ "_get_sso_discovery",
91
+ "_graph_stats_safe",
92
+ "_host_is_loopback",
93
+ "_list_compat_profiles",
94
+ "_llm_generate_sync",
95
+ "_product_hardening_status",
96
+ "_recent_chat_context",
97
+ "_redact_secret_text",
98
+ "_require_graph",
99
+ "_scoped_hybrid_search",
100
+ "_security_audit_events_safe",
101
+ "_security_list_uploaded_files",
102
+ "_spawn",
103
+ "_tool_response",
104
+ "_user_id_for_email",
105
+ "_workspace_graph",
106
+ "_workspace_models_payload",
107
+ "_workspace_scope_from_request",
108
+ "_workspace_settings_payload",
109
+ }
110
+
111
+ LEGACY_PUBLIC_EXPORTS = {
112
+ "ENGINE_MODEL_CATALOG",
113
+ "TOOL_GOVERNANCE",
114
+ "enforce_rate_limit",
115
+ "filter_lower_family_versions",
116
+ "hash_password",
117
+ "normalize_local_model_request",
118
+ "verify_password",
119
+ }
120
+
121
+
122
+ def build_runtime_namespace(
123
+ *, runtime_bundle: RuntimeBundle | Mapping[str, Any], legacy_exports: Mapping[str, Any]
124
+ ) -> Dict[str, Any]:
125
+ """Return only explicit compatibility exports and typed bundle handles."""
126
+ legacy_bundle = (
127
+ runtime_bundle.as_legacy_dict()
128
+ if isinstance(runtime_bundle, RuntimeBundle)
129
+ else dict(runtime_bundle)
130
+ )
131
+ exported: Dict[str, Any] = dict(legacy_bundle)
132
+ allowed = LEGACY_PUBLIC_EXPORTS | LEGACY_UNDERSCORE_EXPORTS
133
+ unexpected = set(legacy_exports) - allowed
134
+ if unexpected:
135
+ raise ValueError(f"unapproved runtime exports: {sorted(unexpected)}")
136
+ for name in sorted(allowed):
137
+ if name in exported:
138
+ continue
139
+ value = legacy_exports.get(name)
140
+ if value is None:
141
+ continue
142
+ exported[name] = value
143
+ exported["RUNTIME_BUNDLE"] = runtime_bundle
144
+ exported["_RUNTIME_BUNDLE"] = legacy_bundle
145
+ return exported
146
+
147
+
148
+ SERVER_APP_EXPORTS = frozenset(
149
+ RUNTIME_BUNDLE_EXPORTS
150
+ | LEGACY_PUBLIC_EXPORTS
151
+ | LEGACY_UNDERSCORE_EXPORTS
152
+ | {"RUNTIME_BUNDLE", "_RUNTIME_BUNDLE"}
153
+ )
154
+
155
+
156
+ __all__ = [
157
+ "LEGACY_UNDERSCORE_EXPORTS",
158
+ "LEGACY_PUBLIC_EXPORTS",
159
+ "RUNTIME_BUNDLE_EXPORTS",
160
+ "RuntimeBundle",
161
+ "SERVER_APP_EXPORTS",
162
+ "build_runtime_namespace",
163
+ ]