Spaces:
Running
Running
| """Author RAG — Analytics CSV/JSON Export API. | |
| Gives authors a way to download their raw conversation data | |
| for use in external analytics tools (Excel, Notion, Google Sheets). | |
| Routes: | |
| GET /api/admin/{slug}/analytics/export/conversations → CSV of all sessions | |
| GET /api/admin/{slug}/analytics/export/events → CSV of raw analytics events | |
| GET /api/admin/{slug}/analytics/summary → JSON summary dashboard data | |
| """ | |
| import csv | |
| import io | |
| from datetime import datetime, timedelta, timezone | |
| from fastapi import APIRouter, Depends, Query, HTTPException | |
| from fastapi.responses import StreamingResponse | |
| from sqlalchemy import select, func, desc | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.dependencies import get_current_author, get_db | |
| from app.models.analytics import AnalyticsEvent | |
| from app.models.chat_session import ChatSession, ChatMessage | |
| router = APIRouter() | |
| async def analytics_summary( | |
| slug: str, | |
| days: int = Query(30, ge=1, le=365), | |
| db: AsyncSession = Depends(get_db), | |
| author=Depends(get_current_author), | |
| ): | |
| """JSON analytics summary — powers the admin dashboard charts.""" | |
| if author.id != slug: | |
| raise HTTPException(403, "Forbidden") | |
| since = datetime.now(timezone.utc) - timedelta(days=days) | |
| # Total sessions | |
| sessions_result = await db.execute( | |
| select(func.count(ChatSession.session_id)) | |
| .where(ChatSession.author_id == slug, ChatSession.created_at >= since) | |
| ) | |
| total_sessions = sessions_result.scalar() or 0 | |
| # Total messages | |
| messages_result = await db.execute( | |
| select(func.count(ChatMessage.id)) | |
| .join(ChatSession, ChatMessage.session_id == ChatSession.session_id) | |
| .where(ChatSession.author_id == slug, ChatSession.created_at >= since) | |
| ) | |
| total_messages = messages_result.scalar() or 0 | |
| # Intent distribution | |
| intent_result = await db.execute( | |
| select(AnalyticsEvent.event_type, func.count(AnalyticsEvent.id)) | |
| .where( | |
| AnalyticsEvent.author_id == slug, | |
| AnalyticsEvent.created_at >= since, | |
| AnalyticsEvent.event_type.like("intent_%"), | |
| ) | |
| .group_by(AnalyticsEvent.event_type) | |
| ) | |
| intent_counts = {row[0].replace("intent_", ""): row[1] for row in intent_result} | |
| # Daily sessions for sparkline chart (last 14 days) | |
| daily_result = await db.execute( | |
| select( | |
| func.date(ChatSession.created_at).label("day"), | |
| func.count(ChatSession.session_id).label("count"), | |
| ) | |
| .where( | |
| ChatSession.author_id == slug, | |
| ChatSession.created_at >= datetime.now(timezone.utc) - timedelta(days=14), | |
| ) | |
| .group_by("day") | |
| .order_by("day") | |
| ) | |
| daily_sessions = [{"date": str(r.day), "sessions": r.count} for r in daily_result] | |
| # Analytics events | |
| events_result = await db.execute( | |
| select(AnalyticsEvent) | |
| .where(AnalyticsEvent.author_id == slug, AnalyticsEvent.created_at >= since) | |
| .order_by(desc(AnalyticsEvent.created_at)) | |
| .limit(5) | |
| ) | |
| recent_events = [ | |
| {"type": e.event_type, "at": e.created_at.isoformat() if e.created_at else None} | |
| for e in events_result.scalars() | |
| ] | |
| return { | |
| "period_days": days, | |
| "total_sessions": total_sessions, | |
| "total_messages": total_messages, | |
| "avg_messages_per_session": round(total_messages / total_sessions, 1) if total_sessions else 0, | |
| "intent_distribution": intent_counts, | |
| "daily_sessions": daily_sessions, | |
| "recent_events": recent_events, | |
| } | |
| async def export_conversations_csv( | |
| slug: str, | |
| days: int = Query(90, ge=1, le=365), | |
| db: AsyncSession = Depends(get_db), | |
| author=Depends(get_current_author), | |
| ): | |
| """Stream conversation history as a CSV file download.""" | |
| if author.id != slug: | |
| raise HTTPException(403, "Forbidden") | |
| since = datetime.now(timezone.utc) - timedelta(days=days) | |
| sessions_result = await db.execute( | |
| select(ChatSession) | |
| .where(ChatSession.author_id == slug, ChatSession.created_at >= since) | |
| .order_by(desc(ChatSession.created_at)) | |
| ) | |
| sessions = sessions_result.scalars().all() | |
| output = io.StringIO() | |
| writer = csv.writer(output) | |
| writer.writerow(["session_id", "started_at", "role", "message", "tokens_used", "intent"]) | |
| for session in sessions: | |
| msgs_result = await db.execute( | |
| select(ChatMessage) | |
| .where(ChatMessage.session_id == session.session_id) | |
| .order_by(ChatMessage.created_at) | |
| ) | |
| for msg in msgs_result.scalars(): | |
| writer.writerow([ | |
| session.session_id, | |
| session.created_at.isoformat() if session.created_at else "", | |
| msg.role, | |
| msg.content[:500], # Truncate very long messages | |
| getattr(msg, "tokens_used", ""), | |
| getattr(msg, "intent", ""), | |
| ]) | |
| output.seek(0) | |
| filename = f"conversations_{slug}_{datetime.now().strftime('%Y%m%d')}.csv" | |
| return StreamingResponse( | |
| iter([output.getvalue()]), | |
| media_type="text/csv", | |
| headers={"Content-Disposition": f'attachment; filename="{filename}"'}, | |
| ) | |
| async def export_events_csv( | |
| slug: str, | |
| days: int = Query(90, ge=1, le=365), | |
| db: AsyncSession = Depends(get_db), | |
| author=Depends(get_current_author), | |
| ): | |
| """Stream raw analytics events as CSV.""" | |
| if author.id != slug: | |
| raise HTTPException(403, "Forbidden") | |
| since = datetime.now(timezone.utc) - timedelta(days=days) | |
| result = await db.execute( | |
| select(AnalyticsEvent) | |
| .where(AnalyticsEvent.author_id == slug, AnalyticsEvent.created_at >= since) | |
| .order_by(desc(AnalyticsEvent.created_at)) | |
| ) | |
| events = result.scalars().all() | |
| output = io.StringIO() | |
| writer = csv.writer(output) | |
| writer.writerow(["event_type", "session_id", "visitor_id", "book_id", "country", "created_at", "metadata"]) | |
| for e in events: | |
| writer.writerow([ | |
| e.event_type, | |
| getattr(e, "session_id", ""), | |
| getattr(e, "visitor_id", ""), | |
| getattr(e, "book_id", ""), | |
| getattr(e, "country", ""), | |
| e.created_at.isoformat() if e.created_at else "", | |
| getattr(e, "metadata_json", ""), | |
| ]) | |
| output.seek(0) | |
| filename = f"events_{slug}_{datetime.now().strftime('%Y%m%d')}.csv" | |
| return StreamingResponse( | |
| iter([output.getvalue()]), | |
| media_type="text/csv", | |
| headers={"Content-Disposition": f'attachment; filename="{filename}"'}, | |
| ) | |