Files
socweb/backend/app/api/conversations.py
T
gongzhiyongandClaude Opus 4.6 efb3c53623 feat: add backend service and Azure deployment workflow
- Add complete Python backend (Litestar + LangGraph) with chat, conversations, tickets APIs
- Add GitHub Actions workflow for auto-deploying backend to Azure Web App (soc-backend)
- Add gunicorn to requirements.txt for production serving
- Update CLAUDE.md and EXTERNAL_SERVICES.md with latest config
- Remove obsolete claudehd.md (merged into gpthd.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:31:00 +08:00

114 lines
3.8 KiB
Python

"""Conversation CRUD endpoints backed by PostgreSQL."""
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from litestar import delete, get, patch, post
from litestar.exceptions import NotFoundException
from sqlalchemy import select
from app.schemas import (
ConversationCreate,
ConversationDetail,
ConversationOut,
ConversationUpdate,
MessageOut,
)
from app.store.postgres import Conversation, Message, async_session_factory
def _conv_to_out(conv: Conversation) -> ConversationOut:
"""Convert a Conversation ORM object to the API response model."""
return ConversationOut(
id=conv.id,
title=conv.title,
created_at=conv.created_at.isoformat(),
updated_at=conv.updated_at.isoformat(),
)
@get("/api/conversations")
async def list_conversations() -> list[ConversationOut]:
"""GET /api/conversations - List all conversations."""
async with async_session_factory() as session:
stmt = select(Conversation).order_by(Conversation.updated_at.desc())
result = await session.execute(stmt)
convs = result.scalars().all()
return [_conv_to_out(c) for c in convs]
@get("/api/conversations/{conversation_id:str}")
async def get_conversation(conversation_id: str) -> ConversationDetail:
"""GET /api/conversations/:id - Get a single conversation with messages."""
async with async_session_factory() as session:
conv = await session.get(Conversation, conversation_id)
if conv is None:
raise NotFoundException(detail=f"Conversation {conversation_id} not found")
# Eagerly load messages
stmt = select(Message).where(
Message.conversation_id == conversation_id
).order_by(Message.created_at)
result = await session.execute(stmt)
msgs = result.scalars().all()
return ConversationDetail(
id=conv.id,
title=conv.title,
created_at=conv.created_at.isoformat(),
updated_at=conv.updated_at.isoformat(),
messages=[
MessageOut(
id=m.id,
role=m.role,
content=m.content,
created_at=m.created_at.isoformat(),
)
for m in msgs
],
)
@post("/api/conversations")
async def create_conversation(data: ConversationCreate) -> ConversationOut:
"""POST /api/conversations - Create a new conversation."""
conv = Conversation(
id=str(uuid.uuid4()),
title=data.title,
)
async with async_session_factory() as session:
session.add(conv)
await session.commit()
await session.refresh(conv)
return _conv_to_out(conv)
@patch("/api/conversations/{conversation_id:str}")
async def update_conversation(
conversation_id: str,
data: ConversationUpdate,
) -> ConversationOut:
"""PATCH /api/conversations/:id - Update conversation title."""
async with async_session_factory() as session:
conv = await session.get(Conversation, conversation_id)
if conv is None:
raise NotFoundException(
detail=f"Conversation {conversation_id} not found"
)
conv.title = data.title
conv.updated_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(conv)
return _conv_to_out(conv)
@delete("/api/conversations/{conversation_id:str}", status_code=200)
async def delete_conversation(conversation_id: str) -> dict:
"""DELETE /api/conversations/:id - Delete a conversation."""
async with async_session_factory() as session:
conv = await session.get(Conversation, conversation_id)
if conv is not None:
await session.delete(conv)
await session.commit()
return {"deleted": True}