main
py 185 lines 6.37 KB
Raw
1 from __future__ import annotations
2
3 import sys
4 from pathlib import Path
5
6 import pytest
7
8 PROJECT_ROOT = Path(__file__).resolve().parents[1]
9 if str(PROJECT_ROOT) not in sys.path:
10 sys.path.insert(0, str(PROJECT_ROOT))
11
12 from helpers.extract_tools import (
13 extract_tool_request,
14 is_misformatted_tool_request,
15 json_parse_dirty,
16 normalize_tool_request,
17 )
18 from helpers import parallel_tools
19
20
21 def test_normalize_tool_request_accepts_canonical_keys() -> None:
22 assert normalize_tool_request({"tool_name": "response", "tool_args": {"text": "ok"}}) == (
23 "response",
24 {"text": "ok"},
25 )
26
27
28 def test_normalize_tool_request_accepts_fallback_keys() -> None:
29 assert normalize_tool_request({"tool": "response", "args": {"text": "ok"}}) == (
30 "response",
31 {"text": "ok"},
32 )
33
34
35 def test_normalize_tool_request_uses_fallback_when_canonical_name_is_empty() -> None:
36 assert normalize_tool_request(
37 {"tool_name": "", "tool": "response", "args": {"text": "ok"}}
38 ) == ("response", {"text": "ok"})
39
40
41 def test_normalize_tool_request_uses_fallback_when_canonical_args_are_invalid() -> None:
42 assert normalize_tool_request(
43 {"tool_name": "response", "tool_args": None, "args": {"text": "ok"}}
44 ) == ("response", {"text": "ok"})
45
46
47 def test_normalize_tool_request_translates_method_suffix_to_action() -> None:
48 assert normalize_tool_request(
49 {"tool_name": "text_editor:read", "tool_args": {"path": "README.md"}}
50 ) == ("text_editor", {"path": "README.md", "action": "read"})
51
52
53 def test_normalize_tool_request_translates_method_arg_to_action() -> None:
54 assert normalize_tool_request(
55 {"tool_name": "scheduler", "tool_args": {"method": "list_tasks"}}
56 ) == ("scheduler", {"method": "list_tasks", "action": "list_tasks"})
57
58
59 def test_normalize_tool_request_preserves_explicit_action_over_method() -> None:
60 assert normalize_tool_request(
61 {
62 "tool_name": "scheduler:delete_task",
63 "tool_args": {"method": "list_tasks", "action": "show_task"},
64 }
65 ) == (
66 "scheduler",
67 {"method": "list_tasks", "action": "show_task"},
68 )
69
70
71 def test_normalize_tool_request_rejects_missing_args() -> None:
72 with pytest.raises(ValueError, match="tool_args"):
73 normalize_tool_request({"tool_name": "response"})
74
75
76 def test_normalize_tool_request_accepts_native_function_format() -> None:
77 request = {
78 "type": "function",
79 "name": "search_engine",
80 "parameters": {"query": "latest Agent Zero release"},
81 }
82
83 assert json_parse_dirty(str(request)) == request
84 assert normalize_tool_request(request) == (
85 "search_engine",
86 {"query": "latest Agent Zero release"},
87 )
88
89
90 def test_normalize_tool_request_accepts_single_action_wrapper() -> None:
91 request = {
92 "thoughts": ["Read the requested file."],
93 "actions": [
94 {
95 "tool_name": "text_editor",
96 "tool_args": {"action": "read", "path": "README.md"},
97 }
98 ],
99 }
100
101 assert json_parse_dirty(str(request)) == request
102 assert normalize_tool_request(request) == (
103 "text_editor",
104 {"action": "read", "path": "README.md"},
105 )
106
107
108 def test_normalize_tool_request_rejects_multiple_wrapped_actions() -> None:
109 with pytest.raises(ValueError, match="exactly one"):
110 normalize_tool_request(
111 {
112 "actions": [
113 {"tool_name": "response", "tool_args": {"text": "first"}},
114 {"tool_name": "response", "tool_args": {"text": "second"}},
115 ]
116 }
117 )
118
119
120 def test_extract_tool_request_requires_a_complete_tool_message() -> None:
121 request = '{"tool_name":"response","tool_args":{"text":"ok"}}'
122
123 assert extract_tool_request(request) == {
124 "tool_name": "response",
125 "tool_args": {"text": "ok"},
126 }
127 assert extract_tool_request('{"status":"ok"}') is None
128 assert extract_tool_request(f"Example: {request}") is None
129 assert extract_tool_request(f"{request} trailing text") is None
130
131
132 def test_is_misformatted_tool_request_requires_agent_tool_envelope() -> None:
133 request = '{"tool_name":"response","tool_args":{"text":"ok"}}'
134 concatenated = (
135 '{"thoughts":[],"headline":"Inspecting","tool_name":"code_execution_tool",'
136 '"tool_args":{"code":"pwd"}}'
137 '{"thoughts":[],"headline":"Answering","tool_name":"response",'
138 '"tool_args":{"text":"done"}}'
139 )
140 malformed = (
141 '{"thoughts":["Plan the work", "Run the tools", '
142 '"headline":"Save results", "tool_name":"parallel", '
143 '"tool_args":{"tool_calls":[{"tool_name":"memory_save",'
144 '"tool_args":{"text":"ok"}}],"wait":true}}'
145 )
146
147 assert extract_tool_request(malformed) is None
148 assert is_misformatted_tool_request(malformed) is True
149 assert extract_tool_request(concatenated) is None
150 assert is_misformatted_tool_request(concatenated) is True
151 assert is_misformatted_tool_request(f"Intro\n```json\n{request}\n```") is True
152 assert is_misformatted_tool_request('{"status":"planning"}') is False
153 assert is_misformatted_tool_request(f"Example: {request}") is False
154 assert is_misformatted_tool_request(
155 malformed.replace('{"thoughts"', '{"status":"planning","thoughts"')
156 ) is False
157
158
159 def test_normalize_parallel_tool_calls_accepts_full_agent_reply_shape() -> None:
160 calls = parallel_tools.normalize_parallel_tool_calls(
161 [
162 {
163 "thoughts": ["This is independent and ready to run."],
164 "headline": "Search Python release notes",
165 "tool_name": "search_engine",
166 "tool_args": {"query": "latest Python version changelog"},
167 }
168 ]
169 )
170
171 assert calls[0].tool_name == "search_engine"
172 assert calls[0].tool_args == {"query": "latest Python version changelog"}
173
174
175 def test_parallel_prompt_encourages_mixed_independent_batches() -> None:
176 prompt = (PROJECT_ROOT / "prompts" / "agent.system.tool.parallel.md").read_text(
177 encoding="utf-8"
178 )
179
180 assert "same `tool_name` and `tool_args` shape as a top-level reply" in prompt
181 assert "planning fields like `thoughts` or `headline` are ignored" in prompt
182 assert "even when they use different tools" in prompt
183 assert "Do not split by tool type" in prompt
184 assert "Never include `document_query`" in prompt
185 assert "Call `response` only as a top-level tool" in prompt