Skip to content

Commit a302068

Browse files
laxmancloAlex-Hunterz
authored andcommitted
refactor: Separate syntheses layer from L0 facts and update locomo benchmarks
1 parent 0f37dac commit a302068

10 files changed

Lines changed: 748 additions & 35 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ env/
4343
.coverage
4444
htmlcov/
4545
.tox/
46-
46+
/data
4747
# Environment variables
4848
.env
4949
.env.local

benchmarks/locomo/locomo_runner.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,12 @@ def _format_retrieved_context(search_results: Any) -> str:
703703
for i, ep in enumerate(episodes, 1):
704704
lines.append(f"{i}. {ep.get('text', str(ep))}")
705705

706+
syntheses = search_results.get("syntheses") or []
707+
if syntheses:
708+
lines.append("\n## Syntheses")
709+
for i, sy in enumerate(syntheses, 1):
710+
lines.append(f"{i}. {sy.get('text', str(sy))}")
711+
706712
sentences = search_results.get("sentences") or []
707713
if sentences:
708714
session_sents: dict[str, list[dict[str, Any]]] = {}

test_synthesis.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ async def mock_emb(*args, **kwargs):
1818

1919
n = await v.synthesize("u1")
2020
print("New synthesized facts:", n)
21-
facts = await v.db.get_active_facts("u1", limit=100)
22-
for f in facts:
23-
if f.get("metadata", {}).get("source") == "synthesis":
24-
print("Synthesized:", f["text"])
21+
syntheses = await v.db.search_syntheses([0.1, 0.2], "u1", limit=100)
22+
for s in syntheses:
23+
print("Synthesized:", s["text"])
2524

2625
asyncio.run(test())

vektori/ingestion/synthesis.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,41 +79,37 @@ async def synthesize(self, user_id: str, agent_id: str | None = None) -> int:
7979
inserted = 0
8080

