Files
20260327-c863ce53/app/memory/store.py
2026-04-25 19:25:22 +08:00

115 lines
3.6 KiB
Python

"""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_<id>.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