fix: link score citations to their source text pages

This commit is contained in:
2026-09-27 17:15:55 +08:00
parent 92906c82bc
commit 039e93d0de
9 changed files with 143 additions and 18 deletions
+2 -1
View File
@@ -83,7 +83,7 @@ def test_get_scores_pages_projects_and_criteria(db, monkeypatch):
"rows": [{"criterion": {"criterion_id": f"C{index}", "name": f"评分项{index}", "max_score": 10},
"score_id": index, "model_score": 8, "teacher_score": None, "final_score": 8,
"status": "scored", "error": "", "evidence": [{"page": index + 1,
"quote": "证据" * 200, "document": "报告.pdf",
"quote": "证据" * 200, "document": "报告.pdf", "document_id": index + 100,
"image_url": f"/api/assets/{item.id}/1" if index == 0 else None}]}
for index in range(14)]}
@@ -95,6 +95,7 @@ def test_get_scores_pages_projects_and_criteria(db, monkeypatch):
assert first_page["next_criterion_offset"] == 6 and first_page["next_project_offset"] == 1
assert first_page["citations"][0]["url"] == f"/?page=scores&batch_id={created['id']}&project_id={first.id}"
assert first_page["citations"][0]["image_url"] == f"/api/assets/{first.id}/1"
assert first_page["citations"][0]["text_url"] == "/api/knowledge/documents/100/pages/1"
assert first_page["citations"][1]["url"] == first_page["citations"][0]["url"]
assert first_page["citations"][1]["image_url"] is None
assert len(json.dumps(first_page, ensure_ascii=False)) < 12000
+63 -2
View File
@@ -2,15 +2,18 @@
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Event
from types import SimpleNamespace
from uuid import uuid4
from fastapi.testclient import TestClient
from sqlalchemy import func, select
from wisegrade import chat_api
from wisegrade import agent_tools, chat_api, services
from wisegrade.db import get_db
from wisegrade.main import app
from wisegrade.models import AgentRun, AuditLog, ChatMessage, Conversation, CourseVersion, PendingAction, Project, ScoringBatch, Student, now
from wisegrade.models import (AgentRun, AuditLog, ChatMessage, Conversation, CourseVersion, CriterionScore,
KnowledgeChunk, KnowledgeDocument, PendingAction, Project, ScoreEvidence,
ScoringBatch, Student, SubmissionFile, now)
def client_for(session_factory):
@@ -176,6 +179,64 @@ def test_tool_record_keeps_bounded_query_but_redacts_review_text_and_secrets():
assert 'secret' not in str(search)
def test_score_citations_link_text_and_restore_legacy_image_only(session_factory):
with session_factory() as db:
student = Student(student_no="987654320", name="测试学生")
db.add(student)
db.flush()
project = Project(course_version_id=1, student_id=student.id, topic="测试项目", source_dir="test/citation")
db.add(project)
db.flush()
file = SubmissionFile(project_id=project.id, category="final_report", relative_path="test/citation/report.pdf",
mime_type="application/pdf", size=1, mtime=1, sha256="a" * 64,
recognition_status="recognized", parse_status="ready")
db.add(file)
db.flush()
document = KnowledgeDocument(course_version_id=1, project_id=project.id, submission_file_id=file.id,
title="报告.pdf", document_type="final_report", source_hash="a" * 64,
index_version=2, status="ready")
db.add(document)
db.flush()
db.add(KnowledgeChunk(document_id=document.id, course_version_id=1, project_id=project.id,
document_type="final_report", page=2, section="测试", text="这里有原文证据和分析"))
created = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db)
batch = db.get(ScoringBatch, created["id"])
score = CriterionScore(batch_id=batch.id, project_id=project.id,
criterion_id=batch.rubric_snapshot["criteria"][0]["criterion_id"],
model_score=8, status="scored")
db.add(score)
db.flush()
db.add(ScoreEvidence(score_id=score.id, document_id=document.id, submission_file_id=file.id,
page=2, quote="原文证据", image_path="page-2.png"))
conversation = Conversation(title="旧评分引用", course_version_id=1, batch_id=batch.id, project_id=project.id)
db.add(conversation)
db.flush()
image_url = f"/api/assets/{file.id}/2"
db.add(ChatMessage(conversation_id=conversation.id, role="assistant", content="历史回答", citations=[
{"title": "报告.pdf", "page": 2, "quote": "原文证据", "url": image_url},
{"title": "报告.pdf", "page": 2, "quote": "不匹配的引文", "url": image_url}]))
db.commit()
expected = f"/api/knowledge/documents/{document.id}/pages/2"
generated = agent_tools.execute(db, conversation, "get_scores",
{"batch_id": batch.id, "project_id": project.id}, "")
assert generated["citations"][0]["text_url"] == expected
assert chat_api._answer_citations(generated["citations"])[0]["text_url"] == expected
assert generated["citations"][0]["image_url"] == image_url
conversation_id = conversation.id
client = client_for(session_factory)
try:
detail = client.get(f"/api/conversations/{conversation_id}").json()
citations = detail["messages"][0]["citations"]
assert citations[0]["text_url"] == expected
assert "text_url" not in citations[1]
assert "原文证据" in client.get(expected).json()["text"]
with session_factory() as db:
stored = db.scalar(select(ChatMessage).where(ChatMessage.conversation_id == conversation_id))
assert "text_url" not in stored.citations[0]
finally:
app.dependency_overrides.clear()
def test_late_citations_and_search_snippets_survive_restoration(session_factory, monkeypatch):
class FakeClient:
def tool_call(self, messages, _tools):