ReplyFlow / app /api /conversations.py
nvikou's picture
Initial commit
582520a verified
Raw History Blame Contribute Delete
5.02 kB
"""Conversation history API."""
from __future__ import annotations
import csv
import io
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db import get_db
from app.models import Conversation, Lead, Message
from app.schemas import (
ConversationDetail,
ConversationSummary,
MessageOut,
)
from app.services.leads import lead_to_dict
router = APIRouter(
prefix="/api/conversations", tags=["conversations"]
)
@router.get("", response_model=list[ConversationSummary])
async def list_conversations(
db: AsyncSession = Depends(get_db),
) -> list[ConversationSummary]:
"""List conversations newest first."""
msg_count = (
select(
Message.conversation_id,
func.count(Message.id).label("cnt"),
)
.group_by(Message.conversation_id)
.subquery()
)
result = await db.execute(
select(Conversation, Lead, msg_count.c.cnt)
.outerjoin(Lead, Lead.conversation_id == Conversation.id)
.outerjoin(
msg_count, msg_count.c.conversation_id == Conversation.id
)
.order_by(Conversation.created_at.desc())
.limit(200)
)
rows = result.all()
items: list[ConversationSummary] = []
for conv, lead, count in rows:
items.append(
ConversationSummary(
id=conv.id,
channel=conv.channel,
language=conv.language,
status=conv.status,
handed_off=conv.handed_off,
appointment_booked=conv.appointment_booked,
resolved_without_human=conv.resolved_without_human,
message_count=int(count or 0),
created_at=conv.created_at,
lead_name=lead.name if lead else None,
)
)
return items
@router.get(
"/{conversation_id}",
response_model=ConversationDetail,
)
async def get_conversation(
conversation_id: int,
db: AsyncSession = Depends(get_db),
) -> ConversationDetail:
"""Get one conversation with messages."""
result = await db.execute(
select(Conversation)
.where(Conversation.id == conversation_id)
.options(
selectinload(Conversation.messages),
selectinload(Conversation.lead),
selectinload(Conversation.appointment),
)
)
conv = result.scalar_one_or_none()
if conv is None:
raise HTTPException(status_code=404, detail="Not found")
appointment: dict[str, Any] | None = None
if conv.appointment:
appointment = {
"slot_start": conv.appointment.slot_start,
"slot_end": conv.appointment.slot_end,
"status": conv.appointment.status,
"attendee_name": conv.appointment.attendee_name,
"attendee_email": conv.appointment.attendee_email,
}
return ConversationDetail(
id=conv.id,
channel=conv.channel,
language=conv.language,
status=conv.status,
handed_off=conv.handed_off,
appointment_booked=conv.appointment_booked,
resolved_without_human=conv.resolved_without_human,
created_at=conv.created_at,
messages=[
MessageOut(
id=m.id,
role=m.role,
content=m.content,
language=m.language,
grounded=m.grounded,
created_at=m.created_at,
)
for m in conv.messages
],
lead=lead_to_dict(conv.lead),
appointment=appointment,
)
@router.get("/{conversation_id}/export")
async def export_conversation(
conversation_id: int,
db: AsyncSession = Depends(get_db),
) -> StreamingResponse:
"""Export conversation messages as CSV."""
result = await db.execute(
select(Conversation)
.where(Conversation.id == conversation_id)
.options(selectinload(Conversation.messages))
)
conv = result.scalar_one_or_none()
if conv is None:
raise HTTPException(status_code=404, detail="Not found")
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(
["id", "role", "language", "grounded", "created_at", "content"]
)
for m in conv.messages:
writer.writerow(
[
m.id,
m.role,
m.language or "",
m.grounded if m.grounded is not None else "",
m.created_at.isoformat(),
m.content,
]
)
buffer.seek(0)
return StreamingResponse(
iter([buffer.getvalue()]),
media_type="text/csv",
headers={
"Content-Disposition": (
f'attachment; filename="conversation_{conversation_id}.csv"'
)
},
)