refactor(synapse): backend updates, add icons module, relocate playbooks to data/playbooks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-11 07:25:08 -05:00
parent 3ec57f9140
commit f4f77c5196
24 changed files with 1820 additions and 288 deletions
Regular → Executable
+94 -11
View File
@@ -13,6 +13,7 @@ Endpoints:
from __future__ import annotations
import asyncio
import math
import uuid
from typing import Any, Dict, List, Optional
@@ -35,6 +36,33 @@ app.add_middleware(
)
@app.on_event("startup")
async def _warm_curator():
"""Preload the curator model (in RAM, num_gpu=0 by default) so the first
extraction isn't a cold load that blows the timeout. Runs in the background
so it never delays startup. keep_alive then holds it warm between messages."""
async def _bg():
try:
mgr = get_ollama_manager()
# Ollama is started by the Synapse backend (a separate process), so
# at our startup it usually isn't reachable yet. Wait for it before
# warming instead of failing with "All connection attempts failed" —
# which leaves the curator cold and makes the first extraction slow.
for _ in range(60): # up to ~2 min
if await asyncio.to_thread(mgr.is_running):
break
await asyncio.sleep(2)
else:
return
settings = store.get_settings()
model = settings.get("memory_model") or await mgr.select_best_model()
num_gpu = await mgr.resolve_num_gpu(settings.get("memory_gpu_offload", 0), model)
await mgr.warm(model, num_gpu=num_gpu)
except Exception:
pass
asyncio.create_task(_bg())
@app.get("/")
async def health():
return {"status": "ok", "count": len(store.all())}
@@ -86,6 +114,13 @@ async def delete_memory(item_id: str):
return {"status": "deleted"}
def _cosine(a: list, b: list) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
class ExtractRequest(BaseModel):
user_message: str
assistant_response: str
@@ -99,25 +134,73 @@ async def extract_and_save(req: ExtractRequest):
existing_sections = list({i.section for i in existing})
existing_texts = [i.text for i in existing]
# Adaptable per-machine curator config (see store _SETTINGS_DEFAULTS):
# which model does extraction, and whether it runs on CPU/RAM or the GPU.
settings = store.get_settings()
mgr = get_ollama_manager()
model = settings.get("memory_model") or await mgr.select_best_model()
num_gpu = await mgr.resolve_num_gpu(settings.get("memory_gpu_offload", 0), model)
try:
result = await asyncio.wait_for(
merge_threshold = float(settings.get("memory_merge_threshold", 0.88))
except (TypeError, ValueError):
merge_threshold = 0.88
try:
results = await asyncio.wait_for(
extract_memory(
req.user_message,
req.assistant_response,
existing_sections,
existing_texts,
get_ollama_manager(),
mgr,
model=model,
num_gpu=num_gpu,
),
timeout=20.0,
timeout=300.0,
)
if result:
item = MemoryItem(
id=str(uuid.uuid4()),
section=result["section"],
text=result["text"],
)
store.add(item)
return {"saved": True, "id": item.id, "section": item.section, "text": item.text}
# Embed existing facts once so each new fact can be matched against them.
# A near-duplicate UPDATES the matched fact in place (edit with new info)
# rather than appending a copy. Best effort: if embeddings are down we
# fall back to plain append. Merge disabled unless 0 < threshold < 1.
existing_embeds: dict = {}
if results and 0 < merge_threshold < 1:
vecs = await asyncio.gather(*(mgr.embed(it.text) for it in existing))
existing_embeds = {it.id: v for it, v in zip(existing, vecs) if v}
saved = []
for result in results:
new_vec = await mgr.embed(result["text"]) if existing_embeds else None
match_id, best = None, 0.0
if new_vec:
for eid, ev in existing_embeds.items():
sim = _cosine(new_vec, ev)
if sim > best:
best, match_id = sim, eid
if best < merge_threshold:
match_id = None
target = store.get(match_id) if match_id else None
if target:
# Near-duplicate of an existing fact — overwrite with the newer
# statement, keeping the original id/section/position.
updated = MemoryItem(id=target.id, section=target.section,
text=result["text"], tags=target.tags)
store.update(updated)
if new_vec:
existing_embeds[updated.id] = new_vec # keep cache fresh for later facts in this batch
saved.append({"id": updated.id, "section": updated.section,
"text": updated.text, "updated": True})
else:
item = MemoryItem(id=str(uuid.uuid4()),
section=result["section"], text=result["text"])
store.add(item)
if new_vec:
existing_embeds[item.id] = new_vec
saved.append({"id": item.id, "section": item.section, "text": item.text})
if saved:
first = {k: saved[0][k] for k in ("id", "section", "text")}
return {"saved": True, "items": saved, **first}
except asyncio.TimeoutError:
pass
except Exception: