swarms 7.7.8__py3-none-any.whl → 7.8.0__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.
- swarms/__init__.py +0 -1
- swarms/agents/cort_agent.py +206 -0
- swarms/agents/react_agent.py +173 -0
- swarms/agents/self_agent_builder.py +40 -0
- swarms/communication/base_communication.py +290 -0
- swarms/communication/duckdb_wrap.py +369 -72
- swarms/communication/pulsar_struct.py +691 -0
- swarms/communication/redis_wrap.py +1362 -0
- swarms/communication/sqlite_wrap.py +547 -44
- swarms/prompts/agent_self_builder_prompt.py +103 -0
- swarms/prompts/safety_prompt.py +50 -0
- swarms/schemas/__init__.py +6 -1
- swarms/schemas/agent_class_schema.py +91 -0
- swarms/schemas/agent_mcp_errors.py +18 -0
- swarms/schemas/agent_tool_schema.py +13 -0
- swarms/schemas/llm_agent_schema.py +92 -0
- swarms/schemas/mcp_schemas.py +43 -0
- swarms/structs/__init__.py +4 -0
- swarms/structs/agent.py +315 -267
- swarms/structs/aop.py +3 -1
- swarms/structs/batch_agent_execution.py +64 -0
- swarms/structs/conversation.py +261 -57
- swarms/structs/council_judge.py +542 -0
- swarms/structs/deep_research_swarm.py +19 -22
- swarms/structs/long_agent.py +424 -0
- swarms/structs/ma_utils.py +11 -8
- swarms/structs/malt.py +30 -28
- swarms/structs/multi_model_gpu_manager.py +1 -1
- swarms/structs/output_types.py +1 -1
- swarms/structs/swarm_router.py +70 -15
- swarms/tools/__init__.py +12 -0
- swarms/tools/base_tool.py +2840 -264
- swarms/tools/create_agent_tool.py +104 -0
- swarms/tools/mcp_client_call.py +504 -0
- swarms/tools/py_func_to_openai_func_str.py +45 -7
- swarms/tools/pydantic_to_json.py +10 -27
- swarms/utils/audio_processing.py +343 -0
- swarms/utils/history_output_formatter.py +5 -5
- swarms/utils/index.py +226 -0
- swarms/utils/litellm_wrapper.py +65 -67
- swarms/utils/try_except_wrapper.py +2 -2
- swarms/utils/xml_utils.py +42 -0
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/METADATA +5 -4
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/RECORD +47 -30
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/WHEEL +1 -1
- swarms/client/__init__.py +0 -15
- swarms/client/main.py +0 -407
- swarms/tools/mcp_client.py +0 -246
- swarms/tools/mcp_integration.py +0 -340
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/LICENSE +0 -0
- {swarms-7.7.8.dist-info → swarms-7.8.0.dist-info}/entry_points.txt +0 -0
swarms/tools/base_tool.py
CHANGED
@@ -1,27 +1,98 @@
|
|
1
1
|
import json
|
2
2
|
from typing import Any, Callable, Dict, List, Optional, Union
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
4
|
|
5
|
+
# from litellm.utils import function_to_dict
|
4
6
|
from pydantic import BaseModel, Field
|
5
7
|
|
6
8
|
from swarms.tools.func_to_str import function_to_str, functions_to_str
|
7
9
|
from swarms.tools.function_util import process_tool_docs
|
8
10
|
from swarms.tools.py_func_to_openai_func_str import (
|
11
|
+
convert_multiple_functions_to_openai_function_schema,
|
9
12
|
get_openai_function_schema_from_func,
|
10
13
|
load_basemodels_if_needed,
|
11
14
|
)
|
12
15
|
from swarms.tools.pydantic_to_json import (
|
13
16
|
base_model_to_openai_function,
|
14
|
-
multi_base_model_to_openai_function,
|
15
17
|
)
|
16
|
-
from swarms.utils.loguru_logger import initialize_logger
|
17
18
|
from swarms.tools.tool_parse_exec import parse_and_execute_json
|
19
|
+
from swarms.utils.loguru_logger import initialize_logger
|
18
20
|
|
19
21
|
logger = initialize_logger(log_folder="base_tool")
|
20
22
|
|
23
|
+
|
24
|
+
# Custom Exceptions
|
25
|
+
class BaseToolError(Exception):
|
26
|
+
"""Base exception class for all BaseTool related errors."""
|
27
|
+
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
class ToolValidationError(BaseToolError):
|
32
|
+
"""Raised when tool validation fails."""
|
33
|
+
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class ToolExecutionError(BaseToolError):
|
38
|
+
"""Raised when tool execution fails."""
|
39
|
+
|
40
|
+
pass
|
41
|
+
|
42
|
+
|
43
|
+
class ToolNotFoundError(BaseToolError):
|
44
|
+
"""Raised when a requested tool is not found."""
|
45
|
+
|
46
|
+
pass
|
47
|
+
|
48
|
+
|
49
|
+
class FunctionSchemaError(BaseToolError):
|
50
|
+
"""Raised when function schema conversion fails."""
|
51
|
+
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class ToolDocumentationError(BaseToolError):
|
56
|
+
"""Raised when tool documentation is missing or invalid."""
|
57
|
+
|
58
|
+
pass
|
59
|
+
|
60
|
+
|
61
|
+
class ToolTypeHintError(BaseToolError):
|
62
|
+
"""Raised when tool type hints are missing or invalid."""
|
63
|
+
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
21
67
|
ToolType = Union[BaseModel, Dict[str, Any], Callable[..., Any]]
|
22
68
|
|
23
69
|
|
24
70
|
class BaseTool(BaseModel):
|
71
|
+
"""
|
72
|
+
A comprehensive tool management system for function calling, schema conversion, and execution.
|
73
|
+
|
74
|
+
This class provides a unified interface for:
|
75
|
+
- Converting functions to OpenAI function calling schemas
|
76
|
+
- Managing Pydantic models and their schemas
|
77
|
+
- Executing tools with proper error handling and validation
|
78
|
+
- Caching expensive operations for improved performance
|
79
|
+
|
80
|
+
Attributes:
|
81
|
+
verbose (Optional[bool]): Enable detailed logging output
|
82
|
+
base_models (Optional[List[type[BaseModel]]]): List of Pydantic models to manage
|
83
|
+
autocheck (Optional[bool]): Enable automatic validation checks
|
84
|
+
auto_execute_tool (Optional[bool]): Enable automatic tool execution
|
85
|
+
tools (Optional[List[Callable[..., Any]]]): List of callable functions to manage
|
86
|
+
tool_system_prompt (Optional[str]): System prompt for tool operations
|
87
|
+
function_map (Optional[Dict[str, Callable]]): Mapping of function names to callables
|
88
|
+
list_of_dicts (Optional[List[Dict[str, Any]]]): List of dictionary representations
|
89
|
+
|
90
|
+
Examples:
|
91
|
+
>>> tool_manager = BaseTool(verbose=True, tools=[my_function])
|
92
|
+
>>> schema = tool_manager.func_to_dict(my_function)
|
93
|
+
>>> result = tool_manager.execute_tool(response_json)
|
94
|
+
"""
|
95
|
+
|
25
96
|
verbose: Optional[bool] = None
|
26
97
|
base_models: Optional[List[type[BaseModel]]] = None
|
27
98
|
autocheck: Optional[bool] = None
|
@@ -34,31 +105,73 @@ class BaseTool(BaseModel):
|
|
34
105
|
function_map: Optional[Dict[str, Callable]] = None
|
35
106
|
list_of_dicts: Optional[List[Dict[str, Any]]] = None
|
36
107
|
|
108
|
+
def _log_if_verbose(
|
109
|
+
self, level: str, message: str, *args, **kwargs
|
110
|
+
) -> None:
|
111
|
+
"""
|
112
|
+
Log message only if verbose mode is enabled.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
level (str): Log level ('info', 'error', 'warning', 'debug')
|
116
|
+
message (str): Message to log
|
117
|
+
*args: Additional arguments for the logger
|
118
|
+
**kwargs: Additional keyword arguments for the logger
|
119
|
+
"""
|
120
|
+
if self.verbose:
|
121
|
+
log_method = getattr(logger, level.lower(), logger.info)
|
122
|
+
log_method(message, *args, **kwargs)
|
123
|
+
|
124
|
+
def _make_hashable(self, obj: Any) -> tuple:
|
125
|
+
"""
|
126
|
+
Convert objects to hashable tuples for caching purposes.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
obj: Object to make hashable
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
tuple: Hashable representation of the object
|
133
|
+
"""
|
134
|
+
if isinstance(obj, dict):
|
135
|
+
return tuple(sorted(obj.items()))
|
136
|
+
elif isinstance(obj, list):
|
137
|
+
return tuple(obj)
|
138
|
+
elif isinstance(obj, type):
|
139
|
+
return (obj.__module__, obj.__name__)
|
140
|
+
else:
|
141
|
+
return obj
|
142
|
+
|
37
143
|
def func_to_dict(
|
38
144
|
self,
|
39
145
|
function: Callable[..., Any] = None,
|
40
|
-
name: Optional[str] = None,
|
41
|
-
description: str = None,
|
42
|
-
*args,
|
43
|
-
**kwargs,
|
44
146
|
) -> Dict[str, Any]:
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
147
|
+
"""
|
148
|
+
Convert a callable function to OpenAI function calling schema dictionary.
|
149
|
+
|
150
|
+
This method transforms a Python function into a dictionary format compatible
|
151
|
+
with OpenAI's function calling API. Results are cached for performance.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
function (Callable[..., Any]): The function to convert
|
155
|
+
name (Optional[str]): Override name for the function
|
156
|
+
description (str): Override description for the function
|
157
|
+
*args: Additional positional arguments
|
158
|
+
**kwargs: Additional keyword arguments
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Dict[str, Any]: OpenAI function calling schema dictionary
|
162
|
+
|
163
|
+
Raises:
|
164
|
+
FunctionSchemaError: If function schema conversion fails
|
165
|
+
ToolValidationError: If function validation fails
|
166
|
+
|
167
|
+
Examples:
|
168
|
+
>>> def add(a: int, b: int) -> int:
|
169
|
+
... '''Add two numbers'''
|
170
|
+
... return a + b
|
171
|
+
>>> tool = BaseTool()
|
172
|
+
>>> schema = tool.func_to_dict(add)
|
173
|
+
"""
|
174
|
+
return self.function_to_dict(function)
|
62
175
|
|
63
176
|
def load_params_from_func_for_pybasemodel(
|
64
177
|
self,
|
@@ -66,115 +179,351 @@ class BaseTool(BaseModel):
|
|
66
179
|
*args: Any,
|
67
180
|
**kwargs: Any,
|
68
181
|
) -> Callable[..., Any]:
|
182
|
+
"""
|
183
|
+
Load and process function parameters for Pydantic BaseModel integration.
|
184
|
+
|
185
|
+
This method prepares function parameters for use with Pydantic BaseModels,
|
186
|
+
ensuring proper type handling and validation.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
func (Callable[..., Any]): The function to process
|
190
|
+
*args: Additional positional arguments
|
191
|
+
**kwargs: Additional keyword arguments
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
Callable[..., Any]: Processed function with loaded parameters
|
195
|
+
|
196
|
+
Raises:
|
197
|
+
ToolValidationError: If function validation fails
|
198
|
+
FunctionSchemaError: If parameter loading fails
|
199
|
+
|
200
|
+
Examples:
|
201
|
+
>>> tool = BaseTool()
|
202
|
+
>>> processed_func = tool.load_params_from_func_for_pybasemodel(my_func)
|
203
|
+
"""
|
204
|
+
if func is None:
|
205
|
+
raise ToolValidationError(
|
206
|
+
"Function parameter cannot be None"
|
207
|
+
)
|
208
|
+
|
69
209
|
try:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
f"An error occurred in load_params_from_func_for_pybasemodel: {e}"
|
210
|
+
self._log_if_verbose(
|
211
|
+
"info",
|
212
|
+
f"Loading parameters for function {func.__name__}",
|
74
213
|
)
|
75
|
-
|
76
|
-
|
214
|
+
|
215
|
+
result = load_basemodels_if_needed(func, *args, **kwargs)
|
216
|
+
|
217
|
+
self._log_if_verbose(
|
218
|
+
"info",
|
219
|
+
f"Successfully loaded parameters for {func.__name__}",
|
77
220
|
)
|
78
|
-
|
79
|
-
|
221
|
+
return result
|
222
|
+
|
223
|
+
except Exception as e:
|
224
|
+
self._log_if_verbose(
|
225
|
+
"error",
|
226
|
+
f"Failed to load parameters for {func.__name__}: {e}",
|
80
227
|
)
|
81
|
-
raise
|
228
|
+
raise FunctionSchemaError(
|
229
|
+
f"Failed to load function parameters: {e}"
|
230
|
+
) from e
|
82
231
|
|
83
232
|
def base_model_to_dict(
|
84
233
|
self,
|
85
234
|
pydantic_type: type[BaseModel],
|
86
|
-
output_str: bool = False,
|
87
235
|
*args: Any,
|
88
236
|
**kwargs: Any,
|
89
237
|
) -> dict[str, Any]:
|
90
|
-
|
91
|
-
|
92
|
-
|
238
|
+
"""
|
239
|
+
Convert a Pydantic BaseModel to OpenAI function calling schema dictionary.
|
240
|
+
|
241
|
+
This method transforms a Pydantic model into a dictionary format compatible
|
242
|
+
with OpenAI's function calling API. Results are cached for performance.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
pydantic_type (type[BaseModel]): The Pydantic model class to convert
|
246
|
+
output_str (bool): Whether to return string output format
|
247
|
+
*args: Additional positional arguments
|
248
|
+
**kwargs: Additional keyword arguments
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
dict[str, Any]: OpenAI function calling schema dictionary
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
ToolValidationError: If pydantic_type validation fails
|
255
|
+
FunctionSchemaError: If schema conversion fails
|
256
|
+
|
257
|
+
Examples:
|
258
|
+
>>> class MyModel(BaseModel):
|
259
|
+
... name: str
|
260
|
+
... age: int
|
261
|
+
>>> tool = BaseTool()
|
262
|
+
>>> schema = tool.base_model_to_dict(MyModel)
|
263
|
+
"""
|
264
|
+
if pydantic_type is None:
|
265
|
+
raise ToolValidationError(
|
266
|
+
"Pydantic type parameter cannot be None"
|
93
267
|
)
|
94
|
-
|
95
|
-
|
96
|
-
|
268
|
+
|
269
|
+
if not issubclass(pydantic_type, BaseModel):
|
270
|
+
raise ToolValidationError(
|
271
|
+
"pydantic_type must be a subclass of BaseModel"
|
97
272
|
)
|
98
|
-
|
99
|
-
|
273
|
+
|
274
|
+
try:
|
275
|
+
self._log_if_verbose(
|
276
|
+
"info",
|
277
|
+
f"Converting Pydantic model {pydantic_type.__name__} to schema",
|
100
278
|
)
|
101
|
-
|
102
|
-
|
279
|
+
|
280
|
+
# Get the base function schema
|
281
|
+
base_result = base_model_to_openai_function(
|
282
|
+
pydantic_type, *args, **kwargs
|
103
283
|
)
|
104
|
-
raise
|
105
284
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
285
|
+
# Extract the function definition from the functions array
|
286
|
+
if (
|
287
|
+
"functions" in base_result
|
288
|
+
and len(base_result["functions"]) > 0
|
289
|
+
):
|
290
|
+
function_def = base_result["functions"][0]
|
291
|
+
|
292
|
+
# Return in proper OpenAI function calling format
|
293
|
+
result = {
|
294
|
+
"type": "function",
|
295
|
+
"function": function_def,
|
296
|
+
}
|
114
297
|
else:
|
115
|
-
|
116
|
-
|
298
|
+
raise FunctionSchemaError(
|
299
|
+
"Failed to extract function definition from base_model_to_openai_function result"
|
117
300
|
)
|
301
|
+
|
302
|
+
self._log_if_verbose(
|
303
|
+
"info",
|
304
|
+
f"Successfully converted model {pydantic_type.__name__}",
|
305
|
+
)
|
306
|
+
return result
|
307
|
+
|
118
308
|
except Exception as e:
|
119
|
-
|
120
|
-
|
309
|
+
self._log_if_verbose(
|
310
|
+
"error",
|
311
|
+
f"Failed to convert model {pydantic_type.__name__}: {e}",
|
121
312
|
)
|
122
|
-
|
123
|
-
"
|
313
|
+
raise FunctionSchemaError(
|
314
|
+
f"Failed to convert Pydantic model to schema: {e}"
|
315
|
+
) from e
|
316
|
+
|
317
|
+
def multi_base_models_to_dict(
|
318
|
+
self, base_models: List[BaseModel]
|
319
|
+
) -> dict[str, Any]:
|
320
|
+
"""
|
321
|
+
Convert multiple Pydantic BaseModels to OpenAI function calling schema.
|
322
|
+
|
323
|
+
This method processes multiple Pydantic models and converts them into
|
324
|
+
a unified OpenAI function calling schema format.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
return_str (bool): Whether to return string format
|
328
|
+
*args: Additional positional arguments
|
329
|
+
**kwargs: Additional keyword arguments
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
dict[str, Any]: Combined OpenAI function calling schema
|
333
|
+
|
334
|
+
Raises:
|
335
|
+
ToolValidationError: If base_models validation fails
|
336
|
+
FunctionSchemaError: If schema conversion fails
|
337
|
+
|
338
|
+
Examples:
|
339
|
+
>>> tool = BaseTool(base_models=[Model1, Model2])
|
340
|
+
>>> schema = tool.multi_base_models_to_dict()
|
341
|
+
"""
|
342
|
+
if base_models is None:
|
343
|
+
raise ToolValidationError(
|
344
|
+
"base_models must be set and be a non-empty list before calling this method"
|
124
345
|
)
|
125
|
-
|
126
|
-
|
346
|
+
|
347
|
+
try:
|
348
|
+
return [
|
349
|
+
self.base_model_to_dict(model)
|
350
|
+
for model in base_models
|
351
|
+
]
|
352
|
+
except Exception as e:
|
353
|
+
self._log_if_verbose(
|
354
|
+
"error", f"Failed to convert multiple models: {e}"
|
127
355
|
)
|
128
|
-
raise
|
356
|
+
raise FunctionSchemaError(
|
357
|
+
f"Failed to convert multiple Pydantic models: {e}"
|
358
|
+
) from e
|
129
359
|
|
130
360
|
def dict_to_openai_schema_str(
|
131
361
|
self,
|
132
362
|
dict: dict[str, Any],
|
133
363
|
) -> str:
|
364
|
+
"""
|
365
|
+
Convert a dictionary to OpenAI function calling schema string.
|
366
|
+
|
367
|
+
This method transforms a dictionary representation into a string format
|
368
|
+
suitable for OpenAI function calling. Results are cached for performance.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
dict (dict[str, Any]): Dictionary to convert
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
str: OpenAI schema string representation
|
375
|
+
|
376
|
+
Raises:
|
377
|
+
ToolValidationError: If dict validation fails
|
378
|
+
FunctionSchemaError: If conversion fails
|
379
|
+
|
380
|
+
Examples:
|
381
|
+
>>> tool = BaseTool()
|
382
|
+
>>> schema_str = tool.dict_to_openai_schema_str(my_dict)
|
383
|
+
"""
|
384
|
+
if dict is None:
|
385
|
+
raise ToolValidationError(
|
386
|
+
"Dictionary parameter cannot be None"
|
387
|
+
)
|
388
|
+
|
389
|
+
if not isinstance(dict, dict):
|
390
|
+
raise ToolValidationError(
|
391
|
+
"Parameter must be a dictionary"
|
392
|
+
)
|
393
|
+
|
134
394
|
try:
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
f"An error occurred in dict_to_openai_schema_str: {e}"
|
395
|
+
self._log_if_verbose(
|
396
|
+
"info",
|
397
|
+
"Converting dictionary to OpenAI schema string",
|
139
398
|
)
|
140
|
-
|
141
|
-
|
399
|
+
|
400
|
+
result = function_to_str(dict)
|
401
|
+
|
402
|
+
self._log_if_verbose(
|
403
|
+
"info",
|
404
|
+
"Successfully converted dictionary to schema string",
|
142
405
|
)
|
143
|
-
|
144
|
-
|
406
|
+
return result
|
407
|
+
|
408
|
+
except Exception as e:
|
409
|
+
self._log_if_verbose(
|
410
|
+
"error",
|
411
|
+
f"Failed to convert dictionary to schema string: {e}",
|
145
412
|
)
|
146
|
-
raise
|
413
|
+
raise FunctionSchemaError(
|
414
|
+
f"Failed to convert dictionary to schema string: {e}"
|
415
|
+
) from e
|
147
416
|
|
148
417
|
def multi_dict_to_openai_schema_str(
|
149
418
|
self,
|
150
419
|
dicts: list[dict[str, Any]],
|
151
420
|
) -> str:
|
421
|
+
"""
|
422
|
+
Convert multiple dictionaries to OpenAI function calling schema string.
|
423
|
+
|
424
|
+
This method processes a list of dictionaries and converts them into
|
425
|
+
a unified OpenAI function calling schema string format.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
dicts (list[dict[str, Any]]): List of dictionaries to convert
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
str: Combined OpenAI schema string representation
|
432
|
+
|
433
|
+
Raises:
|
434
|
+
ToolValidationError: If dicts validation fails
|
435
|
+
FunctionSchemaError: If conversion fails
|
436
|
+
|
437
|
+
Examples:
|
438
|
+
>>> tool = BaseTool()
|
439
|
+
>>> schema_str = tool.multi_dict_to_openai_schema_str([dict1, dict2])
|
440
|
+
"""
|
441
|
+
if dicts is None:
|
442
|
+
raise ToolValidationError(
|
443
|
+
"Dicts parameter cannot be None"
|
444
|
+
)
|
445
|
+
|
446
|
+
if not isinstance(dicts, list) or len(dicts) == 0:
|
447
|
+
raise ToolValidationError(
|
448
|
+
"Dicts parameter must be a non-empty list"
|
449
|
+
)
|
450
|
+
|
451
|
+
for i, d in enumerate(dicts):
|
452
|
+
if not isinstance(d, dict):
|
453
|
+
raise ToolValidationError(
|
454
|
+
f"Item at index {i} is not a dictionary"
|
455
|
+
)
|
456
|
+
|
152
457
|
try:
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
f"An error occurred in multi_dict_to_openai_schema_str: {e}"
|
458
|
+
self._log_if_verbose(
|
459
|
+
"info",
|
460
|
+
f"Converting {len(dicts)} dictionaries to schema string",
|
157
461
|
)
|
158
|
-
|
159
|
-
|
462
|
+
|
463
|
+
result = functions_to_str(dicts)
|
464
|
+
|
465
|
+
self._log_if_verbose(
|
466
|
+
"info",
|
467
|
+
f"Successfully converted {len(dicts)} dictionaries",
|
160
468
|
)
|
161
|
-
|
162
|
-
|
469
|
+
return result
|
470
|
+
|
471
|
+
except Exception as e:
|
472
|
+
self._log_if_verbose(
|
473
|
+
"error",
|
474
|
+
f"Failed to convert dictionaries to schema string: {e}",
|
163
475
|
)
|
164
|
-
raise
|
476
|
+
raise FunctionSchemaError(
|
477
|
+
f"Failed to convert dictionaries to schema string: {e}"
|
478
|
+
) from e
|
165
479
|
|
166
480
|
def get_docs_from_callable(self, item):
|
481
|
+
"""
|
482
|
+
Extract documentation from a callable item.
|
483
|
+
|
484
|
+
This method processes a callable and extracts its documentation
|
485
|
+
for use in tool schema generation.
|
486
|
+
|
487
|
+
Args:
|
488
|
+
item: The callable item to extract documentation from
|
489
|
+
|
490
|
+
Returns:
|
491
|
+
The processed documentation
|
492
|
+
|
493
|
+
Raises:
|
494
|
+
ToolValidationError: If item validation fails
|
495
|
+
ToolDocumentationError: If documentation extraction fails
|
496
|
+
|
497
|
+
Examples:
|
498
|
+
>>> tool = BaseTool()
|
499
|
+
>>> docs = tool.get_docs_from_callable(my_function)
|
500
|
+
"""
|
501
|
+
if item is None:
|
502
|
+
raise ToolValidationError("Item parameter cannot be None")
|
503
|
+
|
504
|
+
if not callable(item):
|
505
|
+
raise ToolValidationError("Item must be callable")
|
506
|
+
|
167
507
|
try:
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
508
|
+
self._log_if_verbose(
|
509
|
+
"info",
|
510
|
+
f"Extracting documentation from {getattr(item, '__name__', 'unnamed callable')}",
|
511
|
+
)
|
512
|
+
|
513
|
+
result = process_tool_docs(item)
|
514
|
+
|
515
|
+
self._log_if_verbose(
|
516
|
+
"info", "Successfully extracted documentation"
|
173
517
|
)
|
174
|
-
|
175
|
-
|
518
|
+
return result
|
519
|
+
|
520
|
+
except Exception as e:
|
521
|
+
self._log_if_verbose(
|
522
|
+
"error", f"Failed to extract documentation: {e}"
|
176
523
|
)
|
177
|
-
raise
|
524
|
+
raise ToolDocumentationError(
|
525
|
+
f"Failed to extract documentation: {e}"
|
526
|
+
) from e
|
178
527
|
|
179
528
|
def execute_tool(
|
180
529
|
self,
|
@@ -182,22 +531,84 @@ class BaseTool(BaseModel):
|
|
182
531
|
*args: Any,
|
183
532
|
**kwargs: Any,
|
184
533
|
) -> Callable:
|
534
|
+
"""
|
535
|
+
Execute a tool based on a response string.
|
536
|
+
|
537
|
+
This method parses a JSON response string and executes the corresponding
|
538
|
+
tool function with proper error handling and validation.
|
539
|
+
|
540
|
+
Args:
|
541
|
+
response (str): JSON response string containing tool execution details
|
542
|
+
*args: Additional positional arguments
|
543
|
+
**kwargs: Additional keyword arguments
|
544
|
+
|
545
|
+
Returns:
|
546
|
+
Callable: Result of the tool execution
|
547
|
+
|
548
|
+
Raises:
|
549
|
+
ToolValidationError: If response validation fails
|
550
|
+
ToolExecutionError: If tool execution fails
|
551
|
+
ToolNotFoundError: If specified tool is not found
|
552
|
+
|
553
|
+
Examples:
|
554
|
+
>>> tool = BaseTool(tools=[my_function])
|
555
|
+
>>> result = tool.execute_tool('{"name": "my_function", "parameters": {...}}')
|
556
|
+
"""
|
557
|
+
if response is None or not isinstance(response, str):
|
558
|
+
raise ToolValidationError(
|
559
|
+
"Response must be a non-empty string"
|
560
|
+
)
|
561
|
+
|
562
|
+
if response.strip() == "":
|
563
|
+
raise ToolValidationError("Response cannot be empty")
|
564
|
+
|
565
|
+
if self.tools is None:
|
566
|
+
raise ToolValidationError(
|
567
|
+
"Tools must be set before executing"
|
568
|
+
)
|
569
|
+
|
185
570
|
try:
|
186
|
-
|
571
|
+
self._log_if_verbose(
|
572
|
+
"info",
|
573
|
+
f"Executing tool with response: {response[:100]}...",
|
574
|
+
)
|
575
|
+
|
576
|
+
result = parse_and_execute_json(
|
187
577
|
self.tools,
|
188
578
|
response,
|
189
579
|
)
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
"Please check the tools and function map and ensure they are valid."
|
580
|
+
|
581
|
+
self._log_if_verbose(
|
582
|
+
"info", "Tool execution completed successfully"
|
194
583
|
)
|
195
|
-
|
196
|
-
|
584
|
+
return result
|
585
|
+
|
586
|
+
except Exception as e:
|
587
|
+
self._log_if_verbose(
|
588
|
+
"error", f"Tool execution failed: {e}"
|
197
589
|
)
|
198
|
-
raise
|
590
|
+
raise ToolExecutionError(
|
591
|
+
f"Failed to execute tool: {e}"
|
592
|
+
) from e
|
199
593
|
|
200
594
|
def detect_tool_input_type(self, input: ToolType) -> str:
|
595
|
+
"""
|
596
|
+
Detect the type of tool input for appropriate processing.
|
597
|
+
|
598
|
+
This method analyzes the input and determines whether it's a Pydantic model,
|
599
|
+
dictionary, function, or unknown type. Results are cached for performance.
|
600
|
+
|
601
|
+
Args:
|
602
|
+
input (ToolType): The input to analyze
|
603
|
+
|
604
|
+
Returns:
|
605
|
+
str: Type of the input ("Pydantic", "Dictionary", "Function", or "Unknown")
|
606
|
+
|
607
|
+
Examples:
|
608
|
+
>>> tool = BaseTool()
|
609
|
+
>>> input_type = tool.detect_tool_input_type(my_function)
|
610
|
+
>>> print(input_type) # "Function"
|
611
|
+
"""
|
201
612
|
if isinstance(input, BaseModel):
|
202
613
|
return "Pydantic"
|
203
614
|
elif isinstance(input, dict):
|
@@ -209,44 +620,99 @@ class BaseTool(BaseModel):
|
|
209
620
|
|
210
621
|
def dynamic_run(self, input: Any) -> str:
|
211
622
|
"""
|
212
|
-
|
623
|
+
Execute a dynamic run based on the input type with automatic type detection.
|
624
|
+
|
625
|
+
This method automatically detects the input type and processes it accordingly,
|
626
|
+
optionally executing the tool if auto_execute_tool is enabled.
|
213
627
|
|
214
628
|
Args:
|
215
|
-
input: The input to be processed
|
629
|
+
input (Any): The input to be processed (Pydantic model, dict, or function)
|
216
630
|
|
217
631
|
Returns:
|
218
|
-
str: The result of the dynamic run
|
632
|
+
str: The result of the dynamic run (schema string or execution result)
|
219
633
|
|
220
634
|
Raises:
|
221
|
-
|
635
|
+
ToolValidationError: If input validation fails
|
636
|
+
ToolExecutionError: If auto-execution fails
|
637
|
+
FunctionSchemaError: If schema conversion fails
|
222
638
|
|
639
|
+
Examples:
|
640
|
+
>>> tool = BaseTool(auto_execute_tool=True)
|
641
|
+
>>> result = tool.dynamic_run(my_function)
|
223
642
|
"""
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
function_str = function_to_str(input)
|
229
|
-
elif tool_input_type == "Function":
|
230
|
-
function_str = get_openai_function_schema_from_func(input)
|
231
|
-
else:
|
232
|
-
return "Unknown tool input type"
|
643
|
+
if input is None:
|
644
|
+
raise ToolValidationError(
|
645
|
+
"Input parameter cannot be None"
|
646
|
+
)
|
233
647
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
648
|
+
try:
|
649
|
+
self._log_if_verbose(
|
650
|
+
"info",
|
651
|
+
"Starting dynamic run with input type detection",
|
652
|
+
)
|
238
653
|
|
239
|
-
|
240
|
-
function_map = {
|
241
|
-
func.__name__: func for func in self.tools
|
242
|
-
}
|
654
|
+
tool_input_type = self.detect_tool_input_type(input)
|
243
655
|
|
244
|
-
|
245
|
-
|
246
|
-
tools=[function_str], function_map=function_map
|
656
|
+
self._log_if_verbose(
|
657
|
+
"info", f"Detected input type: {tool_input_type}"
|
247
658
|
)
|
248
|
-
|
249
|
-
|
659
|
+
|
660
|
+
# Convert input to function schema based on type
|
661
|
+
if tool_input_type == "Pydantic":
|
662
|
+
function_str = base_model_to_openai_function(input)
|
663
|
+
elif tool_input_type == "Dictionary":
|
664
|
+
function_str = function_to_str(input)
|
665
|
+
elif tool_input_type == "Function":
|
666
|
+
function_str = get_openai_function_schema_from_func(
|
667
|
+
input
|
668
|
+
)
|
669
|
+
else:
|
670
|
+
raise ToolValidationError(
|
671
|
+
f"Unknown tool input type: {tool_input_type}"
|
672
|
+
)
|
673
|
+
|
674
|
+
# Execute tool if auto-execution is enabled
|
675
|
+
if self.auto_execute_tool:
|
676
|
+
self._log_if_verbose(
|
677
|
+
"info",
|
678
|
+
"Auto-execution enabled, preparing to execute tool",
|
679
|
+
)
|
680
|
+
|
681
|
+
if tool_input_type == "Function":
|
682
|
+
# Initialize tools list if needed
|
683
|
+
if self.tools is None:
|
684
|
+
self.tools = []
|
685
|
+
|
686
|
+
# Add the function to the tools list if not already present
|
687
|
+
if input not in self.tools:
|
688
|
+
self.tools.append(input)
|
689
|
+
|
690
|
+
# Create or update function map
|
691
|
+
if self.function_map is None:
|
692
|
+
self.function_map = {}
|
693
|
+
|
694
|
+
if self.tools:
|
695
|
+
self.function_map.update(
|
696
|
+
{func.__name__: func for func in self.tools}
|
697
|
+
)
|
698
|
+
|
699
|
+
# Execute the tool
|
700
|
+
return self.execute_tool(
|
701
|
+
tools=[function_str],
|
702
|
+
function_map=self.function_map,
|
703
|
+
)
|
704
|
+
else:
|
705
|
+
self._log_if_verbose(
|
706
|
+
"info",
|
707
|
+
"Auto-execution disabled, returning schema string",
|
708
|
+
)
|
709
|
+
return function_str
|
710
|
+
|
711
|
+
except Exception as e:
|
712
|
+
self._log_if_verbose("error", f"Dynamic run failed: {e}")
|
713
|
+
raise ToolExecutionError(
|
714
|
+
f"Dynamic run failed: {e}"
|
715
|
+
) from e
|
250
716
|
|
251
717
|
def execute_tool_by_name(
|
252
718
|
self,
|
@@ -254,228 +720,2338 @@ class BaseTool(BaseModel):
|
|
254
720
|
response: str,
|
255
721
|
) -> Any:
|
256
722
|
"""
|
257
|
-
Search for a tool by name and execute it.
|
723
|
+
Search for a tool by name and execute it with the provided response.
|
258
724
|
|
259
|
-
|
260
|
-
|
725
|
+
This method finds a specific tool in the function map and executes it
|
726
|
+
using the provided JSON response string.
|
261
727
|
|
728
|
+
Args:
|
729
|
+
tool_name (str): The name of the tool to execute
|
730
|
+
response (str): JSON response string containing execution parameters
|
262
731
|
|
263
732
|
Returns:
|
264
|
-
The result of executing the tool
|
733
|
+
Any: The result of executing the tool
|
265
734
|
|
266
735
|
Raises:
|
267
|
-
|
268
|
-
|
736
|
+
ToolValidationError: If parameters validation fails
|
737
|
+
ToolNotFoundError: If the tool with the specified name is not found
|
738
|
+
ToolExecutionError: If tool execution fails
|
739
|
+
|
740
|
+
Examples:
|
741
|
+
>>> tool = BaseTool(function_map={"add": add_function})
|
742
|
+
>>> result = tool.execute_tool_by_name("add", '{"a": 1, "b": 2}')
|
269
743
|
"""
|
270
|
-
|
271
|
-
|
744
|
+
if not tool_name or not isinstance(tool_name, str):
|
745
|
+
raise ToolValidationError(
|
746
|
+
"Tool name must be a non-empty string"
|
747
|
+
)
|
272
748
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
749
|
+
if not response or not isinstance(response, str):
|
750
|
+
raise ToolValidationError(
|
751
|
+
"Response must be a non-empty string"
|
752
|
+
)
|
753
|
+
|
754
|
+
if self.function_map is None:
|
755
|
+
raise ToolValidationError(
|
756
|
+
"Function map must be set before executing tools by name"
|
757
|
+
)
|
758
|
+
|
759
|
+
try:
|
760
|
+
self._log_if_verbose(
|
761
|
+
"info", f"Searching for tool: {tool_name}"
|
762
|
+
)
|
763
|
+
|
764
|
+
# Find the function in the function map
|
765
|
+
func = self.function_map.get(tool_name)
|
766
|
+
|
767
|
+
if func is None:
|
768
|
+
raise ToolNotFoundError(
|
769
|
+
f"Tool '{tool_name}' not found in function map"
|
770
|
+
)
|
771
|
+
|
772
|
+
self._log_if_verbose(
|
773
|
+
"info",
|
774
|
+
f"Found tool {tool_name}, executing with response",
|
775
|
+
)
|
776
|
+
|
777
|
+
# Execute the tool
|
778
|
+
execution = parse_and_execute_json(
|
779
|
+
functions=[func],
|
780
|
+
json_string=response,
|
781
|
+
verbose=self.verbose,
|
782
|
+
)
|
783
|
+
|
784
|
+
self._log_if_verbose(
|
785
|
+
"info", f"Successfully executed tool {tool_name}"
|
786
|
+
)
|
787
|
+
return execution
|
278
788
|
|
279
|
-
|
789
|
+
except ToolNotFoundError:
|
790
|
+
raise
|
791
|
+
except Exception as e:
|
792
|
+
self._log_if_verbose(
|
793
|
+
"error", f"Failed to execute tool {tool_name}: {e}"
|
794
|
+
)
|
795
|
+
raise ToolExecutionError(
|
796
|
+
f"Failed to execute tool '{tool_name}': {e}"
|
797
|
+
) from e
|
280
798
|
|
281
799
|
def execute_tool_from_text(self, text: str) -> Any:
|
282
800
|
"""
|
283
801
|
Convert a JSON-formatted string into a tool dictionary and execute the tool.
|
284
802
|
|
803
|
+
This method parses a JSON string representation of a tool call and executes
|
804
|
+
the corresponding function with the provided parameters.
|
805
|
+
|
285
806
|
Args:
|
286
|
-
text (str): A JSON-formatted string
|
287
|
-
function_map (Dict[str, Callable]): A dictionary that maps tool names to functions.
|
807
|
+
text (str): A JSON-formatted string representing a tool call with 'name' and 'parameters' keys
|
288
808
|
|
289
809
|
Returns:
|
290
|
-
The result of executing the tool
|
810
|
+
Any: The result of executing the tool
|
291
811
|
|
292
812
|
Raises:
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
# Convert the text into a dictionary
|
297
|
-
tool = json.loads(text)
|
813
|
+
ToolValidationError: If text validation fails or JSON parsing fails
|
814
|
+
ToolNotFoundError: If the tool with the specified name is not found
|
815
|
+
ToolExecutionError: If tool execution fails
|
298
816
|
|
299
|
-
|
300
|
-
|
301
|
-
|
817
|
+
Examples:
|
818
|
+
>>> tool = BaseTool(function_map={"add": add_function})
|
819
|
+
>>> result = tool.execute_tool_from_text('{"name": "add", "parameters": {"a": 1, "b": 2}}')
|
820
|
+
"""
|
821
|
+
if not text or not isinstance(text, str):
|
822
|
+
raise ToolValidationError(
|
823
|
+
"Text parameter must be a non-empty string"
|
824
|
+
)
|
302
825
|
|
303
|
-
|
304
|
-
|
826
|
+
if self.function_map is None:
|
827
|
+
raise ToolValidationError(
|
828
|
+
"Function map must be set before executing tools from text"
|
829
|
+
)
|
305
830
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
831
|
+
try:
|
832
|
+
self._log_if_verbose(
|
833
|
+
"info", f"Parsing tool from text: {text[:100]}..."
|
834
|
+
)
|
835
|
+
|
836
|
+
# Convert the text into a dictionary
|
837
|
+
try:
|
838
|
+
tool = json.loads(text)
|
839
|
+
except json.JSONDecodeError as e:
|
840
|
+
raise ToolValidationError(
|
841
|
+
f"Invalid JSON format: {e}"
|
842
|
+
) from e
|
843
|
+
|
844
|
+
# Get the tool name and parameters from the dictionary
|
845
|
+
tool_name = tool.get("name")
|
846
|
+
if not tool_name:
|
847
|
+
raise ToolValidationError(
|
848
|
+
"Tool JSON must contain a 'name' field"
|
849
|
+
)
|
850
|
+
|
851
|
+
tool_params = tool.get("parameters", {})
|
852
|
+
|
853
|
+
self._log_if_verbose(
|
854
|
+
"info", f"Executing tool {tool_name} with parameters"
|
855
|
+
)
|
856
|
+
|
857
|
+
# Get the function associated with the tool
|
858
|
+
func = self.function_map.get(tool_name)
|
859
|
+
|
860
|
+
# If the function is not found, raise an error
|
861
|
+
if func is None:
|
862
|
+
raise ToolNotFoundError(
|
863
|
+
f"Tool '{tool_name}' is not mapped to a function"
|
864
|
+
)
|
865
|
+
|
866
|
+
# Execute the tool
|
867
|
+
result = func(**tool_params)
|
868
|
+
|
869
|
+
self._log_if_verbose(
|
870
|
+
"info", f"Successfully executed tool {tool_name}"
|
310
871
|
)
|
872
|
+
return result
|
311
873
|
|
312
|
-
|
313
|
-
|
874
|
+
except (ToolValidationError, ToolNotFoundError):
|
875
|
+
raise
|
876
|
+
except Exception as e:
|
877
|
+
self._log_if_verbose(
|
878
|
+
"error", f"Failed to execute tool from text: {e}"
|
879
|
+
)
|
880
|
+
raise ToolExecutionError(
|
881
|
+
f"Failed to execute tool from text: {e}"
|
882
|
+
) from e
|
314
883
|
|
315
|
-
def check_str_for_functions_valid(self, output: str):
|
884
|
+
def check_str_for_functions_valid(self, output: str) -> bool:
|
316
885
|
"""
|
317
|
-
Check if the output is a valid JSON string
|
886
|
+
Check if the output is a valid JSON string with a function name that matches the function map.
|
887
|
+
|
888
|
+
This method validates that the output string is properly formatted JSON containing
|
889
|
+
a function call that exists in the current function map.
|
318
890
|
|
319
891
|
Args:
|
320
|
-
output (str): The output to
|
321
|
-
function_map (dict): A dictionary mapping function names to functions.
|
892
|
+
output (str): The output string to validate
|
322
893
|
|
323
894
|
Returns:
|
324
|
-
bool: True if the output is valid and the function name matches, False otherwise
|
895
|
+
bool: True if the output is valid and the function name matches, False otherwise
|
896
|
+
|
897
|
+
Raises:
|
898
|
+
ToolValidationError: If output parameter validation fails
|
899
|
+
|
900
|
+
Examples:
|
901
|
+
>>> tool = BaseTool(function_map={"add": add_function})
|
902
|
+
>>> is_valid = tool.check_str_for_functions_valid('{"type": "function", "function": {"name": "add"}}')
|
325
903
|
"""
|
904
|
+
if not isinstance(output, str):
|
905
|
+
raise ToolValidationError("Output must be a string")
|
906
|
+
|
907
|
+
if self.function_map is None:
|
908
|
+
self._log_if_verbose(
|
909
|
+
"warning",
|
910
|
+
"Function map is None, cannot validate function names",
|
911
|
+
)
|
912
|
+
return False
|
913
|
+
|
326
914
|
try:
|
915
|
+
self._log_if_verbose(
|
916
|
+
"debug",
|
917
|
+
f"Validating output string: {output[:100]}...",
|
918
|
+
)
|
919
|
+
|
327
920
|
# Parse the output as JSON
|
328
|
-
|
921
|
+
try:
|
922
|
+
data = json.loads(output)
|
923
|
+
except json.JSONDecodeError:
|
924
|
+
self._log_if_verbose(
|
925
|
+
"debug", "Output is not valid JSON"
|
926
|
+
)
|
927
|
+
return False
|
329
928
|
|
330
|
-
# Check if the output matches the schema
|
929
|
+
# Check if the output matches the expected schema
|
331
930
|
if (
|
332
931
|
data.get("type") == "function"
|
333
932
|
and "function" in data
|
334
933
|
and "name" in data["function"]
|
335
934
|
):
|
336
|
-
|
337
935
|
# Check if the function name matches any name in the function map
|
338
936
|
function_name = data["function"]["name"]
|
339
937
|
if function_name in self.function_map:
|
938
|
+
self._log_if_verbose(
|
939
|
+
"debug",
|
940
|
+
f"Valid function call for {function_name}",
|
941
|
+
)
|
340
942
|
return True
|
943
|
+
else:
|
944
|
+
self._log_if_verbose(
|
945
|
+
"debug",
|
946
|
+
f"Function {function_name} not found in function map",
|
947
|
+
)
|
948
|
+
return False
|
949
|
+
else:
|
950
|
+
self._log_if_verbose(
|
951
|
+
"debug",
|
952
|
+
"Output does not match expected function call schema",
|
953
|
+
)
|
954
|
+
return False
|
955
|
+
|
956
|
+
except Exception as e:
|
957
|
+
self._log_if_verbose(
|
958
|
+
"error", f"Error validating output: {e}"
|
959
|
+
)
|
960
|
+
return False
|
341
961
|
|
342
|
-
|
343
|
-
|
344
|
-
|
962
|
+
def convert_funcs_into_tools(self) -> None:
|
963
|
+
"""
|
964
|
+
Convert all functions in the tools list into OpenAI function calling format.
|
965
|
+
|
966
|
+
This method processes all functions in the tools list, validates them for
|
967
|
+
proper documentation and type hints, and converts them to OpenAI schemas.
|
968
|
+
It also creates a function map for execution.
|
969
|
+
|
970
|
+
Raises:
|
971
|
+
ToolValidationError: If tools are not properly configured
|
972
|
+
ToolDocumentationError: If functions lack required documentation
|
973
|
+
ToolTypeHintError: If functions lack required type hints
|
345
974
|
|
346
|
-
|
975
|
+
Examples:
|
976
|
+
>>> tool = BaseTool(tools=[func1, func2])
|
977
|
+
>>> tool.convert_funcs_into_tools()
|
978
|
+
"""
|
979
|
+
if self.tools is None:
|
980
|
+
self._log_if_verbose(
|
981
|
+
"warning", "No tools provided for conversion"
|
982
|
+
)
|
983
|
+
return
|
347
984
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
"Tools provided make sure the functions have documentation ++ type hints, otherwise tool execution won't be reliable."
|
985
|
+
if not isinstance(self.tools, list) or len(self.tools) == 0:
|
986
|
+
raise ToolValidationError(
|
987
|
+
"Tools must be a non-empty list"
|
352
988
|
)
|
353
989
|
|
354
|
-
|
355
|
-
|
356
|
-
|
990
|
+
try:
|
991
|
+
self._log_if_verbose(
|
992
|
+
"info",
|
993
|
+
f"Converting {len(self.tools)} functions into tools",
|
994
|
+
)
|
995
|
+
self._log_if_verbose(
|
996
|
+
"info",
|
997
|
+
"Ensure functions have documentation and type hints for reliable execution",
|
357
998
|
)
|
358
999
|
|
359
|
-
# Transform the tools into
|
360
|
-
self.convert_tool_into_openai_schema()
|
1000
|
+
# Transform the tools into OpenAI schema
|
1001
|
+
schema_result = self.convert_tool_into_openai_schema()
|
361
1002
|
|
362
|
-
|
1003
|
+
if schema_result:
|
1004
|
+
self._log_if_verbose(
|
1005
|
+
"info",
|
1006
|
+
"Successfully converted tools to OpenAI schema",
|
1007
|
+
)
|
1008
|
+
|
1009
|
+
# Create function calling map for all tools
|
363
1010
|
self.function_map = {
|
364
1011
|
tool.__name__: tool for tool in self.tools
|
365
1012
|
}
|
366
1013
|
|
367
|
-
|
1014
|
+
self._log_if_verbose(
|
1015
|
+
"info",
|
1016
|
+
f"Created function map with {len(self.function_map)} tools",
|
1017
|
+
)
|
1018
|
+
|
1019
|
+
except Exception as e:
|
1020
|
+
self._log_if_verbose(
|
1021
|
+
"error",
|
1022
|
+
f"Failed to convert functions into tools: {e}",
|
1023
|
+
)
|
1024
|
+
raise ToolValidationError(
|
1025
|
+
f"Failed to convert functions into tools: {e}"
|
1026
|
+
) from e
|
1027
|
+
|
1028
|
+
def convert_tool_into_openai_schema(self) -> dict[str, Any]:
|
1029
|
+
"""
|
1030
|
+
Convert tools into OpenAI function calling schema format.
|
1031
|
+
|
1032
|
+
This method processes all tools and converts them into a unified OpenAI
|
1033
|
+
function calling schema. Results are cached for performance.
|
1034
|
+
|
1035
|
+
Returns:
|
1036
|
+
dict[str, Any]: Combined OpenAI function calling schema
|
1037
|
+
|
1038
|
+
Raises:
|
1039
|
+
ToolValidationError: If tools validation fails
|
1040
|
+
ToolDocumentationError: If tool documentation is missing
|
1041
|
+
ToolTypeHintError: If tool type hints are missing
|
1042
|
+
FunctionSchemaError: If schema conversion fails
|
1043
|
+
|
1044
|
+
Examples:
|
1045
|
+
>>> tool = BaseTool(tools=[func1, func2])
|
1046
|
+
>>> schema = tool.convert_tool_into_openai_schema()
|
1047
|
+
"""
|
1048
|
+
if self.tools is None:
|
1049
|
+
raise ToolValidationError(
|
1050
|
+
"Tools must be set before schema conversion"
|
1051
|
+
)
|
1052
|
+
|
1053
|
+
if not isinstance(self.tools, list) or len(self.tools) == 0:
|
1054
|
+
raise ToolValidationError(
|
1055
|
+
"Tools must be a non-empty list"
|
1056
|
+
)
|
1057
|
+
|
1058
|
+
try:
|
1059
|
+
self._log_if_verbose(
|
1060
|
+
"info",
|
1061
|
+
"Converting tools into OpenAI function calling schema",
|
1062
|
+
)
|
1063
|
+
|
1064
|
+
tool_schemas = []
|
1065
|
+
failed_tools = []
|
1066
|
+
|
1067
|
+
for tool in self.tools:
|
1068
|
+
try:
|
1069
|
+
# Validate tool has documentation and type hints
|
1070
|
+
if not self.check_func_if_have_docs(tool):
|
1071
|
+
failed_tools.append(
|
1072
|
+
f"{tool.__name__} (missing documentation)"
|
1073
|
+
)
|
1074
|
+
continue
|
1075
|
+
|
1076
|
+
if not self.check_func_if_have_type_hints(tool):
|
1077
|
+
failed_tools.append(
|
1078
|
+
f"{tool.__name__} (missing type hints)"
|
1079
|
+
)
|
1080
|
+
continue
|
1081
|
+
|
1082
|
+
name = tool.__name__
|
1083
|
+
description = tool.__doc__
|
1084
|
+
|
1085
|
+
self._log_if_verbose(
|
1086
|
+
"info", f"Converting tool: {name}"
|
1087
|
+
)
|
1088
|
+
|
1089
|
+
tool_schema = (
|
1090
|
+
get_openai_function_schema_from_func(
|
1091
|
+
tool, name=name, description=description
|
1092
|
+
)
|
1093
|
+
)
|
1094
|
+
|
1095
|
+
self._log_if_verbose(
|
1096
|
+
"info", f"Tool {name} converted successfully"
|
1097
|
+
)
|
1098
|
+
tool_schemas.append(tool_schema)
|
1099
|
+
|
1100
|
+
except Exception as e:
|
1101
|
+
failed_tools.append(
|
1102
|
+
f"{tool.__name__} (conversion error: {e})"
|
1103
|
+
)
|
1104
|
+
self._log_if_verbose(
|
1105
|
+
"error",
|
1106
|
+
f"Failed to convert tool {tool.__name__}: {e}",
|
1107
|
+
)
|
1108
|
+
|
1109
|
+
if failed_tools:
|
1110
|
+
error_msg = f"Failed to convert tools: {', '.join(failed_tools)}"
|
1111
|
+
self._log_if_verbose("error", error_msg)
|
1112
|
+
raise FunctionSchemaError(error_msg)
|
1113
|
+
|
1114
|
+
if not tool_schemas:
|
1115
|
+
raise ToolValidationError(
|
1116
|
+
"No tools were successfully converted"
|
1117
|
+
)
|
1118
|
+
|
1119
|
+
# Combine all tool schemas into a single schema
|
1120
|
+
combined_schema = {
|
1121
|
+
"type": "function",
|
1122
|
+
"functions": [
|
1123
|
+
schema["function"] for schema in tool_schemas
|
1124
|
+
],
|
1125
|
+
}
|
1126
|
+
|
1127
|
+
self._log_if_verbose(
|
1128
|
+
"info",
|
1129
|
+
f"Successfully combined {len(tool_schemas)} tool schemas",
|
1130
|
+
)
|
1131
|
+
return combined_schema
|
1132
|
+
|
1133
|
+
except Exception as e:
|
1134
|
+
if isinstance(
|
1135
|
+
e, (ToolValidationError, FunctionSchemaError)
|
1136
|
+
):
|
1137
|
+
raise
|
1138
|
+
self._log_if_verbose(
|
1139
|
+
"error",
|
1140
|
+
f"Unexpected error during schema conversion: {e}",
|
1141
|
+
)
|
1142
|
+
raise FunctionSchemaError(
|
1143
|
+
f"Schema conversion failed: {e}"
|
1144
|
+
) from e
|
1145
|
+
|
1146
|
+
def check_func_if_have_docs(self, func: callable) -> bool:
|
1147
|
+
"""
|
1148
|
+
Check if a function has proper documentation.
|
1149
|
+
|
1150
|
+
This method validates that a function has a non-empty docstring,
|
1151
|
+
which is required for reliable tool execution.
|
1152
|
+
|
1153
|
+
Args:
|
1154
|
+
func (callable): The function to check
|
1155
|
+
|
1156
|
+
Returns:
|
1157
|
+
bool: True if function has documentation
|
1158
|
+
|
1159
|
+
Raises:
|
1160
|
+
ToolValidationError: If func is not callable
|
1161
|
+
ToolDocumentationError: If function lacks documentation
|
1162
|
+
|
1163
|
+
Examples:
|
1164
|
+
>>> def documented_func():
|
1165
|
+
... '''This function has docs'''
|
1166
|
+
... pass
|
1167
|
+
>>> tool = BaseTool()
|
1168
|
+
>>> has_docs = tool.check_func_if_have_docs(documented_func) # True
|
1169
|
+
"""
|
1170
|
+
if not callable(func):
|
1171
|
+
raise ToolValidationError("Input must be callable")
|
1172
|
+
|
1173
|
+
if func.__doc__ is not None and func.__doc__.strip():
|
1174
|
+
self._log_if_verbose(
|
1175
|
+
"debug", f"Function {func.__name__} has documentation"
|
1176
|
+
)
|
1177
|
+
return True
|
1178
|
+
else:
|
1179
|
+
error_msg = f"Function {func.__name__} does not have documentation"
|
1180
|
+
self._log_if_verbose("error", error_msg)
|
1181
|
+
raise ToolDocumentationError(error_msg)
|
1182
|
+
|
1183
|
+
def check_func_if_have_type_hints(self, func: callable) -> bool:
|
1184
|
+
"""
|
1185
|
+
Check if a function has proper type hints.
|
1186
|
+
|
1187
|
+
This method validates that a function has type annotations,
|
1188
|
+
which are required for reliable tool execution and schema generation.
|
1189
|
+
|
1190
|
+
Args:
|
1191
|
+
func (callable): The function to check
|
1192
|
+
|
1193
|
+
Returns:
|
1194
|
+
bool: True if function has type hints
|
368
1195
|
|
369
|
-
|
370
|
-
|
371
|
-
|
1196
|
+
Raises:
|
1197
|
+
ToolValidationError: If func is not callable
|
1198
|
+
ToolTypeHintError: If function lacks type hints
|
1199
|
+
|
1200
|
+
Examples:
|
1201
|
+
>>> def typed_func(x: int) -> str:
|
1202
|
+
... '''A typed function'''
|
1203
|
+
... return str(x)
|
1204
|
+
>>> tool = BaseTool()
|
1205
|
+
>>> has_hints = tool.check_func_if_have_type_hints(typed_func) # True
|
1206
|
+
"""
|
1207
|
+
if not callable(func):
|
1208
|
+
raise ToolValidationError("Input must be callable")
|
1209
|
+
|
1210
|
+
if func.__annotations__ and len(func.__annotations__) > 0:
|
1211
|
+
self._log_if_verbose(
|
1212
|
+
"debug", f"Function {func.__name__} has type hints"
|
1213
|
+
)
|
1214
|
+
return True
|
1215
|
+
else:
|
1216
|
+
error_msg = (
|
1217
|
+
f"Function {func.__name__} does not have type hints"
|
1218
|
+
)
|
1219
|
+
self._log_if_verbose("error", error_msg)
|
1220
|
+
raise ToolTypeHintError(error_msg)
|
1221
|
+
|
1222
|
+
def find_function_name(
|
1223
|
+
self, func_name: str
|
1224
|
+
) -> Optional[callable]:
|
1225
|
+
"""
|
1226
|
+
Find a function by name in the tools list.
|
1227
|
+
|
1228
|
+
This method searches for a function with the specified name
|
1229
|
+
in the current tools list.
|
1230
|
+
|
1231
|
+
Args:
|
1232
|
+
func_name (str): The name of the function to find
|
1233
|
+
|
1234
|
+
Returns:
|
1235
|
+
Optional[callable]: The function if found, None otherwise
|
1236
|
+
|
1237
|
+
Raises:
|
1238
|
+
ToolValidationError: If func_name is invalid or tools is None
|
1239
|
+
|
1240
|
+
Examples:
|
1241
|
+
>>> tool = BaseTool(tools=[my_function])
|
1242
|
+
>>> func = tool.find_function_name("my_function")
|
1243
|
+
"""
|
1244
|
+
if not func_name or not isinstance(func_name, str):
|
1245
|
+
raise ToolValidationError(
|
1246
|
+
"Function name must be a non-empty string"
|
1247
|
+
)
|
1248
|
+
|
1249
|
+
if self.tools is None:
|
1250
|
+
raise ToolValidationError(
|
1251
|
+
"Tools must be set before searching for functions"
|
1252
|
+
)
|
1253
|
+
|
1254
|
+
self._log_if_verbose(
|
1255
|
+
"debug", f"Searching for function: {func_name}"
|
1256
|
+
)
|
1257
|
+
|
1258
|
+
for func in self.tools:
|
1259
|
+
if func.__name__ == func_name:
|
1260
|
+
self._log_if_verbose(
|
1261
|
+
"debug", f"Found function: {func_name}"
|
1262
|
+
)
|
1263
|
+
return func
|
1264
|
+
|
1265
|
+
self._log_if_verbose(
|
1266
|
+
"debug", f"Function {func_name} not found"
|
372
1267
|
)
|
1268
|
+
return None
|
373
1269
|
|
374
|
-
|
1270
|
+
def function_to_dict(self, func: callable) -> dict:
|
1271
|
+
"""
|
1272
|
+
Convert a function to dictionary representation.
|
1273
|
+
|
1274
|
+
This method converts a callable function to its dictionary representation
|
1275
|
+
using the litellm function_to_dict utility. Results are cached for performance.
|
375
1276
|
|
376
|
-
|
377
|
-
|
378
|
-
if self.check_func_if_have_docs(
|
379
|
-
tool
|
380
|
-
) and self.check_func_if_have_type_hints(tool):
|
381
|
-
name = tool.__name__
|
382
|
-
description = tool.__doc__
|
1277
|
+
Args:
|
1278
|
+
func (callable): The function to convert
|
383
1279
|
|
384
|
-
|
385
|
-
|
1280
|
+
Returns:
|
1281
|
+
dict: Dictionary representation of the function
|
1282
|
+
|
1283
|
+
Raises:
|
1284
|
+
ToolValidationError: If func is not callable
|
1285
|
+
FunctionSchemaError: If conversion fails
|
1286
|
+
|
1287
|
+
Examples:
|
1288
|
+
>>> tool = BaseTool()
|
1289
|
+
>>> func_dict = tool.function_to_dict(my_function)
|
1290
|
+
"""
|
1291
|
+
if not callable(func):
|
1292
|
+
raise ToolValidationError("Input must be callable")
|
1293
|
+
|
1294
|
+
try:
|
1295
|
+
self._log_if_verbose(
|
1296
|
+
"debug",
|
1297
|
+
f"Converting function {func.__name__} to dict",
|
1298
|
+
)
|
1299
|
+
result = get_openai_function_schema_from_func(func)
|
1300
|
+
self._log_if_verbose(
|
1301
|
+
"debug", f"Successfully converted {func.__name__}"
|
1302
|
+
)
|
1303
|
+
return result
|
1304
|
+
except Exception as e:
|
1305
|
+
self._log_if_verbose(
|
1306
|
+
"error",
|
1307
|
+
f"Failed to convert function {func.__name__} to dict: {e}",
|
1308
|
+
)
|
1309
|
+
raise FunctionSchemaError(
|
1310
|
+
f"Failed to convert function to dict: {e}"
|
1311
|
+
) from e
|
1312
|
+
|
1313
|
+
def multiple_functions_to_dict(
|
1314
|
+
self, funcs: list[callable]
|
1315
|
+
) -> list[dict]:
|
1316
|
+
"""
|
1317
|
+
Convert multiple functions to dictionary representations.
|
1318
|
+
|
1319
|
+
This method converts a list of callable functions to their dictionary
|
1320
|
+
representations using the function_to_dict method.
|
1321
|
+
|
1322
|
+
Args:
|
1323
|
+
funcs (list[callable]): List of functions to convert
|
1324
|
+
|
1325
|
+
Returns:
|
1326
|
+
list[dict]: List of dictionary representations
|
1327
|
+
|
1328
|
+
Raises:
|
1329
|
+
ToolValidationError: If funcs validation fails
|
1330
|
+
FunctionSchemaError: If any conversion fails
|
1331
|
+
|
1332
|
+
Examples:
|
1333
|
+
>>> tool = BaseTool()
|
1334
|
+
>>> func_dicts = tool.multiple_functions_to_dict([func1, func2])
|
1335
|
+
"""
|
1336
|
+
if not isinstance(funcs, list):
|
1337
|
+
raise ToolValidationError("Input must be a list")
|
1338
|
+
|
1339
|
+
if len(funcs) == 0:
|
1340
|
+
raise ToolValidationError("Function list cannot be empty")
|
1341
|
+
|
1342
|
+
for i, func in enumerate(funcs):
|
1343
|
+
if not callable(func):
|
1344
|
+
raise ToolValidationError(
|
1345
|
+
f"Item at index {i} is not callable"
|
1346
|
+
)
|
1347
|
+
|
1348
|
+
try:
|
1349
|
+
self._log_if_verbose(
|
1350
|
+
"info",
|
1351
|
+
f"Converting {len(funcs)} functions to dictionaries",
|
1352
|
+
)
|
1353
|
+
result = (
|
1354
|
+
convert_multiple_functions_to_openai_function_schema(
|
1355
|
+
funcs
|
386
1356
|
)
|
387
|
-
|
388
|
-
|
1357
|
+
)
|
1358
|
+
self._log_if_verbose(
|
1359
|
+
"info",
|
1360
|
+
f"Successfully converted {len(funcs)} functions",
|
1361
|
+
)
|
1362
|
+
return result
|
1363
|
+
except Exception as e:
|
1364
|
+
self._log_if_verbose(
|
1365
|
+
"error", f"Failed to convert multiple functions: {e}"
|
1366
|
+
)
|
1367
|
+
raise FunctionSchemaError(
|
1368
|
+
f"Failed to convert multiple functions: {e}"
|
1369
|
+
) from e
|
1370
|
+
|
1371
|
+
def execute_function_with_dict(
|
1372
|
+
self, func_dict: dict, func_name: Optional[str] = None
|
1373
|
+
) -> Any:
|
1374
|
+
"""
|
1375
|
+
Execute a function using a dictionary of parameters.
|
1376
|
+
|
1377
|
+
This method executes a function by looking it up by name and passing
|
1378
|
+
the dictionary as keyword arguments to the function.
|
1379
|
+
|
1380
|
+
Args:
|
1381
|
+
func_dict (dict): Dictionary containing function parameters
|
1382
|
+
func_name (Optional[str]): Name of function to execute (if not in dict)
|
1383
|
+
|
1384
|
+
Returns:
|
1385
|
+
Any: Result of function execution
|
1386
|
+
|
1387
|
+
Raises:
|
1388
|
+
ToolValidationError: If parameters validation fails
|
1389
|
+
ToolNotFoundError: If function is not found
|
1390
|
+
ToolExecutionError: If function execution fails
|
1391
|
+
|
1392
|
+
Examples:
|
1393
|
+
>>> tool = BaseTool(tools=[add_function])
|
1394
|
+
>>> result = tool.execute_function_with_dict({"a": 1, "b": 2}, "add")
|
1395
|
+
"""
|
1396
|
+
if not isinstance(func_dict, dict):
|
1397
|
+
raise ToolValidationError(
|
1398
|
+
"func_dict must be a dictionary"
|
1399
|
+
)
|
1400
|
+
|
1401
|
+
try:
|
1402
|
+
self._log_if_verbose(
|
1403
|
+
"debug", f"Executing function with dict: {func_dict}"
|
1404
|
+
)
|
1405
|
+
|
1406
|
+
# Check if func_name is provided in the dict or as parameter
|
1407
|
+
if func_name is None:
|
1408
|
+
func_name = func_dict.get("name") or func_dict.get(
|
1409
|
+
"function_name"
|
389
1410
|
)
|
1411
|
+
if func_name is None:
|
1412
|
+
raise ToolValidationError(
|
1413
|
+
"Function name not provided and not found in func_dict"
|
1414
|
+
)
|
390
1415
|
|
391
|
-
|
392
|
-
|
1416
|
+
self._log_if_verbose(
|
1417
|
+
"debug", f"Looking for function: {func_name}"
|
1418
|
+
)
|
1419
|
+
|
1420
|
+
# Find the function
|
1421
|
+
func = self.find_function_name(func_name)
|
1422
|
+
if func is None:
|
1423
|
+
raise ToolNotFoundError(
|
1424
|
+
f"Function {func_name} not found"
|
393
1425
|
)
|
394
1426
|
|
395
|
-
|
1427
|
+
# Remove function name from parameters before executing
|
1428
|
+
execution_dict = func_dict.copy()
|
1429
|
+
execution_dict.pop("name", None)
|
1430
|
+
execution_dict.pop("function_name", None)
|
1431
|
+
|
1432
|
+
self._log_if_verbose(
|
1433
|
+
"debug", f"Executing function {func_name}"
|
1434
|
+
)
|
1435
|
+
result = func(**execution_dict)
|
1436
|
+
|
1437
|
+
self._log_if_verbose(
|
1438
|
+
"debug", f"Successfully executed {func_name}"
|
1439
|
+
)
|
1440
|
+
return result
|
1441
|
+
|
1442
|
+
except (ToolValidationError, ToolNotFoundError):
|
1443
|
+
raise
|
1444
|
+
except Exception as e:
|
1445
|
+
self._log_if_verbose(
|
1446
|
+
"error", f"Failed to execute function with dict: {e}"
|
1447
|
+
)
|
1448
|
+
raise ToolExecutionError(
|
1449
|
+
f"Failed to execute function with dict: {e}"
|
1450
|
+
) from e
|
1451
|
+
|
1452
|
+
def execute_multiple_functions_with_dict(
|
1453
|
+
self,
|
1454
|
+
func_dicts: list[dict],
|
1455
|
+
func_names: Optional[list[str]] = None,
|
1456
|
+
) -> list[Any]:
|
1457
|
+
"""
|
1458
|
+
Execute multiple functions using dictionaries of parameters.
|
1459
|
+
|
1460
|
+
This method executes multiple functions by processing a list of parameter
|
1461
|
+
dictionaries and optional function names.
|
1462
|
+
|
1463
|
+
Args:
|
1464
|
+
func_dicts (list[dict]): List of dictionaries containing function parameters
|
1465
|
+
func_names (Optional[list[str]]): Optional list of function names
|
1466
|
+
|
1467
|
+
Returns:
|
1468
|
+
list[Any]: List of results from function executions
|
1469
|
+
|
1470
|
+
Raises:
|
1471
|
+
ToolValidationError: If parameters validation fails
|
1472
|
+
ToolExecutionError: If any function execution fails
|
1473
|
+
|
1474
|
+
Examples:
|
1475
|
+
>>> tool = BaseTool(tools=[add, multiply])
|
1476
|
+
>>> results = tool.execute_multiple_functions_with_dict([
|
1477
|
+
... {"a": 1, "b": 2}, {"a": 3, "b": 4}
|
1478
|
+
... ], ["add", "multiply"])
|
1479
|
+
"""
|
1480
|
+
if not isinstance(func_dicts, list):
|
1481
|
+
raise ToolValidationError("func_dicts must be a list")
|
1482
|
+
|
1483
|
+
if len(func_dicts) == 0:
|
1484
|
+
raise ToolValidationError("func_dicts cannot be empty")
|
1485
|
+
|
1486
|
+
if func_names is not None:
|
1487
|
+
if not isinstance(func_names, list):
|
1488
|
+
raise ToolValidationError(
|
1489
|
+
"func_names must be a list if provided"
|
1490
|
+
)
|
1491
|
+
|
1492
|
+
if len(func_names) != len(func_dicts):
|
1493
|
+
raise ToolValidationError(
|
1494
|
+
"func_names length must match func_dicts length"
|
1495
|
+
)
|
1496
|
+
|
1497
|
+
try:
|
1498
|
+
self._log_if_verbose(
|
1499
|
+
"info",
|
1500
|
+
f"Executing {len(func_dicts)} functions with dictionaries",
|
1501
|
+
)
|
1502
|
+
|
1503
|
+
results = []
|
1504
|
+
|
1505
|
+
if func_names is None:
|
1506
|
+
# Execute using names from dictionaries
|
1507
|
+
for i, func_dict in enumerate(func_dicts):
|
1508
|
+
try:
|
1509
|
+
result = self.execute_function_with_dict(
|
1510
|
+
func_dict
|
1511
|
+
)
|
1512
|
+
results.append(result)
|
1513
|
+
except Exception as e:
|
1514
|
+
self._log_if_verbose(
|
1515
|
+
"error",
|
1516
|
+
f"Failed to execute function at index {i}: {e}",
|
1517
|
+
)
|
1518
|
+
raise ToolExecutionError(
|
1519
|
+
f"Failed to execute function at index {i}: {e}"
|
1520
|
+
) from e
|
396
1521
|
else:
|
397
|
-
|
398
|
-
|
1522
|
+
# Execute using provided names
|
1523
|
+
for i, (func_dict, func_name) in enumerate(
|
1524
|
+
zip(func_dicts, func_names)
|
1525
|
+
):
|
1526
|
+
try:
|
1527
|
+
result = self.execute_function_with_dict(
|
1528
|
+
func_dict, func_name
|
1529
|
+
)
|
1530
|
+
results.append(result)
|
1531
|
+
except Exception as e:
|
1532
|
+
self._log_if_verbose(
|
1533
|
+
"error",
|
1534
|
+
f"Failed to execute function {func_name} at index {i}: {e}",
|
1535
|
+
)
|
1536
|
+
raise ToolExecutionError(
|
1537
|
+
f"Failed to execute function {func_name} at index {i}: {e}"
|
1538
|
+
) from e
|
1539
|
+
|
1540
|
+
self._log_if_verbose(
|
1541
|
+
"info",
|
1542
|
+
f"Successfully executed {len(results)} functions",
|
1543
|
+
)
|
1544
|
+
return results
|
1545
|
+
|
1546
|
+
except ToolExecutionError:
|
1547
|
+
raise
|
1548
|
+
except Exception as e:
|
1549
|
+
self._log_if_verbose(
|
1550
|
+
"error", f"Failed to execute multiple functions: {e}"
|
1551
|
+
)
|
1552
|
+
raise ToolExecutionError(
|
1553
|
+
f"Failed to execute multiple functions: {e}"
|
1554
|
+
) from e
|
1555
|
+
|
1556
|
+
def validate_function_schema(
|
1557
|
+
self,
|
1558
|
+
schema: Optional[Union[List[Dict[str, Any]], Dict[str, Any]]],
|
1559
|
+
provider: str = "auto",
|
1560
|
+
) -> bool:
|
1561
|
+
"""
|
1562
|
+
Validate the schema of a function for different AI providers.
|
1563
|
+
|
1564
|
+
This method validates function call schemas for OpenAI, Anthropic, and other providers
|
1565
|
+
by checking if they conform to the expected structure and contain required fields.
|
1566
|
+
|
1567
|
+
Args:
|
1568
|
+
schema: Function schema(s) to validate - can be a single dict or list of dicts
|
1569
|
+
provider: Target provider format ("openai", "anthropic", "generic", "auto")
|
1570
|
+
"auto" attempts to detect the format automatically
|
1571
|
+
|
1572
|
+
Returns:
|
1573
|
+
bool: True if schema(s) are valid, False otherwise
|
1574
|
+
|
1575
|
+
Raises:
|
1576
|
+
ToolValidationError: If schema parameter is invalid
|
1577
|
+
|
1578
|
+
Examples:
|
1579
|
+
>>> tool = BaseTool()
|
1580
|
+
>>> openai_schema = {
|
1581
|
+
... "type": "function",
|
1582
|
+
... "function": {
|
1583
|
+
... "name": "add_numbers",
|
1584
|
+
... "description": "Add two numbers",
|
1585
|
+
... "parameters": {...}
|
1586
|
+
... }
|
1587
|
+
... }
|
1588
|
+
>>> tool.validate_function_schema(openai_schema, "openai") # True
|
1589
|
+
"""
|
1590
|
+
if schema is None:
|
1591
|
+
self._log_if_verbose(
|
1592
|
+
"warning", "Schema is None, validation skipped"
|
1593
|
+
)
|
1594
|
+
return False
|
1595
|
+
|
1596
|
+
try:
|
1597
|
+
# Handle list of schemas
|
1598
|
+
if isinstance(schema, list):
|
1599
|
+
if len(schema) == 0:
|
1600
|
+
self._log_if_verbose(
|
1601
|
+
"warning", "Empty schema list provided"
|
1602
|
+
)
|
1603
|
+
return False
|
1604
|
+
|
1605
|
+
# Validate each schema in the list
|
1606
|
+
for i, single_schema in enumerate(schema):
|
1607
|
+
if not self._validate_single_schema(
|
1608
|
+
single_schema, provider
|
1609
|
+
):
|
1610
|
+
self._log_if_verbose(
|
1611
|
+
"error",
|
1612
|
+
f"Schema at index {i} failed validation",
|
1613
|
+
)
|
1614
|
+
return False
|
1615
|
+
return True
|
1616
|
+
|
1617
|
+
# Handle single schema
|
1618
|
+
elif isinstance(schema, dict):
|
1619
|
+
return self._validate_single_schema(schema, provider)
|
1620
|
+
|
1621
|
+
else:
|
1622
|
+
raise ToolValidationError(
|
1623
|
+
"Schema must be a dictionary or list of dictionaries"
|
399
1624
|
)
|
400
1625
|
|
401
|
-
|
402
|
-
|
1626
|
+
except Exception as e:
|
1627
|
+
self._log_if_verbose(
|
1628
|
+
"error", f"Schema validation failed: {e}"
|
1629
|
+
)
|
1630
|
+
return False
|
1631
|
+
|
1632
|
+
def _validate_single_schema(
|
1633
|
+
self, schema: Dict[str, Any], provider: str = "auto"
|
1634
|
+
) -> bool:
|
1635
|
+
"""
|
1636
|
+
Validate a single function schema.
|
1637
|
+
|
1638
|
+
Args:
|
1639
|
+
schema: Single function schema dictionary
|
1640
|
+
provider: Target provider format
|
1641
|
+
|
1642
|
+
Returns:
|
1643
|
+
bool: True if schema is valid
|
1644
|
+
"""
|
1645
|
+
if not isinstance(schema, dict):
|
1646
|
+
self._log_if_verbose(
|
1647
|
+
"error", "Schema must be a dictionary"
|
1648
|
+
)
|
1649
|
+
return False
|
1650
|
+
|
1651
|
+
# Auto-detect provider if not specified
|
1652
|
+
if provider == "auto":
|
1653
|
+
provider = self._detect_schema_provider(schema)
|
1654
|
+
self._log_if_verbose(
|
1655
|
+
"debug", f"Auto-detected provider: {provider}"
|
1656
|
+
)
|
1657
|
+
|
1658
|
+
# Validate based on provider
|
1659
|
+
if provider == "openai":
|
1660
|
+
return self._validate_openai_schema(schema)
|
1661
|
+
elif provider == "anthropic":
|
1662
|
+
return self._validate_anthropic_schema(schema)
|
1663
|
+
elif provider == "generic":
|
1664
|
+
return self._validate_generic_schema(schema)
|
1665
|
+
else:
|
1666
|
+
self._log_if_verbose(
|
1667
|
+
"warning",
|
1668
|
+
f"Unknown provider '{provider}', falling back to generic validation",
|
1669
|
+
)
|
1670
|
+
return self._validate_generic_schema(schema)
|
1671
|
+
|
1672
|
+
def _detect_schema_provider(self, schema: Dict[str, Any]) -> str:
|
1673
|
+
"""
|
1674
|
+
Auto-detect the provider format of a schema.
|
1675
|
+
|
1676
|
+
Args:
|
1677
|
+
schema: Function schema dictionary
|
1678
|
+
|
1679
|
+
Returns:
|
1680
|
+
str: Detected provider ("openai", "anthropic", "generic")
|
1681
|
+
"""
|
1682
|
+
# OpenAI format detection
|
1683
|
+
if schema.get("type") == "function" and "function" in schema:
|
1684
|
+
return "openai"
|
1685
|
+
|
1686
|
+
# Anthropic format detection
|
1687
|
+
if "input_schema" in schema and "name" in schema:
|
1688
|
+
return "anthropic"
|
1689
|
+
|
1690
|
+
# Generic format detection
|
1691
|
+
if "name" in schema and (
|
1692
|
+
"parameters" in schema or "arguments" in schema
|
1693
|
+
):
|
1694
|
+
return "generic"
|
1695
|
+
|
1696
|
+
return "generic"
|
1697
|
+
|
1698
|
+
def _validate_openai_schema(self, schema: Dict[str, Any]) -> bool:
|
1699
|
+
"""
|
1700
|
+
Validate OpenAI function calling schema format.
|
1701
|
+
|
1702
|
+
Expected format:
|
1703
|
+
{
|
403
1704
|
"type": "function",
|
404
|
-
"
|
405
|
-
|
406
|
-
|
1705
|
+
"function": {
|
1706
|
+
"name": "function_name",
|
1707
|
+
"description": "Function description",
|
1708
|
+
"parameters": {
|
1709
|
+
"type": "object",
|
1710
|
+
"properties": {...},
|
1711
|
+
"required": [...]
|
1712
|
+
}
|
1713
|
+
}
|
407
1714
|
}
|
1715
|
+
"""
|
1716
|
+
try:
|
1717
|
+
# Check top-level structure
|
1718
|
+
if schema.get("type") != "function":
|
1719
|
+
self._log_if_verbose(
|
1720
|
+
"error",
|
1721
|
+
"OpenAI schema missing 'type': 'function'",
|
1722
|
+
)
|
1723
|
+
return False
|
1724
|
+
|
1725
|
+
if "function" not in schema:
|
1726
|
+
self._log_if_verbose(
|
1727
|
+
"error", "OpenAI schema missing 'function' key"
|
1728
|
+
)
|
1729
|
+
return False
|
1730
|
+
|
1731
|
+
function_def = schema["function"]
|
1732
|
+
if not isinstance(function_def, dict):
|
1733
|
+
self._log_if_verbose(
|
1734
|
+
"error", "OpenAI 'function' must be a dictionary"
|
1735
|
+
)
|
1736
|
+
return False
|
408
1737
|
|
409
|
-
|
1738
|
+
# Check required function fields
|
1739
|
+
if "name" not in function_def:
|
1740
|
+
self._log_if_verbose(
|
1741
|
+
"error", "OpenAI function missing 'name'"
|
1742
|
+
)
|
1743
|
+
return False
|
410
1744
|
|
411
|
-
|
412
|
-
|
1745
|
+
if (
|
1746
|
+
not isinstance(function_def["name"], str)
|
1747
|
+
or not function_def["name"].strip()
|
1748
|
+
):
|
1749
|
+
self._log_if_verbose(
|
1750
|
+
"error",
|
1751
|
+
"OpenAI function 'name' must be a non-empty string",
|
1752
|
+
)
|
1753
|
+
return False
|
1754
|
+
|
1755
|
+
# Description is optional but should be string if present
|
1756
|
+
if "description" in function_def:
|
1757
|
+
if not isinstance(function_def["description"], str):
|
1758
|
+
self._log_if_verbose(
|
1759
|
+
"error",
|
1760
|
+
"OpenAI function 'description' must be a string",
|
1761
|
+
)
|
1762
|
+
return False
|
1763
|
+
|
1764
|
+
# Validate parameters if present
|
1765
|
+
if "parameters" in function_def:
|
1766
|
+
if not self._validate_json_schema(
|
1767
|
+
function_def["parameters"]
|
1768
|
+
):
|
1769
|
+
self._log_if_verbose(
|
1770
|
+
"error", "OpenAI function parameters invalid"
|
1771
|
+
)
|
1772
|
+
return False
|
1773
|
+
|
1774
|
+
self._log_if_verbose(
|
1775
|
+
"debug",
|
1776
|
+
f"OpenAI schema for '{function_def['name']}' is valid",
|
1777
|
+
)
|
413
1778
|
return True
|
414
|
-
|
415
|
-
|
416
|
-
|
1779
|
+
|
1780
|
+
except Exception as e:
|
1781
|
+
self._log_if_verbose(
|
1782
|
+
"error", f"OpenAI schema validation error: {e}"
|
1783
|
+
)
|
1784
|
+
return False
|
1785
|
+
|
1786
|
+
def _validate_anthropic_schema(
|
1787
|
+
self, schema: Dict[str, Any]
|
1788
|
+
) -> bool:
|
1789
|
+
"""
|
1790
|
+
Validate Anthropic tool schema format.
|
1791
|
+
|
1792
|
+
Expected format:
|
1793
|
+
{
|
1794
|
+
"name": "function_name",
|
1795
|
+
"description": "Function description",
|
1796
|
+
"input_schema": {
|
1797
|
+
"type": "object",
|
1798
|
+
"properties": {...},
|
1799
|
+
"required": [...]
|
1800
|
+
}
|
1801
|
+
}
|
1802
|
+
"""
|
1803
|
+
try:
|
1804
|
+
# Check required fields
|
1805
|
+
if "name" not in schema:
|
1806
|
+
self._log_if_verbose(
|
1807
|
+
"error", "Anthropic schema missing 'name'"
|
1808
|
+
)
|
1809
|
+
return False
|
1810
|
+
|
1811
|
+
if (
|
1812
|
+
not isinstance(schema["name"], str)
|
1813
|
+
or not schema["name"].strip()
|
1814
|
+
):
|
1815
|
+
self._log_if_verbose(
|
1816
|
+
"error",
|
1817
|
+
"Anthropic 'name' must be a non-empty string",
|
1818
|
+
)
|
1819
|
+
return False
|
1820
|
+
|
1821
|
+
# Description is optional but should be string if present
|
1822
|
+
if "description" in schema:
|
1823
|
+
if not isinstance(schema["description"], str):
|
1824
|
+
self._log_if_verbose(
|
1825
|
+
"error",
|
1826
|
+
"Anthropic 'description' must be a string",
|
1827
|
+
)
|
1828
|
+
return False
|
1829
|
+
|
1830
|
+
# Validate input_schema if present
|
1831
|
+
if "input_schema" in schema:
|
1832
|
+
if not self._validate_json_schema(
|
1833
|
+
schema["input_schema"]
|
1834
|
+
):
|
1835
|
+
self._log_if_verbose(
|
1836
|
+
"error", "Anthropic input_schema invalid"
|
1837
|
+
)
|
1838
|
+
return False
|
1839
|
+
|
1840
|
+
self._log_if_verbose(
|
1841
|
+
"debug",
|
1842
|
+
f"Anthropic schema for '{schema['name']}' is valid",
|
1843
|
+
)
|
1844
|
+
return True
|
1845
|
+
|
1846
|
+
except Exception as e:
|
1847
|
+
self._log_if_verbose(
|
1848
|
+
"error", f"Anthropic schema validation error: {e}"
|
417
1849
|
)
|
418
|
-
|
419
|
-
|
1850
|
+
return False
|
1851
|
+
|
1852
|
+
def _validate_generic_schema(
|
1853
|
+
self, schema: Dict[str, Any]
|
1854
|
+
) -> bool:
|
1855
|
+
"""
|
1856
|
+
Validate generic function schema format.
|
1857
|
+
|
1858
|
+
Expected format (flexible):
|
1859
|
+
{
|
1860
|
+
"name": "function_name",
|
1861
|
+
"description": "Function description" (optional),
|
1862
|
+
"parameters": {...} or "arguments": {...}
|
1863
|
+
}
|
1864
|
+
"""
|
1865
|
+
try:
|
1866
|
+
# Check required name field
|
1867
|
+
if "name" not in schema:
|
1868
|
+
self._log_if_verbose(
|
1869
|
+
"error", "Generic schema missing 'name'"
|
1870
|
+
)
|
1871
|
+
return False
|
1872
|
+
|
1873
|
+
if (
|
1874
|
+
not isinstance(schema["name"], str)
|
1875
|
+
or not schema["name"].strip()
|
1876
|
+
):
|
1877
|
+
self._log_if_verbose(
|
1878
|
+
"error",
|
1879
|
+
"Generic 'name' must be a non-empty string",
|
1880
|
+
)
|
1881
|
+
return False
|
1882
|
+
|
1883
|
+
# Description is optional
|
1884
|
+
if "description" in schema:
|
1885
|
+
if not isinstance(schema["description"], str):
|
1886
|
+
self._log_if_verbose(
|
1887
|
+
"error",
|
1888
|
+
"Generic 'description' must be a string",
|
1889
|
+
)
|
1890
|
+
return False
|
1891
|
+
|
1892
|
+
# Validate parameters or arguments if present
|
1893
|
+
params_key = None
|
1894
|
+
if "parameters" in schema:
|
1895
|
+
params_key = "parameters"
|
1896
|
+
elif "arguments" in schema:
|
1897
|
+
params_key = "arguments"
|
1898
|
+
|
1899
|
+
if params_key:
|
1900
|
+
if not self._validate_json_schema(schema[params_key]):
|
1901
|
+
self._log_if_verbose(
|
1902
|
+
"error", f"Generic {params_key} invalid"
|
1903
|
+
)
|
1904
|
+
return False
|
1905
|
+
|
1906
|
+
self._log_if_verbose(
|
1907
|
+
"debug",
|
1908
|
+
f"Generic schema for '{schema['name']}' is valid",
|
1909
|
+
)
|
1910
|
+
return True
|
1911
|
+
|
1912
|
+
except Exception as e:
|
1913
|
+
self._log_if_verbose(
|
1914
|
+
"error", f"Generic schema validation error: {e}"
|
420
1915
|
)
|
1916
|
+
return False
|
1917
|
+
|
1918
|
+
def _validate_json_schema(
|
1919
|
+
self, json_schema: Dict[str, Any]
|
1920
|
+
) -> bool:
|
1921
|
+
"""
|
1922
|
+
Validate JSON Schema structure for function parameters.
|
1923
|
+
|
1924
|
+
Args:
|
1925
|
+
json_schema: JSON Schema dictionary
|
1926
|
+
|
1927
|
+
Returns:
|
1928
|
+
bool: True if valid JSON Schema structure
|
1929
|
+
"""
|
1930
|
+
try:
|
1931
|
+
if not isinstance(json_schema, dict):
|
1932
|
+
self._log_if_verbose(
|
1933
|
+
"error", "JSON schema must be a dictionary"
|
1934
|
+
)
|
1935
|
+
return False
|
1936
|
+
|
1937
|
+
# Check type field
|
1938
|
+
if "type" in json_schema:
|
1939
|
+
valid_types = [
|
1940
|
+
"object",
|
1941
|
+
"array",
|
1942
|
+
"string",
|
1943
|
+
"number",
|
1944
|
+
"integer",
|
1945
|
+
"boolean",
|
1946
|
+
"null",
|
1947
|
+
]
|
1948
|
+
if json_schema["type"] not in valid_types:
|
1949
|
+
self._log_if_verbose(
|
1950
|
+
"error",
|
1951
|
+
f"Invalid JSON schema type: {json_schema['type']}",
|
1952
|
+
)
|
1953
|
+
return False
|
1954
|
+
|
1955
|
+
# For object type, validate properties
|
1956
|
+
if json_schema.get("type") == "object":
|
1957
|
+
if "properties" in json_schema:
|
1958
|
+
if not isinstance(
|
1959
|
+
json_schema["properties"], dict
|
1960
|
+
):
|
1961
|
+
self._log_if_verbose(
|
1962
|
+
"error",
|
1963
|
+
"JSON schema 'properties' must be a dictionary",
|
1964
|
+
)
|
1965
|
+
return False
|
1966
|
+
|
1967
|
+
# Validate each property
|
1968
|
+
for prop_name, prop_def in json_schema[
|
1969
|
+
"properties"
|
1970
|
+
].items():
|
1971
|
+
if not isinstance(prop_def, dict):
|
1972
|
+
self._log_if_verbose(
|
1973
|
+
"error",
|
1974
|
+
f"Property '{prop_name}' definition must be a dictionary",
|
1975
|
+
)
|
1976
|
+
return False
|
1977
|
+
|
1978
|
+
# Recursively validate nested schemas
|
1979
|
+
if not self._validate_json_schema(prop_def):
|
1980
|
+
return False
|
1981
|
+
|
1982
|
+
# Validate required field
|
1983
|
+
if "required" in json_schema:
|
1984
|
+
if not isinstance(json_schema["required"], list):
|
1985
|
+
self._log_if_verbose(
|
1986
|
+
"error",
|
1987
|
+
"JSON schema 'required' must be a list",
|
1988
|
+
)
|
1989
|
+
return False
|
1990
|
+
|
1991
|
+
# Check that required fields exist in properties
|
1992
|
+
if "properties" in json_schema:
|
1993
|
+
properties = json_schema["properties"]
|
1994
|
+
for required_field in json_schema["required"]:
|
1995
|
+
if required_field not in properties:
|
1996
|
+
self._log_if_verbose(
|
1997
|
+
"error",
|
1998
|
+
f"Required field '{required_field}' not in properties",
|
1999
|
+
)
|
2000
|
+
return False
|
2001
|
+
|
2002
|
+
# For array type, validate items
|
2003
|
+
if json_schema.get("type") == "array":
|
2004
|
+
if "items" in json_schema:
|
2005
|
+
if not self._validate_json_schema(
|
2006
|
+
json_schema["items"]
|
2007
|
+
):
|
2008
|
+
return False
|
421
2009
|
|
422
|
-
def check_func_if_have_type_hints(self, func: callable):
|
423
|
-
if func.__annotations__ is not None:
|
424
2010
|
return True
|
2011
|
+
|
2012
|
+
except Exception as e:
|
2013
|
+
self._log_if_verbose(
|
2014
|
+
"error", f"JSON schema validation error: {e}"
|
2015
|
+
)
|
2016
|
+
return False
|
2017
|
+
|
2018
|
+
def get_schema_provider_format(
|
2019
|
+
self, schema: Dict[str, Any]
|
2020
|
+
) -> str:
|
2021
|
+
"""
|
2022
|
+
Get the detected provider format of a schema.
|
2023
|
+
|
2024
|
+
Args:
|
2025
|
+
schema: Function schema dictionary
|
2026
|
+
|
2027
|
+
Returns:
|
2028
|
+
str: Provider format ("openai", "anthropic", "generic", "unknown")
|
2029
|
+
|
2030
|
+
Examples:
|
2031
|
+
>>> tool = BaseTool()
|
2032
|
+
>>> provider = tool.get_schema_provider_format(my_schema)
|
2033
|
+
>>> print(provider) # "openai"
|
2034
|
+
"""
|
2035
|
+
if not isinstance(schema, dict):
|
2036
|
+
return "unknown"
|
2037
|
+
|
2038
|
+
return self._detect_schema_provider(schema)
|
2039
|
+
|
2040
|
+
def convert_schema_between_providers(
|
2041
|
+
self, schema: Dict[str, Any], target_provider: str
|
2042
|
+
) -> Dict[str, Any]:
|
2043
|
+
"""
|
2044
|
+
Convert a function schema between different provider formats.
|
2045
|
+
|
2046
|
+
Args:
|
2047
|
+
schema: Source function schema
|
2048
|
+
target_provider: Target provider format ("openai", "anthropic", "generic")
|
2049
|
+
|
2050
|
+
Returns:
|
2051
|
+
Dict[str, Any]: Converted schema
|
2052
|
+
|
2053
|
+
Raises:
|
2054
|
+
ToolValidationError: If conversion fails
|
2055
|
+
|
2056
|
+
Examples:
|
2057
|
+
>>> tool = BaseTool()
|
2058
|
+
>>> anthropic_schema = tool.convert_schema_between_providers(openai_schema, "anthropic")
|
2059
|
+
"""
|
2060
|
+
if not isinstance(schema, dict):
|
2061
|
+
raise ToolValidationError("Schema must be a dictionary")
|
2062
|
+
|
2063
|
+
source_provider = self._detect_schema_provider(schema)
|
2064
|
+
|
2065
|
+
if source_provider == target_provider:
|
2066
|
+
self._log_if_verbose(
|
2067
|
+
"debug", f"Schema already in {target_provider} format"
|
2068
|
+
)
|
2069
|
+
return schema.copy()
|
2070
|
+
|
2071
|
+
try:
|
2072
|
+
# Extract common fields
|
2073
|
+
name = self._extract_function_name(
|
2074
|
+
schema, source_provider
|
2075
|
+
)
|
2076
|
+
description = self._extract_function_description(
|
2077
|
+
schema, source_provider
|
2078
|
+
)
|
2079
|
+
parameters = self._extract_function_parameters(
|
2080
|
+
schema, source_provider
|
2081
|
+
)
|
2082
|
+
|
2083
|
+
# Convert to target format
|
2084
|
+
if target_provider == "openai":
|
2085
|
+
return self._build_openai_schema(
|
2086
|
+
name, description, parameters
|
2087
|
+
)
|
2088
|
+
elif target_provider == "anthropic":
|
2089
|
+
return self._build_anthropic_schema(
|
2090
|
+
name, description, parameters
|
2091
|
+
)
|
2092
|
+
elif target_provider == "generic":
|
2093
|
+
return self._build_generic_schema(
|
2094
|
+
name, description, parameters
|
2095
|
+
)
|
2096
|
+
else:
|
2097
|
+
raise ToolValidationError(
|
2098
|
+
f"Unknown target provider: {target_provider}"
|
2099
|
+
)
|
2100
|
+
|
2101
|
+
except Exception as e:
|
2102
|
+
self._log_if_verbose(
|
2103
|
+
"error", f"Schema conversion failed: {e}"
|
2104
|
+
)
|
2105
|
+
raise ToolValidationError(
|
2106
|
+
f"Failed to convert schema: {e}"
|
2107
|
+
) from e
|
2108
|
+
|
2109
|
+
def _extract_function_name(
|
2110
|
+
self, schema: Dict[str, Any], provider: str
|
2111
|
+
) -> str:
|
2112
|
+
"""Extract function name from schema based on provider format."""
|
2113
|
+
if provider == "openai":
|
2114
|
+
return schema.get("function", {}).get("name", "")
|
2115
|
+
else: # anthropic, generic
|
2116
|
+
return schema.get("name", "")
|
2117
|
+
|
2118
|
+
def _extract_function_description(
|
2119
|
+
self, schema: Dict[str, Any], provider: str
|
2120
|
+
) -> Optional[str]:
|
2121
|
+
"""Extract function description from schema based on provider format."""
|
2122
|
+
if provider == "openai":
|
2123
|
+
return schema.get("function", {}).get("description")
|
2124
|
+
else: # anthropic, generic
|
2125
|
+
return schema.get("description")
|
2126
|
+
|
2127
|
+
def _extract_function_parameters(
|
2128
|
+
self, schema: Dict[str, Any], provider: str
|
2129
|
+
) -> Optional[Dict[str, Any]]:
|
2130
|
+
"""Extract function parameters from schema based on provider format."""
|
2131
|
+
if provider == "openai":
|
2132
|
+
return schema.get("function", {}).get("parameters")
|
2133
|
+
elif provider == "anthropic":
|
2134
|
+
return schema.get("input_schema")
|
2135
|
+
else: # generic
|
2136
|
+
return schema.get("parameters") or schema.get("arguments")
|
2137
|
+
|
2138
|
+
def _build_openai_schema(
|
2139
|
+
self,
|
2140
|
+
name: str,
|
2141
|
+
description: Optional[str],
|
2142
|
+
parameters: Optional[Dict[str, Any]],
|
2143
|
+
) -> Dict[str, Any]:
|
2144
|
+
"""Build OpenAI format schema."""
|
2145
|
+
function_def = {"name": name}
|
2146
|
+
if description:
|
2147
|
+
function_def["description"] = description
|
2148
|
+
if parameters:
|
2149
|
+
function_def["parameters"] = parameters
|
2150
|
+
|
2151
|
+
return {"type": "function", "function": function_def}
|
2152
|
+
|
2153
|
+
def _build_anthropic_schema(
|
2154
|
+
self,
|
2155
|
+
name: str,
|
2156
|
+
description: Optional[str],
|
2157
|
+
parameters: Optional[Dict[str, Any]],
|
2158
|
+
) -> Dict[str, Any]:
|
2159
|
+
"""Build Anthropic format schema."""
|
2160
|
+
schema = {"name": name}
|
2161
|
+
if description:
|
2162
|
+
schema["description"] = description
|
2163
|
+
if parameters:
|
2164
|
+
schema["input_schema"] = parameters
|
2165
|
+
|
2166
|
+
return schema
|
2167
|
+
|
2168
|
+
def _build_generic_schema(
|
2169
|
+
self,
|
2170
|
+
name: str,
|
2171
|
+
description: Optional[str],
|
2172
|
+
parameters: Optional[Dict[str, Any]],
|
2173
|
+
) -> Dict[str, Any]:
|
2174
|
+
"""Build generic format schema."""
|
2175
|
+
schema = {"name": name}
|
2176
|
+
if description:
|
2177
|
+
schema["description"] = description
|
2178
|
+
if parameters:
|
2179
|
+
schema["parameters"] = parameters
|
2180
|
+
|
2181
|
+
return schema
|
2182
|
+
|
2183
|
+
def execute_function_calls_from_api_response(
|
2184
|
+
self,
|
2185
|
+
api_response: Union[Dict[str, Any], str, List[Any]],
|
2186
|
+
sequential: bool = False,
|
2187
|
+
max_workers: int = 4,
|
2188
|
+
return_as_string: bool = True,
|
2189
|
+
) -> Union[List[Any], List[str]]:
|
2190
|
+
"""
|
2191
|
+
Automatically detect and execute function calls from OpenAI or Anthropic API responses.
|
2192
|
+
|
2193
|
+
This method can handle:
|
2194
|
+
- OpenAI API responses with tool_calls
|
2195
|
+
- Anthropic API responses with tool use (including BaseModel objects)
|
2196
|
+
- Direct list of tool call objects (from OpenAI ChatCompletionMessageToolCall or Anthropic BaseModels)
|
2197
|
+
- Pydantic BaseModel objects from Anthropic responses
|
2198
|
+
- Parallel function execution with concurrent.futures or sequential execution
|
2199
|
+
- Multiple function calls in a single response
|
2200
|
+
|
2201
|
+
Args:
|
2202
|
+
api_response (Union[Dict[str, Any], str, List[Any]]): The API response containing function calls
|
2203
|
+
sequential (bool): If True, execute functions sequentially. If False, execute in parallel (default)
|
2204
|
+
max_workers (int): Maximum number of worker threads for parallel execution (default: 4)
|
2205
|
+
return_as_string (bool): If True, return results as formatted strings (default: True)
|
2206
|
+
|
2207
|
+
Returns:
|
2208
|
+
Union[List[Any], List[str]]: List of results from executed functions
|
2209
|
+
|
2210
|
+
Raises:
|
2211
|
+
ToolValidationError: If API response validation fails
|
2212
|
+
ToolNotFoundError: If any function is not found
|
2213
|
+
ToolExecutionError: If function execution fails
|
2214
|
+
|
2215
|
+
Examples:
|
2216
|
+
>>> # OpenAI API response example
|
2217
|
+
>>> openai_response = {
|
2218
|
+
... "choices": [{"message": {"tool_calls": [...]}}]
|
2219
|
+
... }
|
2220
|
+
>>> tool = BaseTool(tools=[weather_function])
|
2221
|
+
>>> results = tool.execute_function_calls_from_api_response(openai_response)
|
2222
|
+
|
2223
|
+
>>> # Direct tool calls list (including BaseModel objects)
|
2224
|
+
>>> tool_calls = [ChatCompletionMessageToolCall(...), ...]
|
2225
|
+
>>> results = tool.execute_function_calls_from_api_response(tool_calls)
|
2226
|
+
"""
|
2227
|
+
if api_response is None:
|
2228
|
+
raise ToolValidationError("API response cannot be None")
|
2229
|
+
|
2230
|
+
# Handle direct list of tool call objects (e.g., from OpenAI ChatCompletionMessageToolCall or Anthropic BaseModels)
|
2231
|
+
if isinstance(api_response, list):
|
2232
|
+
self._log_if_verbose(
|
2233
|
+
"info",
|
2234
|
+
"Processing direct list of tool call objects",
|
2235
|
+
)
|
2236
|
+
function_calls = (
|
2237
|
+
self._extract_function_calls_from_tool_call_objects(
|
2238
|
+
api_response
|
2239
|
+
)
|
2240
|
+
)
|
2241
|
+
# Handle single BaseModel object (common with Anthropic responses)
|
2242
|
+
elif isinstance(api_response, BaseModel):
|
2243
|
+
self._log_if_verbose(
|
2244
|
+
"info",
|
2245
|
+
"Processing single BaseModel object (likely Anthropic response)",
|
2246
|
+
)
|
2247
|
+
# Convert BaseModel to dict and process
|
2248
|
+
api_response_dict = api_response.model_dump()
|
2249
|
+
function_calls = (
|
2250
|
+
self._extract_function_calls_from_response(
|
2251
|
+
api_response_dict
|
2252
|
+
)
|
2253
|
+
)
|
425
2254
|
else:
|
426
|
-
|
427
|
-
|
2255
|
+
# Convert string to dict if needed
|
2256
|
+
if isinstance(api_response, str):
|
2257
|
+
try:
|
2258
|
+
api_response = json.loads(api_response)
|
2259
|
+
except json.JSONDecodeError as e:
|
2260
|
+
raise ToolValidationError(
|
2261
|
+
f"Invalid JSON in API response: {e}"
|
2262
|
+
) from e
|
2263
|
+
|
2264
|
+
if not isinstance(api_response, dict):
|
2265
|
+
raise ToolValidationError(
|
2266
|
+
"API response must be a dictionary, JSON string, BaseModel, or list of tool calls"
|
2267
|
+
)
|
2268
|
+
|
2269
|
+
# Extract function calls from dictionary response
|
2270
|
+
function_calls = (
|
2271
|
+
self._extract_function_calls_from_response(
|
2272
|
+
api_response
|
2273
|
+
)
|
428
2274
|
)
|
429
|
-
|
430
|
-
|
2275
|
+
|
2276
|
+
if self.function_map is None and self.tools is None:
|
2277
|
+
raise ToolValidationError(
|
2278
|
+
"Either function_map or tools must be set before executing function calls"
|
2279
|
+
)
|
2280
|
+
|
2281
|
+
try:
|
2282
|
+
if not function_calls:
|
2283
|
+
self._log_if_verbose(
|
2284
|
+
"warning",
|
2285
|
+
"No function calls found in API response",
|
2286
|
+
)
|
2287
|
+
return []
|
2288
|
+
|
2289
|
+
self._log_if_verbose(
|
2290
|
+
"info",
|
2291
|
+
f"Found {len(function_calls)} function call(s)",
|
2292
|
+
)
|
2293
|
+
|
2294
|
+
# Ensure function_map is available
|
2295
|
+
if self.function_map is None and self.tools is not None:
|
2296
|
+
self.function_map = {
|
2297
|
+
tool.__name__: tool for tool in self.tools
|
2298
|
+
}
|
2299
|
+
|
2300
|
+
# Execute function calls
|
2301
|
+
if sequential:
|
2302
|
+
results = self._execute_function_calls_sequential(
|
2303
|
+
function_calls
|
2304
|
+
)
|
2305
|
+
else:
|
2306
|
+
results = self._execute_function_calls_parallel(
|
2307
|
+
function_calls, max_workers
|
2308
|
+
)
|
2309
|
+
|
2310
|
+
# Format results as strings if requested
|
2311
|
+
if return_as_string:
|
2312
|
+
return self._format_results_as_strings(
|
2313
|
+
results, function_calls
|
2314
|
+
)
|
2315
|
+
else:
|
2316
|
+
return results
|
2317
|
+
|
2318
|
+
except Exception as e:
|
2319
|
+
self._log_if_verbose(
|
2320
|
+
"error",
|
2321
|
+
f"Failed to execute function calls from API response: {e}",
|
2322
|
+
)
|
2323
|
+
raise ToolExecutionError(
|
2324
|
+
f"Failed to execute function calls from API response: {e}"
|
2325
|
+
) from e
|
2326
|
+
|
2327
|
+
def _extract_function_calls_from_response(
|
2328
|
+
self, response: Dict[str, Any]
|
2329
|
+
) -> List[Dict[str, Any]]:
|
2330
|
+
"""
|
2331
|
+
Extract function calls from different API response formats.
|
2332
|
+
|
2333
|
+
Args:
|
2334
|
+
response: API response dictionary
|
2335
|
+
|
2336
|
+
Returns:
|
2337
|
+
List[Dict[str, Any]]: List of standardized function call dictionaries
|
2338
|
+
"""
|
2339
|
+
function_calls = []
|
2340
|
+
|
2341
|
+
# Try OpenAI format first
|
2342
|
+
openai_calls = self._extract_openai_function_calls(response)
|
2343
|
+
if openai_calls:
|
2344
|
+
function_calls.extend(openai_calls)
|
2345
|
+
self._log_if_verbose(
|
2346
|
+
"debug",
|
2347
|
+
f"Extracted {len(openai_calls)} OpenAI function calls",
|
2348
|
+
)
|
2349
|
+
|
2350
|
+
# Try Anthropic format
|
2351
|
+
anthropic_calls = self._extract_anthropic_function_calls(
|
2352
|
+
response
|
2353
|
+
)
|
2354
|
+
if anthropic_calls:
|
2355
|
+
function_calls.extend(anthropic_calls)
|
2356
|
+
self._log_if_verbose(
|
2357
|
+
"debug",
|
2358
|
+
f"Extracted {len(anthropic_calls)} Anthropic function calls",
|
431
2359
|
)
|
432
2360
|
|
2361
|
+
# Try generic format (direct function calls)
|
2362
|
+
generic_calls = self._extract_generic_function_calls(response)
|
2363
|
+
if generic_calls:
|
2364
|
+
function_calls.extend(generic_calls)
|
2365
|
+
self._log_if_verbose(
|
2366
|
+
"debug",
|
2367
|
+
f"Extracted {len(generic_calls)} generic function calls",
|
2368
|
+
)
|
2369
|
+
|
2370
|
+
return function_calls
|
2371
|
+
|
2372
|
+
def _extract_openai_function_calls(
|
2373
|
+
self, response: Dict[str, Any]
|
2374
|
+
) -> List[Dict[str, Any]]:
|
2375
|
+
"""Extract function calls from OpenAI API response format."""
|
2376
|
+
function_calls = []
|
2377
|
+
|
2378
|
+
try:
|
2379
|
+
# Check if the response itself is a single function call object
|
2380
|
+
if (
|
2381
|
+
response.get("type") == "function"
|
2382
|
+
and "function" in response
|
2383
|
+
):
|
2384
|
+
function_info = response.get("function", {})
|
2385
|
+
name = function_info.get("name")
|
2386
|
+
arguments_str = function_info.get("arguments", "{}")
|
2387
|
+
|
2388
|
+
if name:
|
2389
|
+
try:
|
2390
|
+
# Parse arguments JSON string
|
2391
|
+
arguments = (
|
2392
|
+
json.loads(arguments_str)
|
2393
|
+
if isinstance(arguments_str, str)
|
2394
|
+
else arguments_str
|
2395
|
+
)
|
2396
|
+
|
2397
|
+
function_calls.append(
|
2398
|
+
{
|
2399
|
+
"name": name,
|
2400
|
+
"arguments": arguments,
|
2401
|
+
"id": response.get("id"),
|
2402
|
+
"type": "openai",
|
2403
|
+
}
|
2404
|
+
)
|
2405
|
+
except json.JSONDecodeError as e:
|
2406
|
+
self._log_if_verbose(
|
2407
|
+
"error",
|
2408
|
+
f"Failed to parse arguments for {name}: {e}",
|
2409
|
+
)
|
2410
|
+
|
2411
|
+
# Check for choices[].message.tool_calls format
|
2412
|
+
choices = response.get("choices", [])
|
2413
|
+
for choice in choices:
|
2414
|
+
message = choice.get("message", {})
|
2415
|
+
tool_calls = message.get("tool_calls", [])
|
2416
|
+
|
2417
|
+
for tool_call in tool_calls:
|
2418
|
+
if tool_call.get("type") == "function":
|
2419
|
+
function_info = tool_call.get("function", {})
|
2420
|
+
name = function_info.get("name")
|
2421
|
+
arguments_str = function_info.get(
|
2422
|
+
"arguments", "{}"
|
2423
|
+
)
|
2424
|
+
|
2425
|
+
if name:
|
2426
|
+
try:
|
2427
|
+
# Parse arguments JSON string
|
2428
|
+
arguments = (
|
2429
|
+
json.loads(arguments_str)
|
2430
|
+
if isinstance(arguments_str, str)
|
2431
|
+
else arguments_str
|
2432
|
+
)
|
2433
|
+
|
2434
|
+
function_calls.append(
|
2435
|
+
{
|
2436
|
+
"name": name,
|
2437
|
+
"arguments": arguments,
|
2438
|
+
"id": tool_call.get("id"),
|
2439
|
+
"type": "openai",
|
2440
|
+
}
|
2441
|
+
)
|
2442
|
+
except json.JSONDecodeError as e:
|
2443
|
+
self._log_if_verbose(
|
2444
|
+
"error",
|
2445
|
+
f"Failed to parse arguments for {name}: {e}",
|
2446
|
+
)
|
2447
|
+
|
2448
|
+
# Also check for direct tool_calls in response root (array of function calls)
|
2449
|
+
if "tool_calls" in response:
|
2450
|
+
tool_calls = response["tool_calls"]
|
2451
|
+
if isinstance(tool_calls, list):
|
2452
|
+
for tool_call in tool_calls:
|
2453
|
+
if tool_call.get("type") == "function":
|
2454
|
+
function_info = tool_call.get(
|
2455
|
+
"function", {}
|
2456
|
+
)
|
2457
|
+
name = function_info.get("name")
|
2458
|
+
arguments_str = function_info.get(
|
2459
|
+
"arguments", "{}"
|
2460
|
+
)
|
2461
|
+
|
2462
|
+
if name:
|
2463
|
+
try:
|
2464
|
+
arguments = (
|
2465
|
+
json.loads(arguments_str)
|
2466
|
+
if isinstance(
|
2467
|
+
arguments_str, str
|
2468
|
+
)
|
2469
|
+
else arguments_str
|
2470
|
+
)
|
2471
|
+
|
2472
|
+
function_calls.append(
|
2473
|
+
{
|
2474
|
+
"name": name,
|
2475
|
+
"arguments": arguments,
|
2476
|
+
"id": tool_call.get("id"),
|
2477
|
+
"type": "openai",
|
2478
|
+
}
|
2479
|
+
)
|
2480
|
+
except json.JSONDecodeError as e:
|
2481
|
+
self._log_if_verbose(
|
2482
|
+
"error",
|
2483
|
+
f"Failed to parse arguments for {name}: {e}",
|
2484
|
+
)
|
2485
|
+
|
2486
|
+
except Exception as e:
|
2487
|
+
self._log_if_verbose(
|
2488
|
+
"debug",
|
2489
|
+
f"Failed to extract OpenAI function calls: {e}",
|
2490
|
+
)
|
2491
|
+
|
2492
|
+
return function_calls
|
2493
|
+
|
2494
|
+
def _extract_anthropic_function_calls(
|
2495
|
+
self, response: Dict[str, Any]
|
2496
|
+
) -> List[Dict[str, Any]]:
|
2497
|
+
"""Extract function calls from Anthropic API response format."""
|
2498
|
+
function_calls = []
|
2499
|
+
|
2500
|
+
try:
|
2501
|
+
# Check for content[].type == "tool_use" format
|
2502
|
+
content = response.get("content", [])
|
2503
|
+
if isinstance(content, list):
|
2504
|
+
for item in content:
|
2505
|
+
if (
|
2506
|
+
isinstance(item, dict)
|
2507
|
+
and item.get("type") == "tool_use"
|
2508
|
+
):
|
2509
|
+
name = item.get("name")
|
2510
|
+
input_data = item.get("input", {})
|
2511
|
+
|
2512
|
+
if name:
|
2513
|
+
function_calls.append(
|
2514
|
+
{
|
2515
|
+
"name": name,
|
2516
|
+
"arguments": input_data,
|
2517
|
+
"id": item.get("id"),
|
2518
|
+
"type": "anthropic",
|
2519
|
+
}
|
2520
|
+
)
|
2521
|
+
|
2522
|
+
# Also check for direct tool_use format
|
2523
|
+
if response.get("type") == "tool_use":
|
2524
|
+
name = response.get("name")
|
2525
|
+
input_data = response.get("input", {})
|
2526
|
+
|
2527
|
+
if name:
|
2528
|
+
function_calls.append(
|
2529
|
+
{
|
2530
|
+
"name": name,
|
2531
|
+
"arguments": input_data,
|
2532
|
+
"id": response.get("id"),
|
2533
|
+
"type": "anthropic",
|
2534
|
+
}
|
2535
|
+
)
|
2536
|
+
|
2537
|
+
# Check for tool_calls array with Anthropic format (BaseModel converted)
|
2538
|
+
if "tool_calls" in response:
|
2539
|
+
tool_calls = response["tool_calls"]
|
2540
|
+
if isinstance(tool_calls, list):
|
2541
|
+
for tool_call in tool_calls:
|
2542
|
+
# Handle BaseModel objects that have been converted to dict
|
2543
|
+
if isinstance(tool_call, dict):
|
2544
|
+
# Check for Anthropic-style function call
|
2545
|
+
if (
|
2546
|
+
tool_call.get("type") == "tool_use"
|
2547
|
+
or "input" in tool_call
|
2548
|
+
):
|
2549
|
+
name = tool_call.get("name")
|
2550
|
+
input_data = tool_call.get(
|
2551
|
+
"input", {}
|
2552
|
+
)
|
2553
|
+
|
2554
|
+
if name:
|
2555
|
+
function_calls.append(
|
2556
|
+
{
|
2557
|
+
"name": name,
|
2558
|
+
"arguments": input_data,
|
2559
|
+
"id": tool_call.get("id"),
|
2560
|
+
"type": "anthropic",
|
2561
|
+
}
|
2562
|
+
)
|
2563
|
+
# Also check if it has function.name pattern but with input
|
2564
|
+
elif "function" in tool_call:
|
2565
|
+
function_info = tool_call.get(
|
2566
|
+
"function", {}
|
2567
|
+
)
|
2568
|
+
name = function_info.get("name")
|
2569
|
+
# For Anthropic, prioritize 'input' over 'arguments'
|
2570
|
+
input_data = function_info.get(
|
2571
|
+
"input"
|
2572
|
+
) or function_info.get(
|
2573
|
+
"arguments", {}
|
2574
|
+
)
|
2575
|
+
|
2576
|
+
if name:
|
2577
|
+
function_calls.append(
|
2578
|
+
{
|
2579
|
+
"name": name,
|
2580
|
+
"arguments": input_data,
|
2581
|
+
"id": tool_call.get("id"),
|
2582
|
+
"type": "anthropic",
|
2583
|
+
}
|
2584
|
+
)
|
2585
|
+
|
2586
|
+
except Exception as e:
|
2587
|
+
self._log_if_verbose(
|
2588
|
+
"debug",
|
2589
|
+
f"Failed to extract Anthropic function calls: {e}",
|
2590
|
+
)
|
2591
|
+
|
2592
|
+
return function_calls
|
2593
|
+
|
2594
|
+
def _extract_generic_function_calls(
|
2595
|
+
self, response: Dict[str, Any]
|
2596
|
+
) -> List[Dict[str, Any]]:
|
2597
|
+
"""Extract function calls from generic formats."""
|
2598
|
+
function_calls = []
|
2599
|
+
|
2600
|
+
try:
|
2601
|
+
# Check if response itself is a function call
|
2602
|
+
if "name" in response and (
|
2603
|
+
"arguments" in response or "parameters" in response
|
2604
|
+
):
|
2605
|
+
name = response.get("name")
|
2606
|
+
arguments = response.get("arguments") or response.get(
|
2607
|
+
"parameters", {}
|
2608
|
+
)
|
2609
|
+
|
2610
|
+
if name:
|
2611
|
+
function_calls.append(
|
2612
|
+
{
|
2613
|
+
"name": name,
|
2614
|
+
"arguments": arguments,
|
2615
|
+
"id": response.get("id"),
|
2616
|
+
"type": "generic",
|
2617
|
+
}
|
2618
|
+
)
|
2619
|
+
|
2620
|
+
# Check for function_calls list
|
2621
|
+
if "function_calls" in response:
|
2622
|
+
for call in response["function_calls"]:
|
2623
|
+
if isinstance(call, dict) and "name" in call:
|
2624
|
+
name = call.get("name")
|
2625
|
+
arguments = call.get("arguments") or call.get(
|
2626
|
+
"parameters", {}
|
2627
|
+
)
|
2628
|
+
|
2629
|
+
if name:
|
2630
|
+
function_calls.append(
|
2631
|
+
{
|
2632
|
+
"name": name,
|
2633
|
+
"arguments": arguments,
|
2634
|
+
"id": call.get("id"),
|
2635
|
+
"type": "generic",
|
2636
|
+
}
|
2637
|
+
)
|
2638
|
+
|
2639
|
+
except Exception as e:
|
2640
|
+
self._log_if_verbose(
|
2641
|
+
"debug",
|
2642
|
+
f"Failed to extract generic function calls: {e}",
|
2643
|
+
)
|
2644
|
+
|
2645
|
+
return function_calls
|
2646
|
+
|
2647
|
+
def _execute_function_calls_sequential(
|
2648
|
+
self, function_calls: List[Dict[str, Any]]
|
2649
|
+
) -> List[Any]:
|
2650
|
+
"""Execute function calls sequentially."""
|
2651
|
+
results = []
|
2652
|
+
|
2653
|
+
for i, call in enumerate(function_calls):
|
2654
|
+
try:
|
2655
|
+
self._log_if_verbose(
|
2656
|
+
"info",
|
2657
|
+
f"Executing function {call['name']} ({i+1}/{len(function_calls)})",
|
2658
|
+
)
|
2659
|
+
result = self._execute_single_function_call(call)
|
2660
|
+
results.append(result)
|
2661
|
+
self._log_if_verbose(
|
2662
|
+
"info", f"Successfully executed {call['name']}"
|
2663
|
+
)
|
2664
|
+
except Exception as e:
|
2665
|
+
self._log_if_verbose(
|
2666
|
+
"error", f"Failed to execute {call['name']}: {e}"
|
2667
|
+
)
|
2668
|
+
raise ToolExecutionError(
|
2669
|
+
f"Failed to execute function {call['name']}: {e}"
|
2670
|
+
) from e
|
2671
|
+
|
2672
|
+
return results
|
2673
|
+
|
2674
|
+
def _execute_function_calls_parallel(
|
2675
|
+
self, function_calls: List[Dict[str, Any]], max_workers: int
|
2676
|
+
) -> List[Any]:
|
2677
|
+
"""Execute function calls in parallel using concurrent.futures ThreadPoolExecutor."""
|
2678
|
+
self._log_if_verbose(
|
2679
|
+
"info",
|
2680
|
+
f"Executing {len(function_calls)} function calls in parallel with {max_workers} workers",
|
2681
|
+
)
|
2682
|
+
|
2683
|
+
results = [None] * len(
|
2684
|
+
function_calls
|
2685
|
+
) # Pre-allocate results list to maintain order
|
2686
|
+
|
2687
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
2688
|
+
# Submit all function calls to the executor
|
2689
|
+
future_to_index = {}
|
2690
|
+
for i, call in enumerate(function_calls):
|
2691
|
+
future = executor.submit(
|
2692
|
+
self._execute_single_function_call, call
|
2693
|
+
)
|
2694
|
+
future_to_index[future] = i
|
2695
|
+
|
2696
|
+
# Collect results as they complete
|
2697
|
+
for future in as_completed(future_to_index):
|
2698
|
+
index = future_to_index[future]
|
2699
|
+
call = function_calls[index]
|
2700
|
+
|
2701
|
+
try:
|
2702
|
+
result = future.result()
|
2703
|
+
results[index] = result
|
2704
|
+
self._log_if_verbose(
|
2705
|
+
"info",
|
2706
|
+
f"Successfully executed {call['name']} (index {index})",
|
2707
|
+
)
|
2708
|
+
except Exception as e:
|
2709
|
+
self._log_if_verbose(
|
2710
|
+
"error",
|
2711
|
+
f"Failed to execute {call['name']} (index {index}): {e}",
|
2712
|
+
)
|
2713
|
+
raise ToolExecutionError(
|
2714
|
+
f"Failed to execute function {call['name']}: {e}"
|
2715
|
+
) from e
|
2716
|
+
|
2717
|
+
return results
|
2718
|
+
|
2719
|
+
def _execute_single_function_call(
|
2720
|
+
self, call: Union[Dict[str, Any], BaseModel]
|
2721
|
+
) -> Any:
|
2722
|
+
"""Execute a single function call."""
|
2723
|
+
if isinstance(call, BaseModel):
|
2724
|
+
call = call.model_dump()
|
2725
|
+
|
2726
|
+
name = call.get("name")
|
2727
|
+
arguments = call.get("arguments", {})
|
2728
|
+
|
2729
|
+
if not name:
|
2730
|
+
raise ToolValidationError("Function call missing name")
|
2731
|
+
|
2732
|
+
# Find the function
|
2733
|
+
if self.function_map and name in self.function_map:
|
2734
|
+
func = self.function_map[name]
|
2735
|
+
elif self.tools:
|
2736
|
+
func = self.find_function_name(name)
|
2737
|
+
if func is None:
|
2738
|
+
raise ToolNotFoundError(
|
2739
|
+
f"Function {name} not found in tools"
|
2740
|
+
)
|
2741
|
+
else:
|
2742
|
+
raise ToolNotFoundError(f"Function {name} not found")
|
2743
|
+
|
2744
|
+
# Execute the function
|
2745
|
+
try:
|
2746
|
+
if isinstance(arguments, dict):
|
2747
|
+
result = func(**arguments)
|
2748
|
+
else:
|
2749
|
+
result = func(arguments)
|
2750
|
+
return result
|
2751
|
+
except Exception as e:
|
2752
|
+
raise ToolExecutionError(
|
2753
|
+
f"Error executing function {name}: {e}"
|
2754
|
+
) from e
|
2755
|
+
|
2756
|
+
def detect_api_response_format(
|
2757
|
+
self, response: Union[Dict[str, Any], str, BaseModel]
|
2758
|
+
) -> str:
|
2759
|
+
"""
|
2760
|
+
Detect the format of an API response.
|
2761
|
+
|
2762
|
+
Args:
|
2763
|
+
response: API response to analyze (can be BaseModel, dict, or string)
|
2764
|
+
|
2765
|
+
Returns:
|
2766
|
+
str: Detected format ("openai", "anthropic", "generic", "unknown")
|
2767
|
+
|
2768
|
+
Examples:
|
2769
|
+
>>> tool = BaseTool()
|
2770
|
+
>>> format_type = tool.detect_api_response_format(openai_response)
|
2771
|
+
>>> print(format_type) # "openai"
|
2772
|
+
"""
|
2773
|
+
# Handle BaseModel objects
|
2774
|
+
if isinstance(response, BaseModel):
|
2775
|
+
self._log_if_verbose(
|
2776
|
+
"debug",
|
2777
|
+
"Converting BaseModel response for format detection",
|
2778
|
+
)
|
2779
|
+
response = response.model_dump()
|
2780
|
+
|
2781
|
+
if isinstance(response, str):
|
2782
|
+
try:
|
2783
|
+
response = json.loads(response)
|
2784
|
+
except json.JSONDecodeError:
|
2785
|
+
return "unknown"
|
2786
|
+
|
2787
|
+
if not isinstance(response, dict):
|
2788
|
+
return "unknown"
|
2789
|
+
|
2790
|
+
# Check for single OpenAI function call object
|
2791
|
+
if (
|
2792
|
+
response.get("type") == "function"
|
2793
|
+
and "function" in response
|
2794
|
+
):
|
2795
|
+
return "openai"
|
2796
|
+
|
2797
|
+
# Check for OpenAI format with choices
|
2798
|
+
if "choices" in response:
|
2799
|
+
choices = response["choices"]
|
2800
|
+
if isinstance(choices, list) and len(choices) > 0:
|
2801
|
+
message = choices[0].get("message", {})
|
2802
|
+
if "tool_calls" in message:
|
2803
|
+
return "openai"
|
2804
|
+
|
2805
|
+
# Check for direct tool_calls array
|
2806
|
+
if "tool_calls" in response:
|
2807
|
+
return "openai"
|
2808
|
+
|
2809
|
+
# Check for Anthropic format
|
2810
|
+
if "content" in response:
|
2811
|
+
content = response["content"]
|
2812
|
+
if isinstance(content, list):
|
2813
|
+
for item in content:
|
2814
|
+
if (
|
2815
|
+
isinstance(item, dict)
|
2816
|
+
and item.get("type") == "tool_use"
|
2817
|
+
):
|
2818
|
+
return "anthropic"
|
2819
|
+
|
2820
|
+
if response.get("type") == "tool_use":
|
2821
|
+
return "anthropic"
|
2822
|
+
|
2823
|
+
# Check for generic format
|
2824
|
+
if "name" in response and (
|
2825
|
+
"arguments" in response
|
2826
|
+
or "parameters" in response
|
2827
|
+
or "input" in response
|
2828
|
+
):
|
2829
|
+
return "generic"
|
2830
|
+
|
2831
|
+
if "function_calls" in response:
|
2832
|
+
return "generic"
|
2833
|
+
|
2834
|
+
return "unknown"
|
2835
|
+
|
2836
|
+
def _extract_function_calls_from_tool_call_objects(
|
2837
|
+
self, tool_calls: List[Any]
|
2838
|
+
) -> List[Dict[str, Any]]:
|
2839
|
+
"""
|
2840
|
+
Extract function calls from a list of tool call objects (e.g., OpenAI ChatCompletionMessageToolCall or Anthropic BaseModels).
|
2841
|
+
|
2842
|
+
Args:
|
2843
|
+
tool_calls: List of tool call objects (can include BaseModel objects)
|
2844
|
+
|
2845
|
+
Returns:
|
2846
|
+
List[Dict[str, Any]]: List of standardized function call dictionaries
|
2847
|
+
"""
|
2848
|
+
function_calls = []
|
2849
|
+
|
2850
|
+
try:
|
2851
|
+
for tool_call in tool_calls:
|
2852
|
+
# Handle BaseModel objects (common with Anthropic responses)
|
2853
|
+
if isinstance(tool_call, BaseModel):
|
2854
|
+
self._log_if_verbose(
|
2855
|
+
"debug",
|
2856
|
+
"Converting BaseModel tool call to dictionary",
|
2857
|
+
)
|
2858
|
+
tool_call_dict = tool_call.model_dump()
|
2859
|
+
|
2860
|
+
# Process the converted dictionary
|
2861
|
+
extracted_calls = (
|
2862
|
+
self._extract_function_calls_from_response(
|
2863
|
+
tool_call_dict
|
2864
|
+
)
|
2865
|
+
)
|
2866
|
+
function_calls.extend(extracted_calls)
|
2867
|
+
|
2868
|
+
# Also try direct extraction in case it's a simple function call BaseModel
|
2869
|
+
if self._is_direct_function_call(tool_call_dict):
|
2870
|
+
function_calls.extend(
|
2871
|
+
self._extract_direct_function_call(
|
2872
|
+
tool_call_dict
|
2873
|
+
)
|
2874
|
+
)
|
2875
|
+
|
2876
|
+
# Handle OpenAI ChatCompletionMessageToolCall objects
|
2877
|
+
elif hasattr(tool_call, "function") and hasattr(
|
2878
|
+
tool_call, "type"
|
2879
|
+
):
|
2880
|
+
if tool_call.type == "function":
|
2881
|
+
function_info = tool_call.function
|
2882
|
+
name = getattr(function_info, "name", None)
|
2883
|
+
arguments_str = getattr(
|
2884
|
+
function_info, "arguments", "{}"
|
2885
|
+
)
|
2886
|
+
|
2887
|
+
if name:
|
2888
|
+
try:
|
2889
|
+
# Parse arguments JSON string
|
2890
|
+
arguments = (
|
2891
|
+
json.loads(arguments_str)
|
2892
|
+
if isinstance(arguments_str, str)
|
2893
|
+
else arguments_str
|
2894
|
+
)
|
2895
|
+
|
2896
|
+
function_calls.append(
|
2897
|
+
{
|
2898
|
+
"name": name,
|
2899
|
+
"arguments": arguments,
|
2900
|
+
"id": getattr(
|
2901
|
+
tool_call, "id", None
|
2902
|
+
),
|
2903
|
+
"type": "openai",
|
2904
|
+
}
|
2905
|
+
)
|
2906
|
+
except json.JSONDecodeError as e:
|
2907
|
+
self._log_if_verbose(
|
2908
|
+
"error",
|
2909
|
+
f"Failed to parse arguments for {name}: {e}",
|
2910
|
+
)
|
2911
|
+
|
2912
|
+
# Handle dictionary representations of tool calls
|
2913
|
+
elif isinstance(tool_call, dict):
|
2914
|
+
if (
|
2915
|
+
tool_call.get("type") == "function"
|
2916
|
+
and "function" in tool_call
|
2917
|
+
):
|
2918
|
+
function_info = tool_call["function"]
|
2919
|
+
name = function_info.get("name")
|
2920
|
+
arguments_str = function_info.get(
|
2921
|
+
"arguments", "{}"
|
2922
|
+
)
|
2923
|
+
|
2924
|
+
if name:
|
2925
|
+
try:
|
2926
|
+
arguments = (
|
2927
|
+
json.loads(arguments_str)
|
2928
|
+
if isinstance(arguments_str, str)
|
2929
|
+
else arguments_str
|
2930
|
+
)
|
2931
|
+
|
2932
|
+
function_calls.append(
|
2933
|
+
{
|
2934
|
+
"name": name,
|
2935
|
+
"arguments": arguments,
|
2936
|
+
"id": tool_call.get("id"),
|
2937
|
+
"type": "openai",
|
2938
|
+
}
|
2939
|
+
)
|
2940
|
+
except json.JSONDecodeError as e:
|
2941
|
+
self._log_if_verbose(
|
2942
|
+
"error",
|
2943
|
+
f"Failed to parse arguments for {name}: {e}",
|
2944
|
+
)
|
2945
|
+
|
2946
|
+
# Also try other dictionary extraction methods
|
2947
|
+
else:
|
2948
|
+
extracted_calls = self._extract_function_calls_from_response(
|
2949
|
+
tool_call
|
2950
|
+
)
|
2951
|
+
function_calls.extend(extracted_calls)
|
2952
|
+
|
2953
|
+
except Exception as e:
|
2954
|
+
self._log_if_verbose(
|
2955
|
+
"error",
|
2956
|
+
f"Failed to extract function calls from tool call objects: {e}",
|
2957
|
+
)
|
2958
|
+
|
2959
|
+
return function_calls
|
2960
|
+
|
2961
|
+
def _format_results_as_strings(
|
2962
|
+
self, results: List[Any], function_calls: List[Dict[str, Any]]
|
2963
|
+
) -> List[str]:
|
2964
|
+
"""
|
2965
|
+
Format function execution results as formatted strings.
|
2966
|
+
|
2967
|
+
Args:
|
2968
|
+
results: List of function execution results
|
2969
|
+
function_calls: List of function call information
|
2970
|
+
|
2971
|
+
Returns:
|
2972
|
+
List[str]: List of formatted result strings
|
2973
|
+
"""
|
2974
|
+
formatted_results = []
|
2975
|
+
|
2976
|
+
for i, (result, call) in enumerate(
|
2977
|
+
zip(results, function_calls)
|
2978
|
+
):
|
2979
|
+
function_name = call.get("name", f"function_{i}")
|
2980
|
+
|
2981
|
+
try:
|
2982
|
+
if isinstance(result, str):
|
2983
|
+
formatted_result = f"Function '{function_name}' result:\n{result}"
|
2984
|
+
elif isinstance(result, dict):
|
2985
|
+
formatted_result = f"Function '{function_name}' result:\n{json.dumps(result, indent=2, ensure_ascii=False)}"
|
2986
|
+
elif isinstance(result, (list, tuple)):
|
2987
|
+
formatted_result = f"Function '{function_name}' result:\n{json.dumps(list(result), indent=2, ensure_ascii=False)}"
|
2988
|
+
else:
|
2989
|
+
formatted_result = f"Function '{function_name}' result:\n{str(result)}"
|
2990
|
+
|
2991
|
+
formatted_results.append(formatted_result)
|
2992
|
+
|
2993
|
+
except Exception as e:
|
2994
|
+
self._log_if_verbose(
|
2995
|
+
"error",
|
2996
|
+
f"Failed to format result for {function_name}: {e}",
|
2997
|
+
)
|
2998
|
+
formatted_results.append(
|
2999
|
+
f"Function '{function_name}' result: [Error formatting result: {str(e)}]"
|
3000
|
+
)
|
3001
|
+
|
3002
|
+
return formatted_results
|
3003
|
+
|
3004
|
+
def _is_direct_function_call(self, data: Dict[str, Any]) -> bool:
|
3005
|
+
"""
|
3006
|
+
Check if a dictionary represents a direct function call.
|
3007
|
+
|
3008
|
+
Args:
|
3009
|
+
data: Dictionary to check
|
3010
|
+
|
3011
|
+
Returns:
|
3012
|
+
bool: True if it's a direct function call
|
3013
|
+
"""
|
3014
|
+
return (
|
3015
|
+
isinstance(data, dict)
|
3016
|
+
and "name" in data
|
3017
|
+
and (
|
3018
|
+
"arguments" in data
|
3019
|
+
or "parameters" in data
|
3020
|
+
or "input" in data
|
3021
|
+
)
|
3022
|
+
)
|
3023
|
+
|
3024
|
+
def _extract_direct_function_call(
|
3025
|
+
self, data: Dict[str, Any]
|
3026
|
+
) -> List[Dict[str, Any]]:
|
3027
|
+
"""
|
3028
|
+
Extract a direct function call from a dictionary.
|
3029
|
+
|
3030
|
+
Args:
|
3031
|
+
data: Dictionary containing function call data
|
3032
|
+
|
3033
|
+
Returns:
|
3034
|
+
List[Dict[str, Any]]: List containing the extracted function call
|
3035
|
+
"""
|
3036
|
+
function_calls = []
|
3037
|
+
|
3038
|
+
name = data.get("name")
|
3039
|
+
if name:
|
3040
|
+
# Try different argument key names
|
3041
|
+
arguments = (
|
3042
|
+
data.get("arguments")
|
3043
|
+
or data.get("parameters")
|
3044
|
+
or data.get("input")
|
3045
|
+
or {}
|
3046
|
+
)
|
3047
|
+
|
3048
|
+
function_calls.append(
|
3049
|
+
{
|
3050
|
+
"name": name,
|
3051
|
+
"arguments": arguments,
|
3052
|
+
"id": data.get("id"),
|
3053
|
+
"type": "direct",
|
3054
|
+
}
|
3055
|
+
)
|
433
3056
|
|
434
|
-
|
435
|
-
# def get_current_weather(location, unit='celsius'):
|
436
|
-
# return f"Weather in {location} is likely sunny and 75° {unit.title()}"
|
437
|
-
|
438
|
-
# def add(a, b):
|
439
|
-
# return a + b
|
440
|
-
|
441
|
-
# # Example tool configurations
|
442
|
-
# tools = [
|
443
|
-
# {
|
444
|
-
# "type": "function",
|
445
|
-
# "function": {
|
446
|
-
# "name": "get_current_weather",
|
447
|
-
# "parameters": {
|
448
|
-
# "properties": {
|
449
|
-
# "location": "San Francisco, CA",
|
450
|
-
# "unit": "fahrenheit",
|
451
|
-
# },
|
452
|
-
# },
|
453
|
-
# },
|
454
|
-
# },
|
455
|
-
# {
|
456
|
-
# "type": "function",
|
457
|
-
# "function": {
|
458
|
-
# "name": "add",
|
459
|
-
# "parameters": {
|
460
|
-
# "properties": {
|
461
|
-
# "a": 1,
|
462
|
-
# "b": 2,
|
463
|
-
# },
|
464
|
-
# },
|
465
|
-
# },
|
466
|
-
# }
|
467
|
-
# ]
|
468
|
-
|
469
|
-
# function_map = {
|
470
|
-
# "get_current_weather": get_current_weather,
|
471
|
-
# "add": add,
|
472
|
-
# }
|
473
|
-
|
474
|
-
# # Creating and executing the advanced executor
|
475
|
-
# tool_executor = BaseTool(verbose=True).execute_tool(tools, function_map)
|
476
|
-
|
477
|
-
# try:
|
478
|
-
# results = tool_executor()
|
479
|
-
# print(results) # Outputs results from both functions
|
480
|
-
# except Exception as e:
|
481
|
-
# print(f"Error: {e}")
|
3057
|
+
return function_calls
|