- New POST /api/score/session_async endpoint: same session_id calls append to one shared report
- New GET /api/score/sessions/{session_id}: returns call_count, metric_means, all job records
- New GET /api/score/session/jobs/{job_id}: individual call status
- SessionScoreJobManager: deterministic run_id from session_id, per-session mutex for CSV append, advisor regenerated on every call
- SessionScoreRequest (extends ScoreRequest + session_id), SessionScoreJobResponse, SessionStatus models added
- 24 new tests, all passing
chore(weighted-score): comment out 综合加权得分 display and computation
- report.js: hide 综合加权得分 card in report detail page
- score_jobs.js: hide 综合 chip in async job list
- report_builder.py: overall_ws=None (computation disabled)
- summary.py: weighted_score summary line disabled
- evaluator.py: weighted_score/sample_weight columns no longer written to scores.csv
- score.py /api/score: weighted_score always returns null
- score_job_manager.py + session_score_manager.py: weighted=None
- Updated 3 tests to match new behaviour (6 pre-existing failures unchanged)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
127 lines
4.5 KiB
JavaScript
127 lines
4.5 KiB
JavaScript
// score_jobs.js — 评分记录页面(异步 RAGAS 评分任务列表)
|
|
// 每条评分完成后自动写入标准 Run 产物,点击「查看报告」复用现有报告详情页。
|
|
|
|
const ScoreJobs = {
|
|
_pollTimers: {}, // job_id -> setInterval handle
|
|
|
|
async load() {
|
|
const list = document.getElementById("scorejobs-list");
|
|
const empty = document.getElementById("scorejobs-empty");
|
|
list.innerHTML = '<p class="muted">加载中…</p>';
|
|
try {
|
|
const data = await API.listScoreJobs();
|
|
const jobs = data.jobs || [];
|
|
list.innerHTML = "";
|
|
if (jobs.length === 0) {
|
|
empty.hidden = false;
|
|
return;
|
|
}
|
|
empty.hidden = true;
|
|
jobs.forEach(job => list.appendChild(ScoreJobs.renderCard(job)));
|
|
// Auto-poll any pending jobs
|
|
jobs.forEach(job => {
|
|
if (job.status === "queued" || job.status === "running") {
|
|
ScoreJobs._startPoll(job.job_id);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
list.innerHTML = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
|
|
}
|
|
},
|
|
|
|
renderCard(job) {
|
|
const card = document.createElement("div");
|
|
card.className = "run-card";
|
|
card.id = `score-job-${job.job_id}`;
|
|
card.innerHTML = ScoreJobs._cardHtml(job);
|
|
// Bind report button if already completed
|
|
ScoreJobs._bindReportBtn(card, job);
|
|
return card;
|
|
},
|
|
|
|
_cardHtml(job) {
|
|
const time = App.shortTime(job.created_at);
|
|
const question = App.escape((job.request_summary?.question || "—").slice(0, 60));
|
|
const metrics = (job.request_summary?.metrics || []).join(", ");
|
|
|
|
const statusBadge = `<span class="badge ${job.status}">${job.status}</span>`;
|
|
|
|
let scoreHtml = "";
|
|
if (job.status === "completed") {
|
|
scoreHtml = Object.entries(job.scores || {})
|
|
.map(([k, v]) => {
|
|
const cls = App.scoreClass(v);
|
|
const text = v === null || v === undefined ? "n/a" : Number(v).toFixed(3);
|
|
return `<span class="metric-chip" title="${App.escape(k)}">${App.escape(App.shortMetric(k))} <b class="${cls}">${text}</b></span>`;
|
|
})
|
|
.join(" ");
|
|
// 综合加权得分(已暂时隐藏)
|
|
// if (job.weighted_score !== null && job.weighted_score !== undefined) {
|
|
// const cls = App.scoreClass(job.weighted_score);
|
|
// scoreHtml += ` <span class="metric-chip">综合 <b class="${cls}">${Number(job.weighted_score).toFixed(3)}</b></span>`;
|
|
// }
|
|
} else if (job.status === "failed") {
|
|
scoreHtml = `<span style="color:var(--bad);font-size:12px">${App.escape((job.error || "").slice(0, 80))}</span>`;
|
|
} else {
|
|
scoreHtml = `<span class="muted">评分中,请稍候…</span>`;
|
|
}
|
|
|
|
const reportBtn = job.status === "completed" && job.run_id
|
|
? `<button class="btn btn-sm btn-primary score-job-report-btn" data-run-id="${App.escape(job.run_id)}">查看报告</button>`
|
|
: "";
|
|
|
|
return `
|
|
<div class="run-card-head">
|
|
<div class="run-card-title">${question}</div>
|
|
<div style="display:flex;gap:8px;align-items:center">${statusBadge}${reportBtn}</div>
|
|
</div>
|
|
<div class="run-card-meta">
|
|
<div>指标:${App.escape(metrics)} · ${time} · ${job.latency_ms}ms</div>
|
|
</div>
|
|
<div class="run-card-metrics">${scoreHtml}</div>
|
|
`;
|
|
},
|
|
|
|
_bindReportBtn(card, job) {
|
|
const btn = card.querySelector(".score-job-report-btn");
|
|
if (!btn) return;
|
|
btn.addEventListener("click", () => {
|
|
const runId = btn.dataset.runId;
|
|
if (runId) {
|
|
App.enableReportNav();
|
|
App.navigate("report", runId);
|
|
}
|
|
});
|
|
},
|
|
|
|
_startPoll(jobId) {
|
|
if (ScoreJobs._pollTimers[jobId]) return;
|
|
ScoreJobs._pollTimers[jobId] = setInterval(async () => {
|
|
try {
|
|
const job = await API.getScoreJob(jobId);
|
|
const card = document.getElementById(`score-job-${jobId}`);
|
|
if (card) {
|
|
card.innerHTML = ScoreJobs._cardHtml(job);
|
|
ScoreJobs._bindReportBtn(card, job);
|
|
}
|
|
if (job.status === "completed" || job.status === "failed") {
|
|
clearInterval(ScoreJobs._pollTimers[jobId]);
|
|
delete ScoreJobs._pollTimers[jobId];
|
|
// If completed, pre-enable report nav
|
|
if (job.status === "completed" && job.run_id) {
|
|
App.enableReportNav();
|
|
}
|
|
}
|
|
} catch (_e) {
|
|
clearInterval(ScoreJobs._pollTimers[jobId]);
|
|
delete ScoreJobs._pollTimers[jobId];
|
|
}
|
|
}, 5000);
|
|
},
|
|
|
|
stopAllPolls() {
|
|
Object.values(ScoreJobs._pollTimers).forEach(t => clearInterval(t));
|
|
ScoreJobs._pollTimers = {};
|
|
},
|
|
};
|