fix: show tool parameters and skip duplicate calls

This commit is contained in:
2026-09-27 14:47:55 +08:00
parent 40fd07a1bc
commit e5bf1cf250
7 changed files with 192 additions and 42 deletions
+68 -1
View File
@@ -267,7 +267,8 @@ def test_sse_persists_messages_and_tool_failure(session_factory, monkeypatch):
assert [m["role"] for m in restored["messages"]] == ["user", "assistant"]
assert len(restored["messages"][1]["tool_calls"]) == 9
assert len({call["id"] for call in restored["messages"][1]["tool_calls"]}) == 9
assert all(call["status"] == "completed" and call["result"] for call in restored["messages"][1]["tool_calls"])
assert [call["status"] for call in restored["messages"][1]["tool_calls"]] == ["completed", *["skipped"] * 8]
assert all(call["result"] for call in restored["messages"][1]["tool_calls"])
assert restored["runs"][0]["status"] == "completed"
monkeypatch.setattr(chat_api, "client_for_chat", lambda db: (_ for _ in ()).throw(ModelError("不支持工具调用")))
with client.stream("POST", f"/api/conversations/{created['id']}/messages", json={"content": "再试"}) as response:
@@ -285,6 +286,72 @@ def test_sse_persists_messages_and_tool_failure(session_factory, monkeypatch):
app.dependency_overrides.clear()
def test_tool_arguments_cover_registered_fields_without_review_prose():
numbers = {"course_version_id", "batch_id", "project_id", "score_id", "task_id", "rubric_version_id",
"score", "offset", "limit", "project_offset", "criterion_offset"}
strings = {"query", "criterion_id", "format", "status", "student_no", "relative_dir"}
redacted = {"comment", "reason"}
fields = set().union(*(tool.schema.model_fields for tool in agent_tools.TOOLS.values()))
assert fields == numbers | strings | redacted
args = {key: 8 for key in numbers} | {key: "电机控制" for key in strings} | {key: "学生原文" for key in redacted}
saved = chat_api._safe_arguments(args)
assert set(saved) == fields
assert saved["query"] == "电机控制" and saved["criterion_offset"] == 8
assert saved["comment"] == saved["reason"] == "[文本已省略]"
assert "学生原文" not in json.dumps(saved, ensure_ascii=False)
assert chat_api._safe_arguments({"api_key": "secret"}) == {"omitted_fields": 1}
def test_identical_tool_calls_are_skipped_but_distinct_pages_and_queries_run(session_factory, monkeypatch):
requested = [
("get_scores", {"batch_id": 3, "project_id": 1}),
("get_scores", {"batch_id": 3, "project_id": 1, "project_offset": 0, "criterion_offset": 0}),
("get_scores", {"batch_id": 3, "project_id": 1, "criterion_offset": 6}),
("search_project_material", {"project_id": 1, "query": "电机控制"}),
("search_project_material", {"query": "电机控制", "project_id": 1}),
("search_project_material", {"project_id": 1, "query": "视觉方案"}),
]
class FakeClient:
def tool_call(self, messages, _tools):
if any(message["role"] == "tool" for message in messages):
return {"role": "assistant", "content": "查询完成", "tool_calls": []}
return {"role": "assistant", "content": None, "tool_calls": [
{"id": f"call-{index}", "type": "function",
"function": {"name": name, "arguments": json.dumps(args, ensure_ascii=False)}}
for index, (name, args) in enumerate(requested)]}
executed = []
def fake_execute(_db, _conversation, name, arguments, _text):
executed.append((name, arguments))
return {"found": True, "citations": []} if name.startswith("search_") else {
"count": 1, "results": [], "project_offset": arguments.get("project_offset", 0),
"criterion_offset": arguments.get("criterion_offset", 0)}
monkeypatch.setattr(chat_api, "client_for_chat", lambda _db: FakeClient())
monkeypatch.setattr(chat_api, "execute", fake_execute)
def override_db():
with session_factory() as session:
yield session
app.dependency_overrides[get_db] = override_db
try:
client = TestClient(app)
created = client.post("/api/conversations", json={"title": "查重"}).json()
with client.stream("POST", f"/api/conversations/{created['id']}/messages", json={"content": "查询"}) as response:
body = "".join(response.iter_text())
assert "event: done" in body
assert [(name, args.get("criterion_offset", 0), args.get("query")) for name, args in executed] == [
("get_scores", 0, None), ("get_scores", 6, None),
("search_project_material", 0, "电机控制"), ("search_project_material", 0, "视觉方案")]
restored = client.get(f"/api/conversations/{created['id']}").json()
calls = restored["messages"][1]["tool_calls"]
assert [call["status"] for call in calls] == ["completed", "skipped", "completed", "completed", "skipped", "completed"]
assert calls[0]["arguments"]["criterion_offset"] == 0
assert calls[2]["arguments"]["criterion_offset"] == 6
assert calls[3]["arguments"]["query"] == "电机控制"
assert calls[4]["result"]["previous_call_id"] == calls[3]["id"]
assert restored["runs"][0]["tool_calls"] == calls
finally:
app.dependency_overrides.clear()
def test_failed_tool_calls_count_toward_limit(monkeypatch):
class RepeatingClient:
def tool_call(self, messages, tools):
+6 -4
View File
@@ -162,10 +162,12 @@ def test_background_runs_survive_no_stream_and_isolate_parallel_events(session_f
app.dependency_overrides.clear()
def test_tool_record_redacts_text_and_secrets():
saved = chat_api._safe_arguments({"project_id": 2, "query": "学生原文", "api_key": "secret", "comment": "敏感评语"})
def test_tool_record_keeps_bounded_query_but_redacts_review_text_and_secrets():
saved = chat_api._safe_arguments({"project_id": 2, "query": "电机控制", "api_key": "secret", "comment": "敏感评语"})
assert saved["project_id"] == 2
assert "学生原文" not in str(saved) and "secret" not in str(saved) and "敏感评语" not in str(saved)
assert saved["query"] == "电机控制"
assert "secret" not in str(saved) and "敏感评语" not in str(saved)
assert len(chat_api._safe_arguments({"query": "a" * 500})["query"]) == 300
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,
@@ -180,7 +182,7 @@ def test_late_citations_and_search_snippets_survive_restoration(session_factory,
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)]}
"arguments": f'{{"project_id": 1, "query": "检索原文{index}"}}'}} for index in range(5)]}
return {"role": "assistant", "content": "综合报告第48页给出改进方向。", "tool_calls": []}
count = 0