fix: share precise scoring failure labels across API chat and exports

This commit is contained in:
2026-10-18 12:16:04 +08:00
parent 000638718c
commit ba5a6952bb
3 changed files with 47 additions and 6 deletions
+24
View File
@@ -293,6 +293,30 @@ def test_pdf_html_preserves_zero_and_none():
assert "实验(0 / 10)" in html and "模型原始分:0" in html assert "实验(0 / 10)" in html and "模型原始分:0" in html
@pytest.mark.parametrize("error, label", [
("模型证据未关联该片段的检索目标", "模型输出校验失败"),
("模型请求或JSON校验失败(ConnectError:无法连接模型端点),已重试2次", "模型连接失败"),
("模型请求或JSON校验失败(模型请求超时,请稍后重试),已重试2次", "模型连接失败"),
("模型请求或JSON校验失败(HTTP 429:请求限流),已重试2次", "模型请求失败"),
("模型请求或JSON校验失败(ValidationError:模型返回字段不符合格式),已重试2次", "模型输出校验失败"),
("后台数据处理异常", "评分失败"),
])
def test_score_api_and_export_share_precise_failure_labels(sqlite_db, error, label):
from wisegrade.exports import project_result
db = sqlite_db
item = project(db)
created = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db)
batch = db.get(ScoringBatch, created["id"])
criterion = batch.rubric_snapshot["criteria"][0]
db.add(CriterionScore(batch_id=batch.id, project_id=item.id,
criterion_id=criterion["criterion_id"], status="failed", error=error))
db.commit()
result = project_result(db, batch, item)
assert result["rows"][0]["status_label"] == label
assert f"状态:{label}" in _html_report(result, batch)
assert result["rows"][0]["model_score"] is None
def test_public_reindex_uses_saved_pages(sqlite_db): def test_public_reindex_uses_saved_pages(sqlite_db):
db = sqlite_db db = sqlite_db
doc = document(db, None, "课程资料") doc = document(db, None, "课程资料")
+2 -2
View File
@@ -9,7 +9,7 @@ from sqlalchemy import select
from .actions import prepare, require_scope from .actions import prepare, require_scope
from .evidence import CONTENT_ALLOWED, STRUCTURE_ALLOWED from .evidence import CONTENT_ALLOWED, STRUCTURE_ALLOWED
from .exports import STATUS_LABELS, batch_projects, page_type_label, project_result from .exports import batch_projects, page_type_label, project_result
from .knowledge_pages import page_links from .knowledge_pages import page_links
from .models import (Conversation, Course, CourseVersion, IndexTask, KnowledgeChunk, KnowledgeDocument, KnowledgePage, from .models import (Conversation, Course, CourseVersion, IndexTask, KnowledgeChunk, KnowledgeDocument, KnowledgePage,
Project, RubricVersion, ScoringBatch, Student, SubmissionFile) Project, RubricVersion, ScoringBatch, Student, SubmissionFile)
@@ -384,7 +384,7 @@ def scores(db, convo, args, _user):
"max_score": row["criterion"]["max_score"], "score_id": row["score_id"], "max_score": row["criterion"]["max_score"], "score_id": row["score_id"],
"model_score": row["model_score"], "teacher_score": row["teacher_score"], "model_score": row["model_score"], "teacher_score": row["teacher_score"],
"final_score": row["final_score"], "status": row["status"], "final_score": row["final_score"], "status": row["status"],
"status_label": STATUS_LABELS.get(row["status"], row["status"]), "status_label": row["status_label"],
"error": str(row["error"] or "")[:300], "error": str(row["error"] or "")[:300],
# Which body targets still lack evidence, so the answer can # Which body targets still lack evidence, so the answer can
# explain a review item instead of reporting a score. # explain a review item instead of reporting a score.
+21 -4
View File
@@ -1,5 +1,6 @@
from html import escape from html import escape
from pathlib import Path from pathlib import Path
import re
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill from openpyxl.styles import Font, PatternFill
@@ -18,7 +19,7 @@ STATUS_LABELS = {
"reviewed": "教师已复核", "reviewed": "教师已复核",
"needs_review": "待教师复核", "needs_review": "待教师复核",
"unscorable": "材料缺失或解析失败", "unscorable": "材料缺失或解析失败",
"failed": "模型调用失败", "failed": "评分失败",
} }
PAGE_TYPE_LABELS = { PAGE_TYPE_LABELS = {
"cover": "封面", "abstract": "摘要", "toc": "目录", "body": "正文", "cover": "封面", "abstract": "摘要", "toc": "目录", "body": "正文",
@@ -38,6 +39,21 @@ def page_type_label(value) -> str:
return PAGE_TYPE_LABELS.get(str(value or "unknown"), "未分类页面") return PAGE_TYPE_LABELS.get(str(value or "unknown"), "未分类页面")
def criterion_status_label(status: str, error: str = "") -> str:
"""One description for API, chat and exported feedback, including old errors."""
if status != "failed":
return STATUS_LABELS.get(status, status)
# The client wraps transport failures in "模型请求或JSON校验失败" too:
# inspect the concrete cause before that generic wrapper.
if re.search(r"ConnectError|Timeout|Network|无法连接|请求超时", error, re.IGNORECASE):
return "模型连接失败"
if re.search(r"HTTP\s*\d{3}|未配置.*(?:模型|Key)|未配置或无法连接", error, re.IGNORECASE):
return "模型请求失败"
if re.search(r"模型证据|target_id|评分项或满分|JSON校验|ValidationError", error):
return "模型输出校验失败"
return STATUS_LABELS["failed"]
def project_result(db, batch: ScoringBatch, project: Project): def project_result(db, batch: ScoringBatch, project: Project):
scores = db.scalars(select(CriterionScore).where(CriterionScore.batch_id == batch.id, scores = db.scalars(select(CriterionScore).where(CriterionScore.batch_id == batch.id,
CriterionScore.project_id == project.id)).all() CriterionScore.project_id == project.id)).all()
@@ -81,6 +97,7 @@ def project_result(db, batch: ScoringBatch, project: Project):
"model_comment": score.model_comment if score else "", "teacher_comment": score.teacher_comment if score else None, "model_comment": score.model_comment if score else "", "teacher_comment": score.teacher_comment if score else None,
"missing_evidence": score.missing_evidence if score else [], "suggestion": score.suggestion if score else "", "missing_evidence": score.missing_evidence if score else [], "suggestion": score.suggestion if score else "",
"status": score.status if score else "pending", "error": score.error if score else "", "status": score.status if score else "pending", "error": score.error if score else "",
"status_label": criterion_status_label(score.status, score.error or "") if score else STATUS_LABELS["pending"],
"evidence_gaps": (score.evidence_gaps or []) if score else [], "evidence_gaps": (score.evidence_gaps or []) if score else [],
"evidence": [evidence_dict(item, evidence_links(db, item)) for item in evidence]}) "evidence": [evidence_dict(item, evidence_links(db, item)) for item in evidence]})
student = db.get(Student, project.student_id) student = db.get(Student, project.student_id)
@@ -152,12 +169,12 @@ def _html_report(result, batch):
missing = "、".join(row["missing_evidence"] or []) missing = "、".join(row["missing_evidence"] or [])
gaps = ";".join(f"{gap.get('target_label')}(检索范围:{'、'.join(page_type_label(t) for t in gap.get('searched_page_types') or []) or '正文'})" gaps = ";".join(f"{gap.get('target_label')}(检索范围:{'、'.join(page_type_label(t) for t in gap.get('searched_page_types') or []) or '正文'})"
for gap in row.get("evidence_gaps") or []) for gap in row.get("evidence_gaps") or [])
review = "本项缺少正文证据,已转教师复核。" if row["status"] == "needs_review" else "" review = "本项证据尚不足以自动评分,已转教师复核。" if row["status"] == "needs_review" else ""
rows.append(f"<section><h3>{esc(criterion['name'])}({esc(row['final_score'])} / {criterion['max_score']})</h3>" rows.append(f"<section><h3>{esc(criterion['name'])}({esc(row['final_score'])} / {criterion['max_score']})</h3>"
f"<p>模型原始分:{esc(row['model_score'])};教师修改分:{esc(row['teacher_score'])};状态:{esc(STATUS_LABELS.get(row['status'], row['status']))}</p>" f"<p>模型原始分:{esc(row['model_score'])};教师修改分:{esc(row['teacher_score'])};状态:{esc(criterion_status_label(row['status'], row.get('error') or ''))}</p>"
f"<p>模型评语:{esc(row['model_comment'])}</p><p>教师评语:{esc(row['teacher_comment'])}</p>" f"<p>模型评语:{esc(row['model_comment'])}</p><p>教师评语:{esc(row['teacher_comment'])}</p>"
f"<p>{esc(review)}</p>" f"<p>{esc(review)}</p>"
f"<p>缺失证据:{esc(missing)}</p><p>缺正文证据的目标:{esc(gaps)}</p>" f"<p>缺失证据:{esc(missing)}</p><p>待核对的证据目标:{esc(gaps)}</p>"
f"<p>建议:{esc(row['suggestion'])}</p><p>异常:{esc(row['error'])}</p>" f"<p>建议:{esc(row['suggestion'])}</p><p>异常:{esc(row['error'])}</p>"
f"<ul>{evidences}</ul></section>") f"<ul>{evidences}</ul></section>")
totals_ = result["totals"] totals_ = result["totals"]