geo/backend/app/api/reports.py

155 lines
4.6 KiB
Python

import logging
import uuid
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import Response
from app.api.deps import get_current_user
from app.database import get_db
from app.models.brand import Brand
from app.models.user import User
from app.schemas.scoring import CitationResult
from app.services.citation.citation import export_citations_csv, export_citations_pdf
from app.services.scoring.scoring_service import ScoringService, ScoringResultV2
logger = logging.getLogger(__name__)
router = APIRouter()
async def _compute_v2_scores(
db: AsyncSession,
user_id: uuid.UUID,
brand_id: uuid.UUID,
) -> ScoringResultV2 | None:
try:
from app.api.scoring import (
_get_citations_for_brand,
_analyze_sentiments_for_citations,
)
total_queries, brand_citations, _, competitor_mentions = (
await _get_citations_for_brand(db, user_id, brand_id)
)
if total_queries == 0:
return None
brand_stmt = select(Brand).where(
Brand.id == brand_id, Brand.user_id == user_id
)
brand_result = await db.execute(brand_stmt)
brand = brand_result.scalar_one_or_none()
if not brand:
return None
sentiment_counts = await _analyze_sentiments_for_citations(
brand_name=brand.name,
brand_citations=brand_citations,
)
citation_results = [
CitationResult(
cited=c.cited,
position=c.citation_position,
citation_text=c.citation_text,
sentiment=c.sentiment or "neutral",
confidence=c.confidence or 0.0,
)
for c in brand_citations
]
positions = [c.citation_position for c in brand_citations if c.cited]
scoring_service = ScoringService()
return scoring_service.calculate_v2(
mentioned_count=len(brand_citations),
total_queries=total_queries,
positions=positions,
sentiment_counts=sentiment_counts,
citations=citation_results,
brand_mentions=len(brand_citations),
competitor_mentions=competitor_mentions,
)
except Exception:
logger.warning("V2 scoring failed for brand %s", brand_id, exc_info=True)
return None
@router.get("/export/csv")
async def export_report(
query_id: uuid.UUID = Query(...),
brand_id: Optional[uuid.UUID] = Query(None),
format: str = Query("csv"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
if format != "csv":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only CSV format is supported",
)
try:
v2_result = None
if brand_id is not None:
v2_result = await _compute_v2_scores(db, current_user.id, brand_id)
csv_content = await export_citations_csv(
db, current_user.id, query_id, v2_result=v2_result
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
date_str = datetime.now().strftime("%Y%m%d")
filename = f"geo-report-{date_str}.csv"
return StreamingResponse(
iter([csv_content]),
media_type="text/csv",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
},
)
@router.get("/export/pdf")
async def export_pdf(
query_id: Optional[uuid.UUID] = None,
brand_id: Optional[uuid.UUID] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
try:
v2_result = None
if brand_id is not None:
v2_result = await _compute_v2_scores(db, current_user.id, brand_id)
pdf_bytes = await export_citations_pdf(
db, current_user.id, query_id, v2_result=v2_result
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
date_str = datetime.now().strftime("%Y%m%d")
filename = f"geo-report-{date_str}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
},
)