170 lines
5.2 KiB
Python
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")
|