ltcai 6.3.1 → 6.5.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 (37) hide show
  1. package/README.md +35 -41
  2. package/docs/CHANGELOG.md +59 -0
  3. package/frontend/src/api/client.ts +1 -0
  4. package/frontend/src/features/brain/BrainConversation.tsx +12 -3
  5. package/frontend/src/features/brain/BrainHome.tsx +21 -1
  6. package/frontend/src/features/brain/BrainMemoryLayer.tsx +8 -1
  7. package/frontend/src/features/brain/BrainOverviewPanel.tsx +15 -1
  8. package/frontend/src/features/brain/brainData.ts +72 -1
  9. package/frontend/src/features/brain/types.ts +15 -0
  10. package/frontend/src/i18n.ts +22 -0
  11. package/frontend/src/styles.css +128 -0
  12. package/lattice_brain/__init__.py +3 -1
  13. package/lattice_brain/quality.py +409 -0
  14. package/lattice_brain/runtime/multi_agent.py +1 -1
  15. package/latticeai/__init__.py +1 -1
  16. package/latticeai/api/knowledge_graph.py +71 -10
  17. package/latticeai/api/local_files.py +2 -0
  18. package/latticeai/api/memory.py +12 -6
  19. package/latticeai/api/search.py +9 -1
  20. package/latticeai/api/tools.py +3 -0
  21. package/latticeai/core/marketplace.py +1 -1
  22. package/latticeai/core/workspace_os.py +1 -1
  23. package/latticeai/runtime/chat_wiring.py +1 -0
  24. package/latticeai/services/memory_service.py +130 -21
  25. package/latticeai/services/router_context.py +4 -0
  26. package/latticeai/services/search_service.py +61 -10
  27. package/package.json +1 -1
  28. package/src-tauri/Cargo.lock +1 -1
  29. package/src-tauri/Cargo.toml +1 -1
  30. package/src-tauri/tauri.conf.json +1 -1
  31. package/static/app/asset-manifest.json +5 -5
  32. package/static/app/assets/index-C1J_1441.js +16 -0
  33. package/static/app/assets/index-C1J_1441.js.map +1 -0
  34. package/static/app/assets/{index-Div5vMlq.css → index-CdCVz_i4.css} +1 -1
  35. package/static/app/index.html +2 -2
  36. package/static/app/assets/index-t1jx1BR9.js +0 -16
  37. package/static/app/assets/index-t1jx1BR9.js.map +0 -1
@@ -1421,7 +1421,65 @@ body {
1421
1421
  line-height: 1.35;
1422
1422
  }
1423
1423
 
