""" Call History API — Read-only access to persisted call records, transcript chunks, and recording files for the dashboard. """ from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import FileResponse from sqlalchemy import desc, select from sqlalchemy.ext.asyncio import AsyncSession from db.database import ( CallRecord, RecordingRecord, TranscriptChunk, get_db, ) router = APIRouter() @router.get("/history") async def list_history( limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), number: Optional[str] = None, status: Optional[str] = None, since: Optional[datetime] = None, until: Optional[datetime] = None, db: AsyncSession = Depends(get_db), ): """Paged list of past calls, newest first.""" stmt = select(CallRecord).order_by(desc(CallRecord.started_at)) if number: stmt = stmt.where(CallRecord.remote_number == number) if status: stmt = stmt.where(CallRecord.status == status) if since: stmt = stmt.where(CallRecord.started_at >= since) if until: stmt = stmt.where(CallRecord.started_at <= until) rows = (await db.execute(stmt.offset(offset).limit(limit))).scalars().all() return [ { "id": r.id, "direction": r.direction, "remote_number": r.remote_number, "status": r.status, "mode": r.mode, "intent": r.intent, "started_at": r.started_at.isoformat() if r.started_at else None, "ended_at": r.ended_at.isoformat() if r.ended_at else None, "duration": r.duration, "hold_time": r.hold_time, "device_used": r.device_used, "summary": r.summary, } for r in rows ] @router.get("/{call_id}/record") async def get_record(call_id: str, db: AsyncSession = Depends(get_db)): """Full CallRecord with classification_timeline.""" row = (await db.execute( select(CallRecord).where(CallRecord.id == call_id) )).scalar_one_or_none() if not row: raise HTTPException(status_code=404, detail=f"Call {call_id} not found") return { "id": row.id, "direction": row.direction, "remote_number": row.remote_number, "status": row.status, "mode": row.mode, "intent": row.intent, "started_at": row.started_at.isoformat() if row.started_at else None, "ended_at": row.ended_at.isoformat() if row.ended_at else None, "duration": row.duration, "hold_time": row.hold_time, "device_used": row.device_used, "summary": row.summary, "action_items": row.action_items, "sentiment": row.sentiment, "call_flow_id": row.call_flow_id, "classification_timeline": row.classification_timeline, } @router.get("/{call_id}/transcript") async def get_transcript(call_id: str, db: AsyncSession = Depends(get_db)): """Ordered transcript chunks for a call.""" rows = (await db.execute( select(TranscriptChunk) .where(TranscriptChunk.call_id == call_id) .order_by(TranscriptChunk.seq) )).scalars().all() return [ { "seq": c.seq, "t_offset_ms": c.t_offset_ms, "speaker": c.speaker, "text": c.text, "confidence": c.confidence, } for c in rows ] @router.get("/{call_id}/recording") async def get_recording(call_id: str, db: AsyncSession = Depends(get_db)): """Stream the WAV recording for a call.""" row = (await db.execute( select(RecordingRecord) .where(RecordingRecord.call_id == call_id) .order_by(desc(RecordingRecord.started_at)) )).scalar_one_or_none() if not row or not row.path: raise HTTPException(status_code=404, detail="Recording not found") import os if not os.path.exists(row.path): raise HTTPException(status_code=404, detail="Recording file missing on disk") return FileResponse(row.path, media_type="audio/wav", filename=os.path.basename(row.path))