- SQLAlchemy async database layer (SQLite dev, PostgreSQL prod) - Conversation and Message models with UUID primary keys - Token counting utilities using litellm - Context summarization at 80% token threshold - REST API endpoints for multi-turn conversations - 19 conversation tests, 6 token tests (176 total passing) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
"""
|
|
REST API routes for conversations.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.db import get_session
|
|
from src.domains.conversations.schemas import (
|
|
AddMessageRequest,
|
|
AddMessageResponse,
|
|
ConversationDetailResponse,
|
|
ConversationListResponse,
|
|
ConversationResponse,
|
|
CreateConversationRequest,
|
|
MessageResponse,
|
|
)
|
|
from src.domains.conversations.service import ConversationService
|
|
from src.shared.auth import require_auth
|
|
from src.shared.logging import logged, get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/conversations", tags=["Conversations"])
|
|
|
|
|
|
@router.post("/", response_model=ConversationResponse, status_code=201)
|
|
@logged()
|
|
async def create_conversation(
|
|
request: CreateConversationRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
user=Depends(require_auth),
|
|
) -> ConversationResponse:
|
|
"""
|
|
Create a new conversation.
|
|
|
|
Starts an empty conversation with the specified agent type.
|
|
"""
|
|
service = ConversationService(session)
|
|
conversation = await service.create(
|
|
user_id=user.id,
|
|
agent_type=request.agent_type,
|
|
working_dir=request.working_dir,
|
|
title=request.title,
|
|
)
|
|
return ConversationResponse.model_validate(conversation)
|
|
|
|
|
|
@router.get("/", response_model=ConversationListResponse)
|
|
@logged()
|
|
async def list_conversations(
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
session: AsyncSession = Depends(get_session),
|
|
user=Depends(require_auth),
|
|
) -> ConversationListResponse:
|
|
"""
|
|
List user's conversations.
|
|
|
|
Returns conversations sorted by most recently updated.
|
|
"""
|
|
service = ConversationService(session)
|
|
conversations, total = await service.list_by_user(
|
|
user_id=user.id,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return ConversationListResponse(
|
|
conversations=[ConversationResponse.model_validate(c) for c in conversations],
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.get("/{conversation_id}", response_model=ConversationDetailResponse)
|
|
@logged()
|
|
async def get_conversation(
|
|
conversation_id: UUID,
|
|
session: AsyncSession = Depends(get_session),
|
|
user=Depends(require_auth),
|
|
) -> ConversationDetailResponse:
|
|
"""
|
|
Get conversation with all messages.
|
|
|
|
Returns conversation metadata and full message history.
|
|
"""
|
|
service = ConversationService(session)
|
|
conversation = await service.get_with_messages(conversation_id)
|
|
|
|
if not conversation:
|
|
raise HTTPException(status_code=404, detail="Conversation not found")
|
|
|
|
if conversation.user_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Not authorized")
|
|
|
|
return ConversationDetailResponse.model_validate(conversation)
|
|
|
|
|
|
@router.delete("/{conversation_id}", status_code=204)
|
|
@logged()
|
|
async def delete_conversation(
|
|
conversation_id: UUID,
|
|
session: AsyncSession = Depends(get_session),
|
|
user=Depends(require_auth),
|
|
) -> None:
|
|
"""
|
|
Delete a conversation and all its messages.
|
|
"""
|
|
service = ConversationService(session)
|
|
conversation = await service.get(conversation_id)
|
|
|
|
if not conversation:
|
|
raise HTTPException(status_code=404, detail="Conversation not found")
|
|
|
|
if conversation.user_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Not authorized")
|
|
|
|
await service.delete(conversation_id)
|
|
|
|
|
|
@router.post("/{conversation_id}/messages", response_model=AddMessageResponse)
|
|
@logged()
|
|
async def add_message(
|
|
conversation_id: UUID,
|
|
request: AddMessageRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
user=Depends(require_auth),
|
|
) -> AddMessageResponse:
|
|
"""
|
|
Add a message to a conversation and get agent response.
|
|
|
|
This is the main endpoint for continuing conversations.
|
|
It:
|
|
1. Adds the user message
|
|
2. Checks if summarization is needed
|
|
3. Builds context from conversation history
|
|
4. Gets agent response
|
|
5. Adds agent response to conversation
|
|
6. Returns both messages
|
|
"""
|
|
service = ConversationService(session)
|
|
|
|
# Verify conversation exists and user owns it
|
|
conversation = await service.get(conversation_id)
|
|
if not conversation:
|
|
raise HTTPException(status_code=404, detail="Conversation not found")
|
|
|
|
if conversation.user_id != user.id:
|
|
raise HTTPException(status_code=403, detail="Not authorized")
|
|
|
|
# Add user message
|
|
user_message = await service.add_message(
|
|
conversation_id=conversation_id,
|
|
role="user",
|
|
content=request.content,
|
|
)
|
|
|
|
# Check if summarization needed before getting response
|
|
summarized = await service.summarize_if_needed(conversation_id)
|
|
|
|
# Get agent response with context
|
|
try:
|
|
response_text = await service.get_agent_response(
|
|
conversation_id=conversation_id,
|
|
user_message=request.content,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"Agent response failed: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Agent error: {str(e)}"
|
|
)
|
|
|
|
# Add assistant message
|
|
assistant_message = await service.add_message(
|
|
conversation_id=conversation_id,
|
|
role="assistant",
|
|
content=response_text,
|
|
)
|
|
|
|
# Get updated conversation for total tokens
|
|
conversation = await service.get(conversation_id)
|
|
|
|
return AddMessageResponse(
|
|
user_message=MessageResponse.model_validate(user_message),
|
|
assistant_message=MessageResponse.model_validate(assistant_message),
|
|
total_tokens=conversation.total_tokens if conversation else 0,
|
|
summarized=summarized,
|
|
)
|