1424
+ .brain-readiness-strip {
1425
+ display: grid;
1426
+ grid-template-columns: minmax(0, 1fr) minmax(5.6rem, 0.6fr) auto;
1427
+ align-items: center;
1428
+ gap: 0.7rem;
1429
+ margin-top: 0.75rem;
1430
+ border: 1px solid hsl(var(--brain-core) / 0.28);
1431
+ border-radius: 8px;
1432
+ background: hsl(var(--brain-core) / 0.06);
1433
+ padding: 0.66rem;
1434
+ }
1435
+
1436
+ .brain-readiness-strip span {
1437
+ display: block;
1438
+ color: hsl(var(--fg-muted));
1439
+ font-size: 0.64rem;
1440
+ font-weight: 820;
1441
+ text-transform: uppercase;
1442
+ }
1443
+
1444
+ .brain-readiness-strip strong {
1445
+ display: block;
1446
+ margin-top: 0.12rem;
1447
+ font-size: 0.82rem;
1448
+ line-height: 1.2;
1449
+ }
1450
+
1451
+ .brain-readiness-strip button {
1452
+ min-height: 2rem;
1453
+ border: 1px solid hsl(var(--brain-core) / 0.34);
1454
+ border-radius: 999px;
1455
+ background: hsl(var(--surface) / 0.78);
1456
+ color: hsl(var(--fg));
1457
+ padding: 0 0.72rem;
1458
+ font-size: 0.76rem;
1459
+ font-weight: 760;
1460
+ white-space: nowrap;
1461
+ }
1462
+
1463
+ .brain-readiness-meter {
1464
+ height: 0.42rem;
1465
+ overflow: hidden;
1466
+ border-radius: 999px;
1467
+ background: hsl(var(--bg) / 0.55);
1468
+ }
1469
+
1470
+ .brain-readiness-meter i {
1471
+ display: block;
1472
+ height: 100%;
1473
+ border-radius: inherit;
1474
+ background: linear-gradient(90deg, hsl(var(--memory)), hsl(var(--knowledge)), hsl(var(--brain-core)));
1475
+ transition: width 260ms ease;
1476
+ }
1477
+
1424
1478
  .brain-save-feedback {
1479
+ display: grid;
1480
+ grid-template-columns: auto minmax(0, 1fr);
1481
+ align-items: center;
1482
+ gap: 0.22rem 0.48rem;
1425
1483
  border: 1px solid hsl(var(--memory) / 0.38);
1426
1484
  border-radius: 8px;
1427
1485
  background: hsl(var(--memory) / 0.08);
@@ -1431,6 +1489,13 @@ body {
1431
1489
  font-weight: 720;
1432
1490
  }
1433
1491
 
1492
+ .brain-save-feedback small {
1493
+ grid-column: 2;
1494
+ color: hsl(var(--fg-muted));
1495
+ font-size: 0.72rem;
1496
+ font-weight: 560;
1497
+ }
1498
+
1434
1499
  .brain-message {
1435
1500
  max-width: 78%;
1436
1501
  margin-bottom: 1.1rem;
@@ -1911,6 +1976,59 @@ body {
1911
1976
  outline: none;
1912
1977
  }
1913
1978
 
1979
+ .brain-depth-rail {
1980
+ position: absolute;
1981
+ left: 50%;
1982
+ top: 0.85rem;
1983
+ z-index: 6;
1984
+ display: flex;
1985
+ width: min(92vw, 35rem);
1986
+ transform: translateX(-50%);
1987
+ gap: 0.28rem;
1988
+ border: 1px solid hsl(var(--border) / 0.48);
1989
+ border-radius: 999px;
1990
+ background: hsl(var(--surface-glass));
1991
+ padding: 0.32rem;
1992
+ box-shadow: 0 12px 30px hsl(200 30% 3% / 0.18);
1993
+ backdrop-filter: blur(18px);
1994
+ }
1995
+
1996
+ .brain-depth-rail button {
1997
+ display: inline-grid;
1998
+ min-width: 0;
1999
+ flex: 1 1 0;
2000
+ align-items: center;
2001
+ gap: 0.1rem;
2002
+ border: 0;
2003
+ border-radius: 999px;
2004
+ background: transparent;
2005
+ color: hsl(var(--fg-muted));
2006
+ padding: 0.36rem 0.42rem;
2007
+ text-align: center;
2008
+ }
2009
+
2010
+ .brain-depth-rail span {
2011
+ font-size: 0.58rem;
2012
+ font-weight: 840;
2013
+ }
2014
+
2015
+ .brain-depth-rail strong {
2016
+ overflow: hidden;
2017
+ text-overflow: ellipsis;
2018
+ white-space: nowrap;
2019
+ font-size: 0.66rem;
2020
+ line-height: 1.05;
2021
+ }
2022
+
2023
+ .brain-depth-rail button.is-revealed {
2024
+ background: hsl(var(--brain-core) / 0.1);
2025
+ color: hsl(var(--fg));
2026
+ }
2027
+
2028
+ .brain-depth-rail button[aria-current="step"] {
2029
+ box-shadow: 0 0 0 1px hsl(var(--brain-core) / 0.38) inset;
2030
+ }
2031
+
1914
2032
  .memory-fragment,
1915
2033
  .concept-signal,
1916
2034
  .graph-node {
@@ -2833,6 +2951,16 @@ body {
2833
2951
  .brain-depth-actions button {
2834
2952
  flex: 0 0 auto;
2835
2953
  }
2954
+ .brain-depth-rail {
2955
+ top: 0.35rem;
2956
+ width: min(100vw - 1rem, 25rem);
2957
+ }
2958
+ .brain-depth-rail strong {
2959
+ display: none;
2960
+ }
2961
+ .brain-readiness-strip {
2962
+ grid-template-columns: 1fr;
2963
+ }
2836
2964
  .brain-overview-grid {
2837
2965
  grid-template-columns: 1fr;
2838
2966
  }
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.3.1"
29
+ __version__ = "6.5.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -45,6 +45,7 @@ __all__ = [
45
45
  "IngestionPipeline",
46
46
  "KGPortabilityService",
47
47
  "KnowledgeGraphStore",
48
+ "LatticeBrainQuality",
48
49
  "MultiAgentOrchestrator",
49
50
  "PostgresConfig",
50
51
  "PostgresEngine",
@@ -65,6 +66,7 @@ _LAZY = {
65
66
  "ConversationStore": ("conversations", "ConversationStore"),
66
67
  "BrainMemory": ("memory", "BrainMemory"),
67
68
  "KnowledgeGraphStore": ("graph.store", "KnowledgeGraphStore"),
69
+ "LatticeBrainQuality": ("quality", "LatticeBrainQuality"),
68
70
  "IngestionItem": ("ingestion", "IngestionItem"),
69
71
  "IngestionPipeline": ("ingestion", "IngestionPipeline"),
70
72
  "KGPortabilityService": ("portability", "KGPortabilityService"),
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ lattice_brain/quality.py
4
+ Pure Python Quality Layer for Lattice Brain (v6.4+ hardening)
5
+ - Does not modify any DB schema or existing APIs
6
+ - Self-contained, dependency-free core (stdlib only)
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import math
11
+ import time
12
+ import hashlib
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Dict, List, Optional
15
+ from collections import defaultdict
16
+ import re
17
+
18
+ # -----------------------------
19
+ # 1. Embedding Fallback Labelling + Drift/Reindex Plan
20
+ # -----------------------------
21
+ @dataclass
22
+ class EmbeddingLabel:
23
+ vector_id: str
24
+ label: str
25
+ confidence: float
26
+ drift_score: float = 0.0
27
+ needs_reindex: bool = False
28
+
29
+ class EmbeddingFallbackLabeller:
30
+ """Fallback labelling when primary embedding unavailable + drift detection"""
31
+
32
+ def __init__(self, drift_threshold: float = 0.15):
33
+ self.drift_threshold = drift_threshold
34
+ self.label_cache: Dict[str, EmbeddingLabel] = {}
35
+ self.vector_cache: Dict[str, List[float]] = {}
36
+
37
+ def label(self, vector_id: str, embedding: Optional[List[float]] = None,
38
+ metadata: Optional[Dict] = None) -> EmbeddingLabel:
39
+ if embedding is None:
40
+ label = "unembedded_fallback"
41
+ conf = 0.3
42
+ else:
43
+ # Simple hash-based pseudo-label for determinism
44
+ h = hashlib.md5(str(embedding[:4]).encode()).hexdigest()[:8]
45
+ label = f"emb_cluster_{h}"
46
+ conf = 0.85
47
+
48
+ drift = self._compute_drift(vector_id, embedding)
49
+ needs_reindex = drift > self.drift_threshold
50
+
51
+ el = EmbeddingLabel(vector_id, label, conf, drift, needs_reindex)
52
+ self.label_cache[vector_id] = el
53
+ if embedding is not None:
54
+ self.vector_cache[vector_id] = [float(value) for value in embedding]
55
+ return el
56
+
57
+ def _compute_drift(self, vector_id: str, new_emb: Optional[List[float]]) -> float:
58
+ old = self.vector_cache.get(vector_id)
59
+ if old is None or new_emb is None:
60
+ return 0.0
61
+ return self._cosine_distance(old, new_emb)
62
+
63
+ def _cosine_distance(self, a: List[float], b: List[float]) -> float:
64
+ if not a or not b or len(a) != len(b):
65
+ return 1.0
66
+ dot = sum(x*y for x,y in zip(a,b))
67
+ na = math.sqrt(sum(x*x for x in a))
68
+ nb = math.sqrt(sum(x*x for x in b))
69
+ return 1.0 - (dot / (na*nb + 1e-9))
70
+
71
+ def generate_reindex_plan(self) -> List[str]:
72
+ return [vid for vid, lab in self.label_cache.items() if lab.needs_reindex]
73
+
74
+ # -----------------------------
75
+ # 2. BM25 Lexical Scoring + Hybrid Fusion + Reranker Interface
76
+ # -----------------------------
77
+ class BM25Scorer:
78
+ def __init__(self, k1: float = 1.5, b: float = 0.75):
79
+ self.k1 = k1
80
+ self.b = b
81
+ self.doc_freqs: Dict[str, int] = defaultdict(int)
82
+ self.doc_lens: Dict[str, int] = {}
83
+ self.corpus_size = 0
84
+ self.avgdl = 0.0
85
+
86
+ def fit(self, corpus: Dict[str, str]):
87
+ self.corpus_size = len(corpus)
88
+ total_len = 0
89
+ for did, text in corpus.items():
90
+ tokens = self._tokenize(text)
91
+ self.doc_lens[did] = len(tokens)
92
+ total_len += len(tokens)
93
+ for t in set(tokens):
94
+ self.doc_freqs[t] += 1
95
+ self.avgdl = total_len / max(1, self.corpus_size)
96
+
97
+ def score(self, query: str, doc_id: str, doc_text: str) -> float:
98
+ tokens = self._tokenize(query)
99
+ doc_tokens = self._tokenize(doc_text)
100
+ score = 0.0
101
+ for t in tokens:
102
+ if t not in doc_tokens:
103
+ continue
104
+ tf = doc_tokens.count(t)
105
+ df = self.doc_freqs.get(t, 0)
106
+ idf = math.log((self.corpus_size - df + 0.5) / (df + 0.5) + 1)
107
+ denom = tf + self.k1 * (1 - self.b + self.b * self.doc_lens.get(doc_id, 0) / self.avgdl)
108
+ score += idf * tf * (self.k1 + 1) / (denom + 1e-9)
109
+ return score
110
+
111
+ def _tokenize(self, text: str) -> List[str]:
112
+ return re.findall(r'\w+', text.lower())
113
+
114
+ class HybridFusion:
115
+ """Hybrid fusion of lexical (BM25) + vector scores"""
116
+ def __init__(self, alpha: float = 0.6):
117
+ self.alpha = alpha # weight for vector score
118
+ self.bm25 = BM25Scorer()
119
+
120
+ def fuse(self, query: str, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
121
+ # candidates: [{"id": , "text": , "vector_score": }]
122
+ corpus = {c["id"]: c.get("text", "") for c in candidates}
123
+ self.bm25.fit(corpus)
124
+ results = []
125
+ for c in candidates:
126
+ lex = self.bm25.score(query, c["id"], c.get("text", ""))
127
+ vec = c.get("vector_score", 0.5)
128
+ fused = self.alpha * vec + (1 - self.alpha) * (lex / 10.0) # normalize rough
129
+ c["fused_score"] = round(fused, 4)
130
+ results.append(c)
131
+ return sorted(results, key=lambda x: x["fused_score"], reverse=True)
132
+
133
+ class RerankerInterface:
134
+ """Pluggable reranker interface"""
135
+ def rerank(self, query: str, candidates: List[Dict], top_k: int = 5) -> List[Dict]:
136
+ # Default local-safe fallback: preserve fused ordering without making
137
+ # unearned cross-encoder claims.
138
+ for c in candidates:
139
+ c["rerank_score"] = c.get("fused_score", 0.0)
140
+ return sorted(candidates, key=lambda x: x.get("rerank_score", 0), reverse=True)[:top_k]
141
+
142
+ # -----------------------------
143
+ # 3. Memory Candidate Extraction / Scoring / Dedupe / Merge / Conflict / Retention
144
+ # -----------------------------
145
+ @dataclass
146
+ class MemoryCandidate:
147
+ id: str
148
+ content: str
149
+ score: float = 0.0
150
+ source: str = "unknown"
151
+ timestamp: float = field(default_factory=time.time)
152
+ conflicts: List[str] = field(default_factory=list)
153
+
154
+ class MemoryQualityManager:
155
+ def extract_candidates(self, memories: List[Dict]) -> List[MemoryCandidate]:
156
+ return [MemoryCandidate(m["id"], m["content"], m.get("score", 0.6), m.get("source", "mem")) for m in memories]
157
+
158
+ def score_candidates(self, cands: List[MemoryCandidate], query: str) -> List[MemoryCandidate]:
159
+ for c in cands:
160
+ # simple lexical overlap score
161
+ overlap = sum(1 for w in query.lower().split() if w in c.content.lower())
162
+ c.score = min(1.0, 0.4 + overlap * 0.15)
163
+ return sorted(cands, key=lambda x: x.score, reverse=True)
164
+
165
+ def dedupe(self, cands: List[MemoryCandidate], threshold: float = 0.85) -> List[MemoryCandidate]:
166
+ kept = []
167
+ seen = set()
168
+ for c in cands:
169
+ h = hashlib.md5(c.content.encode()).hexdigest()[:16]
170
+ if h not in seen:
171
+ seen.add(h)
172
+ kept.append(c)
173
+ return kept
174
+
175
+ def merge(self, cands: List[MemoryCandidate]) -> List[MemoryCandidate]:
176
+ # naive merge by content prefix
177
+ merged = {}
178
+ for c in cands:
179
+ key = c.content[:30]
180
+ if key not in merged or c.score > merged[key].score:
181
+ merged[key] = c
182
+ return list(merged.values())
183
+
184
+ def detect_conflicts(self, cands: List[MemoryCandidate]) -> List[MemoryCandidate]:
185
+ # Lightweight local heuristic; LLM conflict classifiers can replace it.
186
+ for i, c in enumerate(cands):
187
+ if "not" in c.content.lower() or "반대" in c.content:
188
+ c.conflicts.append("conflict:possible_negation")
189
+ return cands
190
+
191
+ def apply_retention(self, cands: List[MemoryCandidate], max_age_days: int = 90) -> List[MemoryCandidate]:
192
+ now = time.time()
193
+ return [c for c in cands if (now - c.timestamp) < max_age_days * 86400]
194
+
195
+ # -----------------------------
196
+ # 4. Graph Edge Validation / Confidence / Evidence / Duplicate Merge / Quality Metrics
197
+ # -----------------------------
198
+ @dataclass
199
+ class GraphEdgeQuality:
200
+ edge_id: str
201
+ confidence: float
202
+ evidence_count: int
203
+ is_duplicate: bool = False
204
+ quality_score: float = 0.0
205
+
206
+ class GraphEdgeQualityManager:
207
+ def validate_edge(self, edge: Dict[str, Any]) -> GraphEdgeQuality:
208
+ conf = edge.get("confidence", 0.7)
209
+ ev = len(edge.get("evidence", []))
210
+ q = min(1.0, conf * 0.75 + min(ev, 5) / 5 * 0.3)
211
+ return GraphEdgeQuality(edge.get("id", "e0"), conf, ev, quality_score=q)
212
+
213
+ def detect_duplicate_edges(self, edges: List[Dict]) -> List[str]:
214
+ seen = {}
215
+ dups = []
216
+ for e in edges:
217
+ key = (e.get("source"), e.get("target"), e.get("type"))
218
+ if key in seen:
219
+ dups.append(e.get("id"))
220
+ else:
221
+ seen[key] = e.get("id")
222
+ return dups
223
+
224
+ def merge_duplicate_edges(self, edges: List[Dict]) -> List[Dict]:
225
+ # keep highest confidence
226
+ best = {}
227
+ for e in edges:
228
+ key = (e.get("source"), e.get("target"), e.get("type"))
229
+ if key not in best or e.get("confidence", 0) > best[key].get("confidence", 0):
230
+ best[key] = e
231
+ return list(best.values())
232
+
233
+ def compute_quality_metrics(self, edges: List[Dict]) -> Dict[str, float]:
234
+ if not edges:
235
+ return {"avg_conf": 0.0, "avg_evidence": 0.0, "dup_rate": 0.0}
236
+ confs = [e.get("confidence", 0.5) for e in edges]
237
+ evs = [len(e.get("evidence", [])) for e in edges]
238
+ dups = len(self.detect_duplicate_edges(edges))
239
+ return {
240
+ "avg_conf": round(sum(confs)/len(confs), 3),
241
+ "avg_evidence": round(sum(evs)/len(evs), 2),
242
+ "dup_rate": round(dups / len(edges), 3)
243
+ }
244
+
245
+ # -----------------------------
246
+ # 5. Structured Context Assembly + Guardrails
247
+ # -----------------------------
248
+ @dataclass
249
+ class ContextGuardrails:
250
+ known: bool = True
251
+ inferred: bool = False
252
+ stale: bool = False
253
+ unknown: bool = False
254
+ confidence: float = 0.8
255
+ timestamp: float = field(default_factory=time.time)
256
+ attribution: str = "system"
257
+
258
+ class StructuredContextAssembler:
259
+ SECTIONS = ["Facts", "Decisions", "Preferences", "Relationships", "Projects", "Recent Events"]
260
+
261
+ def assemble(self, items: List[Dict[str, Any]]) -> Dict[str, List[Dict]]:
262
+ ctx: Dict[str, List[Dict]] = {s: [] for s in self.SECTIONS}
263
+ for item in items:
264
+ section = item.get("section", "Facts")
265
+ if section not in ctx:
266
+ section = "Facts"
267
+ guard = ContextGuardrails(
268
+ known=item.get("known", True),
269
+ inferred=item.get("inferred", False),
270
+ stale=item.get("stale", False),
271
+ unknown=item.get("unknown", False),
272
+ confidence=item.get("confidence", 0.75),
273
+ attribution=item.get("attribution", "user")
274
+ )
275
+ item["guardrails"] = guard.__dict__
276
+ ctx[section].append(item)
277
+ return ctx
278
+
279
+ def apply_guardrails(self, ctx: Dict[str, List[Dict]]) -> Dict[str, List[Dict]]:
280
+ cleaned = {}
281
+ for sec, items in ctx.items():
282
+ cleaned[sec] = [i for i in items if not i.get("guardrails", {}).get("unknown", False)]
283
+ return cleaned
284
+
285
+ # -----------------------------
286
+ # 6. Retrieval Benchmark Fixture Runner
287
+ # -----------------------------
288
+ class RetrievalBenchmarkRunner:
289
+ def __init__(self):
290
+ self.results: List[Dict] = []
291
+
292
+ def run_fixture(self, fixture_name: str, queries: List[Any], top_k: int = 5) -> Dict[str, Any]:
293
+ start = time.time()
294
+ judged = [q for q in queries if isinstance(q, dict)]
295
+ if judged:
296
+ recalls = []
297
+ precisions = []
298
+ ndcgs = []
299
+ for query in judged:
300
+ relevant = set(query.get("relevant") or [])
301
+ retrieved = list(query.get("retrieved") or [])[:top_k]
302
+ if not relevant:
303
+ continue
304
+ hits = [doc_id for doc_id in retrieved if doc_id in relevant]
305
+ recalls.append(len(hits) / len(relevant))
306
+ precisions.append(len(hits) / max(1, len(retrieved)))
307
+ dcg = sum(
308
+ 1.0 / math.log2(rank + 2)
309
+ for rank, doc_id in enumerate(retrieved)
310
+ if doc_id in relevant
311
+ )
312
+ ideal = sum(1.0 / math.log2(rank + 2) for rank in range(min(len(relevant), top_k)))
313
+ ndcgs.append(dcg / ideal if ideal else 0.0)
314
+ recall = sum(recalls) / len(recalls) if recalls else 0.0
315
+ precision = sum(precisions) / len(precisions) if precisions else 0.0
316
+ ndcg = sum(ndcgs) / len(ndcgs) if ndcgs else 0.0
317
+ else:
318
+ recall = precision = ndcg = 0.0
319
+ metrics = {
320
+ "fixture": fixture_name,
321
+ "queries": len(queries),
322
+ "avg_latency_ms": round((time.time() - start) * 1000 / max(1, len(queries)), 2),
323
+ "recall@5": round(recall, 4),
324
+ "precision@5": round(precision, 4),
325
+ "ndcg@5": round(ndcg, 4),
326
+ "top_k": top_k,
327
+ "judged": len(judged),
328
+ }
329
+ self.results.append(metrics)
330
+ return metrics
331
+
332
+ def summary(self) -> Dict[str, Any]:
333
+ if not self.results:
334
+ return {"status": "no runs"}
335
+ return {
336
+ "total_runs": len(self.results),
337
+ "avg_recall": round(sum(r["recall@5"] for r in self.results) / len(self.results), 3)
338
+ }
339
+
340
+ # -----------------------------
341
+ # Main Quality Layer Facade
342
+ # -----------------------------
343
+ class LatticeBrainQuality:
344
+ """Main entry point - pure Python quality hardening layer"""
345
+ def __init__(self):
346
+ self.embed_labeller = EmbeddingFallbackLabeller()
347
+ self.hybrid = HybridFusion()
348
+ self.reranker = RerankerInterface()
349
+ self.memory_mgr = MemoryQualityManager()
350
+ self.graph_mgr = GraphEdgeQualityManager()
351
+ self.context_assembler = StructuredContextAssembler()
352
+ self.benchmark = RetrievalBenchmarkRunner()
353
+
354
+ def full_quality_pass(self, payload: Dict[str, Any]) -> Dict[str, Any]:
355
+ """End-to-end quality pipeline (non-destructive)"""
356
+ result = {"status": "ok", "timestamp": time.time()}
357
+
358
+ # embedding
359
+ if "embeddings" in payload:
360
+ labels = [self.embed_labeller.label(e["id"], e.get("vector")) for e in payload["embeddings"]]
361
+ result["embedding_labels"] = [label.__dict__ for label in labels]
362
+ result["reindex_plan"] = self.embed_labeller.generate_reindex_plan()
363
+
364
+ # hybrid retrieval
365
+ if "retrieval" in payload:
366
+ fused = self.hybrid.fuse(payload["retrieval"]["query"], payload["retrieval"]["candidates"])
367
+ reranked = self.reranker.rerank(payload["retrieval"]["query"], fused)
368
+ result["retrieval"] = {"fused": fused, "reranked": reranked}
369
+
370
+ # memory
371
+ if "memories" in payload:
372
+ cands = self.memory_mgr.extract_candidates(payload["memories"])
373
+ cands = self.memory_mgr.score_candidates(cands, payload.get("query", ""))
374
+ cands = self.memory_mgr.dedupe(cands)
375
+ cands = self.memory_mgr.merge(cands)
376
+ cands = self.memory_mgr.detect_conflicts(cands)
377
+ cands = self.memory_mgr.apply_retention(cands)
378
+ result["memory_candidates"] = [c.__dict__ for c in cands]
379
+
380
+ # graph
381
+ if "graph_edges" in payload:
382
+ eqs = [self.graph_mgr.validate_edge(e) for e in payload["graph_edges"]]
383
+ result["graph_quality"] = [q.__dict__ for q in eqs]
384
+ result["graph_metrics"] = self.graph_mgr.compute_quality_metrics(payload["graph_edges"])
385
+
386
+ # context
387
+ if "context_items" in payload:
388
+ ctx = self.context_assembler.assemble(payload["context_items"])
389
+ ctx = self.context_assembler.apply_guardrails(ctx)
390
+ result["structured_context"] = ctx
391
+
392
+ return result
393
+
394
+
395
+ __all__ = [
396
+ "BM25Scorer",
397
+ "ContextGuardrails",
398
+ "EmbeddingFallbackLabeller",
399
+ "EmbeddingLabel",
400
+ "GraphEdgeQuality",
401
+ "GraphEdgeQualityManager",
402
+ "HybridFusion",
403
+ "LatticeBrainQuality",
404
+ "MemoryCandidate",
405
+ "MemoryQualityManager",
406
+ "RetrievalBenchmarkRunner",
407
+ "RerankerInterface",
408
+ "StructuredContextAssembler",
409
+ ]
@@ -19,7 +19,7 @@ from datetime import datetime
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
 
21
21
 
22
- MULTI_AGENT_VERSION = "6.3.1"
22
+ MULTI_AGENT_VERSION = "6.5.0"
23
23
 
24
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
25
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "6.3.1"
3
+ __version__ = "6.5.0"