Add RAGAS evaluation web console (FastAPI + vanilla JS)

- webapp/: FastAPI backend with runs/scenarios/evaluations API routers;
  services for run_reader, report_builder, scenario_scanner, task_manager
  (lazy ragas import — server boots even without ragas); Pydantic models
- webapp/static/: single-page console (layout A: left-nav + main area);
  report detail with metric cards, Chart.js distribution histogram,
  grouping table, lowest-score sample review; trigger evaluation + log polling
- webmain.py: uvicorn entry point (alongside existing main.py CLI)
- start.bat: Windows one-click launcher with env checks and auto-browser open
- rag_eval/datasets/: implement missing loader + normalizer modules
  (load_dataset_records, normalize_records) required by evaluator
- scripts/seed_sample_run.py: generate realistic demo run artifacts
- .gitignore: exclude datasets/ data files but keep rag_eval/datasets/ source

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:53:57 +08:00
parent 9cbdc1d95d
commit e89695e490
26 changed files with 2496 additions and 2 deletions

46
webapp/static/js/api.js Normal file
View File

@@ -0,0 +1,46 @@
// api.js — 控制台后端 HTTP 接口的轻量封装。
const API = {
// 通用 JSON GET失败时抛出带状态码的错误。
async get(path) {
const resp = await fetch(path);
if (!resp.ok) {
const detail = await API._extractError(resp);
throw new Error(detail);
}
return resp.json();
},
// 通用 JSON POST。
async post(path, body) {
const resp = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {}),
});
if (!resp.ok) {
const detail = await API._extractError(resp);
throw new Error(detail);
}
return resp.json();
},
// 从错误响应中尽量解析出 detail 文本。
async _extractError(resp) {
try {
const data = await resp.json();
return data.detail || `请求失败 (${resp.status})`;
} catch (_e) {
return `请求失败 (${resp.status})`;
}
},
health() { return API.get("/api/health"); },
runs() { return API.get("/api/runs"); },
runDetail(runId) { return API.get(`/api/runs/${encodeURIComponent(runId)}`); },
scenarios() { return API.get("/api/scenarios"); },
triggerEvaluation(scenarioPath) {
return API.post("/api/evaluations", { scenario_path: scenarioPath });
},
taskStatus(taskId) { return API.get(`/api/evaluations/${encodeURIComponent(taskId)}`); },
};