"""Dedicated memory curator service — run alongside Synapse on port 8001. Endpoints: GET / health check GET /memories list all memory items (optional ?section= filter) POST /memories direct write — no LLM, saves immediately PATCH /memories/{id} update a memory item DELETE /memories/{id} delete a memory item POST /memories/extract LLM-curated: evaluate a conversation exchange and optionally save a new permanent fact """ from __future__ import annotations import asyncio import uuid from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException, Body from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from .store import store, MemoryItem from .extractor import extract_memory from ..ollama_manager import get_ollama_manager app = FastAPI(title="Nexus Memory Service", version="1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") async def health(): return {"status": "ok", "count": len(store.all())} @app.get("/memories") async def list_memories(section: Optional[str] = None): items = store.all() if section: items = [i for i in items if i.section.lower() == section.lower()] return {"items": [{"id": i.id, "section": i.section, "text": i.text, "tags": i.tags} for i in items]} @app.post("/memories") async def add_memory(payload: Dict[str, Any] = Body(...)): text = (payload.get("text") or "").strip() if not text: raise HTTPException(status_code=400, detail="Missing 'text'") item = MemoryItem( id=str(uuid.uuid4()), section=(payload.get("section") or "General").strip(), text=text, tags=payload.get("tags", []), ) store.add(item) return {"id": item.id, "section": item.section, "text": item.text, "tags": item.tags} @app.patch("/memories/{item_id}") async def update_memory(item_id: str, payload: Dict[str, Any] = Body(...)): existing = store.get(item_id) if not existing: raise HTTPException(status_code=404, detail="Not found") updated = MemoryItem( id=item_id, section=(payload.get("section") or existing.section).strip(), text=(payload.get("text") or existing.text).strip(), tags=payload.get("tags", existing.tags), ) store.update(updated) return {"id": updated.id, "section": updated.section, "text": updated.text, "tags": updated.tags} @app.delete("/memories/{item_id}") async def delete_memory(item_id: str): if not store.get(item_id): raise HTTPException(status_code=404, detail="Not found") store.delete(item_id) return {"status": "deleted"} class ExtractRequest(BaseModel): user_message: str assistant_response: str @app.post("/memories/extract") async def extract_and_save(req: ExtractRequest): """LLM-curated extraction — asks Mistral to evaluate the exchange against all existing memory items and save only new, permanent personal facts.""" existing = store.all() existing_sections = list({i.section for i in existing}) existing_texts = [i.text for i in existing] try: result = await asyncio.wait_for( extract_memory( req.user_message, req.assistant_response, existing_sections, existing_texts, get_ollama_manager(), ), timeout=20.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} except asyncio.TimeoutError: pass except Exception: pass return {"saved": False}