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.
Files changed (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. 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