ltcai 11.2.0 → 11.4.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 +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Unified retrieval across the memory tiers, and single-tier inspection.
|
|
2
|
+
|
|
3
|
+
:meth:`recall` blends three sources into one honestly comparable ranking:
|
|
4
|
+
|
|
5
|
+
* **workspace** memories, scored lexically as the fraction of query tokens
|
|
6
|
+
present — the same scorer both text tiers use, so cross-tier ordering is real
|
|
7
|
+
rather than an artifact of per-tier constants;
|
|
8
|
+
* **graph** nodes from the Knowledge Graph search;
|
|
9
|
+
* **vector** neighbours (v9.3.0 hybrid recall), which is what lets recall find
|
|
10
|
+
knowledge phrased differently from the query. The vector tier is optional:
|
|
11
|
+
any failure degrades recall to pure lexical instead of breaking it.
|
|
12
|
+
|
|
13
|
+
A quality gate then drops zero-score rows *only* when at least one row carries
|
|
14
|
+
real evidence, so the gate can never empty a recall. Every row reports its
|
|
15
|
+
confidence and which evidence kinds produced it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from ._contract import MemoryCore as _Core
|
|
23
|
+
from .constants import LOGGER, TIERS, WORKSPACE_KINDS, _visual_fields
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryRecallMixin(_Core):
|
|
27
|
+
"""Recall, tiers, and per-tier inspection. Mixed into ``MemoryService``."""
|
|
28
|
+
|
|
29
|
+
def tiers(self) -> Dict[str, Any]:
|
|
30
|
+
return {"tiers": list(TIERS), "workspace_kinds": list(WORKSPACE_KINDS)}
|
|
31
|
+
|
|
32
|
+
# ── recall (unified retrieval over the memory tiers) ───────────────────
|
|
33
|
+
def recall(
|
|
34
|
+
self,
|
|
35
|
+
query: str,
|
|
36
|
+
*,
|
|
37
|
+
user_email: Optional[str] = None,
|
|
38
|
+
workspace_id: Optional[str] = None,
|
|
39
|
+
limit: int = 20,
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
q = str(query or "").strip()
|
|
42
|
+
query_tokens = [tok for tok in q.lower().split() if tok]
|
|
43
|
+
|
|
44
|
+
def _matched_terms(*texts: Any) -> List[str]:
|
|
45
|
+
haystack = " ".join(str(t or "") for t in texts).lower()
|
|
46
|
+
return [tok for tok in query_tokens if tok in haystack]
|
|
47
|
+
|
|
48
|
+
def _lexical_score(matched: List[str]) -> float:
|
|
49
|
+
# Honest, comparable relevance: fraction of query tokens present.
|
|
50
|
+
# Both tiers share this scorer so the cross-tier ranking is real,
|
|
51
|
+
# not an artifact of per-tier constants.
|
|
52
|
+
if not query_tokens:
|
|
53
|
+
return 0.0
|
|
54
|
+
return round(len(matched) / len(query_tokens), 4)
|
|
55
|
+
|
|
56
|
+
results: List[Dict[str, Any]] = []
|
|
57
|
+
|
|
58
|
+
errors: List[Dict[str, str]] = []
|
|
59
|
+
try:
|
|
60
|
+
mem = self._store.search_memories(q, user_email=user_email, limit=limit, workspace_id=workspace_id).get("memories", [])
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
LOGGER.exception("workspace memory search failed")
|
|
63
|
+
errors.append({"source": "workspace", "detail": str(exc)})
|
|
64
|
+
mem = []
|
|
65
|
+
for m in mem:
|
|
66
|
+
matched = _matched_terms(m.get("content"), " ".join(m.get("tags") or []), m.get("kind"))
|
|
67
|
+
results.append({
|
|
68
|
+
"source": "workspace",
|
|
69
|
+
"id": m.get("id"),
|
|
70
|
+
"title": (m.get("kind") or "memory"),
|
|
71
|
+
"snippet": str(m.get("content") or "")[:240],
|
|
72
|
+
"kind": m.get("kind"),
|
|
73
|
+
"score": _lexical_score(matched),
|
|
74
|
+
"matched_terms": matched,
|
|
75
|
+
"tags": m.get("tags") or [],
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if self._enable_graph and q:
|
|
79
|
+
try:
|
|
80
|
+
# KnowledgeGraph.search returns {"query": ..., "matches": [...]}.
|
|
81
|
+
search_kwargs = (
|
|
82
|
+
{"allowed_workspaces": {workspace_id}}
|
|
83
|
+
if workspace_id is not None
|
|
84
|
+
else {}
|
|
85
|
+
)
|
|
86
|
+
hits = self._kg.search(q, limit, **search_kwargs).get("matches", [])
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
LOGGER.exception("knowledge graph memory search failed")
|
|
89
|
+
errors.append({"source": "graph", "detail": str(exc)})
|
|
90
|
+
hits = []
|
|
91
|
+
for hit in hits[:limit]:
|
|
92
|
+
matched = _matched_terms(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content"))
|
|
93
|
+
results.append({
|
|
94
|
+
"source": "graph",
|
|
95
|
+
"id": hit.get("id") or hit.get("node_id"),
|
|
96
|
+
"title": hit.get("title") or hit.get("name") or "node",
|
|
97
|
+
"snippet": str(hit.get("summary") or hit.get("content") or "")[:240],
|
|
98
|
+
"kind": hit.get("type") or "node",
|
|
99
|
+
"score": _lexical_score(matched),
|
|
100
|
+
"matched_terms": matched,
|
|
101
|
+
**_visual_fields(hit),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# v9.3.0 hybrid recall: blend semantic similarity from the vector
|
|
105
|
+
# index into the lexical ranking. Vector evidence lets recall find
|
|
106
|
+
# knowledge phrased differently from the query — the main lexical
|
|
107
|
+
# blind spot. The vector tier is optional: any failure degrades recall
|
|
108
|
+
# back to pure lexical instead of breaking it.
|
|
109
|
+
vector_used = False
|
|
110
|
+
if self._enable_graph and q and hasattr(self._kg, "vector_search"):
|
|
111
|
+
try:
|
|
112
|
+
vector_hits = list(
|
|
113
|
+
self._kg.vector_search(q, limit=limit).get("matches", [])
|
|
114
|
+
)
|
|
115
|
+
# Workspace scoping is server-owned: the vector index is
|
|
116
|
+
# global, so scoped calls must filter matches to visible
|
|
117
|
+
# nodes before they can influence results.
|
|
118
|
+
if workspace_id is not None and vector_hits and hasattr(self._kg, "filter_scoped_nodes"):
|
|
119
|
+
vector_hits = self._kg.filter_scoped_nodes(
|
|
120
|
+
vector_hits, {workspace_id}, id_key="node_id"
|
|
121
|
+
)
|
|
122
|
+
vector_used = True
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
LOGGER.exception("vector recall failed; falling back to lexical")
|
|
125
|
+
errors.append({"source": "vector", "detail": str(exc)})
|
|
126
|
+
vector_hits = []
|
|
127
|
+
by_node_id = {str(r.get("id")): r for r in results if r.get("source") == "graph"}
|
|
128
|
+
for hit in vector_hits:
|
|
129
|
+
node_id = str(hit.get("node_id") or hit.get("id") or "")
|
|
130
|
+
similarity = round(float(hit.get("score") or 0.0), 4)
|
|
131
|
+
if similarity <= 0:
|
|
132
|
+
continue
|
|
133
|
+
# Citation precision (v9.9.6): a chunk hit knows where in the
|
|
134
|
+
# document it came from (section heading / page). Carry that
|
|
135
|
+
# locator onto the recall row so the citation can say it.
|
|
136
|
+
hit_metadata = hit.get("metadata")
|
|
137
|
+
locator = (
|
|
138
|
+
str(hit_metadata.get("locator") or "")
|
|
139
|
+
if isinstance(hit_metadata, dict)
|
|
140
|
+
else ""
|
|
141
|
+
)
|
|
142
|
+
existing = by_node_id.get(node_id)
|
|
143
|
+
if existing is not None:
|
|
144
|
+
existing["vector_score"] = max(existing.get("vector_score", 0.0), similarity)
|
|
145
|
+
existing["score"] = round(
|
|
146
|
+
max(existing.get("score", 0.0), 0.4 * existing.get("score", 0.0) + 0.6 * similarity),
|
|
147
|
+
4,
|
|
148
|
+
)
|
|
149
|
+
if locator and not existing.get("locator"):
|
|
150
|
+
existing["locator"] = locator
|
|
151
|
+
else:
|
|
152
|
+
matched = _matched_terms(hit.get("title"), hit.get("summary"))
|
|
153
|
+
row = {
|
|
154
|
+
"source": "graph",
|
|
155
|
+
"id": node_id or hit.get("id"),
|
|
156
|
+
"title": hit.get("title") or "node",
|
|
157
|
+
"snippet": str(hit.get("summary") or "")[:240],
|
|
158
|
+
"kind": hit.get("type") or "node",
|
|
159
|
+
"score": round(max(_lexical_score(matched), 0.6 * similarity), 4),
|
|
160
|
+
"matched_terms": matched,
|
|
161
|
+
"vector_score": similarity,
|
|
162
|
+
**({"locator": locator} if locator else {}),
|
|
163
|
+
**_visual_fields(hit),
|
|
164
|
+
}
|
|
165
|
+
results.append(row)
|
|
166
|
+
if node_id:
|
|
167
|
+
by_node_id[node_id] = row
|
|
168
|
+
|
|
169
|
+
# Quality gate: when at least one result carries real evidence
|
|
170
|
+
# (lexical term hits, or semantic similarity in hybrid mode),
|
|
171
|
+
# zero-score rows are noise relative to it and are dropped. When
|
|
172
|
+
# nothing scores (e.g. tokenization mismatch), everything is kept so
|
|
173
|
+
# the tiers' own search filters still decide — the gate never empties
|
|
174
|
+
# a recall.
|
|
175
|
+
candidates = len(results)
|
|
176
|
+
if query_tokens and any(r.get("score", 0) > 0 for r in results):
|
|
177
|
+
results = [r for r in results if r.get("score", 0) > 0]
|
|
178
|
+
for r in results:
|
|
179
|
+
r["confidence"] = "high" if r.get("score", 0) >= 0.65 else "medium" if r.get("score", 0) >= 0.3 else "low"
|
|
180
|
+
evidence = []
|
|
181
|
+
if r.get("matched_terms"):
|
|
182
|
+
evidence.append("lexical")
|
|
183
|
+
if r.get("vector_score"):
|
|
184
|
+
evidence.append("semantic")
|
|
185
|
+
r["evidence_kinds"] = evidence
|
|
186
|
+
|
|
187
|
+
results.sort(key=lambda r: r.get("score", 0), reverse=True)
|
|
188
|
+
return {
|
|
189
|
+
"query": q,
|
|
190
|
+
"results": results[: max(1, min(limit, 100))],
|
|
191
|
+
"count": len(results),
|
|
192
|
+
"source": "live",
|
|
193
|
+
"status": "degraded" if errors else "ok",
|
|
194
|
+
"errors": errors,
|
|
195
|
+
"quality_gate": {
|
|
196
|
+
"candidates": candidates,
|
|
197
|
+
"passed": len(results),
|
|
198
|
+
"filtered": candidates - len(results),
|
|
199
|
+
"gate": "hybrid-evidence/v2" if vector_used else "lexical-evidence/v1",
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# ── inspect a single tier ─────────────────────────────────────────────
|
|
204
|
+
def inspect(self, source: str, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|
|
205
|
+
if source == "workspace":
|
|
206
|
+
items = self._workspace_memories(user_email=user_email, workspace_id=workspace_id or "personal")[:limit]
|
|
207
|
+
return {"source": source, "items": items, "count": len(items)}
|
|
208
|
+
if source == "project":
|
|
209
|
+
if workspace_id is None:
|
|
210
|
+
items = [m for m in self._all_memories() if (m.get("workspace_id") or "personal") != "personal"][:limit]
|
|
211
|
+
else:
|
|
212
|
+
items = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)[:limit]
|
|
213
|
+
return {"source": source, "items": items, "count": len(items)}
|
|
214
|
+
if source == "agent":
|
|
215
|
+
items = self._snapshots(workspace_id=workspace_id)[:limit]
|
|
216
|
+
return {"source": source, "items": items, "count": len(items)}
|
|
217
|
+
if source == "conversation":
|
|
218
|
+
convs = self._scoped_conversations(user_email=user_email, workspace_id=workspace_id)
|
|
219
|
+
items = [{"id": c.get("id"), "title": c.get("title") or c.get("id"), "messages": len(c.get("messages") or [])} for c in convs[:limit]]
|
|
220
|
+
return {"source": source, "items": items, "count": len(convs)}
|
|
221
|
+
if source == "graph":
|
|
222
|
+
return {"source": source, "stats": self._kg_stats() or {}, "available": bool(self._kg_stats())}
|
|
223
|
+
if source == "vector":
|
|
224
|
+
return {"source": source, "index": self._kg_index() or {}, "available": bool(self._kg_index())}
|
|
225
|
+
raise KeyError(source)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""The composed memory service — constructor state plus the six surfaces.
|
|
2
|
+
|
|
3
|
+
``MemoryService`` owns the state every mixin reads (the workspace store, the
|
|
4
|
+
optional Knowledge Graph, the data directory, and whichever conversation
|
|
5
|
+
backing is wired) and inherits its behaviour from the store-read, manager,
|
|
6
|
+
brief, proof, recall and maintenance mixins. Same public surface, same method
|
|
7
|
+
resolution, in seven readable files instead of one twelve-hundred-line one.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .brief import MemoryBriefMixin
|
|
16
|
+
from .maintenance import MemoryMaintenanceMixin
|
|
17
|
+
from .manager import MemoryManagerMixin
|
|
18
|
+
from .proof import MemoryProofMixin
|
|
19
|
+
from .recall import MemoryRecallMixin
|
|
20
|
+
from .stores import MemoryStoreReadsMixin
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MemoryService(
|
|
24
|
+
MemoryStoreReadsMixin,
|
|
25
|
+
MemoryManagerMixin,
|
|
26
|
+
MemoryBriefMixin,
|
|
27
|
+
MemoryProofMixin,
|
|
28
|
+
MemoryRecallMixin,
|
|
29
|
+
MemoryMaintenanceMixin,
|
|
30
|
+
):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
store: Any,
|
|
35
|
+
data_dir: Path,
|
|
36
|
+
knowledge_graph: Any = None,
|
|
37
|
+
enable_graph: bool = True,
|
|
38
|
+
history_file: Optional[Path] = None,
|
|
39
|
+
conversation_store: Any = None,
|
|
40
|
+
):
|
|
41
|
+
self._store = store
|
|
42
|
+
self._kg = knowledge_graph
|
|
43
|
+
self._enable_graph = bool(enable_graph and knowledge_graph is not None)
|
|
44
|
+
self._data_dir = Path(data_dir)
|
|
45
|
+
self._history_file = Path(history_file) if history_file else (self._data_dir / "chat_history.json")
|
|
46
|
+
# v4: the durable SQLite conversation store supersedes the JSON file
|
|
47
|
+
# as the conversation tier's backing store when provided.
|
|
48
|
+
self._conversation_store = conversation_store
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Reads over the real backing stores — the only place the tiers touch them.
|
|
2
|
+
|
|
3
|
+
Six tiers, six backends, one rule: never invent. A workspace/snapshot/
|
|
4
|
+
conversation read that fails raises :class:`MemoryServiceError` so the caller
|
|
5
|
+
learns the tier is unreadable instead of receiving an empty list; a graph or
|
|
6
|
+
vector read that fails degrades to ``None`` because those tiers are optional
|
|
7
|
+
and report themselves as ``unavailable`` upstream.
|
|
8
|
+
|
|
9
|
+
The conversation tier has two backings: the durable SQLite conversation store
|
|
10
|
+
when one is wired (v4), and the legacy JSON history file otherwise.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from ._contract import MemoryCore as _Core
|
|
19
|
+
from .constants import LOGGER, MemoryServiceError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MemoryStoreReadsMixin(_Core):
|
|
23
|
+
"""The store reads. Mixed into ``MemoryService``."""
|
|
24
|
+
|
|
25
|
+
# ── helpers over the underlying stores ────────────────────────────────
|
|
26
|
+
def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
27
|
+
try:
|
|
28
|
+
return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
LOGGER.exception("workspace memory read failed")
|
|
31
|
+
raise MemoryServiceError("workspace memory backend unavailable") from exc
|
|
32
|
+
|
|
33
|
+
def _all_memories(self) -> List[Dict[str, Any]]:
|
|
34
|
+
try:
|
|
35
|
+
return list(self._store.list_memories().get("memories", []))
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
LOGGER.exception("global memory read failed")
|
|
38
|
+
raise MemoryServiceError("memory backend unavailable") from exc
|
|
39
|
+
|
|
40
|
+
def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
41
|
+
try:
|
|
42
|
+
return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
LOGGER.exception("memory snapshot read failed")
|
|
45
|
+
raise MemoryServiceError("memory snapshot backend unavailable") from exc
|
|
46
|
+
|
|
47
|
+
def _conversations(self) -> List[Dict[str, Any]]:
|
|
48
|
+
if self._conversation_store is not None:
|
|
49
|
+
try:
|
|
50
|
+
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
|
51
|
+
for item in self._conversation_store.history():
|
|
52
|
+
grouped.setdefault(item.get("conversation_id") or "legacy-previous-history", []).append(item)
|
|
53
|
+
return [{"id": conv_id, "messages": msgs} for conv_id, msgs in grouped.items()]
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
LOGGER.exception("conversation store read failed")
|
|
56
|
+
raise MemoryServiceError("conversation backend unavailable") from exc
|
|
57
|
+
if not self._history_file.exists():
|
|
58
|
+
return []
|
|
59
|
+
try:
|
|
60
|
+
with open(self._history_file, "r", encoding="utf-8") as fh:
|
|
61
|
+
data = json.load(fh)
|
|
62
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
63
|
+
LOGGER.exception("legacy conversation history read failed")
|
|
64
|
+
raise MemoryServiceError("conversation history is unreadable") from exc
|
|
65
|
+
if isinstance(data, dict):
|
|
66
|
+
convs = data.get("conversations")
|
|
67
|
+
if isinstance(convs, list):
|
|
68
|
+
return convs
|
|
69
|
+
return [{"id": k, **(v if isinstance(v, dict) else {"messages": v})} for k, v in data.items()]
|
|
70
|
+
if isinstance(data, list):
|
|
71
|
+
return data
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
def _scoped_conversations(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
75
|
+
if not user_email:
|
|
76
|
+
return self._conversations()
|
|
77
|
+
target_workspace = workspace_id or "personal"
|
|
78
|
+
scoped: List[Dict[str, Any]] = []
|
|
79
|
+
for conversation in self._conversations():
|
|
80
|
+
messages = conversation.get("messages") or []
|
|
81
|
+
if not isinstance(messages, list):
|
|
82
|
+
continue
|
|
83
|
+
kept = [
|
|
84
|
+
message
|
|
85
|
+
for message in messages
|
|
86
|
+
if isinstance(message, dict)
|
|
87
|
+
and message.get("user_email") == user_email
|
|
88
|
+
and (message.get("workspace_id") or "personal") == target_workspace
|
|
89
|
+
]
|
|
90
|
+
if kept:
|
|
91
|
+
scoped.append({**conversation, "messages": kept})
|
|
92
|
+
return scoped
|
|
93
|
+
|
|
94
|
+
def _kg_stats(self) -> Optional[Dict[str, Any]]:
|
|
95
|
+
if not self._enable_graph:
|
|
96
|
+
return None
|
|
97
|
+
try:
|
|
98
|
+
return self._kg.stats()
|
|
99
|
+
except Exception:
|
|
100
|
+
LOGGER.exception("knowledge graph stats read failed")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _kg_index(self) -> Optional[Dict[str, Any]]:
|
|
104
|
+
if not self._enable_graph:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
return self._kg.index_status()
|
|
108
|
+
except Exception:
|
|
109
|
+
LOGGER.exception("knowledge graph index status read failed")
|
|
110
|
+
return None
|