"""CRUD for the user's memory facts; the Settings > Memory page is the only intended client.""" from contextlib import asynccontextmanager from typing import Any, AsyncIterator, Dict, List from fastapi import HTTPException from pydantic import BaseModel from typeguard import typechecked from backend.config.Apps import SubApp from backend.apps.memory.store import MemoryFact, MemoryOp, add_fact, apply_ops, delete_fact, list_facts, update_fact @asynccontextmanager async def memory_lifespan() -> AsyncIterator[None]: yield memory = SubApp("memory", memory_lifespan) class FactBody(BaseModel): text: str @memory.router.get("") @typechecked async def get_facts() -> Dict[str, List[MemoryFact]]: return {"facts": list_facts()} @memory.router.post("") @typechecked async def create_fact(body: FactBody) -> MemoryFact: fact = add_fact(body.text, source="user") if fact is None: raise HTTPException(status_code=400, detail="Empty fact, or the memory list is full (60 max); delete something first.") return fact class OpsBody(BaseModel): ops: List[MemoryOp] @memory.router.post("/ops") @typechecked async def apply_memory_ops(body: OpsBody) -> Dict[str, Any]: """Atomic batch for the MemoryWrite tool: all ops land or none do, cap checked on the final state.""" if not body.ops: raise HTTPException(status_code=400, detail="ops must be a non-empty list.") return apply_ops(body.ops).model_dump() @memory.router.patch("/{fact_id}") @typechecked async def edit_fact(fact_id: str, body: FactBody) -> MemoryFact: fact = update_fact(fact_id, body.text) if fact is None: raise HTTPException(status_code=404, detail="No such fact (or the new text is empty).") return fact @memory.router.post("/distill/{session_id}") @typechecked async def distill(session_id: str) -> Dict[str, List[str]]: from backend.apps.agents.agents import agent_manager from backend.apps.memory.distill import distill_session_memory session = agent_manager.sessions.get(session_id) if not session: try: session = await agent_manager.resume_session(session_id) except ValueError: return {"added": []} return {"added": await distill_session_memory(session)} @memory.router.delete("/{fact_id}") @typechecked async def remove_fact(fact_id: str) -> Dict[str, bool]: if not delete_fact(fact_id): raise HTTPException(status_code=404, detail="No such fact.") return {"ok": True}