prompt-analyser/backend/main.py

170 lines
5.2 KiB
Python

"""
FastAPI backend — REST API for the Prompt Analyzer and Dashboard.
Serves:
- POST /analyze → Human prompt analysis
- POST /rewrite-choice → Record rewrite acceptance
- GET /dashboard/* → Dashboard data endpoints
- GET /health → Health check
"""
import logging
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from prompt_analyzer import PromptAnalyzer
from prompt_analyzer.models import AnalyzeRequest
from analytics_reporter.reporter import AnalyticsReporter
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
# Shared instances
analyzer = PromptAnalyzer()
reporter = AnalyticsReporter()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize services on startup."""
await reporter.initialize()
logger.info("Backend ready")
yield
logger.info("Backend shutting down")
app = FastAPI(
title="Prompt Performance Analytics",
version="0.1.0",
lifespan=lifespan,
)
# CORS — allow frontend to call from any origin in dev
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Analysis Endpoints ─────────────────────────────────────────
@app.post("/analyze")
async def analyze_prompt(request: AnalyzeRequest):
"""
Analyze a prompt and return quality scores, mistakes, and rewrite.
This is the main endpoint used by both the Web UI and external clients.
"""
try:
result = await analyzer.analyze(
prompt=request.prompt,
context=request.context,
project_id=request.project_id,
source_agent=request.source_agent,
target_agent=request.target_agent,
)
# Agent 2: store the result for dashboard
analysis_id = await reporter.report(result)
# Return result with the DB id so frontend can track rewrite choice
response = result.model_dump(mode="json")
response["analysis_id"] = analysis_id
return response
except Exception as e:
logger.error("Analysis failed: %s", str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
class RewriteChoiceRequest(BaseModel):
analysis_id: int
used_rewrite: bool
@app.post("/rewrite-choice")
async def record_rewrite_choice(request: RewriteChoiceRequest):
"""Record whether the user chose the rewritten prompt."""
try:
await reporter.mark_rewrite_choice(request.analysis_id, request.used_rewrite)
return {"status": "ok"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ── Dashboard Endpoints ────────────────────────────────────────
@app.get("/dashboard/overview")
async def dashboard_overview():
"""Get dashboard KPI overview."""
return await reporter.get_overview()
@app.get("/dashboard/interactions")
async def dashboard_interactions(
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
project_id: Optional[str] = Query(default=None),
):
"""Get paginated interaction feed."""
return await reporter.get_interactions(limit, offset, project_id)
@app.get("/dashboard/trends")
async def dashboard_trends(
days: int = Query(default=30, ge=1, le=365),
hours: Optional[int] = Query(default=None, ge=1, le=720),
):
"""Get quality score trends over time. Use hours for short ranges."""
return await reporter.get_trends(days=days, hours=hours)
@app.get("/dashboard/mistakes")
async def dashboard_mistakes(limit: int = Query(default=10, ge=1, le=50)):
"""Get most common mistake types."""
return await reporter.get_mistake_frequencies(limit)
@app.get("/dashboard/agents")
async def dashboard_agents():
"""Get agent leaderboard."""
return await reporter.get_agent_leaderboard()
# ── Static files & Health ──────────────────────────────────────
@app.get("/health")
async def health():
return {"status": "healthy", "version": "0.1.0"}
# Serve frontend static files
import os
frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
dashboard_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "dashboard")
if os.path.isdir(frontend_dir):
@app.get("/", response_class=FileResponse)
async def serve_frontend():
return FileResponse(os.path.join(frontend_dir, "index.html"))
app.mount("/static", StaticFiles(directory=frontend_dir), name="frontend")
if os.path.isdir(dashboard_dir):
@app.get("/dashboard-ui", response_class=FileResponse)
async def serve_dashboard():
return FileResponse(os.path.join(dashboard_dir, "index.html"))
app.mount("/dashboard-static", StaticFiles(directory=dashboard_dir), name="dashboard")