ltcai 10.0.0 → 10.3.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 (214) hide show
  1. package/README.md +43 -32
  2. package/docs/CHANGELOG.md +204 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
  6. package/docs/MYPY_BACKLOG.md +98 -0
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/archive.py +5 -4
  14. package/lattice_brain/conversations.py +14 -3
  15. package/lattice_brain/embeddings.py +12 -2
  16. package/lattice_brain/graph/_kg_common.py +5 -5
  17. package/lattice_brain/graph/_kg_fsutil.py +5 -4
  18. package/lattice_brain/graph/discovery.py +4 -3
  19. package/lattice_brain/graph/discovery_index.py +0 -1
  20. package/lattice_brain/graph/documents.py +3 -2
  21. package/lattice_brain/graph/fusion.py +4 -0
  22. package/lattice_brain/graph/ingest.py +12 -2
  23. package/lattice_brain/graph/projection.py +3 -2
  24. package/lattice_brain/graph/provenance.py +5 -3
  25. package/lattice_brain/graph/rerank.py +4 -1
  26. package/lattice_brain/graph/retrieval.py +0 -1
  27. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  28. package/lattice_brain/graph/retrieval_reads.py +0 -1
  29. package/lattice_brain/graph/retrieval_vector.py +0 -1
  30. package/lattice_brain/graph/schema.py +4 -3
  31. package/lattice_brain/graph/store.py +18 -4
  32. package/lattice_brain/graph/write_master.py +46 -1
  33. package/lattice_brain/ingestion.py +4 -1
  34. package/lattice_brain/portability.py +5 -2
  35. package/lattice_brain/quality.py +12 -5
  36. package/lattice_brain/quiet.py +43 -0
  37. package/lattice_brain/runtime/agent_runtime.py +19 -10
  38. package/lattice_brain/runtime/hooks.py +3 -2
  39. package/lattice_brain/runtime/multi_agent.py +2 -3
  40. package/lattice_brain/sensitivity.py +94 -0
  41. package/lattice_brain/storage/base.py +30 -2
  42. package/lattice_brain/storage/migration.py +3 -2
  43. package/lattice_brain/storage/postgres.py +2 -2
  44. package/lattice_brain/storage/sqlite.py +6 -4
  45. package/lattice_brain/workflow.py +4 -2
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agents.py +4 -2
  49. package/latticeai/api/auth.py +5 -1
  50. package/latticeai/api/automation_intelligence.py +2 -1
  51. package/latticeai/api/browser.py +3 -2
  52. package/latticeai/api/chat.py +28 -17
  53. package/latticeai/api/chat_agent_http.py +22 -8
  54. package/latticeai/api/chat_contracts.py +4 -0
  55. package/latticeai/api/chat_documents.py +6 -2
  56. package/latticeai/api/chat_hybrid.py +82 -0
  57. package/latticeai/api/computer_use.py +8 -3
  58. package/latticeai/api/knowledge_graph.py +1 -1
  59. package/latticeai/api/mcp.py +4 -4
  60. package/latticeai/api/models.py +5 -2
  61. package/latticeai/api/network_boundary.py +220 -0
  62. package/latticeai/api/permissions.py +0 -1
  63. package/latticeai/api/realtime.py +1 -1
  64. package/latticeai/api/security_dashboard.py +1 -1
  65. package/latticeai/api/setup.py +16 -3
  66. package/latticeai/api/static_routes.py +2 -1
  67. package/latticeai/api/tools.py +12 -8
  68. package/latticeai/api/voice_capture.py +3 -1
  69. package/latticeai/api/workflow_designer.py +1 -1
  70. package/latticeai/api/workspace.py +1 -2
  71. package/latticeai/app_factory.py +174 -145
  72. package/latticeai/cli/entrypoint.py +6 -4
  73. package/latticeai/core/agent.py +55 -495
  74. package/latticeai/core/agent_eval.py +2 -2
  75. package/latticeai/core/agent_helpers.py +493 -0
  76. package/latticeai/core/agent_prompts.py +0 -1
  77. package/latticeai/core/agent_registry.py +3 -1
  78. package/latticeai/core/agent_state.py +41 -0
  79. package/latticeai/core/audit.py +1 -1
  80. package/latticeai/core/builtin_hooks.py +2 -1
  81. package/latticeai/core/config.py +0 -1
  82. package/latticeai/core/embedding_providers.py +12 -1
  83. package/latticeai/core/file_generation.py +3 -0
  84. package/latticeai/core/invitations.py +4 -1
  85. package/latticeai/core/io_utils.py +3 -1
  86. package/latticeai/core/legacy_compatibility.py +1 -2
  87. package/latticeai/core/local_embeddings.py +12 -1
  88. package/latticeai/core/marketplace.py +1 -2
  89. package/latticeai/core/mcp_registry.py +0 -1
  90. package/latticeai/core/model_compat.py +1 -1
  91. package/latticeai/core/model_resolution.py +1 -1
  92. package/latticeai/core/network_boundary.py +168 -0
  93. package/latticeai/core/oidc.py +3 -0
  94. package/latticeai/core/permission_mode.py +6 -6
  95. package/latticeai/core/plugins.py +3 -2
  96. package/latticeai/core/policy.py +0 -1
  97. package/latticeai/core/quiet.py +84 -0
  98. package/latticeai/core/realtime.py +3 -2
  99. package/latticeai/core/run_store.py +4 -2
  100. package/latticeai/core/security.py +4 -0
  101. package/latticeai/core/users.py +5 -3
  102. package/latticeai/core/workspace_graph_trace.py +3 -0
  103. package/latticeai/core/workspace_os.py +65 -273
  104. package/latticeai/core/workspace_os_constants.py +126 -0
  105. package/latticeai/core/workspace_os_state.py +180 -0
  106. package/latticeai/core/workspace_os_utils.py +3 -1
  107. package/latticeai/core/workspace_permissions.py +1 -0
  108. package/latticeai/core/workspace_snapshots.py +3 -1
  109. package/latticeai/core/workspace_timeline.py +3 -1
  110. package/latticeai/integrations/telegram_bot.py +25 -16
  111. package/latticeai/models/router.py +6 -3
  112. package/latticeai/runtime/access_runtime.py +3 -1
  113. package/latticeai/runtime/audit_runtime.py +3 -2
  114. package/latticeai/runtime/chat_wiring.py +4 -1
  115. package/latticeai/runtime/history_runtime.py +1 -1
  116. package/latticeai/runtime/history_writer.py +138 -0
  117. package/latticeai/runtime/lifespan_runtime.py +3 -1
  118. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  119. package/latticeai/runtime/persistence_runtime.py +3 -1
  120. package/latticeai/runtime/router_registration.py +11 -1
  121. package/latticeai/services/architecture_readiness.py +1 -2
  122. package/latticeai/services/change_proposals.py +2 -1
  123. package/latticeai/services/cloud_egress_audit.py +85 -0
  124. package/latticeai/services/cloud_extraction.py +129 -0
  125. package/latticeai/services/cloud_streaming.py +266 -0
  126. package/latticeai/services/cloud_token_guard.py +84 -0
  127. package/latticeai/services/command_center.py +2 -1
  128. package/latticeai/services/folder_watch.py +3 -0
  129. package/latticeai/services/funnel_metrics.py +3 -1
  130. package/latticeai/services/hybrid_chat.py +265 -0
  131. package/latticeai/services/hybrid_context.py +228 -0
  132. package/latticeai/services/hybrid_policy.py +178 -0
  133. package/latticeai/services/memory_service.py +1 -1
  134. package/latticeai/services/model_catalog.py +10 -1
  135. package/latticeai/services/model_engines.py +35 -14
  136. package/latticeai/services/model_loading.py +3 -2
  137. package/latticeai/services/model_runtime.py +68 -17
  138. package/latticeai/services/multimodal_streaming.py +123 -0
  139. package/latticeai/services/network_boundary_service.py +154 -0
  140. package/latticeai/services/openai_compatible_adapter.py +100 -0
  141. package/latticeai/services/p_reinforce.py +4 -0
  142. package/latticeai/services/platform_runtime.py +9 -4
  143. package/latticeai/services/process_audit.py +0 -1
  144. package/latticeai/services/product_readiness.py +1 -1
  145. package/latticeai/services/run_executor.py +3 -2
  146. package/latticeai/services/search_service.py +1 -4
  147. package/latticeai/services/setup_detection.py +2 -1
  148. package/latticeai/services/tool_dispatch.py +7 -2
  149. package/latticeai/services/upload_service.py +2 -1
  150. package/latticeai/setup/auto_setup.py +10 -7
  151. package/latticeai/setup/wizard.py +15 -11
  152. package/latticeai/tools/__init__.py +3 -3
  153. package/latticeai/tools/commands.py +7 -7
  154. package/latticeai/tools/computer.py +3 -2
  155. package/latticeai/tools/documents.py +10 -9
  156. package/latticeai/tools/filesystem.py +7 -4
  157. package/latticeai/tools/knowledge.py +2 -0
  158. package/latticeai/tools/local_files.py +1 -1
  159. package/latticeai/tools/network.py +2 -1
  160. package/package.json +7 -3
  161. package/scripts/bench_agent_smoke.py +0 -1
  162. package/scripts/bench_models.py +0 -1
  163. package/scripts/brain_quality_eval.py +7 -2
  164. package/scripts/bump_version.py +2 -1
  165. package/scripts/check_current_release_docs.mjs +1 -1
  166. package/scripts/migrate_brain_storage.py +5 -1
  167. package/scripts/profile_kg.py +2 -7
  168. package/scripts/verify_hf_model_registry.py +2 -2
  169. package/src-tauri/Cargo.lock +1 -1
  170. package/src-tauri/Cargo.toml +1 -1
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +37 -37
  173. package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
  174. package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
  175. package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
  176. package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
  177. package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
  178. package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
  179. package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
  180. package/static/app/assets/Library-CXnbNiQW.js +1 -0
  181. package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
  182. package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
  183. package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
  184. package/static/app/assets/System-Bp9ZY4ER.js +1 -0
  185. package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
  186. package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
  187. package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
  188. package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
  189. package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
  190. package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
  191. package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
  192. package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
  193. package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
  194. package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
  195. package/static/app/assets/index-CEkL4_M-.css +2 -0
  196. package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
  197. package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
  198. package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
  199. package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
  200. package/static/app/assets/search-CwEJWBEA.js +1 -0
  201. package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
  202. package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
  203. package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
  204. package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
  205. package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
  206. package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
  207. package/static/app/assets/workspace-BMUEiXUm.js +1 -0
  208. package/static/app/index.html +4 -4
  209. package/static/sw.js +1 -1
  210. package/static/app/assets/Library-DhvoPvC7.js +0 -1
  211. package/static/app/assets/System-CSMdYLMy.js +0 -1
  212. package/static/app/assets/index-FxDusbr0.css +0 -2
  213. package/static/app/assets/search-DhbSgW6m.js +0 -1
  214. package/static/app/assets/workspace-DBPB0jkX.js +0 -1
