ltcai 10.0.0 → 10.2.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 +48 -32
- package/docs/CHANGELOG.md +156 -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/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 +4 -3
- 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 +12 -8
- package/lattice_brain/runtime/hooks.py +2 -1
- 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 +131 -78
- 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/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 +5 -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-CbdGD-2i.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-LgCkXpnf.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-CoPGJI1L.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-gVnaxSwp.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-ChxAYHtj.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-DBtgkHZg.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-Ovtv5I0Y.js} +1 -1
- package/static/app/assets/{Library-DhvoPvC7.js → Library-CBia2Fvm.js} +1 -1
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-CNz-6NSd.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-ePX-Y73J.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-C4mpvkwH.js} +1 -1
- package/static/app/assets/System-BvWNK1zc.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CiauPV_3.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-Dwct-Q1x.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-BKDW1F0T.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-CCB9Kuw7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-DNa8WHC5.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-8jmxn5mf.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-mxvF3V1I.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download--SmCcx0p.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-D4Wy5roo.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-BwQcSPlS.js} +1 -1
- package/static/app/assets/index-CQWdDU3z.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-DDV2YZwM.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DFhSjmLS.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-Bts7VO94.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-BuSMEJY8.js} +1 -1
- package/static/app/assets/search-DZzxhWaQ.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-BfTO6X_G.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-BjctW1oh.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-CE43-LrA.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Bv3ejLL5.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-CsNqLZAj.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-KFFdVG_t.js} +2 -2
- package/static/app/assets/workspace-wdCvdyPF.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from ..quiet import quiet
|
|
4
4
|
|
|
5
|
+
# ruff: noqa: F403,F405
|
|
5
6
|
from ._kg_common import * # noqa: F403,F401
|
|
6
7
|
|
|
7
8
|
|
|
@@ -59,7 +60,7 @@ class KnowledgeGraphDiscoveryMixin:
|
|
|
59
60
|
):
|
|
60
61
|
add(volume.name, volume, "volume", recommended=False)
|
|
61
62
|
except OSError:
|
|
62
|
-
|
|
63
|
+
quiet()
|
|
63
64
|
elif os_type == "windows":
|
|
64
65
|
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
65
66
|
drive = Path(f"{letter}:\\")
|
|
@@ -87,7 +88,7 @@ class KnowledgeGraphDiscoveryMixin:
|
|
|
87
88
|
):
|
|
88
89
|
add(mounted.name, mounted, "volume", recommended=False)
|
|
89
90
|
except OSError:
|
|
90
|
-
|
|
91
|
+
quiet()
|
|
91
92
|
|
|
92
93
|
return {
|
|
93
94
|
"os_type": os_type,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from ..quiet import quiet
|
|
4
4
|
|
|
5
|
+
# ruff: noqa: F403,F405
|
|
5
6
|
from ._kg_common import * # noqa: F403,F401
|
|
6
7
|
|
|
7
8
|
|
|
@@ -287,7 +288,7 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
287
288
|
}
|
|
288
289
|
)
|
|
289
290
|
except Exception:
|
|
290
|
-
|
|
291
|
+
quiet()
|
|
291
292
|
result["images"].append(image_info)
|
|
292
293
|
except Exception as exc:
|
|
293
294
|
result["error"] = str(exc)
|
|
@@ -30,6 +30,8 @@ import os
|
|
|
30
30
|
import re
|
|
31
31
|
from typing import Any, Dict, Mapping, Optional
|
|
32
32
|
|
|
33
|
+
from ..quiet import quiet
|
|
34
|
+
|
|
33
35
|
QUERY_CLASSES = ("fact", "code", "person", "recency")
|
|
34
36
|
|
|
35
37
|
FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
|
|
@@ -118,6 +120,7 @@ def _env_overrides() -> Dict[str, Dict[str, float]]:
|
|
|
118
120
|
try:
|
|
119
121
|
cleaned[key] = max(0.0, min(1.0, float(value)))
|
|
120
122
|
except (TypeError, ValueError):
|
|
123
|
+
quiet()
|
|
121
124
|
continue
|
|
122
125
|
if cleaned:
|
|
123
126
|
overrides[str(cls)] = cleaned
|
|
@@ -137,6 +140,7 @@ def fusion_weight_table(
|
|
|
137
140
|
try:
|
|
138
141
|
table[cls][key] = max(0.0, min(1.0, float(value)))
|
|
139
142
|
except (TypeError, ValueError):
|
|
143
|
+
quiet()
|
|
140
144
|
continue
|
|
141
145
|
return table
|
|
142
146
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from ..quiet import quiet
|
|
4
|
+
from ..sensitivity import stamp_sensitivity
|
|
4
5
|
|
|
6
|
+
# ruff: noqa: F403,F405
|
|
5
7
|
from ._kg_common import * # noqa: F403,F401
|
|
6
8
|
|
|
7
9
|
|
|
@@ -36,7 +38,7 @@ def _triple_edge_metadata(triple: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
36
38
|
try:
|
|
37
39
|
metadata["confidence"] = round(float(confidence), 4)
|
|
38
40
|
except (TypeError, ValueError):
|
|
39
|
-
|
|
41
|
+
quiet()
|
|
40
42
|
return metadata
|
|
41
43
|
|
|
42
44
|
|
|
@@ -270,6 +272,14 @@ class KnowledgeGraphIngestMixin:
|
|
|
270
272
|
"extracted": {k: v for k, v in (extracted or {}).items() if k != "content"},
|
|
271
273
|
"structure": doc_meta,
|
|
272
274
|
}
|
|
275
|
+
|
|
276
|
+
# Stamp never-leaves at ingestion. Before 10.2.0 the cloud filter looked
|
|
277
|
+
# for these flags and nothing in the product could set them, so the
|
|
278
|
+
# guard was unreachable. Deriving it from the path means a user who
|
|
279
|
+
# indexes a project folder does not have to remember that it contains a
|
|
280
|
+
# .env — the flag is on the node before any cloud turn can select it.
|
|
281
|
+
stamp_sensitivity(metadata, source_uri or str(path))
|
|
282
|
+
|
|
273
283
|
full_text = f"{filename}\n{text}"
|
|
274
284
|
concepts = _extract_concepts(full_text, limit=15)
|
|
275
285
|
triples = _extract_triples(full_text, concepts)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from ..quiet import quiet
|
|
4
4
|
|
|
5
|
+
# ruff: noqa: F403,F405
|
|
5
6
|
from ._kg_common import * # noqa: F403,F401
|
|
6
7
|
|
|
7
8
|
# ── promotion review mode (review 2026-07-25 Wave 4) ─────────────────────────
|
|
@@ -364,7 +365,7 @@ class KnowledgeGraphProjectionMixin:
|
|
|
364
365
|
if EdgeType.from_legacy(leg_type).value == leg_type:
|
|
365
366
|
leg_type = ""
|
|
366
367
|
except ValueError:
|
|
367
|
-
|
|
368
|
+
quiet()
|
|
368
369
|
norm_type = (
|
|
369
370
|
EdgeType.from_legacy(edge_type).value if EdgeType is not None else edge_type
|
|
370
371
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
# ruff: noqa: F403,F405
|
|
4
|
-
|
|
5
4
|
from ._kg_common import * # noqa: F403,F401
|
|
6
5
|
|
|
7
6
|
|
|
@@ -188,7 +187,8 @@ class KnowledgeGraphProvenanceMixin:
|
|
|
188
187
|
def schema_versions(self) -> Dict[str, Any]:
|
|
189
188
|
"""Versions an exporter stamps and an importer validates against."""
|
|
190
189
|
try:
|
|
191
|
-
from .schema import EMBED_DIM as _EMBED_DIM
|
|
190
|
+
from .schema import EMBED_DIM as _EMBED_DIM
|
|
191
|
+
from .schema import KG_SCHEMA_V2_VERSION as _V2
|
|
192
192
|
except Exception: # pragma: no cover - kg_schema always importable in practice
|
|
193
193
|
_EMBED_DIM, _V2 = 1024, 2
|
|
194
194
|
return {
|
|
@@ -421,7 +421,9 @@ class KnowledgeGraphProvenanceMixin:
|
|
|
421
421
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
422
422
|
if dest.exists():
|
|
423
423
|
dest.unlink() # VACUUM INTO requires the target to not exist
|
|
424
|
-
|
|
424
|
+
# Raw connection, not ``_connect()``: VACUUM cannot run inside a
|
|
425
|
+
# transaction, and ``_connect()`` wraps its block in one.
|
|
426
|
+
conn = self.storage_engine.connect()
|
|
425
427
|
try:
|
|
426
428
|
conn.execute("PRAGMA wal_checkpoint(FULL)")
|
|
427
429
|
conn.execute("VACUUM INTO ?", (str(dest),))
|
|
@@ -111,7 +111,10 @@ def cross_encoder_rerank(
|
|
|
111
111
|
return result
|
|
112
112
|
|
|
113
113
|
ranked = list(candidates)
|
|
114
|
-
|
|
114
|
+
# strict=False on purpose: a cross-encoder that returns fewer scores than
|
|
115
|
+
# candidates leaves the tail un-reranked at its fused score, which is a
|
|
116
|
+
# better outcome than dropping the whole rerank.
|
|
117
|
+
for item, score in zip(ranked, scores, strict=False):
|
|
115
118
|
item["rerank_score"] = float(score)
|
|
116
119
|
# Surface the rerank score as the primary ranking key while keeping
|
|
117
120
|
# the pre-rerank fused score under scores.fused for audit.
|
|
@@ -43,13 +43,14 @@ print(store.stats()) # {"nodes": ..., "by_node_type": {...}, ...}
|
|
|
43
43
|
from __future__ import annotations
|
|
44
44
|
|
|
45
45
|
import json
|
|
46
|
-
import os
|
|
47
46
|
import logging
|
|
47
|
+
import os
|
|
48
48
|
import sqlite3
|
|
49
49
|
from contextlib import contextmanager
|
|
50
50
|
from enum import Enum
|
|
51
51
|
from typing import Any, Dict, Optional
|
|
52
52
|
|
|
53
|
+
from ..quiet import quiet
|
|
53
54
|
|
|
54
55
|
# ── Schema version ──────────────────────────────────────────────────────────
|
|
55
56
|
KG_SCHEMA_V2_VERSION = 2
|
|
@@ -129,7 +130,7 @@ class NodeType(str, Enum):
|
|
|
129
130
|
try:
|
|
130
131
|
return cls(m.upper())
|
|
131
132
|
except ValueError:
|
|
132
|
-
|
|
133
|
+
quiet()
|
|
133
134
|
return _LEGACY_NODE_MAP.get(m.lower(), cls.CONCEPT)
|
|
134
135
|
|
|
135
136
|
|
|
@@ -189,7 +190,7 @@ class EdgeType(str, Enum):
|
|
|
189
190
|
try:
|
|
190
191
|
return cls(m.upper())
|
|
191
192
|
except ValueError:
|
|
192
|
-
|
|
193
|
+
quiet()
|
|
193
194
|
return _LEGACY_EDGE_MAP.get(m.lower(), cls.MENTIONS)
|
|
194
195
|
|
|
195
196
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
# ruff: noqa: F403,F405
|
|
4
|
-
|
|
5
4
|
from ._kg_common import * # noqa: F403,F401
|
|
6
|
-
from .documents import KnowledgeGraphDocumentsMixin
|
|
7
5
|
from .discovery import KnowledgeGraphDiscoveryMixin
|
|
8
6
|
from .discovery_index import KnowledgeGraphLocalIndexMixin
|
|
7
|
+
from .documents import KnowledgeGraphDocumentsMixin
|
|
9
8
|
from .ingest import KnowledgeGraphIngestMixin
|
|
10
9
|
from .projection import KnowledgeGraphProjectionMixin
|
|
11
10
|
from .provenance import KnowledgeGraphProvenanceMixin
|
|
@@ -81,8 +80,23 @@ class KnowledgeGraphStore(
|
|
|
81
80
|
return ("kgv2_nodes", "kgv2_edges")
|
|
82
81
|
return ("nodes", "edges")
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
@contextmanager
|
|
84
|
+
def _connect(self) -> Iterator[sqlite3.Connection]:
|
|
85
|
+
"""Transactional connection that is closed when the block exits.
|
|
86
|
+
|
|
87
|
+
This used to return the raw connection, so ``with self._connect() as
|
|
88
|
+
conn`` inherited sqlite3's context-manager semantics: commit on success,
|
|
89
|
+
rollback on error, **never close**. Each of the ~58 call sites therefore
|
|
90
|
+
held a descriptor open until garbage collection reclaimed it. That is
|
|
91
|
+
invisible while CPython refcounting frees the local immediately, and
|
|
92
|
+
fatal as soon as anything keeps the frame alive — a coverage tracer, a
|
|
93
|
+
profiler, or a traceback captured by ``logger.exception``.
|
|
94
|
+
|
|
95
|
+
Use ``storage_engine.connect()`` directly only for statements that
|
|
96
|
+
cannot run inside a transaction, and close it yourself.
|
|
97
|
+
"""
|
|
98
|
+
with self.storage_engine.session() as conn:
|
|
99
|
+
yield conn
|
|
86
100
|
|
|
87
101
|
def _init_db(self) -> None:
|
|
88
102
|
with self._connect() as conn:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
# ruff: noqa: F403,F405
|
|
4
|
-
|
|
5
4
|
from ._kg_common import * # noqa: F403,F401
|
|
6
5
|
|
|
7
6
|
|
|
@@ -250,3 +249,49 @@ class KnowledgeGraphWriteMixin:
|
|
|
250
249
|
text=text,
|
|
251
250
|
metadata={**metadata, "parent_source_node": source_node},
|
|
252
251
|
)
|
|
252
|
+
|
|
253
|
+
def set_node_sensitivity(
|
|
254
|
+
self,
|
|
255
|
+
node_id: str,
|
|
256
|
+
*,
|
|
257
|
+
local_only: bool,
|
|
258
|
+
reason: Optional[str] = None,
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
"""Mark (or unmark) one node as never-leaving.
|
|
261
|
+
|
|
262
|
+
The cloud filter has always looked for this flag; until 10.2.0 nothing
|
|
263
|
+
could set it, so the guard was unreachable. This is the user-driven
|
|
264
|
+
half — ingestion stamps secret-bearing paths automatically, and this
|
|
265
|
+
covers everything a path cannot tell you, like a note whose *content*
|
|
266
|
+
is private.
|
|
267
|
+
|
|
268
|
+
Unmarking is allowed and audited by the caller: a user who flagged
|
|
269
|
+
something by mistake must be able to undo it, but the reason is cleared
|
|
270
|
+
with the flag so a stale justification cannot linger.
|
|
271
|
+
"""
|
|
272
|
+
with self._connect() as conn:
|
|
273
|
+
row = conn.execute(
|
|
274
|
+
"SELECT metadata_json FROM nodes WHERE id=?", (node_id,)
|
|
275
|
+
).fetchone()
|
|
276
|
+
if row is None:
|
|
277
|
+
return {"ok": False, "node_id": node_id, "reason": "node not found"}
|
|
278
|
+
try:
|
|
279
|
+
metadata = json.loads(row[0] or "{}")
|
|
280
|
+
except Exception:
|
|
281
|
+
metadata = {}
|
|
282
|
+
if local_only:
|
|
283
|
+
metadata["local_only"] = True
|
|
284
|
+
metadata["local_only_reason"] = reason or "marked by the user"
|
|
285
|
+
else:
|
|
286
|
+
metadata.pop("local_only", None)
|
|
287
|
+
metadata.pop("local_only_reason", None)
|
|
288
|
+
conn.execute(
|
|
289
|
+
"UPDATE nodes SET metadata_json=?, updated_at=? WHERE id=?",
|
|
290
|
+
(json.dumps(metadata, ensure_ascii=False), _now(), node_id),
|
|
291
|
+
)
|
|
292
|
+
return {
|
|
293
|
+
"ok": True,
|
|
294
|
+
"node_id": node_id,
|
|
295
|
+
"local_only": bool(local_only),
|
|
296
|
+
"reason": metadata.get("local_only_reason"),
|
|
297
|
+
}
|
|
@@ -360,6 +360,9 @@ from .ingestion_jobs import ( # noqa: E402,F401 — re-export for existing impo
|
|
|
360
360
|
BackgroundIngestionJob,
|
|
361
361
|
BackgroundIngestionQueue,
|
|
362
362
|
)
|
|
363
|
+
from .quiet import ( # noqa: E402 — imported after the module constants it depends on
|
|
364
|
+
quiet, # noqa: E402 — imported after the module constants it depends on
|
|
365
|
+
)
|
|
363
366
|
|
|
364
367
|
|
|
365
368
|
@dataclass
|
|
@@ -577,7 +580,7 @@ class IngestionPipeline:
|
|
|
577
580
|
user_email,
|
|
578
581
|
)
|
|
579
582
|
except Exception: # noqa: BLE001 — audit must never break ingestion
|
|
580
|
-
|
|
583
|
+
quiet()
|
|
581
584
|
|
|
582
585
|
extraction_quality = self._assess_item_quality(
|
|
583
586
|
item, source_type=source_type, text=quality_text, chunk_ids=chunk_ids,
|
|
@@ -20,10 +20,12 @@ import shutil
|
|
|
20
20
|
import sqlite3
|
|
21
21
|
import tempfile
|
|
22
22
|
import zipfile
|
|
23
|
+
from contextlib import closing
|
|
23
24
|
from pathlib import Path, PurePosixPath
|
|
24
25
|
from typing import Any, Dict, Optional
|
|
25
26
|
|
|
26
27
|
from .archive import BrainArchivePaths, EncryptedBrainArchive
|
|
28
|
+
from .quiet import quiet
|
|
27
29
|
from .storage import (
|
|
28
30
|
DockerPostgresWizard,
|
|
29
31
|
PostgresEngine,
|
|
@@ -66,7 +68,7 @@ def _checkpoint_sqlite(db_path: Path) -> None:
|
|
|
66
68
|
if not db_path.exists():
|
|
67
69
|
return
|
|
68
70
|
try:
|
|
69
|
-
with sqlite3.connect(str(db_path)) as conn:
|
|
71
|
+
with closing(sqlite3.connect(str(db_path))) as conn, conn:
|
|
70
72
|
conn.execute("PRAGMA wal_checkpoint(FULL)")
|
|
71
73
|
except sqlite3.Error:
|
|
72
74
|
# Best-effort only. Existing sibling backup/restore still preserves
|
|
@@ -99,6 +101,7 @@ def _replace_sqlite_atomically(src: Path, dest: Path, backup_dir: Path) -> None:
|
|
|
99
101
|
try:
|
|
100
102
|
shutil.copy2(sibling, backup)
|
|
101
103
|
except FileNotFoundError:
|
|
104
|
+
quiet()
|
|
102
105
|
continue
|
|
103
106
|
backups[sibling] = backup
|
|
104
107
|
for sibling in _sqlite_siblings(dest)[1:]:
|
|
@@ -247,7 +250,7 @@ class KGPortabilityService:
|
|
|
247
250
|
"counts": (artifact.get("header") or {}).get("counts")},
|
|
248
251
|
)
|
|
249
252
|
except Exception:
|
|
250
|
-
|
|
253
|
+
quiet()
|
|
251
254
|
return result
|
|
252
255
|
|
|
253
256
|
def import_from_file(self, path, *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
|
package/lattice_brain/quality.py
CHANGED
|
@@ -7,13 +7,15 @@ Pure Python Quality Layer for Lattice Brain (v6.4+ hardening)
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
10
12
|
import math
|
|
13
|
+
import re
|
|
11
14
|
import time
|
|
12
|
-
import
|
|
15
|
+
from collections import defaultdict
|
|
13
16
|
from dataclasses import dataclass, field
|
|
14
17
|
from typing import Any, Dict, List, Optional
|
|
15
|
-
|
|
16
|
-
import re
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
# -----------------------------
|
|
19
21
|
# 1. Embedding Fallback Labelling + Drift/Reindex Plan
|
|
@@ -41,7 +43,12 @@ class EmbeddingFallbackLabeller:
|
|
|
41
43
|
conf = 0.3
|
|
42
44
|
else:
|
|
43
45
|
# Simple hash-based pseudo-label for determinism
|
|
44
|
-
|
|
46
|
+
# Not a security hash: a short, stable label for grouping
|
|
47
|
+
# vectors in reports. usedforsecurity=False states that and
|
|
48
|
+
# keeps it working on FIPS builds where md5 is restricted.
|
|
49
|
+
h = hashlib.md5(
|
|
50
|
+
str(embedding[:4]).encode(), usedforsecurity=False
|
|
51
|
+
).hexdigest()[:8]
|
|
45
52
|
label = f"emb_cluster_{h}"
|
|
46
53
|
conf = 0.85
|
|
47
54
|
|
|
@@ -63,7 +70,7 @@ class EmbeddingFallbackLabeller:
|
|
|
63
70
|
def _cosine_distance(self, a: List[float], b: List[float]) -> float:
|
|
64
71
|
if not a or not b or len(a) != len(b):
|
|
65
72
|
return 1.0
|
|
66
|
-
dot = sum(x*y for x,y in zip(a,b))
|
|
73
|
+
dot = sum(x * y for x, y in zip(a, b, strict=True))
|
|
67
74
|
na = math.sqrt(sum(x*x for x in a))
|
|
68
75
|
nb = math.sqrt(sum(x*x for x in b))
|
|
69
76
|
return 1.0 - (dot / (na*nb + 1e-9))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Deliberately-ignored exceptions, recorded instead of erased (Brain Core).
|
|
2
|
+
|
|
3
|
+
Mirror of :mod:`latticeai.core.quiet`. Brain Core cannot import from the app
|
|
4
|
+
package (``tests/unit/test_import_guard.py``), and the alternative — leaving
|
|
5
|
+
Brain Core's suppressions silent while the app layer's are visible — would make
|
|
6
|
+
the quieter half the harder one to debug.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("lattice_brain.suppressed")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def quiet(reason: Optional[str] = None, *, level: int = logging.DEBUG) -> None:
|
|
19
|
+
"""Record the exception currently being handled, then continue."""
|
|
20
|
+
exc_type, exc, tb = sys.exc_info()
|
|
21
|
+
if exc is None:
|
|
22
|
+
return
|
|
23
|
+
where = "<unknown>"
|
|
24
|
+
if tb is not None:
|
|
25
|
+
deepest = tb
|
|
26
|
+
while deepest.tb_next is not None:
|
|
27
|
+
deepest = deepest.tb_next
|
|
28
|
+
frame = deepest.tb_frame
|
|
29
|
+
where = f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}"
|
|
30
|
+
label = f" ({reason})" if reason else ""
|
|
31
|
+
if logger.isEnabledFor(level):
|
|
32
|
+
logger.log(
|
|
33
|
+
level,
|
|
34
|
+
"suppressed %s at %s%s: %s",
|
|
35
|
+
getattr(exc_type, "__name__", "Exception"),
|
|
36
|
+
where,
|
|
37
|
+
label,
|
|
38
|
+
exc,
|
|
39
|
+
exc_info=exc,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["quiet"]
|
|
@@ -30,12 +30,8 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
from typing import Any, Callable, Dict, List, Optional
|
|
32
32
|
|
|
33
|
-
from
|
|
34
|
-
|
|
35
|
-
CORE_PIPELINE,
|
|
36
|
-
MULTI_AGENT_VERSION,
|
|
37
|
-
ROLE_AGENT_IDS,
|
|
38
|
-
)
|
|
33
|
+
from ..quiet import quiet
|
|
34
|
+
from ..utils import now_iso as _now
|
|
39
35
|
from .contracts import (
|
|
40
36
|
contract_view,
|
|
41
37
|
contract_views,
|
|
@@ -44,11 +40,18 @@ from .contracts import (
|
|
|
44
40
|
run_record_contract,
|
|
45
41
|
runtime_boundary_contract,
|
|
46
42
|
)
|
|
43
|
+
from .multi_agent import (
|
|
44
|
+
AGENT_ROLES,
|
|
45
|
+
CORE_PIPELINE,
|
|
46
|
+
MULTI_AGENT_VERSION,
|
|
47
|
+
ROLE_AGENT_IDS,
|
|
48
|
+
)
|
|
47
49
|
from .statuses import (
|
|
48
50
|
RUN_ACTIVE_STATUSES as _ACTIVE_STATUSES,
|
|
51
|
+
)
|
|
52
|
+
from .statuses import (
|
|
49
53
|
RUN_TERMINAL_STATUSES as _TERMINAL_STATUSES,
|
|
50
54
|
)
|
|
51
|
-
from ..utils import now_iso as _now
|
|
52
55
|
|
|
53
56
|
ROLE_DESCRIPTIONS = {
|
|
54
57
|
"researcher": "Gathers workspace context and memory for the goal.",
|
|
@@ -494,7 +497,7 @@ class AgentRuntime:
|
|
|
494
497
|
)
|
|
495
498
|
except Exception:
|
|
496
499
|
# Synthesis must never break the run record.
|
|
497
|
-
|
|
500
|
+
quiet()
|
|
498
501
|
|
|
499
502
|
@staticmethod
|
|
500
503
|
def _format_synthesis_section(title: str, items: List[str]) -> str:
|
|
@@ -569,6 +572,7 @@ class AgentRuntime:
|
|
|
569
572
|
workspace_id=scope,
|
|
570
573
|
)
|
|
571
574
|
except Exception:
|
|
575
|
+
quiet()
|
|
572
576
|
continue
|
|
573
577
|
|
|
574
578
|
def _post_run_hooks(
|
|
@@ -36,9 +36,9 @@ from collections import deque
|
|
|
36
36
|
from pathlib import Path
|
|
37
37
|
from typing import Any, Callable, Dict, List, Optional
|
|
38
38
|
|
|
39
|
+
from ..quiet import quiet
|
|
39
40
|
from ..utils import now_iso as _now
|
|
40
41
|
|
|
41
|
-
|
|
42
42
|
LOGGER = logging.getLogger(__name__)
|
|
43
43
|
|
|
44
44
|
|
|
@@ -454,6 +454,7 @@ class HooksRegistry:
|
|
|
454
454
|
try:
|
|
455
455
|
self.set_order(hook_id, (idx + 1) * 10)
|
|
456
456
|
except KeyError:
|
|
457
|
+
quiet()
|
|
457
458
|
continue
|
|
458
459
|
return self.list(kind=kind)
|
|
459
460
|
|
|
@@ -44,11 +44,10 @@ from __future__ import annotations
|
|
|
44
44
|
from dataclasses import dataclass, field
|
|
45
45
|
from typing import Any, Callable, Dict, List, Optional
|
|
46
46
|
|
|
47
|
-
from .contracts import multi_agent_contract
|
|
48
47
|
from ..utils import now_iso as _now
|
|
48
|
+
from .contracts import multi_agent_contract
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
MULTI_AGENT_VERSION = "10.0.0"
|
|
50
|
+
MULTI_AGENT_VERSION = "10.2.0"
|
|
52
51
|
|
|
53
52
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
54
53
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Which local sources are never allowed to leave the machine.
|
|
2
|
+
|
|
3
|
+
This lives in Brain Core rather than in the app layer because it is a property
|
|
4
|
+
of the *data*, not of any transport: a node stamped never-leaves stays that way
|
|
5
|
+
whether it is read by the cloud path, an export, or a future surface nobody has
|
|
6
|
+
written yet. Brain Core cannot import from ``latticeai`` (see
|
|
7
|
+
``tests/unit/test_import_guard.py``), so the app-layer network boundary imports
|
|
8
|
+
this, not the other way round.
|
|
9
|
+
|
|
10
|
+
Deliberately narrow. These are locations whose *entire purpose* is to hold
|
|
11
|
+
secrets, so a match is a fact about the path rather than a guess about content.
|
|
12
|
+
A broader heuristic — scanning text for things that look like keys — would
|
|
13
|
+
produce false confidence in both directions: it would miss secrets in unusual
|
|
14
|
+
shapes and quarantine ordinary notes that merely discuss credentials.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
# Path fragments checked case-insensitively against the POSIX form of the path.
|
|
22
|
+
SENSITIVE_PATH_FRAGMENTS: tuple = (
|
|
23
|
+
"/.ssh/",
|
|
24
|
+
"/.gnupg/",
|
|
25
|
+
"/.aws/",
|
|
26
|
+
"/.kube/",
|
|
27
|
+
"/.docker/config.json",
|
|
28
|
+
"/.netrc",
|
|
29
|
+
"/.npmrc",
|
|
30
|
+
"/.pypirc",
|
|
31
|
+
"/.git-credentials",
|
|
32
|
+
"id_rsa",
|
|
33
|
+
"id_ed25519",
|
|
34
|
+
".pem",
|
|
35
|
+
".p12",
|
|
36
|
+
".pfx",
|
|
37
|
+
".keystore",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Exact filenames (in any directory) that are secret-bearing by convention.
|
|
41
|
+
SENSITIVE_FILENAMES: frozenset = frozenset({
|
|
42
|
+
".env",
|
|
43
|
+
".env.local",
|
|
44
|
+
".env.production",
|
|
45
|
+
".env.development",
|
|
46
|
+
"credentials",
|
|
47
|
+
"secrets.yaml",
|
|
48
|
+
"secrets.yml",
|
|
49
|
+
"secrets.json",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
#: Metadata key stamped on a node that must never leave the machine.
|
|
53
|
+
LOCAL_ONLY_FLAG = "local_only"
|
|
54
|
+
#: Companion key holding the human-readable reason.
|
|
55
|
+
LOCAL_ONLY_REASON = "local_only_reason"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sensitive_reason_for_path(path: Any) -> Optional[str]:
|
|
59
|
+
"""Return why ``path`` is never-leaves, or ``None`` if it is ordinary."""
|
|
60
|
+
if not path:
|
|
61
|
+
return None
|
|
62
|
+
lowered = str(path).replace("\\", "/").lower()
|
|
63
|
+
name = lowered.rsplit("/", 1)[-1]
|
|
64
|
+
if name in SENSITIVE_FILENAMES:
|
|
65
|
+
return f"{name!r} is a secret-bearing filename"
|
|
66
|
+
for fragment in SENSITIVE_PATH_FRAGMENTS:
|
|
67
|
+
if fragment in lowered:
|
|
68
|
+
return f"path contains {fragment!r}"
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def stamp_sensitivity(metadata: dict, path: Any) -> Optional[str]:
|
|
73
|
+
"""Stamp never-leaves onto ``metadata`` when ``path`` warrants it.
|
|
74
|
+
|
|
75
|
+
Returns the reason it stamped, or ``None``. Never clears an existing flag —
|
|
76
|
+
a user or another rule may have set it for a reason this function cannot
|
|
77
|
+
see, and downgrading a never-leaves marker is not a decision code should
|
|
78
|
+
make on its own.
|
|
79
|
+
"""
|
|
80
|
+
reason = sensitive_reason_for_path(path)
|
|
81
|
+
if reason:
|
|
82
|
+
metadata[LOCAL_ONLY_FLAG] = True
|
|
83
|
+
metadata.setdefault(LOCAL_ONLY_REASON, reason)
|
|
84
|
+
return reason
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
"SENSITIVE_PATH_FRAGMENTS",
|
|
89
|
+
"SENSITIVE_FILENAMES",
|
|
90
|
+
"LOCAL_ONLY_FLAG",
|
|
91
|
+
"LOCAL_ONLY_REASON",
|
|
92
|
+
"sensitive_reason_for_path",
|
|
93
|
+
"stamp_sensitivity",
|
|
94
|
+
]
|