diff --git a/webapp/api/score.py b/webapp/api/score.py index 966b57f..65f2ea9 100644 --- a/webapp/api/score.py +++ b/webapp/api/score.py @@ -2,10 +2,13 @@ from __future__ import annotations +import logging import time from typing import Annotated -from fastapi import APIRouter, Header, HTTPException +from fastapi import APIRouter, Header, HTTPException, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse from rag_eval.metrics.weights import compute_weighted_score from rag_eval.settings import EvaluationSettings @@ -13,6 +16,7 @@ from webapp.models import ScoreRequest, ScoreResponse from webapp.services.inline_scorer import inline_scorer router = APIRouter(prefix="/api/score", tags=["score"]) +logger = logging.getLogger("webapp.api.score") def _get_settings() -> EvaluationSettings: @@ -58,6 +62,7 @@ def _check_auth(authorization: str | None, token: str) -> None: }, ) def score_sample( + raw_request: Request, request: ScoreRequest, authorization: Annotated[str | None, Header()] = None, ) -> ScoreResponse: @@ -88,6 +93,15 @@ def score_sample( **鉴权**:若 `.env` 中配置了 `SCORE_API_TOKEN`,需在请求头携带 `Authorization: Bearer `;留空则无需鉴权(适合内网部署)。 """ + client = f"{raw_request.client.host}:{raw_request.client.port}" if raw_request.client else "unknown" + logger.info( + "[score] incoming client=%s method=%s content_type=%s metrics=%s has_gt=%s", + client, + raw_request.method, + raw_request.headers.get("content-type", ""), + request.metrics, + request.ground_truth is not None, + ) settings = _get_settings() # Require Bearer auth only when the deployment configured a shared token. @@ -141,6 +155,12 @@ def score_sample( {}, ) + logger.info( + "[score] done latency=%dms skipped=%s scores=%s", + latency_ms, + skipped, + {k: (round(v, 4) if v is not None else None) for k, v in all_scores.items()}, + ) return ScoreResponse( scores=all_scores, weighted_score=round(weighted, 4) if weighted is not None else None, diff --git a/webapp/server.py b/webapp/server.py index edda9bc..e941091 100644 --- a/webapp/server.py +++ b/webapp/server.py @@ -7,15 +7,19 @@ the server starts even when the evaluation dependencies are not yet installed. from __future__ import annotations +import logging from pathlib import Path -from fastapi import FastAPI -from fastapi.responses import FileResponse +from fastapi import FastAPI, Request +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, score STATIC_DIR = Path(__file__).resolve().parent / "static" +logger = logging.getLogger("webapp.server") # OpenAPI tag metadata — controls the grouping and descriptions in /docs. OPENAPI_TAGS = [ @@ -103,6 +107,21 @@ def create_app() -> FastAPI: app.include_router(pipeline.router) app.include_router(score.router) + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + """Log full validation error detail to help diagnose 422 responses.""" + errors = jsonable_encoder(exc.errors()) + logger.warning( + "[422] validation error url=%s content_type=%s errors=%s", + request.url.path, + request.headers.get("content-type", ""), + errors, + ) + return JSONResponse( + status_code=422, + content={"detail": errors}, + ) + @app.get("/api/health", tags=["meta"]) def health() -> dict[str, str]: """Report basic liveness so the UI can confirm the server is reachable."""