@@ -0,0 +1,178 @@
1
+ """User-configurable hybrid policy (Phase 3).
2
+
3
+ Controls:
4
+
5
+ * which node types / metadata flags are blocked from leaving the machine
6
+ * whether cloud-derived KG expansion auto-commits or stages for review
7
+ * whether multimodal (video) cloud calls are allowed when cloud mode is on
8
+
9
+ Persisted under the data dir, scoped like NetworkBoundaryService
10
+ (workspace → user → default).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import threading
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional, Set
19
+
20
+ from latticeai.core.io_utils import atomic_write_json
21
+ from latticeai.core.network_boundary import (
22
+ HARD_BLOCK_METADATA_FLAGS,
23
+ HARD_BLOCK_NODE_TYPES,
24
+ )
25
+
26
+ DEFAULT_BLOCKED_TYPES: List[str] = []
27
+ DEFAULT_BLOCKED_FLAGS: List[str] = sorted(HARD_BLOCK_METADATA_FLAGS)
28
+ DEFAULT_AUTO_COMMIT = False
29
+ DEFAULT_ALLOW_MULTIMODAL = False
30
+
31
+
32
+ def _default_policy() -> Dict[str, Any]:
33
+ return {
34
+ "blocked_node_types": list(DEFAULT_BLOCKED_TYPES),
35
+ "blocked_metadata_flags": list(DEFAULT_BLOCKED_FLAGS),
36
+ "auto_commit": DEFAULT_AUTO_COMMIT,
37
+ "allow_multimodal": DEFAULT_ALLOW_MULTIMODAL,
38
+ "min_extraction_confidence": 0.55,
39
+ }
40
+
41
+
42
+ class HybridPolicyService:
43
+ """Load / save hybrid cloud policy."""
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ data_dir: Path,
49
+ audit: Optional[Callable[..., None]] = None,
50
+ ) -> None:
51
+ self._path = Path(data_dir) / "hybrid_policy.json"
52
+ self._audit = audit or (lambda *a, **kw: None)
53
+ self._lock = threading.Lock()
54
+
55
+ def rebind_data_dir(self, data_dir: Path) -> None:
56
+ with self._lock:
57
+ self._path = Path(data_dir) / "hybrid_policy.json"
58
+
59
+ def rebind_audit(self, audit: Callable[..., None]) -> None:
60
+ with self._lock:
61
+ self._audit = audit
62
+
63
+ def _read(self) -> Dict[str, Any]:
64
+ base = {
65
+ "default": _default_policy(),
66
+ "users": {},
67
+ "workspaces": {},
68
+ }
69
+ if not self._path.exists():
70
+ return base
71
+ try:
72
+ data = json.loads(self._path.read_text(encoding="utf-8"))
73
+ except Exception:
74
+ return base
75
+ if not isinstance(data, dict):
76
+ return base
77
+ data.setdefault("default", _default_policy())
78
+ data.setdefault("users", {})
79
+ data.setdefault("workspaces", {})
80
+ return data
81
+
82
+ def _write(self, data: Dict[str, Any]) -> None:
83
+ self._path.parent.mkdir(parents=True, exist_ok=True)
84
+ atomic_write_json(self._path, data)
85
+
86
+ def _resolve_raw(
87
+ self,
88
+ data: Dict[str, Any],
89
+ *,
90
+ user_email: Optional[str],
91
+ workspace_id: Optional[str],
92
+ ) -> Dict[str, Any]:
93
+ policy = dict(data.get("default") or _default_policy())
94
+ if user_email:
95
+ user = (data.get("users") or {}).get(str(user_email).lower())
96
+ if isinstance(user, dict):
97
+ policy.update(user)
98
+ if workspace_id:
99
+ ws = (data.get("workspaces") or {}).get(str(workspace_id))
100
+ if isinstance(ws, dict):
101
+ policy.update(ws)
102
+ # Always union hard-coded circuit breakers.
103
+ blocked_types: Set[str] = set(policy.get("blocked_node_types") or [])
104
+ blocked_types |= set(HARD_BLOCK_NODE_TYPES)
105
+ blocked_flags: Set[str] = set(policy.get("blocked_metadata_flags") or [])
106
+ blocked_flags |= set(HARD_BLOCK_METADATA_FLAGS)
107
+ policy["blocked_node_types"] = sorted(blocked_types)
108
+ policy["blocked_metadata_flags"] = sorted(blocked_flags)
109
+ policy["auto_commit"] = bool(policy.get("auto_commit", False))
110
+ policy["allow_multimodal"] = bool(policy.get("allow_multimodal", False))
111
+ try:
112
+ policy["min_extraction_confidence"] = float(
113
+ policy.get("min_extraction_confidence", 0.55)
114
+ )
115
+ except (TypeError, ValueError):
116
+ policy["min_extraction_confidence"] = 0.55
117
+ return policy
118
+
119
+ def resolve(
120
+ self,
121
+ *,
122
+ user_email: Optional[str] = None,
123
+ workspace_id: Optional[str] = None,
124
+ ) -> Dict[str, Any]:
125
+ with self._lock:
126
+ data = self._read()
127
+ return self._resolve_raw(data, user_email=user_email, workspace_id=workspace_id)
128
+
129
+ def set_policy(
130
+ self,
131
+ patch: Dict[str, Any],
132
+ *,
133
+ user_email: Optional[str] = None,
134
+ workspace_id: Optional[str] = None,
135
+ source: str = "api",
136
+ ) -> Dict[str, Any]:
137
+ allowed = {
138
+ "blocked_node_types",
139
+ "blocked_metadata_flags",
140
+ "auto_commit",
141
+ "allow_multimodal",
142
+ "min_extraction_confidence",
143
+ }
144
+ clean = {k: v for k, v in (patch or {}).items() if k in allowed}
145
+ with self._lock:
146
+ data = self._read()
147
+ previous = self._resolve_raw(
148
+ data, user_email=user_email, workspace_id=workspace_id
149
+ )
150
+ if workspace_id:
151
+ bucket = data.setdefault("workspaces", {})
152
+ current = dict(bucket.get(str(workspace_id)) or {})
153
+ current.update(clean)
154
+ bucket[str(workspace_id)] = current
155
+ elif user_email:
156
+ bucket = data.setdefault("users", {})
157
+ key = str(user_email).lower()
158
+ current = dict(bucket.get(key) or {})
159
+ current.update(clean)
160
+ bucket[key] = current
161
+ else:
162
+ current = dict(data.get("default") or _default_policy())
163
+ current.update(clean)
164
+ data["default"] = current
165
+ self._write(data)
166
+ resolved = self.resolve(user_email=user_email, workspace_id=workspace_id)
167
+ self._audit(
168
+ "hybrid_policy_changed",
169
+ user_email=user_email,
170
+ workspace_id=workspace_id,
171
+ previous=previous,
172
+ policy=resolved,
173
+ source=source,
174
+ )
175
+ return resolved
176
+
177
+
178
+ __all__ = ["HybridPolicyService", "DEFAULT_AUTO_COMMIT", "DEFAULT_ALLOW_MULTIMODAL"]
@@ -26,8 +26,8 @@ import logging
26
26
  from pathlib import Path
27
27
  from typing import Any, Dict, List, Optional
28
28
 
29
- from latticeai.core.workspace_os_utils import _file_size
30
29
  from latticeai.core.timeutil import now_iso as _now
30
+ from latticeai.core.workspace_os_utils import _file_size
31
31
 
32
32
  # Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
33
33
  WORKSPACE_KINDS = (
@@ -15,14 +15,23 @@ import re
15
15
  import sys
16
16
  from typing import Dict, List, Optional
17
17
 
18
+ from latticeai.services.model_capability_registry import (
19
+ LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
20
+ )
21
+
18
22
  # 5.2.0: Delegate catalog data to the structured capability registry (rich + verified).
19
23
  # This keeps backward compat for every `from ...model_catalog import ENGINE_MODEL_CATALOG`.
20
24
  from latticeai.services.model_capability_registry import (
21
25
  build_engine_model_catalog as _build_engine_model_catalog,
26
+ )
27
+ from latticeai.services.model_capability_registry import (
22
28
  get_all_capabilities as _get_all_capabilities,
29
+ )
30
+ from latticeai.services.model_capability_registry import (
23
31
  get_capability as _get_capability,
32
+ )
33
+ from latticeai.services.model_capability_registry import (
24
34
  get_verified_models as _get_verified_models,
25
- LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
26
35
  )
27
36
 
28
37
  ENGINE_INSTALLERS = {
@@ -22,8 +22,9 @@ import urllib.request
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, List, Optional
24
24
 
25
- from latticeai.services.model_errors import ModelRuntimeError
25
+ from latticeai.core.quiet import quiet
26
26
  from latticeai.services.model_catalog import ENGINE_INSTALLERS
27
+ from latticeai.services.model_errors import ModelRuntimeError
27
28
  from latticeai.services.process_audit import (
28
29
  CommandConfirmationError,
29
30
  append_process_audit_event,
@@ -127,7 +128,9 @@ def _json_request(
127
128
  def lmstudio_api_base() -> str:
128
129
  # late to avoid issues
129
130
  try:
130
- from latticeai.services.model_runtime import OPENAI_COMPATIBLE_PROVIDERS # type: ignore
131
+ from latticeai.services.model_runtime import (
132
+ OPENAI_COMPATIBLE_PROVIDERS, # type: ignore
133
+ )
131
134
  prov = OPENAI_COMPATIBLE_PROVIDERS
132
135
  except Exception:
133
136
  prov = {}
@@ -145,7 +148,7 @@ def ensure_lmstudio_server() -> None:
145
148
  _json_request(f"{base_url}/api/v1/models", headers={"Authorization": "Bearer lmstudio"}, timeout=2.5)
146
149
  return
147
150
  except Exception:
148
- pass
151
+ quiet()
149
152
 
150
153
  cli = find_lmstudio_cli()
151
154
  if not cli:
@@ -180,7 +183,7 @@ def ensure_ollama_server() -> None:
180
183
  if probe.returncode == 0:
181
184
  return
182
185
  except Exception:
183
- pass
186
+ quiet()
184
187
  subprocess.Popen(
185
188
  [ollama, "serve"],
186
189
  stdout=subprocess.DEVNULL,
@@ -194,13 +197,16 @@ def ensure_ollama_server() -> None:
194
197
  if probe.returncode == 0:
195
198
  return
196
199
  except Exception:
197
- pass
200
+ quiet()
198
201
  time.sleep(0.5)
199
202
  raise ModelRuntimeError(status_code=500, detail="Ollama 서버를 자동으로 시작하지 못했습니다.")
200
203
 
201
204
 
202
205
  def get_openai_compatible_server_models(provider: str) -> List[str]:
203
- from latticeai.services.model_runtime import OPENAI_COMPATIBLE_PROVIDERS, get_lmstudio_models
206
+ from latticeai.services.model_runtime import (
207
+ OPENAI_COMPATIBLE_PROVIDERS,
208
+ get_lmstudio_models,
209
+ )
204
210
 
205
211
  if provider == "lmstudio":
206
212
  models = []
@@ -255,7 +261,11 @@ def wait_for_openai_compatible_server(provider: str, model_name: Optional[str] =
255
261
 
256
262
 
257
263
  def ensure_vllm_server(model_name: str) -> None:
258
- from latticeai.services.model_runtime import download_hf_model, hf_model_dir, hf_model_ready
264
+ from latticeai.services.model_runtime import (
265
+ download_hf_model,
266
+ hf_model_dir,
267
+ hf_model_ready,
268
+ )
259
269
 
260
270
  served_models = get_openai_compatible_server_models("vllm")
261
271
  if model_name in served_models:
@@ -301,7 +311,11 @@ def ensure_vllm_server(model_name: str) -> None:
301
311
 
302
312
 
303
313
  def ensure_llamacpp_server(model_name: str) -> None:
304
- from latticeai.services.model_runtime import download_hf_model, hf_model_dir, hf_model_ready
314
+ from latticeai.services.model_runtime import (
315
+ download_hf_model,
316
+ hf_model_dir,
317
+ hf_model_ready,
318
+ )
305
319
 
306
320
  served_models = get_openai_compatible_server_models("llamacpp")
307
321
  if model_name in served_models:
@@ -368,7 +382,7 @@ def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict
368
382
  last_percent: Optional[float] = None
369
383
  lines: List[str] = []
370
384
  try:
371
- assert process.stdout is not None
385
+ assert process.stdout is not None # noqa: S101 — invariant guard on a code path with no caller-facing error
372
386
  for raw_line in process.stdout:
373
387
  for part in re.split(r"[\r\n]+", raw_line):
374
388
  line = part.strip()
@@ -597,17 +611,24 @@ async def _smoke_test_loaded_model(
597
611
  """
