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.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. 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
- )