ai-memory-backend / api /conversations.py
Baida07's picture
sync: 171 files from Baida98/AI [deploy-all]
24480a0 verified
Raw
History Blame
5.3 kB
"""backend/api/conversations.py β€” Conversations + Messages CRUD (S354)."""
import asyncio, json, logging
from .state import safe_json_dumps
from typing import Optional, Any
from fastapi import APIRouter, Depends, Body, HTTPException
from .auth_guard import require_role, AuthRole
from pydantic import BaseModel
from .state import sb
_logger_c = logging.getLogger("conversations")
async def _sb_call(fn, *args, **kwargs):
"""AUD-011: 1 retry with 500ms delay on transient Supabase errors.
HIGH-4: non retryare errori di autenticazione/autorizzazione β€” solo errori transienti.
"""
try:
return fn(*args, **kwargs)
except Exception as _e:
_ename = type(_e).__name__
_emsg = str(_e)
# Non retryare: auth errors, permission errors β€” sarebbero errori permanenti
if any(k in _ename or k in _emsg for k in ("Auth", "JWT", "403", "401", "Unauthorized", "Permission")):
raise
await asyncio.sleep(0.5)
return fn(*args, **kwargs) # let caller handle on second failure
router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
_logger = logging.getLogger("conversations")
class ConversationIn(BaseModel):
id: str
title: str = 'Nuova conversazione'
created_at: int
updated_at: int
class MessageIn(BaseModel):
id: str
conversation_id: str
role: str
content: str
created_at: int
error: Optional[bool] = False
steps: Optional[Any] = None
agent_status: Optional[str] = None
# ── Conversations ──────────────────────────────────────────────────────────────
@router.get('/api/conversations')
async def list_conversations():
try:
data = await _sb_call(lambda: sb().table('conversations').select('*').order('updated_at', desc=True).limit(200).execute()) # BUGFIX: LIMIT 200 β€” AUD-011: +retry
return {'conversations': data.data}
except Exception as exc:
_logger.warning("list_conversations: %s", exc)
# S750-GAP-I: Supabase non configurato o irraggiungibile β†’ lista vuota invece di 500
return {'conversations': [], '_error': str(exc)[:120]}
@router.post('/api/conversations')
async def upsert_conversation(conv: ConversationIn):
try:
data = sb().table('conversations').upsert(conv.model_dump()).execute()
return {'conversation': data.data[0] if data.data else conv.model_dump()}
except Exception as exc:
_logger.warning("upsert_conversation %s: %s", conv.id, exc)
return {'conversation': conv.model_dump(), '_error': str(exc)[:120]}
@router.put('/api/conversations/{conv_id}')
async def update_conversation(conv_id: str, body: dict = Body(...)):
body['id'] = conv_id
try:
data = sb().table('conversations').upsert(body).execute()
return {'conversation': data.data[0] if data.data else body}
except Exception as exc:
_logger.warning("update_conversation %s: %s", conv_id, exc)
return {'conversation': body, '_error': str(exc)[:120]}
@router.delete('/api/conversations/{conv_id}')
async def delete_conversation(conv_id: str):
try:
sb().table('messages').delete().eq('conversation_id', conv_id).execute()
sb().table('conversations').delete().eq('id', conv_id).execute()
except Exception as exc:
_logger.warning("delete_conversation %s: %s", conv_id, exc)
return {'deleted': conv_id}
# ── Messages ───────────────────────────────────────────────────────────────────
@router.get('/api/conversations/{conv_id}/messages')
async def list_messages(conv_id: str):
try:
data = await _sb_call(lambda: sb().table('messages').select('*').eq('conversation_id', conv_id).order('created_at').limit(500).execute()) # BUGFIX: LIMIT 500 β€” AUD-011: +retry
return {'messages': data.data}
except Exception as exc:
_logger.warning("list_messages %s: %s", conv_id, exc)
return {'messages': [], '_error': str(exc)[:120]}
@router.post('/api/conversations/{conv_id}/messages')
async def upsert_messages(conv_id: str, body: dict = Body(...)):
msgs = body.get('messages', [])
if not msgs:
return {'upserted': 0}
for m in msgs:
m['conversation_id'] = conv_id
if 'steps' in m and m['steps'] is not None:
m['steps'] = safe_json_dumps(m['steps']) if not isinstance(m['steps'], str) else m['steps']
try:
data = await _sb_call(lambda: sb().table('messages').upsert(msgs).execute()) # AUD-011
return {'upserted': len(data.data)}
except Exception as exc:
_logger.warning("upsert_messages %s: %s", conv_id, exc)
return {'upserted': 0, '_error': str(exc)[:120]}
@router.delete('/api/conversations/{conv_id}/messages')
async def clear_messages(conv_id: str):
try:
sb().table('messages').delete().eq('conversation_id', conv_id).execute()
except Exception as exc:
_logger.warning("clear_messages %s: %s", conv_id, exc)
return {'cleared': conv_id}