feat: unify scoring and chat evidence pages
This commit is contained in:
@@ -12,8 +12,9 @@ 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, CriterionScore,
|
||||
KnowledgeChunk, KnowledgeDocument, PendingAction, Project, ScoreEvidence,
|
||||
KnowledgeChunk, KnowledgeDocument, KnowledgePage, PendingAction, Project, ScoreEvidence,
|
||||
ScoringBatch, Student, SubmissionFile, now)
|
||||
from wisegrade.rag import ingest, split_pages
|
||||
|
||||
|
||||
def client_for(session_factory):
|
||||
@@ -179,7 +180,7 @@ 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):
|
||||
def test_score_citations_link_the_canonical_text_and_image_page(session_factory, environment):
|
||||
with session_factory() as db:
|
||||
student = Student(student_no="987654320", name="测试学生")
|
||||
db.add(student)
|
||||
@@ -192,13 +193,15 @@ def test_score_citations_link_text_and_restore_legacy_image_only(session_factory
|
||||
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="这里有原文证据和分析"))
|
||||
image_path = environment / "processed" / f"file-{file.id}" / "page-2.png"
|
||||
image_path.parent.mkdir(parents=True)
|
||||
image_path.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
source = {"page_number": 2, "section": "测试", "text": "这里有原文证据和分析",
|
||||
"rendered_image_path": str(image_path)}
|
||||
file.parsed = {"pages": [source]}
|
||||
document = ingest(db, course_version_id=1, project_id=project.id, submission_file_id=file.id,
|
||||
title="报告.pdf", category="final_report", source_hash="a" * 64,
|
||||
pages=[source], embedder=lambda texts: [[0.1] * 1024 for _ in texts])
|
||||
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,
|
||||
@@ -207,32 +210,33 @@ def test_score_citations_link_text_and_restore_legacy_image_only(session_factory
|
||||
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)
|
||||
page=2, quote="原文证据", image_path=str(image_path)))
|
||||
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"
|
||||
db.add(ChatMessage(conversation_id=conversation.id, role="assistant", content="历史回答", citations=[
|
||||
{"title": "报告.pdf", "document_id": document.id, "page": 2, "quote": "原文证据",
|
||||
"url": expected, "image_url": expected + "/image"}]))
|
||||
db.commit()
|
||||
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
|
||||
assert generated["citations"][0]["url"] == expected
|
||||
assert generated["citations"][0]["image_url"] == expected + "/image"
|
||||
assert generated["citations"][0]["score_url"].startswith("/?page=scores")
|
||||
assert chat_api._answer_citations(generated["citations"])[0]["url"] == expected
|
||||
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 citations[0]["url"] == expected
|
||||
assert citations[0]["image_url"] == expected + "/image"
|
||||
assert "原文证据" in client.get(expected).json()["text"]
|
||||
assert client.get(expected + "/image").content.startswith(b"\x89PNG")
|
||||
with session_factory() as db:
|
||||
stored = db.scalar(select(ChatMessage).where(ChatMessage.conversation_id == conversation_id))
|
||||
assert "text_url" not in stored.citations[0]
|
||||
assert stored.citations[0]["url"] == expected
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
@@ -307,7 +311,7 @@ def test_large_score_result_keeps_structured_rows_for_model_and_history(session_
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_upgrade_from_legacy_0003_preserves_history(session_factory):
|
||||
def test_upgrade_from_legacy_0003_migrates_pages_and_citations(session_factory, environment):
|
||||
from pathlib import Path
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
@@ -319,21 +323,77 @@ def test_upgrade_from_legacy_0003_preserves_history(session_factory):
|
||||
db.flush()
|
||||
db.add(ChatMessage(conversation_id=row.id, role="user", content="旧问题"))
|
||||
db.add(AgentRun(conversation_id=row.id, status="completed"))
|
||||
document = KnowledgeDocument(course_version_id=1, title="旧课程资料", document_type="course",
|
||||
source_hash="legacy", index_version=2, status="ready")
|
||||
db.add(document)
|
||||
db.flush()
|
||||
legacy_text = "旧资料第三页的完整文字。" * 80
|
||||
for part in split_pages([{"page_number": 3, "text": legacy_text}]):
|
||||
db.add(KnowledgeChunk(document_id=document.id, course_version_id=1, project_id=None,
|
||||
document_type="course", page=3, text=part["text"]))
|
||||
student = Student(student_no="987654322", name="迁移测试")
|
||||
db.add(student)
|
||||
db.flush()
|
||||
project = Project(course_version_id=1, student_id=student.id, topic="迁移", source_dir="test/migration")
|
||||
db.add(project)
|
||||
db.flush()
|
||||
file = SubmissionFile(project_id=project.id, category="final_report", relative_path="test/migration/report.pdf",
|
||||
mime_type="application/pdf", size=1, mtime=1, sha256="b" * 64,
|
||||
recognition_status="recognized", parse_status="ready")
|
||||
db.add(file)
|
||||
db.flush()
|
||||
image = environment / "processed" / "migration" / "page-2.png"
|
||||
image.parent.mkdir(parents=True)
|
||||
image.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
file.parsed = {"pages": [{"page_number": 2, "text": "迁移评分证据原文", "rendered_image_path": str(image)}]}
|
||||
student_doc = KnowledgeDocument(course_version_id=1, project_id=project.id,
|
||||
submission_file_id=file.id, title="迁移报告.pdf", document_type="final_report",
|
||||
source_hash="student", index_version=2, status="ready")
|
||||
db.add(student_doc)
|
||||
db.flush()
|
||||
db.add(KnowledgeChunk(document_id=student_doc.id, course_version_id=1, project_id=project.id,
|
||||
document_type="final_report", page=2, text="迁移评分证据原文"))
|
||||
batch_id = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db)["id"]
|
||||
batch = db.get(ScoringBatch, batch_id)
|
||||
score = CriterionScore(batch_id=batch_id, project_id=project.id,
|
||||
criterion_id=batch.rubric_snapshot["criteria"][0]["criterion_id"], status="scored")
|
||||
db.add(score)
|
||||
db.flush()
|
||||
db.add(ScoreEvidence(score_id=score.id, document_id=student_doc.id, submission_file_id=file.id,
|
||||
page=2, quote="迁移评分证据", image_path=str(image)))
|
||||
assistant = ChatMessage(conversation_id=row.id, role="assistant", content="旧回答", citations=[
|
||||
{"title": "旧课程资料", "page": 3, "quote": legacy_text[:30],
|
||||
"url": f"/api/knowledge/documents/{document.id}/pages/3"},
|
||||
{"title": "迁移报告.pdf", "page": 2, "quote": "迁移评分证据",
|
||||
"url": f"/api/assets/{file.id}/2"}])
|
||||
db.add(assistant)
|
||||
db.commit()
|
||||
conversation_id = row.id
|
||||
document_id = document.id
|
||||
student_doc_id = student_doc.id
|
||||
engine = session_factory.kw['bind']
|
||||
with engine.begin() as connection:
|
||||
connection.exec_driver_sql('ALTER TABLE conversations DROP COLUMN first_request_id')
|
||||
connection.exec_driver_sql('ALTER TABLE agent_runs DROP COLUMN tool_calls')
|
||||
connection.exec_driver_sql('ALTER TABLE agent_runs DROP COLUMN events')
|
||||
connection.exec_driver_sql('DROP TABLE knowledge_pages')
|
||||
connection.exec_driver_sql("UPDATE alembic_version SET version_num = '0003'")
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
config = Config(str(root / 'alembic.ini'))
|
||||
config.set_main_option('script_location', str(root / 'migrations'))
|
||||
command.upgrade(config, 'head')
|
||||
with session_factory() as db:
|
||||
assert db.scalar(text('SELECT version_num FROM alembic_version')) == '0004'
|
||||
assert db.scalar(text('SELECT version_num FROM alembic_version')) == '0005'
|
||||
assert db.get(Conversation, conversation_id).title == '旧记录'
|
||||
assert db.scalar(select(ChatMessage.content).where(ChatMessage.conversation_id == conversation_id)) == '旧问题'
|
||||
run = db.scalar(select(AgentRun).where(AgentRun.conversation_id == conversation_id))
|
||||
assert run.tool_calls == [] and run.events == []
|
||||
assert db.get(KnowledgePage, (document_id, 3)).text == legacy_text
|
||||
saved = db.scalar(select(ChatMessage).where(ChatMessage.conversation_id == conversation_id,
|
||||
ChatMessage.role == "assistant"))
|
||||
assert saved.citations[0]["document_id"] == document_id
|
||||
assert saved.citations[0]["url"] == f"/api/knowledge/documents/{document_id}/pages/3"
|
||||
score_citation = saved.citations[1]
|
||||
assert score_citation["document_id"] == student_doc_id
|
||||
assert score_citation["url"] == f"/api/knowledge/documents/{student_doc_id}/pages/2"
|
||||
assert score_citation["image_url"] == score_citation["url"] + "/image"
|
||||
|
||||
Reference in New Issue
Block a user