Spaces:
Running
Running
| from __future__ import annotations | |
| import os | |
| import secrets | |
| import sys | |
| from pathlib import Path | |
| import uvicorn | |
| import uuid | |
| from fastapi import Depends, FastAPI, Header, HTTPException, BackgroundTasks | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, Response | |
| from fastapi.staticfiles import StaticFiles | |
| from evaluator import evaluator_capabilities | |
| from data import BENCHMARKS, PROVIDERS | |
| from data import audit_bundle, current_report, export_drift_csv, export_hallucination_csv, export_root_cause_csv, get_alerts | |
| from data import evaluation_run_detail, get_category_scores, get_dataset, get_datasets, get_evaluation_comparison, get_evaluation_history, get_evaluation_pair_comparison, get_hallucination_logs, get_incidents, get_metrics, get_quality_timeseries, get_range_summary | |
| from data import get_root_cause, get_settings, handoff_package, operator_review_summary, readiness_summary | |
| from data import get_scenario | |
| from data import create_dataset, health, reset_state, run_batch_evaluation, run_custom_evaluation, run_dataset_evaluation, run_evaluation, update_run_decision, update_settings, generate_support_answer | |
| ROOT = Path(__file__).resolve().parents[1] | |
| API_KEY_HEADER = "X-Sentinel-API-Key" | |
| app = FastAPI( | |
| title="LLM Sentinel Pro API", | |
| version="0.2.0", | |
| description="Local evaluation API for the LLM Sentinel Pro monitoring dashboard.", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["http://127.0.0.1:8000", "http://localhost:8000"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def configured_api_key() -> str | None: | |
| return os.environ.get("SENTINEL_API_KEY") or os.environ.get("SENTINEL_AUTH_TOKEN") | |
| def require_api_key( | |
| x_sentinel_api_key: str | None = Header(default=None, alias=API_KEY_HEADER), | |
| authorization: str | None = Header(default=None), | |
| ) -> None: | |
| expected = configured_api_key() | |
| if not expected: | |
| return | |
| provided = x_sentinel_api_key or "" | |
| if not provided and authorization: | |
| scheme, _, token = authorization.partition(" ") | |
| if scheme.lower() == "bearer": | |
| provided = token.strip() | |
| if not provided or not secrets.compare_digest(provided, expected): | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Valid Sentinel API key required.", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| PROTECTED = [Depends(require_api_key)] | |
| def index() -> FileResponse: | |
| return FileResponse(ROOT / "index.html") | |
| def api_health() -> dict: | |
| return health() | |
| def api_scenario() -> dict: | |
| return get_scenario() | |
| def api_metrics() -> dict: | |
| return get_metrics() | |
| def api_metrics_timeseries() -> dict: | |
| return get_quality_timeseries() | |
| def api_metrics_range(range_key: str) -> dict: | |
| return get_range_summary(range_key) | |
| def api_drift_categories() -> dict: | |
| return get_category_scores() | |
| def api_incidents() -> dict: | |
| return get_incidents() | |
| def api_evaluation_history() -> dict: | |
| return get_evaluation_history() | |
| def api_evaluation_comparison() -> dict: | |
| return get_evaluation_comparison() | |
| def api_evaluation_pair_comparison(current_id: str, previous_id: str) -> dict: | |
| try: | |
| return get_evaluation_pair_comparison(current_id, previous_id) | |
| except ValueError as exc: | |
| status_code = 404 if str(exc) == "Evaluation run not found." else 400 | |
| raise HTTPException(status_code=status_code, detail=str(exc)) from exc | |
| def api_evaluation_run_detail(run_id: str) -> dict: | |
| try: | |
| return evaluation_run_detail(run_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail=str(exc)) from exc | |
| def api_update_run_decision(run_id: str, payload: dict) -> dict: | |
| try: | |
| return update_run_decision(run_id, payload) | |
| except ValueError as exc: | |
| status_code = 404 if str(exc) == "Evaluation run not found." else 400 | |
| raise HTTPException(status_code=status_code, detail=str(exc)) from exc | |
| def api_datasets() -> dict: | |
| return get_datasets() | |
| def api_create_dataset(payload: dict) -> dict: | |
| try: | |
| return create_dataset(payload) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def api_dataset(dataset_id: str) -> dict: | |
| try: | |
| return get_dataset(dataset_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail=str(exc)) from exc | |
| def api_run_dataset(dataset_id: str) -> dict: | |
| try: | |
| return run_dataset_evaluation(dataset_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404 if str(exc) == "Dataset not found." else 400, detail=str(exc)) from exc | |
| def api_hallucination_logs() -> dict: | |
| return get_hallucination_logs() | |
| def api_root_cause() -> dict: | |
| return get_root_cause() | |
| def api_providers_compare() -> dict: | |
| return {"items": PROVIDERS} | |
| def api_benchmarks() -> dict: | |
| return {"items": BENCHMARKS} | |
| def api_evaluator_capabilities() -> dict: | |
| return evaluator_capabilities() | |
| def api_alerts() -> dict: | |
| return get_alerts() | |
| def api_settings() -> dict: | |
| return get_settings() | |
| def api_update_settings(payload: dict) -> dict: | |
| return update_settings(payload) | |
| def api_current_report() -> dict: | |
| return current_report() | |
| def api_operator_review_summary() -> dict: | |
| return operator_review_summary() | |
| def api_handoff_package() -> dict: | |
| return handoff_package() | |
| def api_readiness_summary() -> dict: | |
| return readiness_summary() | |
| def api_audit_bundle(run_id: str) -> dict: | |
| try: | |
| return audit_bundle(run_id) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=404, detail=str(exc)) from exc | |
| def api_hallucination_csv() -> Response: | |
| return csv_response(export_hallucination_csv(), "hallucination-scoring.csv") | |
| def api_drift_csv() -> Response: | |
| return csv_response(export_drift_csv(), "drift-categories.csv") | |
| def api_root_cause_csv() -> Response: | |
| return csv_response(export_root_cause_csv(), "root-cause-report.csv") | |
| def api_evaluate() -> dict: | |
| return run_evaluation() | |
| def api_evaluate_custom( | |
| payload: dict, | |
| x_sentinel_api_key: str | None = Header(default=None, alias=API_KEY_HEADER), | |
| authorization: str | None = Header(default=None), | |
| ) -> dict: | |
| category = payload.get("category", "") | |
| if category != "Landing Sandbox": | |
| require_api_key(x_sentinel_api_key, authorization) | |
| try: | |
| return run_custom_evaluation(payload) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def api_evaluate_batch(payload: dict) -> dict: | |
| try: | |
| return run_batch_evaluation(payload) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| BACKGROUND_TASKS_STATUS = {} | |
| def run_batch_evaluation_background(task_id: str, payload: dict): | |
| try: | |
| BACKGROUND_TASKS_STATUS[task_id] = {"status": "processing", "result": None, "error": None} | |
| result = run_batch_evaluation(payload) | |
| BACKGROUND_TASKS_STATUS[task_id] = {"status": "completed", "result": result, "error": None} | |
| except Exception as e: | |
| BACKGROUND_TASKS_STATUS[task_id] = {"status": "failed", "result": None, "error": str(e)} | |
| def api_evaluate_batch_async(payload: dict, background_tasks: BackgroundTasks) -> dict: | |
| try: | |
| rows = payload.get("rows", []) | |
| if not isinstance(rows, list) or not rows: | |
| raise HTTPException(status_code=400, detail="At least one batch row is required.") | |
| task_id = str(uuid.uuid4()) | |
| background_tasks.add_task(run_batch_evaluation_background, task_id, payload) | |
| return {"task_id": task_id, "status": "processing"} | |
| except Exception as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def api_evaluate_batch_status(task_id: str) -> dict: | |
| if task_id not in BACKGROUND_TASKS_STATUS: | |
| raise HTTPException(status_code=404, detail="Task not found.") | |
| return BACKGROUND_TASKS_STATUS[task_id] | |
| def api_generate( | |
| payload: dict, | |
| x_gemini_api_key: str | None = Header(default=None, alias="X-Gemini-API-Key"), | |
| x_openai_api_key: str | None = Header(default=None, alias="X-OpenAI-API-Key"), | |
| ) -> dict: | |
| try: | |
| return generate_support_answer(payload, x_gemini_api_key, x_openai_api_key) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) from exc | |
| def api_reset() -> dict: | |
| return reset_state() | |
| def csv_response(content: str, filename: str) -> Response: | |
| return Response( | |
| content=content, | |
| media_type="text/csv", | |
| headers={"Content-Disposition": f'attachment; filename="{filename}"'}, | |
| ) | |
| app.mount("/", StaticFiles(directory=ROOT), name="static") | |
| def main() -> None: | |
| if "PORT" in os.environ: | |
| host = "0.0.0.0" | |
| port = int(os.environ["PORT"]) | |
| else: | |
| host = os.environ.get("SENTINEL_HOST", "127.0.0.1") | |
| port = int(sys.argv[1]) if len(sys.argv) > 1 else int(os.environ.get("SENTINEL_PORT", 8000)) | |
| uvicorn.run(app, host=host, port=port, reload=False) | |
| if __name__ == "__main__": | |
| main() | |