8181
for fact_dict, emb in zip(new_facts, embeddings):
82-
# Check dedup against existing synthesis facts
83-
existing = await self.db.search_facts(
82+
# Check dedup against existing synthesis
83+
existing = await self.db.search_syntheses(
8484
embedding=emb,
8585
user_id=user_id,
8686
agent_id=agent_id,
87-
limit=1,
88-
active_only=True
87+
limit=1
8988
)
9089

9190
skip = False
9291
if existing:
9392
best = existing[0]
9493
sim = 1.0 - best.get("distance", 1.0)
95-
if sim > 0.85 and best.get("metadata", {}).get("source") == "synthesis":
96-
# We already have this synthesis fact, let's just update mentions
97-
await self.db.increment_fact_mentions(best["id"])
94+
if sim > 0.85:
95+
# We already have this synthesis, let's just skip it
9896
skip = True
9997

10098
if skip:
10199
continue
102100

103101
try:
104-
await self.db.insert_fact(
102+
# Need to link the synthesis to the facts that generated it if we want graph traversal
103+
# But for now we just insert it
104+
synthesis_id = await self.db.insert_synthesis(
105105
text=fact_dict["text"],
106106
embedding=emb,
107107
user_id=user_id,
108108
agent_id=agent_id,
109-
session_id="synthesis", # Special session ID
110-
subject="user",
111-
confidence=fact_dict.get("confidence", 0.9),
112-
metadata={"source": "synthesis"},
113-
event_time=now
109+
session_id="synthesis"
114110
)
115111
inserted += 1
116112
except Exception as e:
117-
logger.warning("Failed to insert synthesis fact: %s", e)
113+
logger.warning("Failed to insert synthesis: %s", e)
118114

119115
return inserted

vektori/retrieval/search.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,10 @@ async def _search_stepped(
207207
if not seed_facts:
208208
logger.debug("search: no facts for user=%s, falling back to sentence search", user_id)
209209
if depth == "l0":
210-
return {"facts": [], "episodes": [], "memory_found": False}
210+
return {"facts": [], "episodes": [], "syntheses": [], "memory_found": False}
211211
return {
212212
"facts": [],
213-
"episodes": [],
213+
"episodes": [], "syntheses": [],
214214
"sentences": direct_sentences,
215215
"memory_found": False,
216216
}
@@ -239,16 +239,19 @@ async def _search_stepped(
239239
if depth == "l0":
240240
# L0: graph traversal only — keep it light
241241
episodes = await self.db.get_episodes_for_facts(seed_fact_ids)
242+
syntheses = await self.db.get_syntheses_for_facts(seed_fact_ids)
242243
top = _clean(top_k_facts)
243-
return {"facts": top, "episodes": episodes, "memory_found": len(top) > 0}
244+
return {"facts": top, "episodes": episodes, "syntheses": syntheses, "memory_found": len(top) > 0}
244245

245246
# ── Step 2: Episodes (L1/L2) — graph traversal + vector search ────────
246247
# Run both concurrently: graph edges from matched facts, and direct
247248
# cosine search over episode embeddings. Merge by ID so the same
248249
# episode doesn't appear twice.
249-
graph_episodes, vec_episodes = await asyncio.gather(
250+
graph_episodes, vec_episodes, graph_syntheses, vec_syntheses = await asyncio.gather(
250251
self.db.get_episodes_for_facts(seed_fact_ids),
251252
self.db.search_episodes(query_embedding, user_id, agent_id),
253+
self.db.get_syntheses_for_facts(seed_fact_ids),
254+
self.db.search_syntheses(query_embedding, user_id, agent_id),
252255
)
253256
seen_episode_ids: set[str] = set()
254257
episodes: list[dict[str, Any]] = []
@@ -258,6 +261,14 @@ async def _search_stepped(
258261
seen_episode_ids.add(iid)
259262
episodes.append(ins)
260263

264+
seen_synthesis_ids: set[str] = set()
265+
syntheses: list[dict[str, Any]] = []
266+
for ins in (*graph_syntheses, *vec_syntheses):
267+
iid = ins.get("id")
268+
if iid and iid not in seen_synthesis_ids:
269+
seen_synthesis_ids.add(iid)
270+
syntheses.append(ins)
271+
261272
# ── Step 3: Trace facts → source sentences ────────────────────────────
262273
source_sentence_ids = await self.db.get_source_sentences(seed_fact_ids)
263274

@@ -289,7 +300,7 @@ async def _search_stepped(
289300
if not all_candidate_ids:
290301
return {
291302
"facts": top_facts,
292-
"episodes": episodes,
303+
"episodes": episodes, "syntheses": syntheses,
293304
"sentences": [],
294305
"memory_found": memory_found,
295306
}
@@ -342,7 +353,7 @@ async def _search_stepped(
342353

343354
return {
344355
"facts": top_facts,
345-
"episodes": episodes,
356+
"episodes": episodes, "syntheses": syntheses,
346357
"sentences": _dedup(result_sentences),
347358
"memory_found": memory_found,
348359
}
@@ -394,9 +405,11 @@ async def _search_l2_fast(
394405
top_facts = _clean(_diverse_top_k(scored_facts, top_k))
395406
top_fact_ids = [f["id"] for f in top_facts]
396407

397-
graph_episodes, vec_episodes = await asyncio.gather(
408+
graph_episodes, vec_episodes, graph_syntheses, vec_syntheses = await asyncio.gather(
398409
self.db.get_episodes_for_facts(top_fact_ids),
399410
self.db.search_episodes(query_embedding, user_id, agent_id),
411+
self.db.get_syntheses_for_facts(top_fact_ids),
412+
self.db.search_syntheses(query_embedding, user_id, agent_id),
400413
)
401414
seen_episode_ids: set[str] = set()
402415
episodes: list[dict[str, Any]] = []
@@ -406,9 +419,17 @@ async def _search_l2_fast(
406419
seen_episode_ids.add(iid)
407420
episodes.append(ins)
408421

422+
seen_synthesis_ids: set[str] = set()
423+
syntheses: list[dict[str, Any]] = []
424+
for ins in (*graph_syntheses, *vec_syntheses):
425+
iid = ins.get("id")
426+
if iid and iid not in seen_synthesis_ids:
427+
seen_synthesis_ids.add(iid)
428+
syntheses.append(ins)
429+
409430
return {
410431
"facts": top_facts,
411-
"episodes": episodes,
432+
"episodes": episodes, "syntheses": syntheses,
412433
"sentences": _dedup(raw.get("sentences", [])),
413434
"memory_found": len(top_facts) > 0,
414435
}
@@ -482,7 +503,7 @@ async def _search_one(embedding: list[float]) -> list[dict[str, Any]]:
482503
logger.debug("search_expanded: no facts found, falling back to sentence search")
483504
return {
484505
"facts": [],
485-
"episodes": [],
506+
"episodes": [], "syntheses": [],
486507
"sentences": direct_sentences,
487508
"memory_found": False,
488509
}
@@ -508,9 +529,11 @@ async def _search_one(embedding: list[float]) -> list[dict[str, Any]]:
508529
top_facts = _diverse_top_k(scored_facts, top_k)
509530
fact_ids = [f["id"] for f in top_facts]
510531

511-
graph_episodes, vec_episodes, source_sentence_ids = await asyncio.gather(
532+
graph_episodes, vec_episodes, graph_syntheses, vec_syntheses, source_sentence_ids = await asyncio.gather(
512533
self.db.get_episodes_for_facts(fact_ids),
513534
self.db.search_episodes(embeddings[0], user_id, agent_id),
535+
self.db.get_syntheses_for_facts(fact_ids),
536+
self.db.search_syntheses(embeddings[0], user_id, agent_id),
514537
self.db.get_source_sentences(fact_ids),
515538
)
516539
seen_episode_ids: set[str] = set()
@@ -521,6 +544,14 @@ async def _search_one(embedding: list[float]) -> list[dict[str, Any]]:
521544
seen_episode_ids.add(iid)
522545
episodes.append(ins)
523546

547+
seen_synthesis_ids: set[str] = set()
548+
syntheses: list[dict[str, Any]] = []
549+
for ins in (*graph_syntheses, *vec_syntheses):
550+
iid = ins.get("id")
551+
if iid and iid not in seen_synthesis_ids:
552+
seen_synthesis_ids.add(iid)
553+
syntheses.append(ins)
554+
524555
# Session scoring: merge direct search + fact-linked sentences
525556
direct_sent_by_id: dict[str, dict[str, Any]] = {s["id"]: s for s in direct_sentences}
526557
missing_ids = [sid for sid in source_sentence_ids if sid not in direct_sent_by_id]
@@ -574,7 +605,7 @@ async def _search_one(embedding: list[float]) -> list[dict[str, Any]]:
574605

575606
return {
576607
"facts": _clean(top_facts),
577-
"episodes": episodes,
608+
"episodes": episodes, "syntheses": syntheses,
578609
"sentences": _dedup(result_sentences),
579610
"memory_found": len(top_facts) > 0,
580611
}
@@ -596,23 +627,23 @@ async def _sentence_fallback(
596627
since they're stored synchronously in add().
597628
"""
598629
if depth == "l0":
599-
return {"facts": [], "episodes": [], "memory_found": False}
630+
return {"facts": [], "episodes": [], "syntheses": [], "memory_found": False}
600631

601632
sentences = await self.db.search_sentences(
602633
embedding=query_embedding,
603634
user_id=user_id,
604635
agent_id=agent_id,
605636
limit=top_k,
606637
)
607-
return {"facts": [], "episodes": [], "sentences": sentences, "memory_found": False}
638+
return {"facts": [], "episodes": [], "syntheses": [], "sentences": sentences, "memory_found": False}
608639

609640

610641
# ── Helpers ───────────────────────────────────────────────────────────────────
611642

612643

613644
def _empty(depth: str) -> dict[str, Any]:
614645
"""Return the correct empty structure for a given depth."""
615-
result: dict[str, Any] = {"facts": [], "episodes": []}
646+
result: dict[str, Any] = {"facts": [], "episodes": [], "syntheses": []}
616647
if depth in ("l1", "l2"):
617648
result["sentences"] = []
618649
return result

vektori/storage/base.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,64 @@ async def insert_fact_sources(self, pairs: list[tuple[str, str]]) -> None:
196196
for fact_id, sentence_id in pairs:
197197
await self.insert_fact_source(fact_id, sentence_id)
198198

199+
# ── Syntheses ──
200+
201+
202+
async def insert_synthesis(self, text: str, embedding: list[float], user_id: str, agent_id: str | None = None, score: float = 1.0, mentions: int = 1, metadata: dict | None = None) -> str:
203+
raise NotImplementedError()
204+
205+
206+
async def insert_synthesis_fact(self, synthesis_id: str, fact_id: str) -> None:
207+
raise NotImplementedError()
208+
209+
210+
async def get_syntheses_for_facts(self, fact_ids: list[str]) -> list[dict[str, Any]]:
211+
return []
212+
213+
214+
async def search_syntheses(self, embedding: list[float], user_id: str, agent_id: str | None = None, limit: int = 10, threshold: float = 0.0) -> list[dict[str, Any]]:
215+
return []
216+
217+
@abstractmethod
218+
async def get_source_sentences(self, fact_ids: list[str]) -> list[str]:
219+
"""Return sentence IDs that are sources for the given facts (via fact_sources)."""
220+
...
221+
222+
@abstractmethod
223+
async def get_sentences_by_ids(self, sentence_ids: list[str]) -> list[dict[str, Any]]:
224+
"""Fetch full sentence rows for the given IDs."""
225+
...
226+
227+
228+
# ── Syntheses ──
229+
230+
231+
async def insert_synthesis(self, text: str, embedding: list[float], user_id: str, agent_id: str | None = None, score: float = 1.0, mentions: int = 1, metadata: dict | None = None) -> str:
232+
raise NotImplementedError()
233+
234+
235+
async def insert_synthesis_fact(self, synthesis_id: str, fact_id: str) -> None:
236+
raise NotImplementedError()
237+
238+
239+
async def get_syntheses_for_facts(self, fact_ids: list[str]) -> list[dict[str, Any]]:
240+
return []
241+
242+
243+
async def search_syntheses(self, embedding: list[float], user_id: str, agent_id: str | None = None, limit: int = 10, threshold: float = 0.0) -> list[dict[str, Any]]:
244+
return []
245+
246+
@abstractmethod
247+
async def get_source_sentences(self, fact_ids: list[str]) -> list[str]:
248+
"""Return sentence IDs that are sources for the given facts (via fact_sources)."""
249+
...
250+
251+
@abstractmethod
252+
async def get_sentences_by_ids(self, sentence_ids: list[str]) -> list[dict[str, Any]]:
253+
"""Fetch full sentence rows for the given IDs."""
254+
...
255+
256+
199257
# ── Episodes ──
200258

201259
@abstractmethod

0 commit comments

Comments
 (0)