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.
- package/README.md +43 -32
- package/docs/CHANGELOG.md +204 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
- package/docs/MYPY_BACKLOG.md +98 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +5 -4
- package/lattice_brain/conversations.py +14 -3
- package/lattice_brain/embeddings.py +12 -2
- package/lattice_brain/graph/_kg_common.py +5 -5
- package/lattice_brain/graph/_kg_fsutil.py +5 -4
- package/lattice_brain/graph/discovery.py +4 -3
- package/lattice_brain/graph/discovery_index.py +0 -1
- package/lattice_brain/graph/documents.py +3 -2
- package/lattice_brain/graph/fusion.py +4 -0
- package/lattice_brain/graph/ingest.py +12 -2
- package/lattice_brain/graph/projection.py +3 -2
- package/lattice_brain/graph/provenance.py +5 -3
- package/lattice_brain/graph/rerank.py +4 -1
- package/lattice_brain/graph/retrieval.py +0 -1
- package/lattice_brain/graph/retrieval_docgen.py +0 -1
- package/lattice_brain/graph/retrieval_reads.py +0 -1
- package/lattice_brain/graph/retrieval_vector.py +0 -1
- package/lattice_brain/graph/schema.py +4 -3
- package/lattice_brain/graph/store.py +18 -4
- package/lattice_brain/graph/write_master.py +46 -1
- package/lattice_brain/ingestion.py +4 -1
- package/lattice_brain/portability.py +5 -2
- package/lattice_brain/quality.py +12 -5
- package/lattice_brain/quiet.py +43 -0
- package/lattice_brain/runtime/agent_runtime.py +19 -10
- package/lattice_brain/runtime/hooks.py +3 -2
- package/lattice_brain/runtime/multi_agent.py +2 -3
- package/lattice_brain/sensitivity.py +94 -0
- package/lattice_brain/storage/base.py +30 -2
- package/lattice_brain/storage/migration.py +3 -2
- package/lattice_brain/storage/postgres.py +2 -2
- package/lattice_brain/storage/sqlite.py +6 -4
- package/lattice_brain/workflow.py +4 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +4 -2
- package/latticeai/api/auth.py +5 -1
- package/latticeai/api/automation_intelligence.py +2 -1
- package/latticeai/api/browser.py +3 -2
- package/latticeai/api/chat.py +28 -17
- package/latticeai/api/chat_agent_http.py +22 -8
- package/latticeai/api/chat_contracts.py +4 -0
- package/latticeai/api/chat_documents.py +6 -2
- package/latticeai/api/chat_hybrid.py +82 -0
- package/latticeai/api/computer_use.py +8 -3
- package/latticeai/api/knowledge_graph.py +1 -1
- package/latticeai/api/mcp.py +4 -4
- package/latticeai/api/models.py +5 -2
- package/latticeai/api/network_boundary.py +220 -0
- package/latticeai/api/permissions.py +0 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/security_dashboard.py +1 -1
- package/latticeai/api/setup.py +16 -3
- package/latticeai/api/static_routes.py +2 -1
- package/latticeai/api/tools.py +12 -8
- package/latticeai/api/voice_capture.py +3 -1
- package/latticeai/api/workflow_designer.py +1 -1
- package/latticeai/api/workspace.py +1 -2
- package/latticeai/app_factory.py +174 -145
- package/latticeai/cli/entrypoint.py +6 -4
- package/latticeai/core/agent.py +55 -495
- package/latticeai/core/agent_eval.py +2 -2
- package/latticeai/core/agent_helpers.py +493 -0
- package/latticeai/core/agent_prompts.py +0 -1
- package/latticeai/core/agent_registry.py +3 -1
- package/latticeai/core/agent_state.py +41 -0
- package/latticeai/core/audit.py +1 -1
- package/latticeai/core/builtin_hooks.py +2 -1
- package/latticeai/core/config.py +0 -1
- package/latticeai/core/embedding_providers.py +12 -1
- package/latticeai/core/file_generation.py +3 -0
- package/latticeai/core/invitations.py +4 -1
- package/latticeai/core/io_utils.py +3 -1
- package/latticeai/core/legacy_compatibility.py +1 -2
- package/latticeai/core/local_embeddings.py +12 -1
- package/latticeai/core/marketplace.py +1 -2
- package/latticeai/core/mcp_registry.py +0 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/network_boundary.py +168 -0
- package/latticeai/core/oidc.py +3 -0
- package/latticeai/core/permission_mode.py +6 -6
- package/latticeai/core/plugins.py +3 -2
- package/latticeai/core/policy.py +0 -1
- package/latticeai/core/quiet.py +84 -0
- package/latticeai/core/realtime.py +3 -2
- package/latticeai/core/run_store.py +4 -2
- package/latticeai/core/security.py +4 -0
- package/latticeai/core/users.py +5 -3
- package/latticeai/core/workspace_graph_trace.py +3 -0
- package/latticeai/core/workspace_os.py +65 -273
- package/latticeai/core/workspace_os_constants.py +126 -0
- package/latticeai/core/workspace_os_state.py +180 -0
- package/latticeai/core/workspace_os_utils.py +3 -1
- package/latticeai/core/workspace_permissions.py +1 -0
- package/latticeai/core/workspace_snapshots.py +3 -1
- package/latticeai/core/workspace_timeline.py +3 -1
- package/latticeai/integrations/telegram_bot.py +25 -16
- package/latticeai/models/router.py +6 -3
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/audit_runtime.py +3 -2
- package/latticeai/runtime/chat_wiring.py +4 -1
- package/latticeai/runtime/history_runtime.py +1 -1
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/lifespan_runtime.py +3 -1
- package/latticeai/runtime/network_boundary_wiring.py +124 -0
- package/latticeai/runtime/persistence_runtime.py +3 -1
- package/latticeai/runtime/router_registration.py +11 -1
- package/latticeai/services/architecture_readiness.py +1 -2
- package/latticeai/services/change_proposals.py +2 -1
- package/latticeai/services/cloud_egress_audit.py +85 -0
- package/latticeai/services/cloud_extraction.py +129 -0
- package/latticeai/services/cloud_streaming.py +266 -0
- package/latticeai/services/cloud_token_guard.py +84 -0
- package/latticeai/services/command_center.py +2 -1
- package/latticeai/services/folder_watch.py +3 -0
- package/latticeai/services/funnel_metrics.py +3 -1
- package/latticeai/services/hybrid_chat.py +265 -0
- package/latticeai/services/hybrid_context.py +228 -0
- package/latticeai/services/hybrid_policy.py +178 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +10 -1
- package/latticeai/services/model_engines.py +35 -14
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime.py +68 -17
- package/latticeai/services/multimodal_streaming.py +123 -0
- package/latticeai/services/network_boundary_service.py +154 -0
- package/latticeai/services/openai_compatible_adapter.py +100 -0
- package/latticeai/services/p_reinforce.py +4 -0
- package/latticeai/services/platform_runtime.py +9 -4
- package/latticeai/services/process_audit.py +0 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +3 -2
- package/latticeai/services/search_service.py +1 -4
- package/latticeai/services/setup_detection.py +2 -1
- package/latticeai/services/tool_dispatch.py +7 -2
- package/latticeai/services/upload_service.py +2 -1
- package/latticeai/setup/auto_setup.py +10 -7
- package/latticeai/setup/wizard.py +15 -11
- package/latticeai/tools/__init__.py +3 -3
- package/latticeai/tools/commands.py +7 -7
- package/latticeai/tools/computer.py +3 -2
- package/latticeai/tools/documents.py +10 -9
- package/latticeai/tools/filesystem.py +7 -4
- package/latticeai/tools/knowledge.py +2 -0
- package/latticeai/tools/local_files.py +1 -1
- package/latticeai/tools/network.py +2 -1
- package/package.json +7 -3
- package/scripts/bench_agent_smoke.py +0 -1
- package/scripts/bench_models.py +0 -1
- package/scripts/brain_quality_eval.py +7 -2
- package/scripts/bump_version.py +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/migrate_brain_storage.py +5 -1
- package/scripts/profile_kg.py +2 -7
- package/scripts/verify_hf_model_registry.py +2 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
- package/static/app/assets/Library-CXnbNiQW.js +1 -0
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
- package/static/app/assets/System-Bp9ZY4ER.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
- package/static/app/assets/search-CwEJWBEA.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
- package/static/app/assets/workspace-BMUEiXUm.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-DhvoPvC7.js +0 -1
- package/static/app/assets/System-CSMdYLMy.js +0 -1
- package/static/app/assets/index-FxDusbr0.css +0 -2
- package/static/app/assets/search-DhbSgW6m.js +0 -1
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
624
|
+
)
|
|
625
|
+
from latticeai.core.model_compat import (
|
|
608
626
|
record_smoke_result as _record_smoke_result,
|
|
609
627
|
)
|
|
610
|
-
import
|
|
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 .
|
|
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
|
-
|
|
35
|
-
from
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
63
|
+
)
|
|
64
|
+
from .model_engines import (
|
|
45
65
|
find_lmstudio_cli as _find_lmstudio_cli,
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
)
|
|
67
|
+
from .model_engines import (
|
|
48
68
|
get_ollama_pulled_models as _get_ollama_pulled_models,
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
]
|