"""File-based memory store — persistent facts with confidence ranking.""" from __future__ import annotations import json import logging import uuid from datetime import datetime from pathlib import Path from typing import Any from pydantic import BaseModel, Field logger = logging.getLogger(__name__) MEMORY_DIR = Path(__file__).resolve().parent.parent.parent / "memory" class Fact(BaseModel): id: str = Field(default_factory=lambda: uuid.uuid4().hex[:8]) content: str category: str = "context" # preference | knowledge | context | behavior | goal confidence: float = 0.7 source: str = "" # which report/session created this created_at: str = Field(default_factory=lambda: datetime.now().isoformat()) class MemoryFile(BaseModel): """One memory file per client (or global).""" client_id: str = "global" preferences: dict[str, str] = Field(default_factory=dict) facts: list[Fact] = Field(default_factory=list) class MemoryStore: """Read/write persistent memory as JSON files. Storage layout: memory/ ├── global.json — system-wide facts └── client_.json — per-client facts """ def __init__(self): MEMORY_DIR.mkdir(parents=True, exist_ok=True) def _path(self, client_id: str = "global") -> Path: safe_name = client_id.replace("/", "_").replace("..", "_") return MEMORY_DIR / f"{safe_name}.json" def load(self, client_id: str = "global") -> MemoryFile: path = self._path(client_id) if not path.exists(): return MemoryFile(client_id=client_id) try: data = json.loads(path.read_text(encoding="utf-8")) return MemoryFile(**data) except Exception as e: logger.warning(f"[memory] failed to load {path}: {e}") return MemoryFile(client_id=client_id) def save(self, mem: MemoryFile): path = self._path(mem.client_id) path.write_text( json.dumps(mem.model_dump(), ensure_ascii=False, indent=2), encoding="utf-8", ) logger.info(f"[memory] saved {len(mem.facts)} facts to {path}") def add_fact( self, content: str, client_id: str = "global", category: str = "context", confidence: float = 0.7, source: str = "", ) -> Fact: mem = self.load(client_id) # Deduplicate by content (normalized) normalized = content.strip().lower() for existing in mem.facts: if existing.content.strip().lower() == normalized: # Update confidence if higher if confidence > existing.confidence: existing.confidence = confidence self.save(mem) return existing fact = Fact( content=content, category=category, confidence=confidence, source=source, ) mem.facts.append(fact) self.save(mem) return fact def get_top_facts( self, client_id: str = "global", limit: int = 15 ) -> list[str]: """Get top N facts sorted by confidence, formatted for prompt injection.""" mem = self.load(client_id) sorted_facts = sorted(mem.facts, key=lambda f: f.confidence, reverse=True) return [f.content for f in sorted_facts[:limit]] def set_preference(self, key: str, value: str, client_id: str = "global"): mem = self.load(client_id) mem.preferences[key] = value self.save(mem) def get_preferences(self, client_id: str = "global") -> dict[str, str]: return self.load(client_id).preferences