Avoid duplicate agent searches and resolve simple retrieved calculations
This commit is contained in:
@@ -295,8 +295,45 @@ class AgentSkillTests(unittest.TestCase):
|
||||
third_messages = model.await_args_list[2].args[0]["messages"]
|
||||
self.assertEqual([message["role"] for message in third_messages[-2:]], ["assistant", "tool"])
|
||||
|
||||
def test_agent_reuses_duplicate_read_only_tool_result_and_finishes(self) -> None:
|
||||
repeated_call = {
|
||||
"id": "search-repeat",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_workspace",
|
||||
"arguments": '{"query":"same query"}',
|
||||
},
|
||||
}
|
||||
completions = [
|
||||
{"choices": [{"message": {"tool_calls": [repeated_call]}}]},
|
||||
{"choices": [{"message": {"tool_calls": [{**repeated_call, "id": "search-repeat-2"}]}}]},
|
||||
{"choices": [{"message": {"content": "وجدت المعلومة في الملف."}}]},
|
||||
]
|
||||
with (
|
||||
patch(
|
||||
"app.main.workspace.retrieve",
|
||||
return_value=[("app/answer.py", "المعلومة المطلوبة")],
|
||||
) as search,
|
||||
patch("app.main.get_completion", new=AsyncMock(side_effect=completions)) as model,
|
||||
):
|
||||
result = asyncio.run(
|
||||
_execute_agent(
|
||||
AgentRequest(task="ابحث عن المعلومة", workspace_path=self.workspace)
|
||||
)
|
||||
)
|
||||
|
||||
search.assert_called_once_with("same query", Path(self.workspace).resolve())
|
||||
self.assertEqual(model.await_count, 3)
|
||||
self.assertNotIn("tools", model.await_args_list[2].args[0])
|
||||
self.assertEqual(result["files"], ["app/answer.py"])
|
||||
self.assertEqual(
|
||||
[step["tool"] for step in result["steps"]],
|
||||
["search_workspace", "search_workspace"],
|
||||
)
|
||||
self.assertEqual(result["result"], "وجدت المعلومة في الملف.")
|
||||
|
||||
def test_explicit_workspace_search_prefetches_before_followup_tool(self) -> None:
|
||||
task = "ابحث في ملفات المشروع عن قيمة MAX_AGENT_TOOL_CALLS، ثم احسب القيمة مضروبة في 7."
|
||||
task = "ابحث في ملفات المشروع عن كلمة privacy، ثم احسب 3 × 7."
|
||||
completions = [
|
||||
{"choices": [{"message": {"tool_calls": [{
|
||||
"id": "calc-after-search",
|
||||
@@ -305,7 +342,7 @@ class AgentSkillTests(unittest.TestCase):
|
||||
{"choices": [{"message": {"content": "القيمة 3، والناتج 21 من app/main.py."}}]},
|
||||
]
|
||||
with (
|
||||
patch("app.main.workspace.retrieve", return_value=[("app/main.py", "MAX_AGENT_TOOL_CALLS = 3")]) as search,
|
||||
patch("app.main.workspace.retrieve", return_value=[("README.md", "Local workspace stays private.")]) as search,
|
||||
patch("app.main.get_completion", new=AsyncMock(side_effect=completions)) as model,
|
||||
):
|
||||
result = asyncio.run(
|
||||
@@ -320,23 +357,92 @@ class AgentSkillTests(unittest.TestCase):
|
||||
|
||||
search.assert_called_once_with(task, Path(self.workspace).resolve())
|
||||
initial_payload = model.await_args_list[0].args[0]
|
||||
self.assertIn(
|
||||
self.assertNotIn(
|
||||
"search_workspace",
|
||||
[tool["function"]["name"] for tool in initial_payload["tools"]],
|
||||
)
|
||||
self.assertIn("MAX_AGENT_TOOL_CALLS = 3", initial_payload["messages"][1]["content"])
|
||||
self.assertNotIn(
|
||||
"search_knowledge",
|
||||
[tool["function"]["name"] for tool in initial_payload["tools"]],
|
||||
)
|
||||
self.assertEqual(
|
||||
[tool["function"]["name"] for tool in initial_payload["tools"]],
|
||||
["calculator"],
|
||||
)
|
||||
system_message = initial_payload["messages"][0]["content"]
|
||||
self.assertIn("بحث الخادم في مساحة العمل المسموحة مسبقًا", system_message)
|
||||
self.assertNotIn("استخدم search_workspace", system_message)
|
||||
self.assertIn("Local workspace stays private.", initial_payload["messages"][1]["content"])
|
||||
self.assertEqual(
|
||||
[step["tool"] for step in result["steps"]],
|
||||
["search_workspace", "calculator"],
|
||||
)
|
||||
self.assertEqual(result["files"], ["README.md"])
|
||||
self.assertIn("21", result["result"])
|
||||
|
||||
def test_explicit_search_multiplies_one_retrieved_numeric_constant_locally(self) -> None:
|
||||
task = "ابحث في ملفات المشروع عن قيمة MAX_AGENT_TOOL_CALLS، ثم احسبها مضروبة في 7."
|
||||
with (
|
||||
patch(
|
||||
"app.main.workspace.retrieve",
|
||||
return_value=[("app/main.py", "MAX_AGENT_TOOL_CALLS = 3")],
|
||||
) as search,
|
||||
patch("app.main.get_completion", new=AsyncMock()) as model,
|
||||
):
|
||||
result = asyncio.run(
|
||||
_execute_agent(
|
||||
AgentRequest(
|
||||
task=task,
|
||||
workspace_path=self.workspace,
|
||||
skill_id="code_explain",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
search.assert_called_once_with(task, Path(self.workspace).resolve())
|
||||
model.assert_not_awaited()
|
||||
self.assertEqual(
|
||||
[step["tool"] for step in result["steps"]],
|
||||
["search_workspace", "calculator"],
|
||||
)
|
||||
self.assertEqual(result["files"], ["app/main.py"])
|
||||
self.assertIn("21", result["result"])
|
||||
self.assertIn("3 × 7 = 21", result["result"])
|
||||
|
||||
def test_explicit_workspace_search_rejects_repeating_prefetched_search(self) -> None:
|
||||
task = "ابحث في ملفات المشروع عن قيمة MAX_AGENT_TOOL_CALLS."
|
||||
completion = {
|
||||
"choices": [{"message": {"tool_calls": [{
|
||||
"id": "duplicate-prefetch",
|
||||
"function": {
|
||||
"name": "search_workspace",
|
||||
"arguments": '{"query":"MAX_AGENT_TOOL_CALLS"}',
|
||||
},
|
||||
}]}}]
|
||||
}
|
||||
with (
|
||||
patch("app.main.workspace.retrieve", return_value=[("app/main.py", "MAX_AGENT_TOOL_CALLS = 3")]) as search,
|
||||
patch("app.main.get_completion", new=AsyncMock(return_value=completion)) as model,
|
||||
):
|
||||
with self.assertRaises(HTTPException) as error:
|
||||
asyncio.run(
|
||||
_execute_agent(
|
||||
AgentRequest(task=task, workspace_path=self.workspace)
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(error.exception.status_code, 422)
|
||||
search.assert_called_once_with(task, Path(self.workspace).resolve())
|
||||
offered = model.await_args.args[0]["tools"]
|
||||
self.assertNotIn("search_workspace", [item["function"]["name"] for item in offered])
|
||||
|
||||
def test_agent_caps_sequential_tools_and_forces_final_model_turn(self) -> None:
|
||||
tool_calls = [
|
||||
{"choices": [{"message": {"tool_calls": [{
|
||||
"id": f"calc-{index}",
|
||||
"function": {"name": "calculator", "arguments": '{"expression":"2 + 3"}'},
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"arguments": json.dumps({"expression": f"2 + {index + 3}"}),
|
||||
},
|
||||
}]}}]}
|
||||
for index in range(3)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user