openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -1,691 +0,0 @@
|
|
|
1
|
-
"""Test for FunctionCallingConverter."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from litellm import ChatCompletionToolParam
|
|
7
|
-
|
|
8
|
-
from openhands.sdk.llm.exceptions import (
|
|
9
|
-
FunctionCallConversionError,
|
|
10
|
-
FunctionCallValidationError,
|
|
11
|
-
)
|
|
12
|
-
from openhands.sdk.llm.utils.fn_call_converter import (
|
|
13
|
-
STOP_WORDS,
|
|
14
|
-
convert_fncall_messages_to_non_fncall_messages,
|
|
15
|
-
convert_non_fncall_messages_to_fncall_messages,
|
|
16
|
-
convert_tool_call_to_string,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
FNCALL_TOOLS: list[ChatCompletionToolParam] = [
|
|
21
|
-
{
|
|
22
|
-
"type": "function",
|
|
23
|
-
"function": {
|
|
24
|
-
"name": "execute_bash",
|
|
25
|
-
"description": "Execute a bash command in the terminal.",
|
|
26
|
-
"parameters": {
|
|
27
|
-
"type": "object",
|
|
28
|
-
"properties": {
|
|
29
|
-
"command": {
|
|
30
|
-
"type": "string",
|
|
31
|
-
"description": "The bash command to execute.",
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"required": ["command"],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"type": "function",
|
|
40
|
-
"function": {
|
|
41
|
-
"name": "finish",
|
|
42
|
-
"description": "Finish the interaction when the task is complete.",
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_stop_words_defined():
|
|
49
|
-
"""Test that STOP_WORDS is properly defined."""
|
|
50
|
-
assert isinstance(STOP_WORDS, list)
|
|
51
|
-
assert len(STOP_WORDS) > 0
|
|
52
|
-
assert all(isinstance(word, str) for word in STOP_WORDS)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_convert_fncall_to_non_fncall_basic():
|
|
56
|
-
"""Test basic conversion from function call messages to non-function call
|
|
57
|
-
messages."""
|
|
58
|
-
fncall_messages = [
|
|
59
|
-
{"role": "user", "content": "Please run ls command"},
|
|
60
|
-
{
|
|
61
|
-
"role": "assistant",
|
|
62
|
-
"content": "I'll run the ls command for you.",
|
|
63
|
-
"tool_calls": [
|
|
64
|
-
{
|
|
65
|
-
"id": "call_123",
|
|
66
|
-
"type": "function",
|
|
67
|
-
"function": {
|
|
68
|
-
"name": "execute_bash",
|
|
69
|
-
"arguments": '{"command": "ls"}',
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
{"role": "tool", "content": "file1.txt\nfile2.txt", "tool_call_id": "call_123"},
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
non_fncall_messages = convert_fncall_messages_to_non_fncall_messages(
|
|
78
|
-
fncall_messages, FNCALL_TOOLS
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
assert isinstance(non_fncall_messages, list)
|
|
82
|
-
assert len(non_fncall_messages) >= len(fncall_messages)
|
|
83
|
-
|
|
84
|
-
# Check that tool calls are converted to text format
|
|
85
|
-
assistant_msg = None
|
|
86
|
-
for msg in non_fncall_messages:
|
|
87
|
-
if msg.get("role") == "assistant" and "execute_bash" in str(
|
|
88
|
-
msg.get("content", "")
|
|
89
|
-
):
|
|
90
|
-
assistant_msg = msg
|
|
91
|
-
break
|
|
92
|
-
|
|
93
|
-
assert assistant_msg is not None
|
|
94
|
-
assert "execute_bash" in assistant_msg["content"]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_convert_non_fncall_to_fncall_basic():
|
|
98
|
-
"""Test basic conversion from non-function call messages to function call
|
|
99
|
-
messages."""
|
|
100
|
-
non_fncall_messages = [
|
|
101
|
-
{"role": "user", "content": "Please run ls command"},
|
|
102
|
-
{
|
|
103
|
-
"role": "assistant",
|
|
104
|
-
"content": (
|
|
105
|
-
"I'll run the ls command for you.\n\n<function=execute_bash>\n"
|
|
106
|
-
"<parameter=command>ls</parameter>\n</function>"
|
|
107
|
-
),
|
|
108
|
-
},
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
fncall_messages = convert_non_fncall_messages_to_fncall_messages(
|
|
112
|
-
non_fncall_messages, FNCALL_TOOLS
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
assert isinstance(fncall_messages, list)
|
|
116
|
-
assert len(fncall_messages) >= len(non_fncall_messages)
|
|
117
|
-
|
|
118
|
-
# Check that function calls are properly converted
|
|
119
|
-
assistant_msg = None
|
|
120
|
-
for msg in fncall_messages:
|
|
121
|
-
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
122
|
-
assistant_msg = msg
|
|
123
|
-
break
|
|
124
|
-
|
|
125
|
-
assert assistant_msg is not None
|
|
126
|
-
assert "tool_calls" in assistant_msg
|
|
127
|
-
assert len(assistant_msg["tool_calls"]) == 1
|
|
128
|
-
assert assistant_msg["tool_calls"][0]["function"]["name"] == "execute_bash"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_convert_fncall_to_non_fncall_with_in_context_learning():
|
|
132
|
-
"""Test conversion with in-context learning examples."""
|
|
133
|
-
fncall_messages = [{"role": "user", "content": "Please run ls command"}]
|
|
134
|
-
|
|
135
|
-
non_fncall_messages = convert_fncall_messages_to_non_fncall_messages(
|
|
136
|
-
fncall_messages, FNCALL_TOOLS, add_in_context_learning_example=True
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
assert isinstance(non_fncall_messages, list)
|
|
140
|
-
# Agent-sdk may combine examples into existing messages rather than creating
|
|
141
|
-
# new ones
|
|
142
|
-
assert len(non_fncall_messages) >= len(fncall_messages)
|
|
143
|
-
|
|
144
|
-
# Check that examples are added to the content
|
|
145
|
-
has_example = False
|
|
146
|
-
for msg in non_fncall_messages:
|
|
147
|
-
content = str(msg.get("content", "")).lower()
|
|
148
|
-
if "example" in content or "start of example" in content:
|
|
149
|
-
has_example = True
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
# Examples should be present when requested
|
|
153
|
-
assert has_example, (
|
|
154
|
-
"In-context learning examples should be added to message content"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def test_convert_fncall_to_non_fncall_without_in_context_learning():
|
|
159
|
-
"""Test conversion without in-context learning examples."""
|
|
160
|
-
fncall_messages = [{"role": "user", "content": "Please run ls command"}]
|
|
161
|
-
|
|
162
|
-
non_fncall_messages = convert_fncall_messages_to_non_fncall_messages(
|
|
163
|
-
fncall_messages, FNCALL_TOOLS, add_in_context_learning_example=False
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
assert isinstance(non_fncall_messages, list)
|
|
167
|
-
# Without examples, should be same length or similar
|
|
168
|
-
assert len(non_fncall_messages) >= len(fncall_messages)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_convert_with_multiple_tool_calls():
|
|
172
|
-
"""Test that multiple tool calls in one message raise an error."""
|
|
173
|
-
fncall_messages = [
|
|
174
|
-
{"role": "user", "content": "Please run ls and then pwd"},
|
|
175
|
-
{
|
|
176
|
-
"role": "assistant",
|
|
177
|
-
"content": "I'll run both commands for you.",
|
|
178
|
-
"tool_calls": [
|
|
179
|
-
{
|
|
180
|
-
"id": "call_123",
|
|
181
|
-
"type": "function",
|
|
182
|
-
"function": {
|
|
183
|
-
"name": "execute_bash",
|
|
184
|
-
"arguments": '{"command": "ls"}',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"id": "call_456",
|
|
189
|
-
"type": "function",
|
|
190
|
-
"function": {
|
|
191
|
-
"name": "execute_bash",
|
|
192
|
-
"arguments": '{"command": "pwd"}',
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
]
|
|
198
|
-
|
|
199
|
-
# Agent-SDK doesn't support multiple tool calls per message
|
|
200
|
-
with pytest.raises(
|
|
201
|
-
FunctionCallConversionError, match="Expected exactly one tool call"
|
|
202
|
-
):
|
|
203
|
-
convert_fncall_messages_to_non_fncall_messages(fncall_messages, FNCALL_TOOLS)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def test_convert_with_tool_response():
|
|
207
|
-
"""Test conversion including tool responses."""
|
|
208
|
-
fncall_messages = [
|
|
209
|
-
{"role": "user", "content": "Please run ls command"},
|
|
210
|
-
{
|
|
211
|
-
"role": "assistant",
|
|
212
|
-
"content": "I'll run the ls command.",
|
|
213
|
-
"tool_calls": [
|
|
214
|
-
{
|
|
215
|
-
"id": "call_123",
|
|
216
|
-
"type": "function",
|
|
217
|
-
"function": {
|
|
218
|
-
"name": "execute_bash",
|
|
219
|
-
"arguments": '{"command": "ls"}',
|
|
220
|
-
},
|
|
221
|
-
}
|
|
222
|
-
],
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
"role": "tool",
|
|
226
|
-
"content": "file1.txt\nfile2.txt\nfolder1/",
|
|
227
|
-
"tool_call_id": "call_123",
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
"role": "assistant",
|
|
231
|
-
"content": "The directory contains two files and one folder.",
|
|
232
|
-
},
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
non_fncall_messages = convert_fncall_messages_to_non_fncall_messages(
|
|
236
|
-
fncall_messages, FNCALL_TOOLS
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
assert isinstance(non_fncall_messages, list)
|
|
240
|
-
assert len(non_fncall_messages) >= 3 # At least user, assistant, final assistant
|
|
241
|
-
|
|
242
|
-
# Check that tool response is incorporated
|
|
243
|
-
has_tool_output = False
|
|
244
|
-
for msg in non_fncall_messages:
|
|
245
|
-
content = str(msg.get("content", ""))
|
|
246
|
-
if "file1.txt" in content or "folder1" in content:
|
|
247
|
-
has_tool_output = True
|
|
248
|
-
break
|
|
249
|
-
|
|
250
|
-
assert has_tool_output
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def test_convert_roundtrip():
|
|
254
|
-
"""Test that conversion is somewhat reversible."""
|
|
255
|
-
original_fncall = [
|
|
256
|
-
{"role": "user", "content": "Please run ls command"},
|
|
257
|
-
{
|
|
258
|
-
"role": "assistant",
|
|
259
|
-
"content": "I'll run the ls command.",
|
|
260
|
-
"tool_calls": [
|
|
261
|
-
{
|
|
262
|
-
"id": "call_123",
|
|
263
|
-
"type": "function",
|
|
264
|
-
"function": {
|
|
265
|
-
"name": "execute_bash",
|
|
266
|
-
"arguments": '{"command": "ls"}',
|
|
267
|
-
},
|
|
268
|
-
}
|
|
269
|
-
],
|
|
270
|
-
},
|
|
271
|
-
]
|
|
272
|
-
|
|
273
|
-
# Convert to non-function call format
|
|
274
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(
|
|
275
|
-
original_fncall, FNCALL_TOOLS
|
|
276
|
-
)
|
|
277
|
-
# Convert back to function call format
|
|
278
|
-
back_to_fncall = convert_non_fncall_messages_to_fncall_messages(
|
|
279
|
-
non_fncall, FNCALL_TOOLS
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
assert isinstance(back_to_fncall, list)
|
|
283
|
-
|
|
284
|
-
# Check that we have tool calls in the result
|
|
285
|
-
has_tool_calls = False
|
|
286
|
-
for msg in back_to_fncall:
|
|
287
|
-
if msg.get("tool_calls"):
|
|
288
|
-
has_tool_calls = True
|
|
289
|
-
break
|
|
290
|
-
|
|
291
|
-
assert has_tool_calls
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def test_convert_with_invalid_function_call():
|
|
295
|
-
"""Test handling of invalid function call format."""
|
|
296
|
-
non_fncall_messages = [
|
|
297
|
-
{"role": "user", "content": "Please run ls command"},
|
|
298
|
-
{
|
|
299
|
-
"role": "assistant",
|
|
300
|
-
"content": (
|
|
301
|
-
"I'll run the ls command.\n\n<function=invalid_function>\n"
|
|
302
|
-
"<parameter=command>ls</parameter>\n</function>"
|
|
303
|
-
),
|
|
304
|
-
},
|
|
305
|
-
]
|
|
306
|
-
|
|
307
|
-
# This should handle invalid function calls gracefully
|
|
308
|
-
try:
|
|
309
|
-
fncall_messages = convert_non_fncall_messages_to_fncall_messages(
|
|
310
|
-
non_fncall_messages, FNCALL_TOOLS
|
|
311
|
-
)
|
|
312
|
-
# If no exception, check that result is reasonable
|
|
313
|
-
assert isinstance(fncall_messages, list)
|
|
314
|
-
except (
|
|
315
|
-
FunctionCallConversionError,
|
|
316
|
-
FunctionCallValidationError,
|
|
317
|
-
ValueError,
|
|
318
|
-
KeyError,
|
|
319
|
-
):
|
|
320
|
-
# These exceptions are acceptable for invalid function calls
|
|
321
|
-
pass
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def test_convert_with_malformed_parameters():
|
|
325
|
-
"""Test handling of malformed function parameters."""
|
|
326
|
-
non_fncall_messages = [
|
|
327
|
-
{"role": "user", "content": "Please run ls command"},
|
|
328
|
-
{
|
|
329
|
-
"role": "assistant",
|
|
330
|
-
"content": (
|
|
331
|
-
"I'll run the ls command.\n\n<function=execute_bash>\n"
|
|
332
|
-
"<parameter=invalid_param>ls</parameter>\n</function>"
|
|
333
|
-
),
|
|
334
|
-
},
|
|
335
|
-
]
|
|
336
|
-
|
|
337
|
-
# This should handle malformed parameters gracefully
|
|
338
|
-
try:
|
|
339
|
-
fncall_messages = convert_non_fncall_messages_to_fncall_messages(
|
|
340
|
-
non_fncall_messages, FNCALL_TOOLS
|
|
341
|
-
)
|
|
342
|
-
assert isinstance(fncall_messages, list)
|
|
343
|
-
except (
|
|
344
|
-
FunctionCallConversionError,
|
|
345
|
-
FunctionCallValidationError,
|
|
346
|
-
ValueError,
|
|
347
|
-
KeyError,
|
|
348
|
-
):
|
|
349
|
-
# These exceptions are acceptable for malformed parameters
|
|
350
|
-
pass
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def test_convert_empty_messages():
|
|
354
|
-
"""Test conversion with empty message list."""
|
|
355
|
-
empty_messages = []
|
|
356
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(
|
|
357
|
-
empty_messages, FNCALL_TOOLS
|
|
358
|
-
)
|
|
359
|
-
assert isinstance(non_fncall, list)
|
|
360
|
-
fncall = convert_non_fncall_messages_to_fncall_messages(
|
|
361
|
-
empty_messages, FNCALL_TOOLS
|
|
362
|
-
)
|
|
363
|
-
assert isinstance(fncall, list)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def test_convert_with_no_tools():
|
|
367
|
-
"""Test conversion with empty tools list."""
|
|
368
|
-
messages = [
|
|
369
|
-
{"role": "user", "content": "Hello"},
|
|
370
|
-
{"role": "assistant", "content": "Hi there!"},
|
|
371
|
-
]
|
|
372
|
-
|
|
373
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(messages, [])
|
|
374
|
-
assert isinstance(non_fncall, list)
|
|
375
|
-
assert len(non_fncall) >= len(messages)
|
|
376
|
-
|
|
377
|
-
fncall = convert_non_fncall_messages_to_fncall_messages(messages, [])
|
|
378
|
-
assert isinstance(fncall, list)
|
|
379
|
-
assert len(fncall) >= len(messages)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def test_convert_preserves_user_messages():
|
|
383
|
-
"""Test that user messages are preserved during conversion."""
|
|
384
|
-
messages = [
|
|
385
|
-
{"role": "user", "content": "Please help me with this task"},
|
|
386
|
-
{"role": "assistant", "content": "I'll help you with that."},
|
|
387
|
-
]
|
|
388
|
-
|
|
389
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(messages, FNCALL_TOOLS)
|
|
390
|
-
|
|
391
|
-
# Find user message in result
|
|
392
|
-
user_msg = None
|
|
393
|
-
for msg in non_fncall:
|
|
394
|
-
if msg.get("role") == "user":
|
|
395
|
-
user_msg = msg
|
|
396
|
-
break
|
|
397
|
-
|
|
398
|
-
assert user_msg is not None
|
|
399
|
-
assert "Please help me with this task" in user_msg["content"]
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def test_convert_with_system_message():
|
|
403
|
-
"""Test conversion with system messages."""
|
|
404
|
-
messages = [
|
|
405
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
|
406
|
-
{"role": "user", "content": "Please run ls command"},
|
|
407
|
-
{
|
|
408
|
-
"role": "assistant",
|
|
409
|
-
"content": "I'll run the ls command.",
|
|
410
|
-
"tool_calls": [
|
|
411
|
-
{
|
|
412
|
-
"id": "call_123",
|
|
413
|
-
"type": "function",
|
|
414
|
-
"function": {
|
|
415
|
-
"name": "execute_bash",
|
|
416
|
-
"arguments": '{"command": "ls"}',
|
|
417
|
-
},
|
|
418
|
-
}
|
|
419
|
-
],
|
|
420
|
-
},
|
|
421
|
-
]
|
|
422
|
-
|
|
423
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(messages, FNCALL_TOOLS)
|
|
424
|
-
|
|
425
|
-
# System message should be preserved
|
|
426
|
-
system_msg = None
|
|
427
|
-
for msg in non_fncall:
|
|
428
|
-
if msg.get("role") == "system":
|
|
429
|
-
system_msg = msg
|
|
430
|
-
break
|
|
431
|
-
|
|
432
|
-
assert system_msg is not None
|
|
433
|
-
assert "helpful assistant" in system_msg["content"]
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def test_convert_with_finish_tool():
|
|
437
|
-
"""Test conversion with finish tool call."""
|
|
438
|
-
fncall_messages = [
|
|
439
|
-
{"role": "user", "content": "Please finish the task"},
|
|
440
|
-
{
|
|
441
|
-
"role": "assistant",
|
|
442
|
-
"content": "Task completed.",
|
|
443
|
-
"tool_calls": [
|
|
444
|
-
{
|
|
445
|
-
"id": "call_finish",
|
|
446
|
-
"type": "function",
|
|
447
|
-
"function": {"name": "finish", "arguments": "{}"},
|
|
448
|
-
}
|
|
449
|
-
],
|
|
450
|
-
},
|
|
451
|
-
]
|
|
452
|
-
|
|
453
|
-
non_fncall = convert_fncall_messages_to_non_fncall_messages(
|
|
454
|
-
fncall_messages, FNCALL_TOOLS
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
assert isinstance(non_fncall, list)
|
|
458
|
-
|
|
459
|
-
# Check that finish call is represented
|
|
460
|
-
has_finish = False
|
|
461
|
-
for msg in non_fncall:
|
|
462
|
-
content = str(msg.get("content", ""))
|
|
463
|
-
if "finish" in content.lower():
|
|
464
|
-
has_finish = True
|
|
465
|
-
break
|
|
466
|
-
|
|
467
|
-
assert has_finish
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
@pytest.mark.parametrize(
|
|
471
|
-
"tool_call, expected",
|
|
472
|
-
[
|
|
473
|
-
# Basic single parameter
|
|
474
|
-
(
|
|
475
|
-
{
|
|
476
|
-
"id": "test_id",
|
|
477
|
-
"type": "function",
|
|
478
|
-
"function": {
|
|
479
|
-
"name": "execute_bash",
|
|
480
|
-
"arguments": '{"command": "ls -la"}',
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
(
|
|
484
|
-
"<function=execute_bash>\n<parameter=command>ls -la</parameter>\n"
|
|
485
|
-
"</function>"
|
|
486
|
-
),
|
|
487
|
-
),
|
|
488
|
-
# Multiple parameters with different types
|
|
489
|
-
(
|
|
490
|
-
{
|
|
491
|
-
"id": "test_id",
|
|
492
|
-
"type": "function",
|
|
493
|
-
"function": {
|
|
494
|
-
"name": "str_replace_editor",
|
|
495
|
-
"arguments": (
|
|
496
|
-
'{"command": "view", "path": "/test/file.py", '
|
|
497
|
-
'"view_range": [1, 10]}'
|
|
498
|
-
),
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
(
|
|
502
|
-
"<function=str_replace_editor>\n<parameter=command>view</parameter>\n"
|
|
503
|
-
"<parameter=path>/test/file.py</parameter>\n"
|
|
504
|
-
"<parameter=view_range>[1, 10]</parameter>\n</function>"
|
|
505
|
-
),
|
|
506
|
-
),
|
|
507
|
-
# Indented code blocks (whitespace preservation)
|
|
508
|
-
(
|
|
509
|
-
{
|
|
510
|
-
"id": "test_id",
|
|
511
|
-
"type": "function",
|
|
512
|
-
"function": {
|
|
513
|
-
"name": "str_replace_editor",
|
|
514
|
-
"arguments": json.dumps(
|
|
515
|
-
{
|
|
516
|
-
"command": "str_replace",
|
|
517
|
-
"path": "/test/file.py",
|
|
518
|
-
"old_str": "def example():\n pass",
|
|
519
|
-
"new_str": (
|
|
520
|
-
"def example():\n # This is indented\n"
|
|
521
|
-
' print("hello")\n return True'
|
|
522
|
-
),
|
|
523
|
-
}
|
|
524
|
-
),
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
(
|
|
528
|
-
"<function=str_replace_editor>\n<parameter=command>str_replace</parameter>\n"
|
|
529
|
-
"<parameter=path>/test/file.py</parameter>\n<parameter=old_str>\n"
|
|
530
|
-
"def example():\n pass\n</parameter>\n<parameter=new_str>\n"
|
|
531
|
-
'def example():\n # This is indented\n print("hello")\n'
|
|
532
|
-
" return True\n</parameter>\n</function>"
|
|
533
|
-
),
|
|
534
|
-
),
|
|
535
|
-
# List parameter values
|
|
536
|
-
(
|
|
537
|
-
{
|
|
538
|
-
"id": "test_id",
|
|
539
|
-
"type": "function",
|
|
540
|
-
"function": {
|
|
541
|
-
"name": "test_function",
|
|
542
|
-
"arguments": (
|
|
543
|
-
'{"command": "test", "path": "/test/file.py", '
|
|
544
|
-
'"tags": ["tag1", "tag2", "tag with spaces"]}'
|
|
545
|
-
),
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
(
|
|
549
|
-
"<function=test_function>\n<parameter=command>test</parameter>\n"
|
|
550
|
-
"<parameter=path>/test/file.py</parameter>\n"
|
|
551
|
-
'<parameter=tags>["tag1", "tag2", "tag with spaces"]</parameter>\n'
|
|
552
|
-
"</function>"
|
|
553
|
-
),
|
|
554
|
-
),
|
|
555
|
-
# Dictionary parameter values
|
|
556
|
-
(
|
|
557
|
-
{
|
|
558
|
-
"id": "test_id",
|
|
559
|
-
"type": "function",
|
|
560
|
-
"function": {
|
|
561
|
-
"name": "test_function",
|
|
562
|
-
"arguments": json.dumps(
|
|
563
|
-
{
|
|
564
|
-
"command": "test",
|
|
565
|
-
"path": "/test/file.py",
|
|
566
|
-
"metadata": {
|
|
567
|
-
"key1": "value1",
|
|
568
|
-
"key2": 42,
|
|
569
|
-
"nested": {"subkey": "subvalue"},
|
|
570
|
-
},
|
|
571
|
-
}
|
|
572
|
-
),
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
(
|
|
576
|
-
"<function=test_function>\n<parameter=command>test</parameter>\n"
|
|
577
|
-
"<parameter=path>/test/file.py</parameter>\n"
|
|
578
|
-
'<parameter=metadata>{"key1": "value1", "key2": 42, '
|
|
579
|
-
'"nested": {"subkey": "subvalue"}}</parameter>\n</function>'
|
|
580
|
-
),
|
|
581
|
-
),
|
|
582
|
-
],
|
|
583
|
-
)
|
|
584
|
-
def test_convert_tool_call_to_string_parameterized(tool_call, expected):
|
|
585
|
-
"""Test tool call to string conversion with various parameter types and formats."""
|
|
586
|
-
converted = convert_tool_call_to_string(tool_call)
|
|
587
|
-
assert converted == expected
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def test_convert_fncall_messages_with_cache_control():
|
|
591
|
-
"""Test that cache_control is properly handled in tool messages."""
|
|
592
|
-
messages = [
|
|
593
|
-
{
|
|
594
|
-
"role": "tool",
|
|
595
|
-
"name": "test_tool",
|
|
596
|
-
"content": [{"type": "text", "text": "test content"}],
|
|
597
|
-
"cache_control": {"type": "ephemeral"},
|
|
598
|
-
"tool_call_id": "call_123",
|
|
599
|
-
}
|
|
600
|
-
]
|
|
601
|
-
|
|
602
|
-
result = convert_fncall_messages_to_non_fncall_messages(messages, FNCALL_TOOLS)
|
|
603
|
-
|
|
604
|
-
# Verify the result
|
|
605
|
-
assert len(result) == 1
|
|
606
|
-
assert result[0]["role"] == "user"
|
|
607
|
-
|
|
608
|
-
# Check that cache_control is preserved in the converted message
|
|
609
|
-
assert "cache_control" in result[0]["content"][-1]
|
|
610
|
-
assert result[0]["content"][-1]["cache_control"] == {"type": "ephemeral"}
|
|
611
|
-
|
|
612
|
-
# Check that the tool result content is properly formatted
|
|
613
|
-
assert (
|
|
614
|
-
result[0]["content"][0]["text"]
|
|
615
|
-
== "EXECUTION RESULT of [test_tool]:\ntest content"
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
def test_convert_fncall_messages_without_cache_control():
|
|
620
|
-
"""Test that tool messages without cache_control work as expected."""
|
|
621
|
-
messages = [
|
|
622
|
-
{
|
|
623
|
-
"role": "tool",
|
|
624
|
-
"name": "test_tool",
|
|
625
|
-
"content": [{"type": "text", "text": "test content"}],
|
|
626
|
-
"tool_call_id": "call_123",
|
|
627
|
-
}
|
|
628
|
-
]
|
|
629
|
-
|
|
630
|
-
result = convert_fncall_messages_to_non_fncall_messages(messages, FNCALL_TOOLS)
|
|
631
|
-
|
|
632
|
-
# Verify the result
|
|
633
|
-
assert len(result) == 1
|
|
634
|
-
assert result[0]["role"] == "user"
|
|
635
|
-
|
|
636
|
-
# Check that no cache_control is added when not present
|
|
637
|
-
assert "cache_control" not in result[0]["content"][-1]
|
|
638
|
-
|
|
639
|
-
# Check that the tool result content is properly formatted
|
|
640
|
-
assert (
|
|
641
|
-
result[0]["content"][0]["text"]
|
|
642
|
-
== "EXECUTION RESULT of [test_tool]:\ntest content"
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
def test_convert_fncall_messages_with_image_url():
|
|
647
|
-
"""Test that convert_fncall_messages_to_non_fncall_messages handles image URLs
|
|
648
|
-
correctly."""
|
|
649
|
-
messages = [
|
|
650
|
-
{
|
|
651
|
-
"role": "tool",
|
|
652
|
-
"name": "browser",
|
|
653
|
-
"content": [
|
|
654
|
-
{
|
|
655
|
-
"type": "text",
|
|
656
|
-
"text": "some browser tool results",
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
"type": "image_url",
|
|
660
|
-
"image_url": {"url": "data:image/gif;base64,R0lGODlhAQABAAAAACw="},
|
|
661
|
-
},
|
|
662
|
-
],
|
|
663
|
-
"tool_call_id": "call_123",
|
|
664
|
-
}
|
|
665
|
-
]
|
|
666
|
-
|
|
667
|
-
converted_messages = convert_fncall_messages_to_non_fncall_messages(
|
|
668
|
-
messages, FNCALL_TOOLS
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
assert len(converted_messages) == 1
|
|
672
|
-
assert converted_messages[0]["role"] == "user"
|
|
673
|
-
assert len(converted_messages[0]["content"]) == len(messages[0]["content"])
|
|
674
|
-
|
|
675
|
-
# Check that text content is properly formatted with tool execution result
|
|
676
|
-
text_content = next(
|
|
677
|
-
c for c in converted_messages[0]["content"] if c["type"] == "text"
|
|
678
|
-
)
|
|
679
|
-
assert text_content["text"] == (
|
|
680
|
-
f"EXECUTION RESULT of [{messages[0]['name']}]:\n"
|
|
681
|
-
f"{messages[0]['content'][0]['text']}"
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
# Check that image URL is preserved
|
|
685
|
-
image_content = next(
|
|
686
|
-
c for c in converted_messages[0]["content"] if c["type"] == "image_url"
|
|
687
|
-
)
|
|
688
|
-
assert (
|
|
689
|
-
image_content["image_url"]["url"]
|
|
690
|
-
== "data:image/gif;base64,R0lGODlhAQABAAAAACw="
|
|
691
|
-
)
|