swarms 7.7.9__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/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
- try:
46
- return get_openai_function_schema_from_func(
47
- function=function,
48
- name=name,
49
- description=description,
50
- *args,
51
- **kwargs,
52
- )
53
- except Exception as e:
54
- logger.error(f"An error occurred in func_to_dict: {e}")
55
- logger.error(
56
- "Please check the function and ensure it is valid."
57
- )
58
- logger.error(
59
- "If the issue persists, please seek further assistance."
60
- )
61
- raise
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
- return load_basemodels_if_needed(func, *args, **kwargs)
71
- except Exception as e:
72
- logger.error(
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
- logger.error(
76
- "Please check the function and ensure it is valid."
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
- logger.error(
79
- "If the issue persists, please seek further assistance."
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
- try:
91
- return base_model_to_openai_function(
92
- pydantic_type, output_str, *args, **kwargs
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
- except Exception as e:
95
- logger.error(
96
- f"An error occurred in base_model_to_dict: {e}"
268
+
269
+ if not issubclass(pydantic_type, BaseModel):
270
+ raise ToolValidationError(
271
+ "pydantic_type must be a subclass of BaseModel"
97
272
  )
98
- logger.error(
99
- "Please check the Pydantic type and ensure it is valid."
273
+
274
+ try:
275
+ self._log_if_verbose(
276
+ "info",
277
+ f"Converting Pydantic model {pydantic_type.__name__} to schema",
100
278
  )
101
- logger.error(
102
- "If the issue persists, please seek further assistance."
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
- def multi_base_models_to_dict(
107
- self, return_str: bool = False, *args, **kwargs
108
- ) -> dict[str, Any]:
109
- try:
110
- if return_str:
111
- return multi_base_model_to_openai_function(
112
- self.base_models, *args, **kwargs
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
- return multi_base_model_to_openai_function(
116
- self.base_models, *args, **kwargs
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
- logger.error(
120
- f"An error occurred in multi_base_models_to_dict: {e}"
309
+ self._log_if_verbose(
310
+ "error",
311
+ f"Failed to convert model {pydantic_type.__name__}: {e}",
121
312
  )
122
- logger.error(
123
- "Please check the Pydantic types and ensure they are valid."
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
- logger.error(
126
- "If the issue persists, please seek further assistance."
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
- return function_to_str(dict)
136
- except Exception as e:
137
- logger.error(
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
- logger.error(
141
- "Please check the dictionary and ensure it is valid."
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
- logger.error(
144
- "If the issue persists, please seek further assistance."
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
- return functions_to_str(dicts)
154
- except Exception as e:
155
- logger.error(
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
- logger.error(
159
- "Please check the dictionaries and ensure they are valid."
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
- logger.error(
162
- "If the issue persists, please seek further assistance."
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
- return process_tool_docs(item)
169
- except Exception as e:
170
- logger.error(f"An error occurred in get_docs: {e}")
171
- logger.error(
172
- "Please check the item and ensure it is valid."
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
- logger.error(
175
- "If the issue persists, please seek further assistance."
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
- return parse_and_execute_json(
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
- except Exception as e:
191
- logger.error(f"An error occurred in execute_tool: {e}")
192
- logger.error(
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
- logger.error(
196
- "If the issue persists, please seek further assistance."
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
- Executes the dynamic run based on the input type.
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
- None
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
- tool_input_type = self.detect_tool_input_type(input)
225
- if tool_input_type == "Pydantic":
226
- function_str = base_model_to_openai_function(input)
227
- elif tool_input_type == "Dictionary":
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
- if self.auto_execute_tool:
235
- if tool_input_type == "Function":
236
- # Add the function to the functions list
237
- self.tools.append(input)
648
+ try:
649
+ self._log_if_verbose(
650
+ "info",
651
+ "Starting dynamic run with input type detection",
652
+ )
238
653
 
239
- # Create a function map from the functions list
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
- # Execute the tool
245
- return self.execute_tool(
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
- else:
249
- return function_str
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
- Args:
260
- tool_name (str): The name of the tool to execute.
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
- ValueError: If the tool with the specified name is not found.
268
- TypeError: If the tool name is not mapped to a function in the function map.
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
- # Step 1. find the function in the function map
271
- func = self.function_map.get(tool_name)
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
- execution = parse_and_execute_json(
274
- functions=[func],
275
- json_string=response,
276
- verbose=self.verbose,
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
- return execution
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 that represents a tool. The string should be convertible into a dictionary that includes a 'name' key and a 'parameters' key.
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
- ValueError: If the tool with the specified name is not found.
294
- TypeError: If the tool name is not mapped to a function in the function map.
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
- # Get the tool name and parameters from the dictionary
300
- tool_name = tool.get("name")
301
- tool_params = tool.get("parameters", {})
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
- # Get the function associated with the tool
304
- func = self.function_map.get(tool_name)
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
- # If the function is not found, raise an error
307
- if func is None:
308
- raise TypeError(
309
- f"Tool '{tool_name}' is not mapped to a function"
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
- # Execute the tool
313
- return func(**tool_params)
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, and if the function name in the JSON matches any name in the function map.
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 check.
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
- data = json.loads(output)
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
- except json.JSONDecodeError:
343
- logger.error("Error decoding JSON with output")
344
- pass
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
- return False
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
- def convert_funcs_into_tools(self):
349
- if self.tools is not None:
350
- logger.info(
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
- # Log the tools
355
- logger.info(
356
- f"Tools provided: Accessing {len(self.tools)} tools"
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 an openai schema
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
- # Now update the function calling map for every tools
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
- return None
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
- def convert_tool_into_openai_schema(self):
370
- logger.info(
371
- "Converting tools into OpenAI function calling schema"
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
- tool_schemas = []
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
- for tool in self.tools:
377
- # Transform the tool into a openai function calling schema
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
- logger.info(
385
- f"Converting tool: {name} into a OpenAI certified function calling schema. Add documentation and type hints."
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
- tool_schema = get_openai_function_schema_from_func(
388
- tool, name=name, description=description
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
- logger.info(
392
- f"Tool {name} converted successfully into OpenAI schema"
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
- tool_schemas.append(tool_schema)
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
- logger.error(
398
- f"Tool {tool.__name__} does not have documentation or type hints, please add them to make the tool execution reliable."
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
- # Combine all tool schemas into a single schema
402
- combined_schema = {
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
- "functions": [
405
- schema["function"] for schema in tool_schemas
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
- return combined_schema
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
- def check_func_if_have_docs(self, func: callable):
412
- if func.__doc__ is not None:
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
- else:
415
- logger.error(
416
- f"Function {func.__name__} does not have documentation"
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
- raise ValueError(
419
- f"Function {func.__name__} does not have documentation"
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
- logger.info(
427
- f"Function {func.__name__} does not have type hints"
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
- raise ValueError(
430
- f"Function {func.__name__} does not have type hints"
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
- # # Example function definitions and mappings
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