598
612
  # late imports to avoid circular and keep lattice_brain/latticeai clean
599
613
  try:
600
- from latticeai.services.model_runtime import (
601
- _LOCAL_SMOKE_ENGINES,
602
- _SMOKE_PROMPT,
614
+ import asyncio
615
+
616
+ from latticeai.core.model_compat import (
617
+ classify_smoke_response as _classify_smoke_response,
603
618
  )
604
619
  from latticeai.core.model_compat import (
605
620
  ensure_profile as _ensure_compat_profile,
621
+ )
622
+ from latticeai.core.model_compat import (
606
623
  fast_postprocess as _compat_fast_postprocess,
607
- classify_smoke_response as _classify_smoke_response,
624
+ )
625
+ from latticeai.core.model_compat import (
608
626
  record_smoke_result as _record_smoke_result,
609
627
  )
610
- import asyncio
628
+ from latticeai.services.model_runtime import (
629
+ _LOCAL_SMOKE_ENGINES,
630
+ _SMOKE_PROMPT,
631
+ )
611
632
  except Exception as e:
612
633
  return {"ok": False, "reason": f"smoke import failed: {e}", "skipped": True}
613
634
 
@@ -17,16 +17,18 @@ from typing import Any, AsyncIterator, Dict, Optional
17
17
 
18
18
  from .model_errors import ModelRuntimeError
19
19
 
20
+
20
21
  # Late imports to avoid circulars during extraction. The state argument is
21
22
  # mandatory so this module cannot fall back to ambient process configuration.
22
23
  def _get_model_runtime_deps(runtime_state: Any):
23
24
  from .model_runtime import (
25
+ MODEL_ENGINE_ALIASES,
24
26
  _download_allowed,
25
27
  _download_block,
26
28
  _engine_install_block,
27
29
  _friendly_model_runtime_error,
28
- _ModelResolution,
29
30
  _model_runtime_compatibility,
31
+ _ModelResolution,
30
32
  _smoke_test_loaded_model,
31
33
  download_hf_model,
32
34
  engine_installed,
@@ -40,7 +42,6 @@ def _get_model_runtime_deps(runtime_state: Any):
40
42
  hf_model_dir,
41
43
  hf_model_ready,
42
44
  local_binary,
43
- MODEL_ENGINE_ALIASES,
44
45
  model_download_progress_payload,
45
46
  normalize_local_model_request,
46
47
  parse_model_ref,
@@ -20,42 +20,78 @@ from dataclasses import dataclass, field, fields
20
20
  from pathlib import Path
21
21
  from typing import Any, AsyncIterator, Callable, Dict, List, Optional
22
22
 
23
- from .model_errors import ModelRuntimeError
24
-
23
+ from latticeai.core.model_compat import (
24
+ SMOKE_PROMPT as _SMOKE_PROMPT,
25
+ )
26
+ from latticeai.core.model_compat import (
27
+ friendly_model_runtime_error as _friendly_model_runtime_error,
28
+ )
29
+ from latticeai.core.model_compat import (
30
+ model_runtime_compatibility as _model_runtime_compatibility,
31
+ )
32
+ from latticeai.core.model_resolution import ModelResolution as _ModelResolution
25
33
  from latticeai.models.router import (
26
- AsyncOpenAI,
27
34
  HF_MODELS_ROOT,
28
35
  OPENAI_COMPATIBLE_PROVIDERS,
36
+ AsyncOpenAI,
29
37
  ensure_mlx_runtime,
30
38
  hf_cache_model_dir,
31
39
  hf_model_dir,
32
40
  parse_model_ref,
33
41
  )
34
- from latticeai.core.model_resolution import ModelResolution as _ModelResolution
35
- from latticeai.core.model_compat import (
36
- friendly_model_runtime_error as _friendly_model_runtime_error,
37
- model_runtime_compatibility as _model_runtime_compatibility,
38
- SMOKE_PROMPT as _SMOKE_PROMPT,
42
+
43
+ from .model_engines import (
44
+ LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
45
+ )
46
+ from .model_engines import (
47
+ engine_install_plan as _engine_install_plan,
48
+ )
49
+ from .model_engines import (
50
+ engine_support_status as _engine_support_status,
51
+ )
52
+ from .model_engines import (
53
+ ensure_llamacpp_server as _ensure_llamacpp_server,
39
54
  )
40
55
  from .model_engines import (
41
56
  ensure_lmstudio_server as _ensure_lmstudio_server,
57
+ )
58
+ from .model_engines import (
42
59
  ensure_ollama_server as _ensure_ollama_server,
60
+ )
61
+ from .model_engines import (
43
62
  ensure_vllm_server as _ensure_vllm_server,
44
- ensure_llamacpp_server as _ensure_llamacpp_server,
63
+ )
64
+ from .model_engines import (
45
65
  find_lmstudio_cli as _find_lmstudio_cli,
46
- get_openai_compatible_server_models as _get_openai_compatible_server_models,
47
- pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
66
+ )
67
+ from .model_engines import (
48
68
  get_ollama_pulled_models as _get_ollama_pulled_models,
49
- engine_support_status as _engine_support_status,
50
- engine_install_plan as _engine_install_plan,
69
+ )
70
+ from .model_engines import (
71
+ get_openai_compatible_server_models as _get_openai_compatible_server_models,
72
+ )
73
+ from .model_engines import (
51
74
  install_engine as _install_engine,
75
+ )
76
+ from .model_engines import (
52
77
  local_binary as _local_binary,
78
+ )
79
+ from .model_engines import (
80
+ pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
81
+ )
82
+ from .model_engines import (
53
83
  vllm_executable as _vllm_executable,
84
+ )
85
+ from .model_engines import (
54
86
  vllm_metal_python as _vllm_metal_python,
87
+ )
88
+ from .model_engines import (
55
89
  wait_for_openai_compatible_server as _wait_for_openai_compatible_server,
90
+ )
91
+ from .model_engines import (
56
92
  windows_binary_candidates as _windows_binary_candidates,
57
- LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
58
93
  )
94
+ from .model_errors import ModelRuntimeError
59
95
 
60
96
  # ``model_loading._get_model_runtime_deps`` imports these private names from
61
97
  # this module to preserve the historical model_runtime wiring surface.
@@ -174,16 +210,20 @@ def configure_model_runtime(**deps: Any) -> "ModelRuntimeService":
174
210
  # Catalog data + version-dedup helpers live in ``model_catalog``; re-exported
175
211
  # here so existing ``from ...model_runtime import ENGINE_MODEL_CATALOG`` imports
176
212
  # keep working.
213
+ from latticeai.core.quiet import ( # noqa: E402 — re-export placed after the globals it documents
214
+ quiet, # noqa: E402 — re-export placed after the globals it documents
215
+ )
177
216
  from latticeai.services.model_catalog import ( # noqa: E402, F401 (re-export after the module globals it documents)
217
+ _VERSIONED_MODEL_PATTERNS,
178
218
  ENGINE_INSTALLERS,
179
219
  ENGINE_MODEL_CATALOG,
180
220
  MODEL_ENGINE_ALIASES,
181
- _VERSIONED_MODEL_PATTERNS,
182
221
  _model_family_version,
183
222
  _version_tuple,
184
223
  filter_lower_family_versions,
185
224
  )
186
225
 
226
+
187
227
  def _update_env_file(env_file: Path, key: str, value: str) -> None:
188
228
  lines = []
189
229
  found = False
@@ -471,7 +511,7 @@ def hf_repo_files_with_sizes(repo_id: str) -> List[Dict[str, object]]:
471
511
  if files:
472
512
  return files
473
513
  except TypeError:
474
- pass
514
+ quiet()
475
515
  except Exception as e:
476
516
  logging.warning("huggingface model_info failed for %s: %s", repo_id, e)
477
517
 
@@ -562,7 +602,18 @@ def download_hf_model(
562
602
  downloaded_before = downloaded_bytes
563
603
  last_emit = {"at": 0.0, "percent": -1.0}
564
604
 
565
- def emit_byte_progress(done_bytes: float) -> None:
605
+ def emit_byte_progress(
606
+ done_bytes: float,
607
+ # Bound per iteration: this callback outlives the loop
608
+ # body when a download runs long, and late binding
609
+ # would report every file's progress against the last
610
+ # file's offsets.
611
+ downloaded_before: int = downloaded_before,
612
+ size: Any = size,
613
+ index: int = index,
614
+ last_emit: dict = last_emit,
615
+ filename: str = filename,
616
+ ) -> None:
566
617
  done = max(0, int(done_bytes or 0))
567
618
  if total_bytes:
568
619
  aggregate = min(total_bytes, downloaded_before + done)
@@ -0,0 +1,123 @@
1
+ """Multimodal / video streaming contracts behind NetworkBoundaryMode (Phase 3).
2
+
3
+ Video generation and other multimodal cloud calls share the same boundary dial
4
+ as text: nothing leaves the machine unless mode is CLOUD_ALLOWED *and* the
5
+ hybrid policy has ``allow_multimodal=True``.
6
+
7
+ This module is intentionally adapter-shaped: concrete providers (Runway, Luma,
8
+ Veo-compatible endpoints, etc.) plug in later without changing the chat path.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, AsyncIterator, Dict, List, Optional, Protocol
15
+
16
+ from latticeai.core.network_boundary import (
17
+ NetworkBoundaryMode,
18
+ normalize_network_mode,
19
+ )
20
+ from latticeai.services.hybrid_context import MinimalContext
21
+
22
+
23
+ @dataclass
24
+ class MultimodalTurnResult:
25
+ user_message: str
26
+ media_urls: List[str] = field(default_factory=list)
27
+ media_kind: str = "video" # video | image | audio
28
+ answer_text: str = ""
29
+ sent_node_ids: List[str] = field(default_factory=list)
30
+ provider: str = ""
31
+ model: str = ""
32
+ usage: Dict[str, Any] = field(default_factory=dict)
33
+
34
+ def to_dict(self) -> Dict[str, Any]:
35
+ return {
36
+ "user_message": self.user_message,
37
+ "media_urls": list(self.media_urls),
38
+ "media_kind": self.media_kind,
39
+ "answer_text": self.answer_text,
40
+ "sent_node_ids": list(self.sent_node_ids),
41
+ "provider": self.provider,
42
+ "model": self.model,
43
+ "usage": dict(self.usage),
44
+ }
45
+
46
+
47
+ class MultimodalAdapter(Protocol):
48
+ async def stream_media(
49
+ self,
50
+ *,
51
+ prompt: str,
52
+ context: str,
53
+ model: Optional[str] = None,
54
+ ) -> AsyncIterator[Dict[str, Any]]:
55
+ """Yield progress events, then a final event with media_urls."""
56
+ ...
57
+
58
+
59
+ class MultimodalStreamingBridge:
60
+ """Boundary-gated multimodal turn orchestrator."""
61
+
62
+ def __init__(self, adapter: Optional[MultimodalAdapter] = None) -> None:
63
+ self._adapter = adapter
64
+
65
+ async def run_turn(
66
+ self,
67
+ *,
68
+ user_message: str,
69
+ minimal: MinimalContext,
70
+ mode: NetworkBoundaryMode | str,
71
+ allow_multimodal: bool,
72
+ model: Optional[str] = None,
73
+ ) -> MultimodalTurnResult:
74
+ mode = normalize_network_mode(mode)
75
+ if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
76
+ raise PermissionError(
77
+ f"multimodal cloud refused under NetworkBoundaryMode={mode.value!r}"
78
+ )
79
+ if not allow_multimodal:
80
+ raise PermissionError(
81
+ "multimodal cloud is disabled by hybrid policy (allow_multimodal=false)"
82
+ )
83
+ if self._adapter is None:
84
+ return MultimodalTurnResult(
85
+ user_message=user_message,
86
+ media_urls=[],
87
+ answer_text=(
88
+ "[multimodal adapter not configured] "
89
+ f"Would stream media grounded on {len(minimal.node_ids)} local node(s)."
90
+ ),
91
+ sent_node_ids=list(minimal.node_ids),
92
+ provider="none",
93
+ model=model or "",
94
+ )
95
+
96
+ urls: List[str] = []
97
+ notes: List[str] = []
98
+ async for event in self._adapter.stream_media(
99
+ prompt=user_message,
100
+ context=minimal.compact_text,
101
+ model=model,
102
+ ):
103
+ if not isinstance(event, dict):
104
+ continue
105
+ if event.get("media_url"):
106
+ urls.append(str(event["media_url"]))
107
+ if event.get("text"):
108
+ notes.append(str(event["text"]))
109
+ return MultimodalTurnResult(
110
+ user_message=user_message,
111
+ media_urls=urls,
112
+ answer_text="\n".join(notes),
113
+ sent_node_ids=list(minimal.node_ids),
114
+ provider=getattr(self._adapter, "provider_name", "multimodal"),
115
+ model=model or getattr(self._adapter, "default_model", ""),
116
+ )
117
+
118
+
119
+ __all__ = [
120
+ "MultimodalTurnResult",
121
+ "MultimodalAdapter",
122
+ "MultimodalStreamingBridge",
123
+ ]