Assistant: persist the chat thread in Postgres

Back MAF's opt-in AG-UI snapshot persistence with a DB store so the
conversation survives page reloads and restarts:

- ChatThreadSnapshot: one upserted row per (scope, thread_id), JSONB
  messages/state/interrupt (migration 794baf50635b)
- PostgresSnapshotStore implements the AGUIThreadSnapshotStore protocol.
  It opens its own sessions (MAF saves during SSE streaming, after the
  request middleware has closed the ContextVar session) and sanitizes on
  save: camelCase toolCalls/toolCallId (the @ag-ui/client Zod schema
  strips snake_case, which would lose cards on replay), drop BFF ctx-*
  context messages, dedupe MAF's re-recorded multi-run turns, and drop
  orphaned tool results
- Endpoint mount gains snapshot_store + a constant scope resolver
  (single-user app; auth already enforced by agent_auth_and_session)
- DELETE /api/v1/chat/thread resets the conversation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 18:09:55 -06:00
parent 0e03284c95
commit 6bce5539f7
8 changed files with 500 additions and 2 deletions

View File

@@ -0,0 +1,41 @@
"""Assistant chat thread management.
The conversation itself is persisted by the AG-UI snapshot store
(app/agent/snapshot_store.py) under a single ("default", "main") thread;
this router only exposes the reset used by "Nueva conversación".
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlmodel import Session, select
from app.auth import get_current_user
from app.db import get_session
from app.models.models import ChatThreadSnapshot
router = APIRouter(prefix="/chat", tags=["chat"])
CHAT_SCOPE = "default"
CHAT_THREAD_ID = "main"
class ClearThreadResponse(BaseModel):
cleared: bool
@router.delete("/thread", response_model=ClearThreadResponse)
def clear_thread(
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
row = session.exec(
select(ChatThreadSnapshot).where(
ChatThreadSnapshot.scope == CHAT_SCOPE,
ChatThreadSnapshot.thread_id == CHAT_THREAD_ID,
)
).first()
if row is None:
return {"cleared": False}
session.delete(row)
session.commit()
return {"cleared": True}

View File

@@ -6,6 +6,7 @@ from app.api.v1.endpoints import (
auth,
budget,
categories,
chat,
exchange_rate,
import_transactions,
installments,
@@ -38,3 +39,4 @@ api_router.include_router(pensions.router)
api_router.include_router(municipal_receipts.router)
api_router.include_router(savings_accrual.router)
api_router.include_router(sync_status.router)
api_router.include_router(chat.router)