fix: restore chat citations and tool details

This commit is contained in:
2026-09-27 12:06:44 +08:00
parent 09d730058c
commit c7d1ba1cd6
10 changed files with 281 additions and 63 deletions
+31 -1
View File
@@ -9,7 +9,7 @@ from fastapi import HTTPException
from fastapi.testclient import TestClient
from sqlalchemy import select
from wisegrade import chat_api, services
from wisegrade import agent_tools, chat_api, services
from wisegrade.actions import confirm, prepare
from wisegrade.agent_tools import execute
from wisegrade.db import get_db
@@ -70,6 +70,36 @@ def test_tool_scope_snapshots_and_project_citations(db):
assert rules["source"] == "batch_snapshot"
def test_get_scores_pages_projects_and_criteria(db, monkeypatch):
first = add_project(db, "123450010")
second = add_project(db, "123450011")
created = services.create_batch(SimpleNamespace(course_version_id=1, rubric_version_id=1), db)
conversation = Conversation(title="分页评分", batch_id=created["id"])
db.add(conversation)
db.commit()
def result(_db, _batch, item):
return {"student_no": f"{item.id}", "name": "学生", "totals": {"overall": 80},
"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", "image_url": None}]}
for index in range(14)]}
monkeypatch.setattr(agent_tools, "project_result", result)
first_page = execute(db, conversation, "get_scores", {"batch_id": created["id"]}, "")
assert first_page["count"] == 2 and len(first_page["results"]) == 1
assert first_page["results"][0]["project_id"] == first.id
assert len(first_page["results"][0]["rows"]) == 6
assert first_page["next_criterion_offset"] == 6 and first_page["next_project_offset"] == 1
assert len(json.dumps(first_page, ensure_ascii=False)) < 12000
later = execute(db, conversation, "get_scores", {"batch_id": created["id"],
"project_offset": 1, "criterion_offset": 12}, "")
assert later["results"][0]["project_id"] == second.id
assert [row["criterion_id"] for row in later["results"][0]["rows"]] == ["C12", "C13"]
assert later["next_project_offset"] is None and later["next_criterion_offset"] is None
def test_import_confirmation_hash_change_index_and_idempotency(environment, db, monkeypatch, session_factory):
folder = environment / "inbox" / "batch" / "123450003_张三_机器人"
folder.mkdir(parents=True)
+74
View File
@@ -168,6 +168,80 @@ def test_tool_record_redacts_text_and_secrets():
assert "学生原文" not in str(saved) and "secret" not in str(saved) and "敏感评语" not in str(saved)
assert "学生原文" not in chat_api._result_summary({"found": True, "citations": [{"quote": "学生原文"}]})
assert "学生原文" not in str(chat_api._safe_result({"citations": [{"quote": "学生原文"}]}))
search = chat_api._safe_result({"found": True, "citations": [{"title": "报告.pdf", "page": 48,
"quote": "可核验片段", "url": "/api/pages/48", "api_key": "secret"}]}, "search_project_material")
assert search['citations'][0]['quote'] == '可核验片段'
assert 'secret' not in str(search)
def test_late_citations_and_search_snippets_survive_restoration(session_factory, monkeypatch):
class FakeClient:
def tool_call(self, messages, _tools):
if not any(message['role'] == 'tool' for message in messages):
return {"role": "assistant", "content": None, "tool_calls": [
{"id": f"find-{index}", "type": "function", "function": {"name": "search_project_material",
"arguments": '{"project_id": 1, "query": "检索原文"}'}} for index in range(5)]}
return {"role": "assistant", "content": "综合报告第48页给出改进方向。", "tool_calls": []}
count = 0
def execute(_db, _conversation, _name, _arguments, _text):
nonlocal count
count += 1
page = 48 if count == 5 else count
return {"found": True, "citations": [{"document_id": 10, "chunk_id": count * 10 + index,
"title": "综合报告.pdf", "page": page, "quote": f"第{page}页原文片段{index}",
"url": f"/api/knowledge/documents/10/pages/{page}"} for index in range(6)]}
monkeypatch.setattr(chat_api, "client_for_chat", lambda _db: FakeClient())
monkeypatch.setattr(chat_api, "execute", execute)
client = client_for(session_factory)
try:
created = client.post('/api/conversations', json={"title": "引用恢复"}).json()
response = client.post(f"/api/conversations/{created['id']}/messages", json={"content": "总结"})
assert response.status_code == 200
detail = client.get(f"/api/conversations/{created['id']}").json()
answer = detail['messages'][-1]
assert len(answer['citations']) == 30
assert [item['page'] for item in answer['citations'][-6:]] == [48] * 6
assert answer['tool_calls'][-1]['result']['citations'][0]['quote'] == '第48页原文片段0'
assert detail['runs'][0]['tool_calls'][-1]['result']['citations'][0]['page'] == 48
finally:
app.dependency_overrides.clear()
def test_large_score_result_keeps_structured_rows_for_model_and_history(session_factory, monkeypatch):
seen = []
class FakeClient:
def tool_call(self, messages, _tools):
if not any(message['role'] == 'tool' for message in messages):
return {"role": "assistant", "content": None, "tool_calls": [{"id": "score-call", "type": "function",
"function": {"name": "get_scores", "arguments": '{"batch_id": 3}'}}]}
seen.append(messages[-1]['content'])
return {"role": "assistant", "content": "已读取评分项目。", "tool_calls": []}
def execute(_db, _conversation, _name, _arguments, _text):
return {"batch_id": 3, "count": 1, "project_offset": 0, "criterion_offset": 0,
"next_project_offset": None, "next_criterion_offset": 6,
"results": [{"project_id": 1, "student_no": "1001", "name": "学生甲", "totals": {},
"url": "/?page=scores&batch_id=3&project_id=1", "total_rows": 22,
"rows": [{"criterion_id": "C1", "criterion_name": "功能", "score_id": 7,
"model_score": 8, "teacher_score": None, "final_score": 8,
"max_score": 10, "status": "scored", "error": "", "evidence": []}]}],
"citations": [], "unrelated_large_field": "x" * 13000}
monkeypatch.setattr(chat_api, "client_for_chat", lambda _db: FakeClient())
monkeypatch.setattr(chat_api, "execute", execute)
client = client_for(session_factory)
try:
created = client.post('/api/conversations', json={"title": "评分恢复"}).json()
assert client.post(f"/api/conversations/{created['id']}/messages", json={"content": "查分"}).status_code == 200
assert seen and 'C1' in seen[0] and 'next_criterion_offset' in seen[0]
assert '结果过长' not in seen[0]
result = client.get(f"/api/conversations/{created['id']}").json()['messages'][-1]['tool_calls'][0]['result']
assert result['results'][0]['rows'][0]['criterion_id'] == 'C1'
assert result['next_criterion_offset'] == 6
finally:
app.dependency_overrides.clear()
def test_upgrade_from_legacy_0003_preserves_history(session_factory):