-
Notifications
You must be signed in to change notification settings - Fork 597
Expand file tree
/
Copy pathtool_manager.py
More file actions
334 lines (271 loc) · 10.9 KB
/
Copy pathtool_manager.py
File metadata and controls
334 lines (271 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
"""Tool registry, routing, and execution helpers for the TIR example."""
from __future__ import annotations
import importlib.util
import re
from areal.utils import logging
from tools import ( # isort: skip
BaseTool,
CalculatorTool,
PythonTool,
ToolCallStatus,
ToolType,
)
logger = logging.getLogger("Tool Manager")
def _build_daytona_python_tool(timeout: int, debug_mode: bool) -> BaseTool:
if importlib.util.find_spec("daytona") is None:
raise ImportError(
"daytona_python requires the optional 'daytona' dependency. Install it with `uv sync --extra sandbox`."
)
from tools.daytona_python_tool import DaytonaPythonTool
return DaytonaPythonTool(timeout, debug_mode)
class ToolRegistry:
"""Tool registry that manages all available tools."""
TOOL_NAMES = {
"python": ToolType.PYTHON,
"calculator": ToolType.CALCULATOR,
"daytona_python": ToolType.DAYTONA_PYTHON,
}
DEFAULT_TOOL_NAMES = ("python", "calculator")
def __init__(
self,
timeout: int = 30,
enabled_tools: str = "python;calculator",
debug_mode: bool = False,
):
self._tool_factories = {
ToolType.PYTHON: lambda: PythonTool(timeout, debug_mode),
ToolType.CALCULATOR: lambda: CalculatorTool(timeout, debug_mode),
ToolType.DAYTONA_PYTHON: lambda: _build_daytona_python_tool(
timeout, debug_mode
),
}
if enabled_tools is None:
requested_tool_names = list(self.DEFAULT_TOOL_NAMES)
else:
requested_tool_names = enabled_tools.split(";")
self.enabled_tools = []
for tool_name in requested_tool_names:
if tool_name in self.TOOL_NAMES:
self.enabled_tools.append(self.TOOL_NAMES[tool_name])
else:
logger.warning(f"Unknown tool type: {tool_name}, skipping")
if (
ToolType.PYTHON in self.enabled_tools
and ToolType.DAYTONA_PYTHON in self.enabled_tools
):
raise ValueError(
"python and daytona_python use the same markers; enable only one Python backend"
)
self.tools = {
tool_type: self._tool_factories[tool_type]()
for tool_type in self.enabled_tools
}
logger.info(
f"ToolRegistry initialized with enabled tools: {[t.value for t in self.enabled_tools]}"
)
def get_tool(self, tool_type: ToolType) -> BaseTool | None:
"""Get tool instance."""
return self.tools.get(tool_type)
def get_all_tools(self) -> dict[ToolType, BaseTool]:
"""Get all tool instances."""
return self.tools
def get_tool_markers(self) -> dict[ToolType, tuple[list[str], list[str]]]:
"""Get marker information for enabled tools only.
Returns:
Dict[ToolType, Tuple[List[str], List[str]]]: Tool type ->
(start markers list, end markers list)
"""
return {
tool_type: (tool.markers.start_markers, tool.markers.end_markers)
for tool_type, tool in self.tools.items()
}
def get_all_start_markers(self) -> list[str]:
"""Get all start markers for enabled tools only.
Returns:
List[str]: List of all start markers.
"""
start_markers = []
for tool in self.tools.values():
start_markers.extend(tool.markers.start_markers)
return start_markers
def get_all_end_markers(self) -> list[str]:
"""Get all end markers for enabled tools only.
Returns:
List[str]: List of all end markers.
"""
end_markers = []
for tool in self.tools.values():
end_markers.extend(tool.markers.end_markers)
return end_markers
def get_all_markers(self) -> list[str]:
"""Get all markers (start and end) for enabled tools only.
Returns:
List[str]: List of all markers.
"""
all_markers = []
all_markers.extend(self.get_all_start_markers())
all_markers.extend(self.get_all_end_markers())
return all_markers
def get_tool_descriptions_prompt(self) -> str:
"""Generate tool description prompt text for external calls."""
prompt_parts = ["Tools List:\n"]
for tool in self.tools.values():
desc = tool.description
prompt_parts.append(f"Tool Name: {desc.name}")
prompt_parts.append(f"Description: {desc.description}")
prompt_parts.append(f"Parameter Description: {desc.parameter_prompt}")
prompt_parts.append(f"Usage Example: {desc.example}")
prompt_parts.append("---")
return "\n".join(prompt_parts)
def get_enabled_tools(self) -> list[ToolType]:
"""Get list of enabled tools.
Returns:
List[ToolType]: List of enabled tool types.
"""
return self.enabled_tools.copy()
class ToolRouter:
"""Tool router that determines which tool to call based on markers."""
def __init__(self, registry: ToolRegistry):
self.registry = registry
self.tool_markers = self._build_tool_markers()
def _build_tool_markers(self) -> list[tuple[ToolType, str]]:
"""Build tool markers based on enabled tools."""
markers = []
for tool_type, tool in self.registry.tools.items():
for start_marker in tool.markers.start_markers:
for end_marker in tool.markers.end_markers:
escaped_start = re.escape(start_marker)
escaped_end = re.escape(end_marker)
pattern = f"{escaped_start}(.*?){escaped_end}"
markers.append((tool_type, pattern))
return markers
def route(self, text: str) -> ToolType | None:
"""Determine tool type to call based on markers."""
text = text.strip()
for tool_type, pattern in self.tool_markers:
if re.search(pattern, text, re.DOTALL | re.IGNORECASE):
return tool_type
return None
class ToolManager:
"""General tool manager responsible for coordinating tool calls."""
def __init__(
self,
timeout: int = 30,
enabled_tools: str = "python;calculator",
debug_mode: bool = False,
):
self.timeout = timeout
self.debug_mode = debug_mode
self.registry = ToolRegistry(timeout, enabled_tools, debug_mode)
self.router = ToolRouter(self.registry)
logger.info(
f"Initialized ToolManager (debug_mode={debug_mode}, enabled_tools={[t.value for t in self.registry.get_enabled_tools()]})"
)
def get_tool_descriptions_prompt(self) -> str:
"""Get tool description prompt text for external calls."""
return self.registry.get_tool_descriptions_prompt()
def get_tool_markers(self) -> dict[ToolType, tuple[list[str], list[str]]]:
"""Get marker information for all tools.
Returns:
Dict[ToolType, Tuple[List[str], List[str]]]: Tool type ->
(start markers list, end markers list)
"""
return self.registry.get_tool_markers()
def get_all_start_markers(self) -> list[str]:
"""Get all start markers for setting stop tokens.
Returns:
List[str]: List of all start markers, e.g. ['```python\\n', '<calculator>']
"""
return self.registry.get_all_start_markers()
def get_all_end_markers(self) -> list[str]:
"""Get all end markers for setting stop tokens.
Returns:
List[str]: List of all end markers, e.g. ['\\n```', '</calculator>']
"""
return self.registry.get_all_end_markers()
def get_all_markers(self) -> list[str]:
"""Get all markers (start and end) for setting stop tokens.
Returns:
List[str]: List of all markers, e.g. ['```python\\n', '\\n```', '<calculator>', '</calculator>']
"""
return self.registry.get_all_markers()
def _prepare_tool_call(
self, text: str
) -> tuple[
BaseTool | None, dict[str, str] | None, tuple[str, ToolCallStatus] | None
]:
tool_type = self.router.route(text)
if not tool_type:
return (
None,
None,
(
"Error: No suitable tool found for the given text",
ToolCallStatus.NOT_FOUND,
),
)
tool = self.registry.get_tool(tool_type)
if not tool:
return (
None,
None,
(
f"Error: Tool {tool_type.value} not found",
ToolCallStatus.NOT_FOUND,
),
)
try:
parameters = tool.parse_parameters(text)
logger.debug(f"Parsed parameters: {parameters}")
except Exception as exc:
logger.error(f"Parameter parsing error: {exc}")
return (
None,
None,
(
f"Error: Failed to parse parameters - {exc}",
ToolCallStatus.ERROR,
),
)
return tool, parameters, None
@staticmethod
def _finalize_tool_result(
result: str, status: ToolCallStatus
) -> tuple[str, ToolCallStatus]:
if status == ToolCallStatus.SUCCESS:
logger.debug(f"Tool execution completed: {result}")
return result, status
logger.error(f"Tool execution error: {result}")
return f"Error: Tool execution failed - {result}", status
def execute_tool_call(self, text: str) -> tuple[str, ToolCallStatus]:
"""Unified synchronous tool call interface.
Returns:
Tuple[str, ToolCallStatus]: (result, status)
"""
tool, parameters, error = self._prepare_tool_call(text)
if error is not None:
return error
assert tool is not None
assert parameters is not None
result, status = tool.execute(parameters)
return self._finalize_tool_result(result, status)
async def aexecute_tool_call(self, text: str) -> tuple[str, ToolCallStatus]:
"""Unified asynchronous tool call interface.
Returns:
Tuple[str, ToolCallStatus]: (result, status)
"""
tool, parameters, error = self._prepare_tool_call(text)
if error is not None:
return error
assert tool is not None
assert parameters is not None
result, status = await tool.aexecute(parameters)
return self._finalize_tool_result(result, status)
def cleanup(self) -> None:
"""Release any resources held by enabled tools."""
for tool in self.registry.get_all_tools().values():
tool.close()
async def acleanup(self) -> None:
"""Asynchronously release any resources held by enabled tools."""
for tool in self.registry.get_all_tools().values():
await tool.aclose()