fix: show tool parameters and skip duplicate calls
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user