use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{Response, StatusCode};
use llmtrace_core::JudgeVerdictQuery;
use llmtrace_security::golden_set::{load_golden_set, replay_golden_set};
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use uuid::Uuid;
use crate::proxy::AppState;
pub const GOLDEN_SET_PATH_ENV: &str = "LLMTRACE_GOLDEN_SET_PATH";
#[derive(Debug, Deserialize)]
pub struct VerdictByTraceIdParams {
pub trace_id: String,
}
pub async fn verdict_by_trace_id_handler(
State(state): State<Arc<AppState>>,
Query(params): Query<VerdictByTraceIdParams>,
) -> Response<Body> {
let trace_id = match Uuid::parse_str(params.trace_id.trim()) {
Ok(id) => id,
Err(_) => return error_json(StatusCode::BAD_REQUEST, "trace_id must be a UUID"),
};
let query = JudgeVerdictQuery {
trace_id: Some(trace_id),
limit: Some(1),
..JudgeVerdictQuery::default()
};
let verdicts = match state.storage.judge_verdicts.query_verdicts(&query).await {
Ok(rows) => rows,
Err(err) => {
tracing::warn!(%trace_id, error = %err, "verdict_by_trace_id query failed");
return error_json(StatusCode::INTERNAL_SERVER_ERROR, "verdict query failed");
}
};
let Some(verdict) = verdicts.into_iter().next() else {
return error_json(StatusCode::NOT_FOUND, "no verdict for trace_id");
};
let body = match serde_json::to_vec(&verdict) {
Ok(bytes) => bytes,
Err(err) => {
tracing::error!(%trace_id, error = %err, "verdict serialization failed");
return error_json(
StatusCode::INTERNAL_SERVER_ERROR,
"verdict serialization failed",
);
}
};
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(body))
.expect("infallible response build")
}
fn error_json(status: StatusCode, message: &str) -> Response<Body> {
let body = serde_json::json!({ "error": { "message": message, "type": "debug_error" } });
Response::builder()
.status(status)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.expect("infallible response build")
}
pub async fn golden_set_replay_handler(State(state): State<Arc<AppState>>) -> Response<Body> {
let path = match std::env::var(GOLDEN_SET_PATH_ENV) {
Ok(p) if !p.trim().is_empty() => PathBuf::from(p),
_ => {
return error_json(
StatusCode::SERVICE_UNAVAILABLE,
concat!(
"golden-set replay disabled: set ",
"LLMTRACE_GOLDEN_SET_PATH to the fixture root ",
"(e.g. crates/llmtrace-security/fixtures/judge_golden_set)"
),
);
}
};
let entries = match load_golden_set(&path) {
Ok(v) => v,
Err(err) => {
tracing::warn!(error = %err, root = %path.display(), "golden-set load failed");
return error_json(StatusCode::BAD_REQUEST, &format!("load failed: {err}"));
}
};
let result = match replay_golden_set(&path, &entries, state.security.as_ref()).await {
Ok(r) => r,
Err(err) => {
tracing::error!(error = %err, "golden-set replay failed");
return error_json(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("replay failed: {err}"),
);
}
};
for cat in &result.categories {
state.metrics.record_golden_set_alignment(
&cat.category,
cat.alignment_rate,
cat.false_positive_rate,
);
}
let body = match serde_json::to_vec(&result) {
Ok(bytes) => bytes,
Err(err) => {
tracing::error!(error = %err, "golden-set serialization failed");
return error_json(StatusCode::INTERNAL_SERVER_ERROR, "serialization failed");
}
};
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(body))
.expect("infallible response build")
}