File size: 5,022 Bytes
582520a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""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"'
            )
        },
    )