ltcai 11.5.1 → 11.5.2
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 +85 -53
- package/docs/CHANGELOG.md +54 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- 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/WORKFLOW_DESIGNER.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +1 -1
- package/lattice_brain/graph/projection/v2_schema.py +0 -28
- package/lattice_brain/graph/vector_index/base.py +0 -3
- package/lattice_brain/graph/vector_index/brute_force.py +0 -4
- package/lattice_brain/graph/vector_index/hnsw.py +0 -3
- package/lattice_brain/graph/vector_index/quantized.py +0 -3
- package/lattice_brain/ingestion_jobs.py +0 -3
- package/lattice_brain/memory.py +0 -27
- package/lattice_brain/portability/sharing.py +0 -4
- package/lattice_brain/quality.py +0 -49
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/hooks.py +0 -13
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +0 -4
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +12 -12
- package/latticeai/api/auth.py +25 -2
- package/latticeai/api/chat.py +3 -3
- package/latticeai/api/chat_agent_http.py +3 -12
- package/latticeai/api/chat_helpers.py +10 -13
- package/latticeai/api/computer_use.py +31 -42
- package/latticeai/api/health.py +21 -1
- package/latticeai/api/local_files.py +14 -0
- package/latticeai/api/permissions.py +38 -12
- package/latticeai/api/search.py +24 -0
- package/latticeai/api/static_routes.py +4 -1
- package/latticeai/cli/runtime.py +7 -3
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/enterprise.py +1 -4
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/invitations.py +3 -2
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +5 -0
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/run_explain.py +5 -2
- package/latticeai/core/run_store.py +2 -2
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/core/workspace_graph_trace.py +2 -8
- package/latticeai/core/workspace_os.py +0 -10
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +34 -0
- package/latticeai/core/workspace_review_items.py +2 -8
- package/latticeai/core/workspace_runs.py +2 -8
- package/latticeai/core/workspace_skills.py +2 -7
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -4
- package/latticeai/runtime/build_phases/features.py +2 -0
- package/latticeai/runtime/build_phases/web.py +2 -0
- package/latticeai/runtime/feature_toggle_wiring.py +12 -18
- package/latticeai/runtime/network_boundary_wiring.py +8 -19
- package/latticeai/runtime/permission_mode_wiring.py +6 -13
- package/latticeai/runtime/router_registration.py +4 -0
- package/latticeai/runtime/runtime_context.py +1 -13
- package/latticeai/runtime/service_singletons.py +55 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence/proposals.py +0 -5
- package/latticeai/services/change_proposals.py +2 -36
- package/latticeai/services/chronicle.py +4 -6
- package/latticeai/services/command_center.py +4 -4
- package/latticeai/services/evidence_actions.py +5 -2
- package/latticeai/services/hybrid_chat.py +2 -2
- package/latticeai/services/hybrid_policy.py +8 -57
- package/latticeai/services/mode_store.py +132 -0
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +0 -57
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- package/latticeai/services/network_boundary_service.py +7 -44
- package/latticeai/services/permission_mode_service.py +8 -56
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/setup_detection.py +67 -1
- package/latticeai/services/tool_dispatch.py +0 -4
- package/latticeai/services/upload_service.py +7 -20
- package/latticeai/services/workspace_service.py +0 -6
- package/latticeai/setup/auto_setup.py +15 -26
- package/latticeai/setup/wizard/catalog.py +5 -2
- package/latticeai/setup/wizard/detect.py +6 -27
- package/latticeai/setup/wizard/paths.py +2 -5
- package/latticeai/setup/wizard/plans.py +2 -2
- package/package.json +2 -4
- package/scripts/bump_version.py +14 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_legacy_debt.mjs +1 -1
- package/scripts/generate_agent_loop_fixtures.py +87 -0
- package/scripts/generate_rust_parity_fixtures.py +3 -95
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/release_screen_claims.json +10 -0
- package/src-tauri/Cargo.lock +7 -7
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +76 -2
- package/src-tauri/src/main.rs +13 -3
- package/src-tauri/tauri.conf.json +2 -2
- package/static/app/asset-manifest.json +40 -40
- package/static/app/assets/{Act-Drs_jd-O.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-C7bdNmz-.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-CYxeuraA.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-DcRAq7wj.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-BK2xQ9mN.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BZNztFBb.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-CjueubVZ.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-B_MyTfYr.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-CTy0EByV.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-Dd6abJHX.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/{input-bVgIRN3s.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-BLzo7U4L.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-D3u_yv7B.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-H3bMjYXC.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +3 -3
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- package/latticeai/core/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/local_embeddings.py +0 -104
- package/latticeai/core/multi_agent.py +0 -11
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/kg_portability.py +0 -11
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/static/app/assets/arrow-left-BHYmOTWU.js +0 -1
- package/static/app/assets/circle-check-CLeWJr67.js +0 -1
- package/static/app/assets/search-BgSVX6NG.js +0 -1
|
@@ -83,6 +83,7 @@ def build_auth_admin_security_router_bundle(
|
|
|
83
83
|
secure_cookies: bool,
|
|
84
84
|
invite_authorized: Any,
|
|
85
85
|
ensure_identity: Any,
|
|
86
|
+
sso_default_redirect_uri: str,
|
|
86
87
|
create_admin_router: Any,
|
|
87
88
|
require_admin: Any,
|
|
88
89
|
get_history: Any,
|
|
@@ -138,6 +139,7 @@ def build_auth_admin_security_router_bundle(
|
|
|
138
139
|
invite_gate_enabled=invite_gate_enabled,
|
|
139
140
|
invite_authorized=invite_authorized,
|
|
140
141
|
ensure_identity=ensure_identity,
|
|
142
|
+
default_redirect_uri=sso_default_redirect_uri,
|
|
141
143
|
)
|
|
142
144
|
|
|
143
145
|
def graph_stats_safe() -> dict[str, Any]:
|
|
@@ -376,6 +378,7 @@ def register_health_and_model_routers(
|
|
|
376
378
|
engine_status: Any,
|
|
377
379
|
get_current_user: Any,
|
|
378
380
|
require_auth: bool,
|
|
381
|
+
externally_reachable: bool,
|
|
379
382
|
app_version: str,
|
|
380
383
|
app_mode: str,
|
|
381
384
|
create_models_router: Any,
|
|
@@ -411,6 +414,7 @@ def register_health_and_model_routers(
|
|
|
411
414
|
engine_status=engine_status,
|
|
412
415
|
get_current_user=get_current_user,
|
|
413
416
|
require_auth=require_auth,
|
|
417
|
+
externally_reachable=externally_reachable,
|
|
414
418
|
app_version=app_version,
|
|
415
419
|
app_mode=app_mode,
|
|
416
420
|
),
|
|
@@ -25,7 +25,7 @@ Two properties keep it honest:
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
-
from typing import Any, Dict,
|
|
28
|
+
from typing import Any, Dict, Set
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class RuntimeContext:
|
|
@@ -257,21 +257,9 @@ class RuntimeContext:
|
|
|
257
257
|
f"RuntimeContext.{name} was read before it was built{hint}"
|
|
258
258
|
) from None
|
|
259
259
|
|
|
260
|
-
@property
|
|
261
|
-
def produced_by(self) -> Dict[str, str]:
|
|
262
|
-
"""Attribute name → the phase that published it, for the order test."""
|
|
263
|
-
return dict(self._produced)
|
|
264
|
-
|
|
265
260
|
def names(self) -> Set[str]:
|
|
266
261
|
return set(self._produced)
|
|
267
262
|
|
|
268
|
-
def phases_run(self) -> List[str]:
|
|
269
|
-
seen: List[str] = []
|
|
270
|
-
for phase in self._produced.values():
|
|
271
|
-
if phase not in seen:
|
|
272
|
-
seen.append(phase)
|
|
273
|
-
return seen
|
|
274
|
-
|
|
275
263
|
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
276
264
|
return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
|
|
277
265
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The rebinding half of the process-wide service singletons.
|
|
2
|
+
|
|
3
|
+
Four wiring modules create one service each — the permission dial, the network
|
|
4
|
+
boundary, the hybrid policy, the feature switchboard — and all four had the
|
|
5
|
+
same problem and the same four-line answer to it: a lazy first caller (a tool
|
|
6
|
+
dispatch, a feature gate) can ask for the service *before* routers know the
|
|
7
|
+
configured data dir or the audit sink, and if the later explicit call were
|
|
8
|
+
allowed to be a no-op the service would stay pinned to the fallback path with
|
|
9
|
+
its audit events going nowhere.
|
|
10
|
+
|
|
11
|
+
So explicit arguments rebind rather than being dropped. That rule was written
|
|
12
|
+
out four times; it lives here once, because "the second caller silently loses"
|
|
13
|
+
is precisely the kind of divergence that produces a second, invisible copy of
|
|
14
|
+
the user's data and no failing test.
|
|
15
|
+
|
|
16
|
+
The singleton *slot* deliberately stays a module global in each wiring module:
|
|
17
|
+
the modules are the unit of process state, and the suite resets them by
|
|
18
|
+
assigning ``_SHARED = None``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
25
|
+
|
|
26
|
+
from latticeai.core.config import default_data_dir
|
|
27
|
+
|
|
28
|
+
ServiceT = TypeVar("ServiceT")
|
|
29
|
+
|
|
30
|
+
__all__ = ["singleton_data_dir", "rebind_singleton"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def singleton_data_dir(data_dir: Optional[Path]) -> Path:
|
|
34
|
+
"""Where a *first* lazy caller should put the store."""
|
|
35
|
+
return Path(data_dir) if data_dir is not None else default_data_dir()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def rebind_singleton(
|
|
39
|
+
service: ServiceT,
|
|
40
|
+
*,
|
|
41
|
+
data_dir: Optional[Path] = None,
|
|
42
|
+
audit: Optional[Callable[..., None]] = None,
|
|
43
|
+
) -> ServiceT:
|
|
44
|
+
"""Apply explicit arguments to an already-created singleton, then return it.
|
|
45
|
+
|
|
46
|
+
``None`` means "the caller did not say", never "unset it" — a router that
|
|
47
|
+
mounts without an audit sink must not silence a sink an earlier caller
|
|
48
|
+
already supplied.
|
|
49
|
+
"""
|
|
50
|
+
rebindable: Any = service
|
|
51
|
+
if data_dir is not None:
|
|
52
|
+
rebindable.rebind_data_dir(Path(data_dir))
|
|
53
|
+
if audit is not None:
|
|
54
|
+
rebindable.rebind_audit(audit)
|
|
55
|
+
return service
|
|
@@ -31,11 +31,6 @@ class BrainProposalsMixin(_Core):
|
|
|
31
31
|
# is absent they report ``available: False`` rather than falling back to a
|
|
32
32
|
# direct write.
|
|
33
33
|
|
|
34
|
-
def attach_review_queue(self, review_queue: Any) -> None:
|
|
35
|
-
"""Bind the review queue proposals are written to (composition root)."""
|
|
36
|
-
self._review_queue_service = review_queue
|
|
37
|
-
self._synthesizer = None
|
|
38
|
-
|
|
39
34
|
def _review_queue(self) -> Any:
|
|
40
35
|
"""The queue to propose into, or ``None`` when there is none.
|
|
41
36
|
|
|
@@ -21,7 +21,6 @@ change audit log. The classification itself lives in
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import difflib
|
|
24
|
-
import hashlib
|
|
25
24
|
import logging
|
|
26
25
|
import os
|
|
27
26
|
import tempfile
|
|
@@ -30,6 +29,7 @@ from pathlib import Path
|
|
|
30
29
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
31
30
|
|
|
32
31
|
from latticeai.core.quiet import quiet
|
|
32
|
+
from latticeai.core.security import sha256_hex
|
|
33
33
|
from latticeai.core.tool_governor import classify_tool_call
|
|
34
34
|
from latticeai.core.workspace_reorganization import (
|
|
35
35
|
REORG_KIND,
|
|
@@ -90,7 +90,7 @@ class ProposalConflictError(ValueError):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def _sha256_text(content: str) -> str:
|
|
93
|
-
return
|
|
93
|
+
return sha256_hex(content)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def _unified_diff(before: str, after: str, path: str) -> List[str]:
|
|
@@ -272,40 +272,6 @@ class ChangeProposalService:
|
|
|
272
272
|
)
|
|
273
273
|
return item
|
|
274
274
|
|
|
275
|
-
def propose_file_delete(
|
|
276
|
-
self,
|
|
277
|
-
*,
|
|
278
|
-
path: str,
|
|
279
|
-
proposed_by: str = "agent",
|
|
280
|
-
reason: str = "",
|
|
281
|
-
user_email: Optional[str] = None,
|
|
282
|
-
workspace_id: Optional[str] = None,
|
|
283
|
-
) -> Dict[str, Any]:
|
|
284
|
-
base_exists, before = self._snapshot(path)
|
|
285
|
-
item = self._review_queue.create(
|
|
286
|
-
title=f"파일 삭제 제안: {path}",
|
|
287
|
-
summary=reason or "기존 파일을 삭제하는 작업이라 검토 후 적용됩니다.",
|
|
288
|
-
source="change_proposal",
|
|
289
|
-
kind="file_delete",
|
|
290
|
-
payload={
|
|
291
|
-
"path": path,
|
|
292
|
-
"diff": _unified_diff(before, "", path),
|
|
293
|
-
"tier": "large",
|
|
294
|
-
"before_bytes": len(before.encode("utf-8")),
|
|
295
|
-
"after_bytes": 0,
|
|
296
|
-
"base_exists": base_exists,
|
|
297
|
-
"base_sha256": _sha256_text(before) if base_exists else "",
|
|
298
|
-
},
|
|
299
|
-
provenance={"proposed_by": proposed_by, "reason": reason},
|
|
300
|
-
user_email=user_email,
|
|
301
|
-
workspace_id=workspace_id,
|
|
302
|
-
)
|
|
303
|
-
self._audit(
|
|
304
|
-
"change_proposal_created", user_email=user_email,
|
|
305
|
-
proposal_id=item.get("id"), path=path, kind="file_delete", tier="large",
|
|
306
|
-
)
|
|
307
|
-
return item
|
|
308
|
-
|
|
309
275
|
def propose_reorganization(
|
|
310
276
|
self,
|
|
311
277
|
*,
|
|
@@ -48,6 +48,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
|
48
48
|
|
|
49
49
|
from latticeai.core.timeutil import parse_iso
|
|
50
50
|
from latticeai.core.timezones import get_timezone
|
|
51
|
+
from latticeai.core.workspace_os_utils import graph_scope_kwargs
|
|
51
52
|
|
|
52
53
|
LOGGER = logging.getLogger(__name__)
|
|
53
54
|
|
|
@@ -249,12 +250,9 @@ def _empty_lanes() -> Dict[str, int]:
|
|
|
249
250
|
return {lane: 0 for lane in _LANES}
|
|
250
251
|
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
256
|
-
"include_legacy_global": workspace_id is None,
|
|
257
|
-
}
|
|
253
|
+
|
|
254
|
+
#: Historical module-local name for the shared rule.
|
|
255
|
+
_scope_kwargs = graph_scope_kwargs
|
|
258
256
|
|
|
259
257
|
|
|
260
258
|
def _on_day(rows: Sequence[Any], day: str) -> List[Any]:
|
|
@@ -28,6 +28,7 @@ from typing import Any, Dict, List, Optional
|
|
|
28
28
|
|
|
29
29
|
from latticeai.core.timeutil import local_now, parse_iso
|
|
30
30
|
from latticeai.core.timeutil import now_iso as _now
|
|
31
|
+
from latticeai.core.workspace_os_utils import graph_scope_kwargs
|
|
31
32
|
|
|
32
33
|
LOGGER = logging.getLogger(__name__)
|
|
33
34
|
|
|
@@ -48,6 +49,8 @@ def _clip(text: Any, limit: int = 160) -> str:
|
|
|
48
49
|
return value[:limit]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
|
|
53
|
+
|
|
51
54
|
class CommandCenterService:
|
|
52
55
|
def __init__(
|
|
53
56
|
self,
|
|
@@ -73,10 +76,7 @@ class CommandCenterService:
|
|
|
73
76
|
# ── scoped reads ─────────────────────────────────────────────────────
|
|
74
77
|
|
|
75
78
|
def _scope_kwargs(self, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
76
|
-
return
|
|
77
|
-
"allowed_workspaces": {workspace_id} if workspace_id is not None else None,
|
|
78
|
-
"include_legacy_global": workspace_id is None,
|
|
79
|
-
}
|
|
79
|
+
return graph_scope_kwargs(workspace_id)
|
|
80
80
|
|
|
81
81
|
def _history(
|
|
82
82
|
self, *, user_email: Optional[str], workspace_id: Optional[str], limit: int
|
|
@@ -28,6 +28,8 @@ import logging
|
|
|
28
28
|
import re
|
|
29
29
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
|
|
30
30
|
|
|
31
|
+
from latticeai.core.messages import bilingual
|
|
32
|
+
|
|
31
33
|
LOGGER = logging.getLogger(__name__)
|
|
32
34
|
|
|
33
35
|
__all__ = ["EvidenceActionService", "EVIDENCE_ACTIONS"]
|
|
@@ -38,8 +40,9 @@ _EXCERPT_CHARS = 600
|
|
|
38
40
|
_MAX_SOURCES = 8
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
|
|
44
|
+
#: Historical module-local name for the shared phrase-pair helper.
|
|
45
|
+
_phrase = bilingual
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
# The action catalog is closed and deterministic — the UI renders exactly
|
|
@@ -13,7 +13,6 @@ auto-commit — so a headless caller stages nothing rather than writing.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import json
|
|
17
16
|
import logging
|
|
18
17
|
from typing import Any, AsyncIterator, Dict, Optional
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ from latticeai.core.network_boundary import (
|
|
|
21
20
|
NetworkBoundaryMode,
|
|
22
21
|
normalize_network_mode,
|
|
23
22
|
)
|
|
23
|
+
from latticeai.core.sse import sse_frame
|
|
24
24
|
from latticeai.services.cloud_egress_audit import record_cloud_egress
|
|
25
25
|
from latticeai.services.cloud_extraction import plan_kg_expansion_rich
|
|
26
26
|
from latticeai.services.cloud_streaming import (
|
|
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _sse(data: Dict[str, Any]) -> str:
|
|
39
|
-
return
|
|
39
|
+
return sse_frame(None, data)
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _scope_key(user_email: Optional[str], workspace_id: Optional[str]) -> str:
|
|
@@ -12,16 +12,13 @@ Persisted under the data dir, scoped like NetworkBoundaryService
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import threading
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any, Callable, Dict, List, Optional, Set
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set
|
|
19
16
|
|
|
20
|
-
from latticeai.core.io_utils import atomic_write_json
|
|
21
17
|
from latticeai.core.network_boundary import (
|
|
22
18
|
HARD_BLOCK_METADATA_FLAGS,
|
|
23
19
|
HARD_BLOCK_NODE_TYPES,
|
|
24
20
|
)
|
|
21
|
+
from latticeai.services.mode_store import JsonBackedModeService
|
|
25
22
|
|
|
26
23
|
DEFAULT_BLOCKED_TYPES: List[str] = []
|
|
27
24
|
DEFAULT_BLOCKED_FLAGS: List[str] = sorted(HARD_BLOCK_METADATA_FLAGS)
|
|
@@ -39,51 +36,15 @@ def _default_policy() -> Dict[str, Any]:
|
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
|
|
42
|
-
class HybridPolicyService:
|
|
39
|
+
class HybridPolicyService(JsonBackedModeService[Dict[str, Any]]):
|
|
43
40
|
"""Load / save hybrid cloud policy."""
|
|
44
41
|
|
|
45
|
-
|
|
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"
|
|
42
|
+
FILENAME = "hybrid_policy.json"
|
|
58
43
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
self._audit = audit
|
|
44
|
+
def _default_entry(self) -> Any:
|
|
45
|
+
return _default_policy()
|
|
62
46
|
|
|
63
|
-
def
|
|
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(
|
|
47
|
+
def _resolve_from(
|
|
87
48
|
self,
|
|
88
49
|
data: Dict[str, Any],
|
|
89
50
|
*,
|
|
@@ -116,16 +77,6 @@ class HybridPolicyService:
|
|
|
116
77
|
policy["min_extraction_confidence"] = 0.55
|
|
117
78
|
return policy
|
|
118
79
|
|
|
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
80
|
def set_policy(
|
|
130
81
|
self,
|
|
131
82
|
patch: Dict[str, Any],
|
|
@@ -144,7 +95,7 @@ class HybridPolicyService:
|
|
|
144
95
|
clean = {k: v for k, v in (patch or {}).items() if k in allowed}
|
|
145
96
|
with self._lock:
|
|
146
97
|
data = self._read()
|
|
147
|
-
previous = self.
|
|
98
|
+
previous = self._resolve_from(
|
|
148
99
|
data, user_email=user_email, workspace_id=workspace_id
|
|
149
100
|
)
|
|
150
101
|
if workspace_id:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""The shared storage half of the scoped preference dials.
|
|
2
|
+
|
|
3
|
+
``PermissionModeService``, ``NetworkBoundaryService`` and ``HybridPolicyService``
|
|
4
|
+
are three different policies over one identical store: a JSON file under the
|
|
5
|
+
data dir holding ``{"default": …, "users": {…}, "workspaces": {…}}``, guarded by
|
|
6
|
+
a lock, rebindable after lazy construction, and read defensively enough that a
|
|
7
|
+
truncated or hand-edited file degrades to defaults instead of taking the app
|
|
8
|
+
down.
|
|
9
|
+
|
|
10
|
+
That half was written three times, character for character. Three copies of a
|
|
11
|
+
"corrupt file falls back to defaults" rule is three chances for one of them to
|
|
12
|
+
grow an exception path the others do not have — and the symptom would be one
|
|
13
|
+
dial silently forgetting a user's choice while the other two remember. The
|
|
14
|
+
storage lives here once; each service supplies only what actually differs:
|
|
15
|
+
the file name, what an unset ``default`` means, and how a scope resolves.
|
|
16
|
+
|
|
17
|
+
Deliberately *not* shared: ``set_mode``/``set_policy``. They look similar but
|
|
18
|
+
their write shapes differ (a scalar mode replaces, a policy patch merges) and
|
|
19
|
+
their audit events carry different payloads, so folding them together would
|
|
20
|
+
mean a parameterised method with more branches than the two bodies it replaced.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import threading
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Callable, ClassVar, Dict, Generic, Optional, TypeVar
|
|
29
|
+
|
|
30
|
+
from latticeai.core.io_utils import atomic_write_json
|
|
31
|
+
|
|
32
|
+
#: What ``resolve`` hands back — an enum member for the mode dials, a plain
|
|
33
|
+
#: dict for the policy service.
|
|
34
|
+
ResolvedT = TypeVar("ResolvedT")
|
|
35
|
+
|
|
36
|
+
__all__ = ["JsonBackedModeService"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class JsonBackedModeService(Generic[ResolvedT]):
|
|
40
|
+
"""A lock-guarded, scope-aware JSON preference file.
|
|
41
|
+
|
|
42
|
+
Subclasses set :attr:`FILENAME` and implement :meth:`_default_entry` and
|
|
43
|
+
:meth:`_resolve_from`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
#: File name under the data dir. Subclasses must set this.
|
|
47
|
+
FILENAME: ClassVar[str] = ""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
data_dir: Path,
|
|
53
|
+
audit: Optional[Callable[..., None]] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._path = Path(data_dir) / self.FILENAME
|
|
56
|
+
self._audit = audit or (lambda *a, **kw: None)
|
|
57
|
+
self._lock = threading.Lock()
|
|
58
|
+
|
|
59
|
+
# ── rebinding ────────────────────────────────────────────────────────────
|
|
60
|
+
def rebind_data_dir(self, data_dir: Path) -> None:
|
|
61
|
+
"""Point the store at the app's real data dir.
|
|
62
|
+
|
|
63
|
+
The wiring may instantiate a service lazily before routers know the
|
|
64
|
+
configured data dir; rebinding keeps one file of record instead of
|
|
65
|
+
stranding writes under the fallback path.
|
|
66
|
+
"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._path = Path(data_dir) / self.FILENAME
|
|
69
|
+
|
|
70
|
+
def rebind_audit(self, audit: Callable[..., None]) -> None:
|
|
71
|
+
"""Attach the real audit sink once app wiring provides one."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
self._audit = audit
|
|
74
|
+
|
|
75
|
+
# ── what each subclass must decide ───────────────────────────────────────
|
|
76
|
+
def _default_entry(self) -> Any:
|
|
77
|
+
"""The ``default`` bucket's value when the file does not say.
|
|
78
|
+
|
|
79
|
+
Called fresh at every use because the policy service's answer is a
|
|
80
|
+
mutable dict; a shared instance would let one caller's edit leak into
|
|
81
|
+
the next caller's defaults.
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
def _resolve_from(
|
|
86
|
+
self,
|
|
87
|
+
data: Dict[str, Any],
|
|
88
|
+
*,
|
|
89
|
+
user_email: Optional[str],
|
|
90
|
+
workspace_id: Optional[str],
|
|
91
|
+
) -> ResolvedT:
|
|
92
|
+
"""Apply this dial's scope precedence to already-loaded ``data``.
|
|
93
|
+
|
|
94
|
+
Pure over ``data`` and lock-free, so holders of ``_lock`` can reuse it
|
|
95
|
+
without re-entering a non-reentrant lock.
|
|
96
|
+
"""
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
# ── storage ──────────────────────────────────────────────────────────────
|
|
100
|
+
def _empty(self) -> Dict[str, Any]:
|
|
101
|
+
return {"default": self._default_entry(), "users": {}, "workspaces": {}}
|
|
102
|
+
|
|
103
|
+
def _read(self) -> Dict[str, Any]:
|
|
104
|
+
if not self._path.exists():
|
|
105
|
+
return self._empty()
|
|
106
|
+
try:
|
|
107
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
108
|
+
except Exception:
|
|
109
|
+
return self._empty()
|
|
110
|
+
if not isinstance(data, dict):
|
|
111
|
+
return self._empty()
|
|
112
|
+
data.setdefault("default", self._default_entry())
|
|
113
|
+
data.setdefault("users", {})
|
|
114
|
+
data.setdefault("workspaces", {})
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
def _write(self, data: Dict[str, Any]) -> None:
|
|
118
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
atomic_write_json(self._path, data)
|
|
120
|
+
|
|
121
|
+
# ── reading ──────────────────────────────────────────────────────────────
|
|
122
|
+
def resolve(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
user_email: Optional[str] = None,
|
|
126
|
+
workspace_id: Optional[str] = None,
|
|
127
|
+
) -> ResolvedT:
|
|
128
|
+
with self._lock:
|
|
129
|
+
data = self._read()
|
|
130
|
+
return self._resolve_from(
|
|
131
|
+
data, user_email=user_email, workspace_id=workspace_id,
|
|
132
|
+
)
|
|
@@ -636,11 +636,6 @@ def get_all_capabilities() -> List[ModelCapability]:
|
|
|
636
636
|
return [*_REGISTRY, *_LEGACY_REGISTRY]
|
|
637
637
|
|
|
638
638
|
|
|
639
|
-
def get_recommended_capabilities() -> List[ModelCapability]:
|
|
640
|
-
"""Current-generation entries: catalog, download and recommendation input."""
|
|
641
|
-
return list(_REGISTRY)
|
|
642
|
-
|
|
643
|
-
|
|
644
639
|
def get_legacy_capabilities() -> List[ModelCapability]:
|
|
645
640
|
"""Recognised-only entries: never offered, never recommended, still named."""
|
|
646
641
|
return list(_LEGACY_REGISTRY)
|
|
@@ -663,12 +658,6 @@ def is_recognized_model(model_id: str) -> bool:
|
|
|
663
658
|
return get_capability(model_id) is not None
|
|
664
659
|
|
|
665
660
|
|
|
666
|
-
def is_recommended_model(model_id: str) -> bool:
|
|
667
|
-
"""True only for current-generation entries — the download/offer gate."""
|
|
668
|
-
cap = get_capability(model_id)
|
|
669
|
-
return cap is not None and cap.lifecycle == RECOMMENDED
|
|
670
|
-
|
|
671
|
-
|
|
672
661
|
def build_engine_model_catalog() -> Dict[str, List[Dict[str, Any]]]:
|
|
673
662
|
"""Return legacy ENGINE_MODEL_CATALOG shape, enriched with rich fields.
|
|
674
663
|
|
|
@@ -717,7 +706,3 @@ def get_verified_models() -> List[Dict[str, Any]]:
|
|
|
717
706
|
if c.verification.hf_exists and c.verification.has_config and c.verification.has_tokenizer
|
|
718
707
|
]
|
|
719
708
|
|
|
720
|
-
|
|
721
|
-
# Back-compat: expose a simple list mirroring the old top-level for mlx.
|
|
722
|
-
# Recommended entries only — same reasoning as build_engine_model_catalog.
|
|
723
|
-
LOCAL_MLX_MODELS = [c.to_legacy_dict() for c in _REGISTRY if "local_mlx" in c.provider_hints]
|
|
@@ -15,10 +15,6 @@ import re
|
|
|
15
15
|
import sys
|
|
16
16
|
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
|
-
from latticeai.services.model_capability_registry import (
|
|
19
|
-
LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
18
|
# 5.2.0: Delegate catalog data to the structured capability registry (rich + verified).
|
|
23
19
|
# This keeps backward compat for every `from ...model_catalog import ENGINE_MODEL_CATALOG`.
|
|
24
20
|
from latticeai.services.model_capability_registry import (
|
|
@@ -178,9 +174,6 @@ get_all_capabilities = _get_all_capabilities
|
|
|
178
174
|
get_capability = _get_capability
|
|
179
175
|
get_verified_models = _get_verified_models
|
|
180
176
|
|
|
181
|
-
# Convenience re-export for tests / places that did `from ...model_catalog import LOCAL_MLX_MODELS`
|
|
182
|
-
LOCAL_MLX_MODELS = _LOCAL_MLX_MODELS # type: ignore[name-defined]
|
|
183
|
-
|
|
184
177
|
_VERSIONED_MODEL_PATTERNS = (
|
|
185
178
|
("gemma", re.compile(r"\bgemma[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
|
|
186
179
|
("qwen", re.compile(r"\bqwen[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
|
|
@@ -6,7 +6,6 @@ Re-exports will be added in model_runtime for compat.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
import json
|
|
10
9
|
import logging
|
|
11
10
|
import queue
|
|
12
11
|
import subprocess
|
|
@@ -15,6 +14,8 @@ from functools import partial
|
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
from typing import Any, AsyncIterator, Dict, Optional
|
|
17
16
|
|
|
17
|
+
from latticeai.core.sse import sse_frame
|
|
18
|
+
|
|
18
19
|
from .model_errors import ModelRuntimeError
|
|
19
20
|
|
|
20
21
|
|
|
@@ -243,7 +244,7 @@ async def prepare_and_load_model(
|
|
|
243
244
|
|
|
244
245
|
|
|
245
246
|
def sse_event(event: str, data: Dict[str, object]) -> str:
|
|
246
|
-
return
|
|
247
|
+
return sse_frame(event, data)
|
|
247
248
|
|
|
248
249
|
|
|
249
250
|
async def prepare_and_load_model_stream(
|