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,1049 +0,0 @@
|
|
|
1
|
-
"""Convert function calling messages to non-function calling messages and vice versa.
|
|
2
|
-
|
|
3
|
-
This will inject prompts so that models that doesn't support function calling
|
|
4
|
-
can still be used with function calling agents.
|
|
5
|
-
|
|
6
|
-
We follow format from: https://docs.litellm.ai/docs/completion/function_call
|
|
7
|
-
""" # noqa: E501
|
|
8
|
-
|
|
9
|
-
import copy
|
|
10
|
-
import json
|
|
11
|
-
import re
|
|
12
|
-
import sys
|
|
13
|
-
from typing import Iterable, Literal, NotRequired, TypedDict, cast
|
|
14
|
-
|
|
15
|
-
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
|
16
|
-
|
|
17
|
-
from openhands.sdk.llm.exceptions import (
|
|
18
|
-
FunctionCallConversionError,
|
|
19
|
-
FunctionCallValidationError,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class CacheControl(TypedDict):
|
|
24
|
-
type: Literal["ephemeral"]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class TextPart(TypedDict):
|
|
28
|
-
type: Literal["text"]
|
|
29
|
-
text: str
|
|
30
|
-
cache_control: NotRequired[CacheControl]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Content = str | list[TextPart]
|
|
34
|
-
|
|
35
|
-
EXECUTE_BASH_TOOL_NAME = "execute_bash"
|
|
36
|
-
STR_REPLACE_EDITOR_TOOL_NAME = "str_replace_editor"
|
|
37
|
-
BROWSER_TOOL_NAME = "browser"
|
|
38
|
-
FINISH_TOOL_NAME = "finish"
|
|
39
|
-
LLM_BASED_EDIT_TOOL_NAME = "edit_file"
|
|
40
|
-
TASK_TRACKER_TOOL_NAME = "task_tracker"
|
|
41
|
-
|
|
42
|
-
# Inspired by: https://docs.together.ai/docs/llama-3-function-calling#function-calling-w-llama-31-70b
|
|
43
|
-
system_message_suffix_TEMPLATE = """
|
|
44
|
-
You have access to the following functions:
|
|
45
|
-
|
|
46
|
-
{description}
|
|
47
|
-
|
|
48
|
-
If you choose to call a function ONLY reply in the following format with NO suffix:
|
|
49
|
-
|
|
50
|
-
<function=example_function_name>
|
|
51
|
-
<parameter=example_parameter_1>value_1</parameter>
|
|
52
|
-
<parameter=example_parameter_2>
|
|
53
|
-
This is the value for the second parameter
|
|
54
|
-
that can span
|
|
55
|
-
multiple lines
|
|
56
|
-
</parameter>
|
|
57
|
-
</function>
|
|
58
|
-
|
|
59
|
-
<IMPORTANT>
|
|
60
|
-
Reminder:
|
|
61
|
-
- Function calls MUST follow the specified format, start with <function= and end with </function>
|
|
62
|
-
- Required parameters MUST be specified
|
|
63
|
-
- Only call one function at a time
|
|
64
|
-
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after.
|
|
65
|
-
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
|
|
66
|
-
</IMPORTANT>
|
|
67
|
-
""" # noqa: E501
|
|
68
|
-
|
|
69
|
-
STOP_WORDS = ["</function"]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def refine_prompt(prompt: str) -> str:
|
|
73
|
-
if sys.platform == "win32":
|
|
74
|
-
return prompt.replace("bash", "powershell")
|
|
75
|
-
return prompt
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# NOTE: we need to make sure these examples are always in-sync with the tool
|
|
79
|
-
# interface designed in openhands/agenthub/agent/function_calling.py
|
|
80
|
-
|
|
81
|
-
# Example snippets for each tool
|
|
82
|
-
TOOL_EXAMPLES = {
|
|
83
|
-
"execute_bash": {
|
|
84
|
-
"check_dir": """
|
|
85
|
-
ASSISTANT: Sure! Let me first check the current directory:
|
|
86
|
-
<function=execute_bash>
|
|
87
|
-
<parameter=command>
|
|
88
|
-
pwd && ls
|
|
89
|
-
</parameter>
|
|
90
|
-
</function>
|
|
91
|
-
|
|
92
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
93
|
-
/workspace
|
|
94
|
-
openhands@runtime:~/workspace$
|
|
95
|
-
""", # noqa: E501
|
|
96
|
-
"run_server": """
|
|
97
|
-
ASSISTANT:
|
|
98
|
-
Let me run the Python file for you:
|
|
99
|
-
<function=execute_bash>
|
|
100
|
-
<parameter=command>
|
|
101
|
-
python3 app.py > server.log 2>&1 &
|
|
102
|
-
</parameter>
|
|
103
|
-
</function>
|
|
104
|
-
|
|
105
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
106
|
-
[1] 121
|
|
107
|
-
[1]+ Exit 1 python3 app.py > server.log 2>&1
|
|
108
|
-
|
|
109
|
-
ASSISTANT:
|
|
110
|
-
Looks like the server was running with PID 121 then crashed. Let me check the server log:
|
|
111
|
-
<function=execute_bash>
|
|
112
|
-
<parameter=command>
|
|
113
|
-
cat server.log
|
|
114
|
-
</parameter>
|
|
115
|
-
</function>
|
|
116
|
-
|
|
117
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
118
|
-
Traceback (most recent call last):
|
|
119
|
-
File "/workspace/app.py", line 2, in <module>
|
|
120
|
-
from flask import Flask
|
|
121
|
-
ModuleNotFoundError: No module named 'flask'
|
|
122
|
-
|
|
123
|
-
ASSISTANT:
|
|
124
|
-
Looks like the server crashed because the `flask` module is not installed. Let me install the `flask` module for you:
|
|
125
|
-
<function=execute_bash>
|
|
126
|
-
<parameter=command>
|
|
127
|
-
pip3 install flask
|
|
128
|
-
</parameter>
|
|
129
|
-
</function>
|
|
130
|
-
|
|
131
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
132
|
-
Defaulting to user installation because normal site-packages is not writeable
|
|
133
|
-
Collecting flask
|
|
134
|
-
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
|
|
135
|
-
Collecting blinker>=1.6.2
|
|
136
|
-
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
|
|
137
|
-
Collecting Werkzeug>=3.0.0
|
|
138
|
-
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
|
|
139
|
-
Collecting click>=8.1.3
|
|
140
|
-
Using cached click-8.1.7-py3-none-any.whl (97 kB)
|
|
141
|
-
Collecting itsdangerous>=2.1.2
|
|
142
|
-
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
|
|
143
|
-
Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
|
|
144
|
-
Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
|
|
145
|
-
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
|
|
146
|
-
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
|
|
147
|
-
|
|
148
|
-
ASSISTANT:
|
|
149
|
-
The `flask` module is installed successfully. Let me run the Python file again:
|
|
150
|
-
<function=execute_bash>
|
|
151
|
-
<parameter=command>
|
|
152
|
-
python3 app.py > server.log 2>&1 &
|
|
153
|
-
</parameter>
|
|
154
|
-
</function>
|
|
155
|
-
|
|
156
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
157
|
-
[1] 124
|
|
158
|
-
|
|
159
|
-
ASSISTANT:
|
|
160
|
-
Let me check the server log again:
|
|
161
|
-
<function=execute_bash>
|
|
162
|
-
<parameter=command>
|
|
163
|
-
cat server.log
|
|
164
|
-
</parameter>
|
|
165
|
-
</function>
|
|
166
|
-
|
|
167
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
168
|
-
* Serving Flask app 'app'
|
|
169
|
-
* Debug mode: off
|
|
170
|
-
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
|
171
|
-
* Running on http://127.0.0.1:5000
|
|
172
|
-
Press CTRL+C to quit
|
|
173
|
-
|
|
174
|
-
ASSISTANT:
|
|
175
|
-
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
|
|
176
|
-
""", # noqa: E501
|
|
177
|
-
"kill_server": """
|
|
178
|
-
USER: Now kill the server, make it display the numbers in a table format.
|
|
179
|
-
|
|
180
|
-
ASSISTANT:
|
|
181
|
-
Sure! Let me stop the server first:
|
|
182
|
-
<function=execute_bash>
|
|
183
|
-
<parameter=command>
|
|
184
|
-
kill 124
|
|
185
|
-
</parameter>
|
|
186
|
-
</function>
|
|
187
|
-
|
|
188
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
189
|
-
[1]+ Terminated python3 app.py > server.log 2>&1
|
|
190
|
-
""", # noqa: E501
|
|
191
|
-
"run_server_again": """
|
|
192
|
-
ASSISTANT:
|
|
193
|
-
Running the updated file:
|
|
194
|
-
<function=execute_bash>
|
|
195
|
-
<parameter=command>
|
|
196
|
-
python3 app.py > server.log 2>&1 &
|
|
197
|
-
</parameter>
|
|
198
|
-
</function>
|
|
199
|
-
|
|
200
|
-
USER: EXECUTION RESULT of [execute_bash]:
|
|
201
|
-
[1] 126
|
|
202
|
-
|
|
203
|
-
ASSISTANT:
|
|
204
|
-
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000.
|
|
205
|
-
""", # noqa: E501
|
|
206
|
-
},
|
|
207
|
-
"str_replace_editor": {
|
|
208
|
-
"create_file": """
|
|
209
|
-
ASSISTANT:
|
|
210
|
-
There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
|
|
211
|
-
<function=str_replace_editor>
|
|
212
|
-
<parameter=command>create</parameter>
|
|
213
|
-
<parameter=path>/workspace/app.py</parameter>
|
|
214
|
-
<parameter=file_text>
|
|
215
|
-
from flask import Flask
|
|
216
|
-
app = Flask(__name__)
|
|
217
|
-
|
|
218
|
-
@app.route('/')
|
|
219
|
-
def index():
|
|
220
|
-
numbers = list(range(1, 11))
|
|
221
|
-
return str(numbers)
|
|
222
|
-
|
|
223
|
-
if __name__ == '__main__':
|
|
224
|
-
app.run(port=5000)
|
|
225
|
-
</parameter>
|
|
226
|
-
</function>
|
|
227
|
-
|
|
228
|
-
USER: EXECUTION RESULT of [str_replace_editor]:
|
|
229
|
-
File created successfully at: /workspace/app.py
|
|
230
|
-
""", # noqa: E501
|
|
231
|
-
"edit_file": """
|
|
232
|
-
ASSISTANT:
|
|
233
|
-
Now let me display the numbers in a table format:
|
|
234
|
-
<function=str_replace_editor>
|
|
235
|
-
<parameter=command>str_replace</parameter>
|
|
236
|
-
<parameter=path>/workspace/app.py</parameter>
|
|
237
|
-
<parameter=old_str>return str(numbers)</parameter>
|
|
238
|
-
<parameter=new_str>return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'</parameter>
|
|
239
|
-
</function>
|
|
240
|
-
|
|
241
|
-
USER: EXECUTION RESULT of [str_replace_editor]:
|
|
242
|
-
The file /workspace/app.py has been edited. Here's the result of running `cat -n` on a snippet of /workspace/app.py:
|
|
243
|
-
3
|
|
244
|
-
4 @app.route('/')
|
|
245
|
-
5 def index():
|
|
246
|
-
6 numbers = list(range(1, 11))
|
|
247
|
-
7 return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
|
248
|
-
8
|
|
249
|
-
9 if __name__ == '__main__':
|
|
250
|
-
10 app.run(port=5000)
|
|
251
|
-
Review the changes and make sure they are as expected. Edit the file again if necessary.
|
|
252
|
-
|
|
253
|
-
""", # noqa: E501
|
|
254
|
-
},
|
|
255
|
-
"browser": {
|
|
256
|
-
"view_page": """
|
|
257
|
-
ASSISTANT:
|
|
258
|
-
Let me check how the page looks in the browser:
|
|
259
|
-
<function=browser>
|
|
260
|
-
<parameter=code>
|
|
261
|
-
goto('http://127.0.0.1:5000')
|
|
262
|
-
noop(1000) # Wait for page to load
|
|
263
|
-
</parameter>
|
|
264
|
-
</function>
|
|
265
|
-
|
|
266
|
-
USER: EXECUTION RESULT of [browser]:
|
|
267
|
-
[Browser shows the numbers in a table format]
|
|
268
|
-
""" # noqa: E501
|
|
269
|
-
},
|
|
270
|
-
"edit_file": {
|
|
271
|
-
"create_file": """
|
|
272
|
-
ASSISTANT: There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
|
|
273
|
-
<function=edit_file>
|
|
274
|
-
<parameter=path>/workspace/app.py</parameter>
|
|
275
|
-
<parameter=start>1</parameter>
|
|
276
|
-
<parameter=end>-1</parameter>
|
|
277
|
-
<parameter=content>
|
|
278
|
-
from flask import Flask
|
|
279
|
-
app = Flask(__name__)
|
|
280
|
-
|
|
281
|
-
@app.route('/')
|
|
282
|
-
def index():
|
|
283
|
-
numbers = list(range(1, 11))
|
|
284
|
-
return str(numbers)
|
|
285
|
-
|
|
286
|
-
if __name__ == '__main__':
|
|
287
|
-
app.run(port=5000)
|
|
288
|
-
</parameter>
|
|
289
|
-
</function>
|
|
290
|
-
|
|
291
|
-
USER: EXECUTION RESULT of [edit_file]:
|
|
292
|
-
File created successfully at: /workspace/app.py
|
|
293
|
-
""", # noqa: E501
|
|
294
|
-
"edit_file": """
|
|
295
|
-
ASSISTANT:
|
|
296
|
-
Now let me display the numbers in a table format:
|
|
297
|
-
<function=edit_file>
|
|
298
|
-
<parameter=path>/workspace/app.py</parameter>
|
|
299
|
-
<parameter=start>6</parameter>
|
|
300
|
-
<parameter=end>9</parameter>
|
|
301
|
-
<parameter=content>
|
|
302
|
-
numbers = list(range(1, 11))
|
|
303
|
-
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
|
304
|
-
# ... existing code ...
|
|
305
|
-
if __name__ == '__main__':
|
|
306
|
-
</parameter>
|
|
307
|
-
</function>
|
|
308
|
-
|
|
309
|
-
USER: EXECUTION RESULT of [edit_file]:
|
|
310
|
-
The file /workspace/app.py has been edited. Here's the result of running `cat -n` on a snippet of /workspace/app.py:
|
|
311
|
-
3
|
|
312
|
-
4 @app.route('/')
|
|
313
|
-
5 def index():
|
|
314
|
-
6 numbers = list(range(1, 11))
|
|
315
|
-
7 return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
|
316
|
-
8
|
|
317
|
-
9 if __name__ == '__main__':
|
|
318
|
-
10 app.run(port=5000)
|
|
319
|
-
Review the changes and make sure they are as expected. Edit the file again if necessary.
|
|
320
|
-
""", # noqa: E501
|
|
321
|
-
},
|
|
322
|
-
"finish": {
|
|
323
|
-
"example": """
|
|
324
|
-
ASSISTANT:
|
|
325
|
-
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
|
|
326
|
-
<function=finish>
|
|
327
|
-
<parameter=message>The task has been completed. The web server is running and displaying numbers 1-10 in a table format at http://127.0.0.1:5000.</parameter>
|
|
328
|
-
</function>
|
|
329
|
-
""" # noqa: E501
|
|
330
|
-
},
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def get_example_for_tools(tools: list[ChatCompletionToolParam]) -> str:
|
|
335
|
-
"""Generate an in-context learning example based on available tools."""
|
|
336
|
-
available_tools = set()
|
|
337
|
-
for tool in tools:
|
|
338
|
-
if tool["type"] == "function":
|
|
339
|
-
name = tool["function"]["name"]
|
|
340
|
-
if name == EXECUTE_BASH_TOOL_NAME:
|
|
341
|
-
available_tools.add("execute_bash")
|
|
342
|
-
elif name == STR_REPLACE_EDITOR_TOOL_NAME:
|
|
343
|
-
available_tools.add("str_replace_editor")
|
|
344
|
-
elif name == BROWSER_TOOL_NAME:
|
|
345
|
-
available_tools.add("browser")
|
|
346
|
-
elif name == FINISH_TOOL_NAME:
|
|
347
|
-
available_tools.add("finish")
|
|
348
|
-
elif name == LLM_BASED_EDIT_TOOL_NAME:
|
|
349
|
-
available_tools.add("edit_file")
|
|
350
|
-
|
|
351
|
-
if not available_tools:
|
|
352
|
-
return ""
|
|
353
|
-
|
|
354
|
-
example = """Here's a running example of how to perform a task with the provided tools.
|
|
355
|
-
|
|
356
|
-
--------------------- START OF EXAMPLE ---------------------
|
|
357
|
-
|
|
358
|
-
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
|
|
359
|
-
|
|
360
|
-
""" # noqa: E501
|
|
361
|
-
|
|
362
|
-
# Build example based on available tools
|
|
363
|
-
if "execute_bash" in available_tools:
|
|
364
|
-
example += TOOL_EXAMPLES["execute_bash"]["check_dir"]
|
|
365
|
-
|
|
366
|
-
if "str_replace_editor" in available_tools:
|
|
367
|
-
example += TOOL_EXAMPLES["str_replace_editor"]["create_file"]
|
|
368
|
-
elif "edit_file" in available_tools:
|
|
369
|
-
example += TOOL_EXAMPLES["edit_file"]["create_file"]
|
|
370
|
-
|
|
371
|
-
if "execute_bash" in available_tools:
|
|
372
|
-
example += TOOL_EXAMPLES["execute_bash"]["run_server"]
|
|
373
|
-
|
|
374
|
-
if "browser" in available_tools:
|
|
375
|
-
example += TOOL_EXAMPLES["browser"]["view_page"]
|
|
376
|
-
|
|
377
|
-
if "execute_bash" in available_tools:
|
|
378
|
-
example += TOOL_EXAMPLES["execute_bash"]["kill_server"]
|
|
379
|
-
|
|
380
|
-
if "str_replace_editor" in available_tools:
|
|
381
|
-
example += TOOL_EXAMPLES["str_replace_editor"]["edit_file"]
|
|
382
|
-
elif "edit_file" in available_tools:
|
|
383
|
-
example += TOOL_EXAMPLES["edit_file"]["edit_file"]
|
|
384
|
-
|
|
385
|
-
if "execute_bash" in available_tools:
|
|
386
|
-
example += TOOL_EXAMPLES["execute_bash"]["run_server_again"]
|
|
387
|
-
|
|
388
|
-
if "finish" in available_tools:
|
|
389
|
-
example += TOOL_EXAMPLES["finish"]["example"]
|
|
390
|
-
|
|
391
|
-
example += """
|
|
392
|
-
--------------------- END OF EXAMPLE ---------------------
|
|
393
|
-
|
|
394
|
-
Do NOT assume the environment is the same as in the example above.
|
|
395
|
-
|
|
396
|
-
--------------------- NEW TASK DESCRIPTION ---------------------
|
|
397
|
-
""" # noqa: E501
|
|
398
|
-
example = example.lstrip()
|
|
399
|
-
|
|
400
|
-
return refine_prompt(example)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
IN_CONTEXT_LEARNING_EXAMPLE_PREFIX = get_example_for_tools
|
|
404
|
-
|
|
405
|
-
IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX = """
|
|
406
|
-
--------------------- END OF NEW TASK DESCRIPTION ---------------------
|
|
407
|
-
|
|
408
|
-
PLEASE follow the format strictly! PLEASE EMIT ONE AND ONLY ONE FUNCTION CALL PER MESSAGE.
|
|
409
|
-
""" # noqa: E501
|
|
410
|
-
|
|
411
|
-
# Regex patterns for function call parsing
|
|
412
|
-
FN_REGEX_PATTERN = r"<function=([^>]+)>\n(.*?)</function>"
|
|
413
|
-
FN_PARAM_REGEX_PATTERN = r"<parameter=([^>]+)>(.*?)</parameter>"
|
|
414
|
-
|
|
415
|
-
# Add new regex pattern for tool execution results
|
|
416
|
-
TOOL_RESULT_REGEX_PATTERN = r"EXECUTION RESULT of \[(.*?)\]:\n(.*)"
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def convert_tool_call_to_string(tool_call: dict) -> str:
|
|
420
|
-
"""Convert tool call to content in string format."""
|
|
421
|
-
if "function" not in tool_call:
|
|
422
|
-
raise FunctionCallConversionError("Tool call must contain 'function' key.")
|
|
423
|
-
if "id" not in tool_call:
|
|
424
|
-
raise FunctionCallConversionError("Tool call must contain 'id' key.")
|
|
425
|
-
if "type" not in tool_call:
|
|
426
|
-
raise FunctionCallConversionError("Tool call must contain 'type' key.")
|
|
427
|
-
if tool_call["type"] != "function":
|
|
428
|
-
raise FunctionCallConversionError("Tool call type must be 'function'.")
|
|
429
|
-
|
|
430
|
-
ret = f"<function={tool_call['function']['name']}>\n"
|
|
431
|
-
try:
|
|
432
|
-
args = json.loads(tool_call["function"]["arguments"])
|
|
433
|
-
except json.JSONDecodeError as e:
|
|
434
|
-
raise FunctionCallConversionError(
|
|
435
|
-
f"Failed to parse arguments as JSON. "
|
|
436
|
-
f"Arguments: {tool_call['function']['arguments']}"
|
|
437
|
-
) from e
|
|
438
|
-
for param_name, param_value in args.items():
|
|
439
|
-
is_multiline = isinstance(param_value, str) and "\n" in param_value
|
|
440
|
-
ret += f"<parameter={param_name}>"
|
|
441
|
-
if is_multiline:
|
|
442
|
-
ret += "\n"
|
|
443
|
-
if isinstance(param_value, list) or isinstance(param_value, dict):
|
|
444
|
-
ret += json.dumps(param_value)
|
|
445
|
-
else:
|
|
446
|
-
ret += f"{param_value}"
|
|
447
|
-
if is_multiline:
|
|
448
|
-
ret += "\n"
|
|
449
|
-
ret += "</parameter>\n"
|
|
450
|
-
ret += "</function>"
|
|
451
|
-
return ret
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def convert_tools_to_description(tools: list[ChatCompletionToolParam]) -> str:
|
|
455
|
-
ret = ""
|
|
456
|
-
for i, tool in enumerate(tools):
|
|
457
|
-
assert tool["type"] == "function"
|
|
458
|
-
fn = tool["function"]
|
|
459
|
-
if i > 0:
|
|
460
|
-
ret += "\n"
|
|
461
|
-
ret += f"---- BEGIN FUNCTION #{i + 1}: {fn['name']} ----\n"
|
|
462
|
-
if "description" in fn:
|
|
463
|
-
ret += f"Description: {fn['description']}\n"
|
|
464
|
-
|
|
465
|
-
if "parameters" in fn:
|
|
466
|
-
ret += "Parameters:\n"
|
|
467
|
-
properties = fn["parameters"].get("properties", {})
|
|
468
|
-
required_params = set(fn["parameters"].get("required", []))
|
|
469
|
-
|
|
470
|
-
for j, (param_name, param_info) in enumerate(properties.items()):
|
|
471
|
-
# Indicate required/optional in parentheses with type
|
|
472
|
-
is_required = param_name in required_params
|
|
473
|
-
param_status = "required" if is_required else "optional"
|
|
474
|
-
param_type = param_info.get("type", "string")
|
|
475
|
-
|
|
476
|
-
# Get parameter description
|
|
477
|
-
desc = param_info.get("description", "No description provided")
|
|
478
|
-
|
|
479
|
-
# Handle enum values if present
|
|
480
|
-
if "enum" in param_info:
|
|
481
|
-
enum_values = ", ".join(f"`{v}`" for v in param_info["enum"])
|
|
482
|
-
desc += f"\nAllowed values: [{enum_values}]"
|
|
483
|
-
|
|
484
|
-
ret += (
|
|
485
|
-
f" ({j + 1}) {param_name} ({param_type}, {param_status}): {desc}\n"
|
|
486
|
-
)
|
|
487
|
-
else:
|
|
488
|
-
ret += "No parameters are required for this function.\n"
|
|
489
|
-
|
|
490
|
-
ret += f"---- END FUNCTION #{i + 1} ----\n"
|
|
491
|
-
return ret
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def convert_fncall_messages_to_non_fncall_messages(
|
|
495
|
-
messages: list[dict],
|
|
496
|
-
tools: list[ChatCompletionToolParam],
|
|
497
|
-
add_in_context_learning_example: bool = True,
|
|
498
|
-
) -> list[dict]:
|
|
499
|
-
"""Convert function calling messages to non-function calling messages."""
|
|
500
|
-
messages = copy.deepcopy(messages)
|
|
501
|
-
|
|
502
|
-
formatted_tools = convert_tools_to_description(tools)
|
|
503
|
-
system_message_suffix = system_message_suffix_TEMPLATE.format(
|
|
504
|
-
description=formatted_tools
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
converted_messages = []
|
|
508
|
-
first_user_message_encountered = False
|
|
509
|
-
for message in messages:
|
|
510
|
-
role = message["role"]
|
|
511
|
-
content: Content = message["content"]
|
|
512
|
-
|
|
513
|
-
# 1. SYSTEM MESSAGES
|
|
514
|
-
# append system prompt suffix to content
|
|
515
|
-
if role == "system":
|
|
516
|
-
if isinstance(content, str):
|
|
517
|
-
content += system_message_suffix
|
|
518
|
-
elif isinstance(content, list):
|
|
519
|
-
if content and content[-1]["type"] == "text":
|
|
520
|
-
content[-1]["text"] += system_message_suffix
|
|
521
|
-
else:
|
|
522
|
-
content.append({"type": "text", "text": system_message_suffix})
|
|
523
|
-
else:
|
|
524
|
-
raise FunctionCallConversionError(
|
|
525
|
-
f"Unexpected content type {type(content)}. "
|
|
526
|
-
f"Expected str or list. "
|
|
527
|
-
f"Content: {content}"
|
|
528
|
-
)
|
|
529
|
-
converted_messages.append({"role": "system", "content": content})
|
|
530
|
-
|
|
531
|
-
# 2. USER MESSAGES (no change)
|
|
532
|
-
elif role == "user":
|
|
533
|
-
# Add in-context learning example for the first user message
|
|
534
|
-
if not first_user_message_encountered and add_in_context_learning_example:
|
|
535
|
-
first_user_message_encountered = True
|
|
536
|
-
|
|
537
|
-
# Generate example based on available tools
|
|
538
|
-
example = IN_CONTEXT_LEARNING_EXAMPLE_PREFIX(tools)
|
|
539
|
-
|
|
540
|
-
# Add example if we have any tools
|
|
541
|
-
if example:
|
|
542
|
-
# add in-context learning example
|
|
543
|
-
if isinstance(content, str):
|
|
544
|
-
content = example + content + IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX
|
|
545
|
-
elif isinstance(content, list):
|
|
546
|
-
if content and content[0]["type"] == "text":
|
|
547
|
-
content[0]["text"] = (
|
|
548
|
-
example
|
|
549
|
-
+ content[0]["text"]
|
|
550
|
-
+ IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX
|
|
551
|
-
)
|
|
552
|
-
else:
|
|
553
|
-
content = (
|
|
554
|
-
[
|
|
555
|
-
cast(
|
|
556
|
-
TextPart,
|
|
557
|
-
{
|
|
558
|
-
"type": "text",
|
|
559
|
-
"text": example,
|
|
560
|
-
},
|
|
561
|
-
)
|
|
562
|
-
]
|
|
563
|
-
+ content
|
|
564
|
-
+ [
|
|
565
|
-
cast(
|
|
566
|
-
TextPart,
|
|
567
|
-
{
|
|
568
|
-
"type": "text",
|
|
569
|
-
"text": IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX,
|
|
570
|
-
},
|
|
571
|
-
)
|
|
572
|
-
]
|
|
573
|
-
)
|
|
574
|
-
else:
|
|
575
|
-
raise FunctionCallConversionError(
|
|
576
|
-
f"Unexpected content type {type(content)}. "
|
|
577
|
-
f"Expected str or list. "
|
|
578
|
-
f"Content: {content}"
|
|
579
|
-
)
|
|
580
|
-
converted_messages.append(
|
|
581
|
-
{
|
|
582
|
-
"role": "user",
|
|
583
|
-
"content": content,
|
|
584
|
-
}
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
# 3. ASSISTANT MESSAGES
|
|
588
|
-
# - 3.1 no change if no function call
|
|
589
|
-
# - 3.2 change if function call
|
|
590
|
-
elif role == "assistant":
|
|
591
|
-
if "tool_calls" in message and message["tool_calls"] is not None:
|
|
592
|
-
if len(message["tool_calls"]) != 1:
|
|
593
|
-
raise FunctionCallConversionError(
|
|
594
|
-
f"Expected exactly one tool call in the message. "
|
|
595
|
-
f"More than one tool call is not supported. "
|
|
596
|
-
f"But got {len(message['tool_calls'])} tool calls. "
|
|
597
|
-
f"Content: {content}"
|
|
598
|
-
)
|
|
599
|
-
try:
|
|
600
|
-
tool_content = convert_tool_call_to_string(message["tool_calls"][0])
|
|
601
|
-
except FunctionCallConversionError as e:
|
|
602
|
-
raise FunctionCallConversionError(
|
|
603
|
-
f"Failed to convert tool call to string.\n"
|
|
604
|
-
f"Current tool call: {message['tool_calls'][0]}.\n"
|
|
605
|
-
f"Raw messages: {json.dumps(messages, indent=2)}"
|
|
606
|
-
) from e
|
|
607
|
-
if isinstance(content, str):
|
|
608
|
-
content += "\n\n" + tool_content
|
|
609
|
-
content = content.lstrip()
|
|
610
|
-
elif isinstance(content, list):
|
|
611
|
-
if content and content[-1]["type"] == "text":
|
|
612
|
-
content[-1]["text"] += "\n\n" + tool_content
|
|
613
|
-
content[-1]["text"] = content[-1]["text"].lstrip()
|
|
614
|
-
else:
|
|
615
|
-
content.append({"type": "text", "text": tool_content})
|
|
616
|
-
else:
|
|
617
|
-
raise FunctionCallConversionError(
|
|
618
|
-
f"Unexpected content type {type(content)}. "
|
|
619
|
-
f"Expected str or list. Content: {content}"
|
|
620
|
-
)
|
|
621
|
-
converted_messages.append({"role": "assistant", "content": content})
|
|
622
|
-
|
|
623
|
-
# 4. TOOL MESSAGES (tool outputs)
|
|
624
|
-
elif role == "tool":
|
|
625
|
-
# Convert tool result as user message
|
|
626
|
-
tool_name = message.get("name", "function")
|
|
627
|
-
prefix = f"EXECUTION RESULT of [{tool_name}]:\n"
|
|
628
|
-
# and omit "tool_call_id" AND "name"
|
|
629
|
-
if isinstance(content, str):
|
|
630
|
-
content = prefix + content
|
|
631
|
-
elif isinstance(content, list):
|
|
632
|
-
if content and (
|
|
633
|
-
first_text_content := next(
|
|
634
|
-
(c for c in content if c["type"] == "text"), None
|
|
635
|
-
)
|
|
636
|
-
):
|
|
637
|
-
first_text_content["text"] = prefix + first_text_content["text"]
|
|
638
|
-
else:
|
|
639
|
-
content = [
|
|
640
|
-
cast(TextPart, {"type": "text", "text": prefix})
|
|
641
|
-
] + content
|
|
642
|
-
|
|
643
|
-
if "cache_control" in message:
|
|
644
|
-
content[-1]["cache_control"] = cast(
|
|
645
|
-
CacheControl, {"type": "ephemeral"}
|
|
646
|
-
)
|
|
647
|
-
else:
|
|
648
|
-
raise FunctionCallConversionError(
|
|
649
|
-
f"Unexpected content type {type(content)}. "
|
|
650
|
-
f"Expected str or list. "
|
|
651
|
-
f"Content: {content}"
|
|
652
|
-
)
|
|
653
|
-
|
|
654
|
-
converted_messages.append({"role": "user", "content": content})
|
|
655
|
-
else:
|
|
656
|
-
raise FunctionCallConversionError(
|
|
657
|
-
f"Unexpected role {role}. Expected system, user, assistant or tool."
|
|
658
|
-
)
|
|
659
|
-
return converted_messages
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def _extract_and_validate_params(
|
|
663
|
-
matching_tool: ChatCompletionToolParamFunctionChunk,
|
|
664
|
-
param_matches: Iterable[re.Match],
|
|
665
|
-
fn_name: str,
|
|
666
|
-
) -> dict:
|
|
667
|
-
params = {}
|
|
668
|
-
# Parse and validate parameters
|
|
669
|
-
required_params = set()
|
|
670
|
-
if "parameters" in matching_tool and "required" in matching_tool["parameters"]:
|
|
671
|
-
required_params = set(matching_tool["parameters"].get("required", []))
|
|
672
|
-
|
|
673
|
-
allowed_params = set()
|
|
674
|
-
if "parameters" in matching_tool and "properties" in matching_tool["parameters"]:
|
|
675
|
-
allowed_params = set(matching_tool["parameters"]["properties"].keys())
|
|
676
|
-
|
|
677
|
-
param_name_to_type = {}
|
|
678
|
-
if "parameters" in matching_tool and "properties" in matching_tool["parameters"]:
|
|
679
|
-
param_name_to_type = {
|
|
680
|
-
name: val.get("type", "string")
|
|
681
|
-
for name, val in matching_tool["parameters"]["properties"].items()
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
# Collect parameters
|
|
685
|
-
found_params = set()
|
|
686
|
-
for param_match in param_matches:
|
|
687
|
-
param_name = param_match.group(1)
|
|
688
|
-
param_value = param_match.group(2)
|
|
689
|
-
|
|
690
|
-
# Validate parameter is allowed
|
|
691
|
-
if allowed_params and param_name not in allowed_params:
|
|
692
|
-
raise FunctionCallValidationError(
|
|
693
|
-
f"Parameter '{param_name}' is not allowed for function '{fn_name}'. "
|
|
694
|
-
f"Allowed parameters: {allowed_params}"
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
# Validate and convert parameter type
|
|
698
|
-
# supported: string, integer, array
|
|
699
|
-
if param_name in param_name_to_type:
|
|
700
|
-
if param_name_to_type[param_name] == "integer":
|
|
701
|
-
try:
|
|
702
|
-
param_value = int(param_value)
|
|
703
|
-
except ValueError:
|
|
704
|
-
raise FunctionCallValidationError(
|
|
705
|
-
f"Parameter '{param_name}' is expected to be an integer."
|
|
706
|
-
)
|
|
707
|
-
elif param_name_to_type[param_name] == "array":
|
|
708
|
-
try:
|
|
709
|
-
param_value = json.loads(param_value)
|
|
710
|
-
except json.JSONDecodeError:
|
|
711
|
-
raise FunctionCallValidationError(
|
|
712
|
-
f"Parameter '{param_name}' is expected to be an array."
|
|
713
|
-
)
|
|
714
|
-
else:
|
|
715
|
-
# string
|
|
716
|
-
pass
|
|
717
|
-
|
|
718
|
-
# Enum check
|
|
719
|
-
if (
|
|
720
|
-
"parameters" in matching_tool
|
|
721
|
-
and "enum" in matching_tool["parameters"]["properties"][param_name]
|
|
722
|
-
):
|
|
723
|
-
if (
|
|
724
|
-
param_value
|
|
725
|
-
not in matching_tool["parameters"]["properties"][param_name]["enum"]
|
|
726
|
-
):
|
|
727
|
-
raise FunctionCallValidationError(
|
|
728
|
-
f"Parameter '{param_name}' is expected to be one of "
|
|
729
|
-
f"{matching_tool['parameters']['properties'][param_name]['enum']}."
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
params[param_name] = param_value
|
|
733
|
-
found_params.add(param_name)
|
|
734
|
-
|
|
735
|
-
# Check all required parameters are present
|
|
736
|
-
missing_params = required_params - found_params
|
|
737
|
-
if missing_params:
|
|
738
|
-
raise FunctionCallValidationError(
|
|
739
|
-
f"Missing required parameters for function '{fn_name}': {missing_params}"
|
|
740
|
-
)
|
|
741
|
-
return params
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
def _fix_stopword(content: str) -> str:
|
|
745
|
-
"""Fix the issue when some LLM would NOT return the stopword."""
|
|
746
|
-
if "<function=" in content and content.count("<function=") == 1:
|
|
747
|
-
if content.endswith("</"):
|
|
748
|
-
content = content.rstrip() + "function>"
|
|
749
|
-
else:
|
|
750
|
-
content = content + "\n</function>"
|
|
751
|
-
return content
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
def _normalize_parameter_tags(fn_body: str) -> str:
|
|
755
|
-
"""Normalize malformed parameter tags to the canonical format.
|
|
756
|
-
|
|
757
|
-
Some models occasionally emit malformed parameter tags like:
|
|
758
|
-
<parameter=command=str_replace</parameter>
|
|
759
|
-
instead of the correct:
|
|
760
|
-
<parameter=command>str_replace</parameter>
|
|
761
|
-
|
|
762
|
-
This function rewrites the malformed form into the correct one to allow
|
|
763
|
-
downstream parsing to succeed.
|
|
764
|
-
"""
|
|
765
|
-
# Replace '<parameter=name=value</parameter>'
|
|
766
|
-
# with '<parameter=name>value</parameter>'
|
|
767
|
-
return re.sub(
|
|
768
|
-
r"<parameter=([a-zA-Z0-9_]+)=([^<]*)</parameter>",
|
|
769
|
-
r"<parameter=\1>\2</parameter>",
|
|
770
|
-
fn_body,
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
def convert_non_fncall_messages_to_fncall_messages(
|
|
775
|
-
messages: list[dict],
|
|
776
|
-
tools: list[ChatCompletionToolParam],
|
|
777
|
-
) -> list[dict]:
|
|
778
|
-
"""Convert non-function calling messages back to function calling messages."""
|
|
779
|
-
messages = copy.deepcopy(messages)
|
|
780
|
-
formatted_tools = convert_tools_to_description(tools)
|
|
781
|
-
system_message_suffix = system_message_suffix_TEMPLATE.format(
|
|
782
|
-
description=formatted_tools
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
converted_messages = []
|
|
786
|
-
tool_call_counter = 1 # Counter for tool calls
|
|
787
|
-
|
|
788
|
-
first_user_message_encountered = False
|
|
789
|
-
for message in messages:
|
|
790
|
-
role, content = message["role"], message["content"]
|
|
791
|
-
content = content or "" # handle cases where content is None
|
|
792
|
-
# For system messages, remove the added suffix
|
|
793
|
-
if role == "system":
|
|
794
|
-
if isinstance(content, str):
|
|
795
|
-
# Remove the suffix if present
|
|
796
|
-
content = content.split(system_message_suffix)[0]
|
|
797
|
-
elif isinstance(content, list):
|
|
798
|
-
if content and content[-1]["type"] == "text":
|
|
799
|
-
# Remove the suffix from the last text item
|
|
800
|
-
content[-1]["text"] = content[-1]["text"].split(
|
|
801
|
-
system_message_suffix
|
|
802
|
-
)[0]
|
|
803
|
-
converted_messages.append({"role": "system", "content": content})
|
|
804
|
-
# Skip user messages (no conversion needed)
|
|
805
|
-
elif role == "user":
|
|
806
|
-
# Check & replace in-context learning example
|
|
807
|
-
if not first_user_message_encountered:
|
|
808
|
-
first_user_message_encountered = True
|
|
809
|
-
if isinstance(content, str):
|
|
810
|
-
# Remove any existing example
|
|
811
|
-
if content.startswith(IN_CONTEXT_LEARNING_EXAMPLE_PREFIX(tools)):
|
|
812
|
-
content = content.replace(
|
|
813
|
-
IN_CONTEXT_LEARNING_EXAMPLE_PREFIX(tools), "", 1
|
|
814
|
-
)
|
|
815
|
-
if content.endswith(IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX):
|
|
816
|
-
content = content.replace(
|
|
817
|
-
IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX, "", 1
|
|
818
|
-
)
|
|
819
|
-
elif isinstance(content, list):
|
|
820
|
-
for item in content:
|
|
821
|
-
if item["type"] == "text":
|
|
822
|
-
# Remove any existing example
|
|
823
|
-
example = IN_CONTEXT_LEARNING_EXAMPLE_PREFIX(tools)
|
|
824
|
-
if item["text"].startswith(example):
|
|
825
|
-
item["text"] = item["text"].replace(example, "", 1)
|
|
826
|
-
if item["text"].endswith(
|
|
827
|
-
IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX
|
|
828
|
-
):
|
|
829
|
-
item["text"] = item["text"].replace(
|
|
830
|
-
IN_CONTEXT_LEARNING_EXAMPLE_SUFFIX, "", 1
|
|
831
|
-
)
|
|
832
|
-
else:
|
|
833
|
-
raise FunctionCallConversionError(
|
|
834
|
-
f"Unexpected content type {type(content)}. "
|
|
835
|
-
f"Expected str or list. "
|
|
836
|
-
f"Content: {content}"
|
|
837
|
-
)
|
|
838
|
-
|
|
839
|
-
# Check for tool execution result pattern
|
|
840
|
-
if isinstance(content, str):
|
|
841
|
-
tool_result_match = re.search(
|
|
842
|
-
TOOL_RESULT_REGEX_PATTERN, content, re.DOTALL
|
|
843
|
-
)
|
|
844
|
-
elif isinstance(content, list):
|
|
845
|
-
tool_result_match = next(
|
|
846
|
-
(
|
|
847
|
-
_match
|
|
848
|
-
for item in content
|
|
849
|
-
if item.get("type") == "text"
|
|
850
|
-
and (
|
|
851
|
-
_match := re.search(
|
|
852
|
-
TOOL_RESULT_REGEX_PATTERN, item["text"], re.DOTALL
|
|
853
|
-
)
|
|
854
|
-
)
|
|
855
|
-
),
|
|
856
|
-
None,
|
|
857
|
-
)
|
|
858
|
-
else:
|
|
859
|
-
raise FunctionCallConversionError(
|
|
860
|
-
f"Unexpected content type {type(content)}. "
|
|
861
|
-
f"Expected str or list. "
|
|
862
|
-
f"Content: {content}"
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
if tool_result_match:
|
|
866
|
-
if isinstance(content, list):
|
|
867
|
-
text_content_items = [
|
|
868
|
-
item for item in content if item.get("type") == "text"
|
|
869
|
-
]
|
|
870
|
-
if not text_content_items:
|
|
871
|
-
raise FunctionCallConversionError(
|
|
872
|
-
f"Could not find text content in message with tool result. "
|
|
873
|
-
f"Content: {content}"
|
|
874
|
-
)
|
|
875
|
-
elif not isinstance(content, str):
|
|
876
|
-
raise FunctionCallConversionError(
|
|
877
|
-
f"Unexpected content type {type(content)}. "
|
|
878
|
-
f"Expected str or list. "
|
|
879
|
-
f"Content: {content}"
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
tool_name = tool_result_match.group(1)
|
|
883
|
-
tool_result = tool_result_match.group(2).strip()
|
|
884
|
-
|
|
885
|
-
# Convert to tool message format
|
|
886
|
-
converted_messages.append(
|
|
887
|
-
{
|
|
888
|
-
"role": "tool",
|
|
889
|
-
"name": tool_name,
|
|
890
|
-
"content": [{"type": "text", "text": tool_result}]
|
|
891
|
-
if isinstance(content, list)
|
|
892
|
-
else tool_result,
|
|
893
|
-
"tool_call_id": f"toolu_{tool_call_counter - 1:02d}",
|
|
894
|
-
# Use last generated ID
|
|
895
|
-
}
|
|
896
|
-
)
|
|
897
|
-
else:
|
|
898
|
-
converted_messages.append({"role": "user", "content": content})
|
|
899
|
-
|
|
900
|
-
# Handle assistant messages
|
|
901
|
-
elif role == "assistant":
|
|
902
|
-
if isinstance(content, str):
|
|
903
|
-
content = _fix_stopword(content)
|
|
904
|
-
fn_match = re.search(FN_REGEX_PATTERN, content, re.DOTALL)
|
|
905
|
-
elif isinstance(content, list):
|
|
906
|
-
if content and content[-1]["type"] == "text":
|
|
907
|
-
content[-1]["text"] = _fix_stopword(content[-1]["text"])
|
|
908
|
-
fn_match = re.search(
|
|
909
|
-
FN_REGEX_PATTERN, content[-1]["text"], re.DOTALL
|
|
910
|
-
)
|
|
911
|
-
else:
|
|
912
|
-
fn_match = None
|
|
913
|
-
fn_match_exists = any(
|
|
914
|
-
item.get("type") == "text"
|
|
915
|
-
and re.search(FN_REGEX_PATTERN, item["text"], re.DOTALL)
|
|
916
|
-
for item in content
|
|
917
|
-
)
|
|
918
|
-
if fn_match_exists and not fn_match:
|
|
919
|
-
raise FunctionCallConversionError(
|
|
920
|
-
f"Expecting function call in the LAST index of content list. "
|
|
921
|
-
f"But got content={content}"
|
|
922
|
-
)
|
|
923
|
-
else:
|
|
924
|
-
raise FunctionCallConversionError(
|
|
925
|
-
f"Unexpected content type {type(content)}. "
|
|
926
|
-
f"Expected str or list. "
|
|
927
|
-
f"Content: {content}"
|
|
928
|
-
)
|
|
929
|
-
|
|
930
|
-
if fn_match:
|
|
931
|
-
fn_name = fn_match.group(1)
|
|
932
|
-
fn_body = _normalize_parameter_tags(fn_match.group(2))
|
|
933
|
-
matching_tool: ChatCompletionToolParamFunctionChunk | None = next(
|
|
934
|
-
(
|
|
935
|
-
tool["function"]
|
|
936
|
-
for tool in tools
|
|
937
|
-
if tool["type"] == "function"
|
|
938
|
-
and tool["function"]["name"] == fn_name
|
|
939
|
-
),
|
|
940
|
-
None,
|
|
941
|
-
)
|
|
942
|
-
# Validate function exists in tools
|
|
943
|
-
if not matching_tool:
|
|
944
|
-
available_tools = [
|
|
945
|
-
tool["function"]["name"]
|
|
946
|
-
for tool in tools
|
|
947
|
-
if tool["type"] == "function"
|
|
948
|
-
]
|
|
949
|
-
raise FunctionCallValidationError(
|
|
950
|
-
f"Function '{fn_name}' not found in available tools: "
|
|
951
|
-
f"{available_tools}"
|
|
952
|
-
)
|
|
953
|
-
|
|
954
|
-
# Parse parameters
|
|
955
|
-
param_matches = re.finditer(FN_PARAM_REGEX_PATTERN, fn_body, re.DOTALL)
|
|
956
|
-
params = _extract_and_validate_params(
|
|
957
|
-
matching_tool, param_matches, fn_name
|
|
958
|
-
)
|
|
959
|
-
|
|
960
|
-
# Create tool call with unique ID
|
|
961
|
-
tool_call_id = f"toolu_{tool_call_counter:02d}"
|
|
962
|
-
tool_call = {
|
|
963
|
-
"index": 1, # always 1 because we only support
|
|
964
|
-
# **one tool call per message**
|
|
965
|
-
"id": tool_call_id,
|
|
966
|
-
"type": "function",
|
|
967
|
-
"function": {"name": fn_name, "arguments": json.dumps(params)},
|
|
968
|
-
}
|
|
969
|
-
tool_call_counter += 1 # Increment counter
|
|
970
|
-
|
|
971
|
-
# Remove the function call part from content
|
|
972
|
-
if isinstance(content, list):
|
|
973
|
-
assert content and content[-1]["type"] == "text"
|
|
974
|
-
content[-1]["text"] = (
|
|
975
|
-
content[-1]["text"].split("<function=")[0].strip()
|
|
976
|
-
)
|
|
977
|
-
elif isinstance(content, str):
|
|
978
|
-
content = content.split("<function=")[0].strip()
|
|
979
|
-
else:
|
|
980
|
-
raise FunctionCallConversionError(
|
|
981
|
-
f"Unexpected content type {type(content)}. "
|
|
982
|
-
f"Expected str or list. "
|
|
983
|
-
f"Content: {content}"
|
|
984
|
-
)
|
|
985
|
-
|
|
986
|
-
converted_messages.append(
|
|
987
|
-
{"role": "assistant", "content": content, "tool_calls": [tool_call]}
|
|
988
|
-
)
|
|
989
|
-
else:
|
|
990
|
-
# No function call, keep message as is
|
|
991
|
-
converted_messages.append(message)
|
|
992
|
-
|
|
993
|
-
else:
|
|
994
|
-
raise FunctionCallConversionError(
|
|
995
|
-
f"Unexpected role {role}. Expected system, user, or assistant "
|
|
996
|
-
f"in non-function calling messages."
|
|
997
|
-
)
|
|
998
|
-
return converted_messages
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
def convert_from_multiple_tool_calls_to_single_tool_call_messages(
|
|
1002
|
-
messages: list[dict],
|
|
1003
|
-
ignore_final_tool_result: bool = False,
|
|
1004
|
-
) -> list[dict]:
|
|
1005
|
-
"""Break one message with multiple tool calls into multiple messages."""
|
|
1006
|
-
converted_messages = []
|
|
1007
|
-
|
|
1008
|
-
pending_tool_calls: dict[str, dict] = {}
|
|
1009
|
-
for message in messages:
|
|
1010
|
-
role: str
|
|
1011
|
-
content: Content
|
|
1012
|
-
role, content = message["role"], message["content"]
|
|
1013
|
-
if role == "assistant":
|
|
1014
|
-
if message.get("tool_calls") and len(message["tool_calls"]) > 1:
|
|
1015
|
-
# handle multiple tool calls by breaking them into multiple messages
|
|
1016
|
-
for i, tool_call in enumerate(message["tool_calls"]):
|
|
1017
|
-
pending_tool_calls[tool_call["id"]] = {
|
|
1018
|
-
"role": "assistant",
|
|
1019
|
-
"content": content if i == 0 else "",
|
|
1020
|
-
"tool_calls": [tool_call],
|
|
1021
|
-
}
|
|
1022
|
-
else:
|
|
1023
|
-
converted_messages.append(message)
|
|
1024
|
-
elif role == "tool":
|
|
1025
|
-
if message["tool_call_id"] in pending_tool_calls:
|
|
1026
|
-
# remove the tool call from the pending list
|
|
1027
|
-
_tool_call_message = pending_tool_calls.pop(message["tool_call_id"])
|
|
1028
|
-
converted_messages.append(_tool_call_message)
|
|
1029
|
-
# add the tool result
|
|
1030
|
-
converted_messages.append(message)
|
|
1031
|
-
else:
|
|
1032
|
-
assert len(pending_tool_calls) == 0, (
|
|
1033
|
-
f"Found pending tool calls but not found in pending list: "
|
|
1034
|
-
f"{pending_tool_calls=}"
|
|
1035
|
-
)
|
|
1036
|
-
converted_messages.append(message)
|
|
1037
|
-
else:
|
|
1038
|
-
assert len(pending_tool_calls) == 0, (
|
|
1039
|
-
f"Found pending tool calls but not expect to handle it "
|
|
1040
|
-
f"with role {role}: "
|
|
1041
|
-
f"{pending_tool_calls=}, {message=}"
|
|
1042
|
-
)
|
|
1043
|
-
converted_messages.append(message)
|
|
1044
|
-
|
|
1045
|
-
if not ignore_final_tool_result and len(pending_tool_calls) > 0:
|
|
1046
|
-
raise FunctionCallConversionError(
|
|
1047
|
-
f"Found pending tool calls but no tool result: {pending_tool_calls=}"
|
|
1048
|
-
)
|
|
1049
|
-
return converted_messages
|