Cachy / backend /app /api /library_chat.py
Vatxzz's picture
increased security
fb4cdd5
Raw
History Blame Contribute Delete
2.89 kB
"""Chat-with-your-library endpoint (docs/09 P3): cross-card grounded Q&A.
The conversation is PERSISTED per owner (docs/14): the client replays the
conversation each turn and the model answers from the cards retrieved for the
latest question, then the full turn is saved so reopening library chat restores
the thread. Isolation is by the X-Owner-Id header."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from app import quota
from app.auth import OwnerDep
from app.services import llm_library_chat
from app.store import db
log = logging.getLogger("api.library_chat")
router = APIRouter(prefix="/library", tags=["library"])
class ChatMessage(BaseModel):
role: str # "user" | "assistant"
content: str
class LibraryChatRequest(BaseModel):
messages: list[ChatMessage]
class SourceCard(BaseModel):
card_id: str
one_liner: str
class LibraryChatResponse(BaseModel):
reply: str
sources: list[SourceCard]
class LibraryChatHistoryResponse(BaseModel):
messages: list[ChatMessage]
@router.post("/chat", response_model=LibraryChatResponse,
dependencies=[Depends(quota.spend("chat", "quota_chat_per_day"))])
async def library_chat(
req: LibraryChatRequest,
owner_id: OwnerDep,
) -> LibraryChatResponse:
if not req.messages or req.messages[-1].role != "user":
raise HTTPException(status_code=422, detail="last message must be from user")
history = [m.model_dump() for m in req.messages]
result = await llm_library_chat.answer(history, owner_id)
if result is None:
raise HTTPException(
status_code=503, detail="chat is unavailable (no LLM backend configured)"
)
reply, cards = result
sources = [
SourceCard(card_id=c.card_id, one_liner=c.base.one_liner or c.base.tldr)
for c in cards
]
# Persist the full conversation (history + this reply), owner-scoped.
async with db.session() as session:
await db.save_conversation(
session,
owner_id=owner_id,
kind="library_chat",
payload=[*history, {"role": "assistant", "content": reply}],
)
return LibraryChatResponse(reply=reply, sources=sources)
@router.get("/chat", response_model=LibraryChatHistoryResponse)
async def get_library_chat_history(
owner_id: OwnerDep,
) -> LibraryChatHistoryResponse:
"""Restore this owner's saved library chat (docs/14). Empty when none."""
async with db.session() as session:
row = await db.get_conversation(
session, owner_id=owner_id, kind="library_chat"
)
messages = list(row.payload) if row else []
return LibraryChatHistoryResponse(
messages=[ChatMessage(**m) for m in messages if isinstance(m, dict)]
)