quantalogic 0.61.3__py3-none-any.whl → 0.80__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.
Files changed (46) hide show
  1. quantalogic/agent.py +0 -1
  2. quantalogic/codeact/TODO.md +14 -0
  3. quantalogic/codeact/agent.py +400 -421
  4. quantalogic/codeact/cli.py +42 -224
  5. quantalogic/codeact/cli_commands/__init__.py +0 -0
  6. quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
  7. quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
  8. quantalogic/codeact/cli_commands/list_executor.py +15 -0
  9. quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
  10. quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
  11. quantalogic/codeact/cli_commands/task.py +215 -0
  12. quantalogic/codeact/cli_commands/tool_info.py +24 -0
  13. quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
  14. quantalogic/codeact/config.yaml +21 -0
  15. quantalogic/codeact/constants.py +1 -1
  16. quantalogic/codeact/events.py +12 -5
  17. quantalogic/codeact/examples/README.md +342 -0
  18. quantalogic/codeact/examples/agent_sample.yaml +29 -0
  19. quantalogic/codeact/executor.py +186 -0
  20. quantalogic/codeact/history_manager.py +94 -0
  21. quantalogic/codeact/llm_util.py +3 -22
  22. quantalogic/codeact/plugin_manager.py +92 -0
  23. quantalogic/codeact/prompts/generate_action.j2 +65 -14
  24. quantalogic/codeact/prompts/generate_program.j2 +32 -19
  25. quantalogic/codeact/react_agent.py +318 -0
  26. quantalogic/codeact/reasoner.py +185 -0
  27. quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
  28. quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
  29. quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
  30. quantalogic/codeact/templates.py +7 -0
  31. quantalogic/codeact/tools_manager.py +242 -119
  32. quantalogic/codeact/utils.py +16 -89
  33. quantalogic/codeact/xml_utils.py +126 -0
  34. quantalogic/flow/flow.py +151 -41
  35. quantalogic/flow/flow_extractor.py +61 -1
  36. quantalogic/flow/flow_generator.py +34 -6
  37. quantalogic/flow/flow_manager.py +64 -25
  38. quantalogic/flow/flow_manager_schema.py +32 -0
  39. quantalogic/tools/action_gen.py +1 -1
  40. quantalogic/tools/tool.py +531 -109
  41. {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -3
  42. {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/RECORD +45 -22
  43. {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
  44. quantalogic-0.80.dist-info/entry_points.txt +3 -0
  45. quantalogic-0.61.3.dist-info/entry_points.txt +0 -6
  46. {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/LICENSE +0 -0
quantalogic/tools/tool.py CHANGED
@@ -5,9 +5,9 @@ with type-validated arguments and execution methods.
5
5
  """
6
6
 
7
7
  import ast
8
- import asyncio # Added for asynchronous support
8
+ import asyncio
9
9
  import inspect
10
- from typing import Any, Callable, Literal, TypeVar
10
+ from typing import Any, Callable, TypeVar, Union, get_args, get_origin
11
11
 
12
12
  from docstring_parser import parse as parse_docstring
13
13
  from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -15,21 +15,165 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
15
15
  # Type variable for create_tool to preserve function signature
16
16
  F = TypeVar('F', bound=Callable[..., Any])
17
17
 
18
+
19
+ def type_hint_to_str(type_hint):
20
+ """Convert a type hint to a string representation.
21
+
22
+ Args:
23
+ type_hint: The type hint to convert.
24
+
25
+ Returns:
26
+ A string representation of the type hint, e.g., 'list[int]', 'dict[str, float]'.
27
+ """
28
+ origin = get_origin(type_hint)
29
+ if origin is not None:
30
+ origin_name = origin.__name__
31
+ args = get_args(type_hint)
32
+ args_str = ", ".join(type_hint_to_str(arg) for arg in args)
33
+ return f"{origin_name}[{args_str}]"
34
+ elif hasattr(type_hint, "__name__"):
35
+ return type_hint.__name__
36
+ else:
37
+ return str(type_hint)
38
+
39
+
40
+ def get_type_description(type_hint):
41
+ """Generate a detailed, natural-language description of a type hint.
42
+
43
+ Args:
44
+ type_hint: The type hint to describe.
45
+
46
+ Returns:
47
+ A string with a detailed description of the type, e.g., 'a list of int', or a structured class description.
48
+ """
49
+ basic_types = {
50
+ int: "integer",
51
+ str: "string",
52
+ float: "float",
53
+ bool: "boolean",
54
+ type(None): "None",
55
+ }
56
+
57
+ if type_hint in basic_types:
58
+ return basic_types[type_hint]
59
+
60
+ try:
61
+ origin = get_origin(type_hint)
62
+ if origin is None:
63
+ if inspect.isclass(type_hint):
64
+ doc = inspect.getdoc(type_hint)
65
+ desc_prefix = f"{doc} " if doc else ""
66
+ if hasattr(type_hint, "__annotations__"):
67
+ annotations = type_hint.__annotations__
68
+ attrs = ", ".join(f"{name}: {get_type_description(typ)}" for name, typ in annotations.items())
69
+ return f"{desc_prefix}an instance of {type_hint.__name__} with attributes: {attrs}"
70
+ return f"{desc_prefix}{type_hint.__name__}"
71
+ return str(type_hint)
72
+
73
+ args = get_args(type_hint)
74
+
75
+ if origin is list:
76
+ if args and len(args) >= 1:
77
+ return f"a list of {get_type_description(args[0])}"
78
+ return "a list"
79
+
80
+ elif origin is dict:
81
+ if args and len(args) == 2:
82
+ return f"a dictionary with {get_type_description(args[0])} keys and {get_type_description(args[1])} values"
83
+ return "a dictionary with any keys and values"
84
+
85
+ elif origin is tuple:
86
+ if args:
87
+ types_desc = ", ".join(get_type_description(t) for t in args)
88
+ return f"a tuple containing {types_desc}"
89
+ return "a tuple"
90
+
91
+ elif origin is Union:
92
+ if args:
93
+ if len(args) == 2 and type(None) in args:
94
+ non_none_type = next(t for t in args if t is not type(None))
95
+ return f"an optional {get_type_description(non_none_type)} (can be None)"
96
+ types_desc = ", ".join(get_type_description(t) for t in args)
97
+ return f"one of {types_desc}"
98
+ return "any type"
99
+
100
+ return type_hint_to_str(type_hint)
101
+ except Exception:
102
+ return str(type_hint)
103
+
104
+
105
+ def get_type_schema(type_hint):
106
+ """Generate a schema-like string representation of a type hint.
107
+
108
+ Args:
109
+ type_hint: The type hint to convert.
110
+
111
+ Returns:
112
+ A string representing the type's structure, e.g., '[integer, ...]' or "{'x': 'integer', 'y': 'integer'}".
113
+ """
114
+ basic_types = {
115
+ int: "integer",
116
+ str: "string",
117
+ float: "float",
118
+ bool: "boolean",
119
+ type(None): "null",
120
+ }
121
+
122
+ if type_hint in basic_types:
123
+ return basic_types[type_hint]
124
+
125
+ origin = get_origin(type_hint)
126
+ if origin is None:
127
+ if inspect.isclass(type_hint) and hasattr(type_hint, "__annotations__"):
128
+ annotations = type_hint.__annotations__
129
+ fields = {name: get_type_schema(typ) for name, typ in annotations.items()}
130
+ return "{" + ", ".join(f"'{name}': {schema}" for name, schema in fields.items()) + "}"
131
+ return type_hint.__name__
132
+
133
+ elif origin is list:
134
+ item_type = get_args(type_hint)[0]
135
+ return f"[{get_type_schema(item_type)}, ...]"
136
+
137
+ elif origin is dict:
138
+ args = get_args(type_hint)
139
+ if len(args) == 2:
140
+ key_type, value_type = args
141
+ if key_type is str:
142
+ return f"{{{get_type_schema(value_type)}}}"
143
+ return f"dictionary with keys of type {get_type_schema(key_type)} and values of type {get_type_schema(value_type)}"
144
+ return "dictionary"
145
+
146
+ elif origin is tuple:
147
+ tuple_types = get_args(type_hint)
148
+ return f"[{', '.join(get_type_schema(t) for t in tuple_types)}]"
149
+
150
+ elif origin is Union:
151
+ union_types = get_args(type_hint)
152
+ if len(union_types) == 2 and type(None) in union_types:
153
+ non_none_type = next(t for t in union_types if t is not type(None))
154
+ return f"{get_type_schema(non_none_type)} or null"
155
+ return " or ".join(get_type_schema(t) for t in union_types)
156
+
157
+ else:
158
+ return type_hint_to_str(type_hint)
159
+
160
+
18
161
  class ToolArgument(BaseModel):
19
162
  """Represents an argument for a tool with validation and description.
20
163
 
21
164
  Attributes:
22
165
  name: The name of the argument.
23
- arg_type: The type of the argument (integer, float, boolean).
166
+ arg_type: The type of the argument, e.g., 'string', 'int', 'list[int]', 'dict[str, float]'.
24
167
  description: Optional description of the argument.
25
168
  required: Indicates if the argument is mandatory.
26
169
  default: Optional default value for the argument.
27
170
  example: Optional example value to illustrate the argument's usage.
171
+ type_details: Detailed description of the argument's type.
28
172
  """
29
173
 
30
174
  name: str = Field(..., description="The name of the argument.")
31
- arg_type: Literal["string", "int", "float", "boolean"] = Field(
32
- ..., description="The type of the argument. Must be one of: string, integer, float, boolean."
175
+ arg_type: str = Field(
176
+ ..., description="The type of the argument, e.g., 'string', 'int', 'list[int]', 'dict[str, float]', etc."
33
177
  )
34
178
  description: str | None = Field(default=None, description="A brief description of the argument.")
35
179
  required: bool = Field(default=False, description="Indicates if the argument is required.")
@@ -37,6 +181,8 @@ class ToolArgument(BaseModel):
37
181
  default=None, description="The default value for the argument. This parameter is required."
38
182
  )
39
183
  example: str | None = Field(default=None, description="An example value to illustrate the argument's usage.")
184
+ type_details: str | None = Field(default=None, description="Detailed description of the argument's type.")
185
+
40
186
 
41
187
  class ToolDefinition(BaseModel):
42
188
  """Base class for defining tool configurations without execution logic.
@@ -46,7 +192,12 @@ class ToolDefinition(BaseModel):
46
192
  description: Brief description of the tool's functionality.
47
193
  arguments: List of arguments the tool accepts.
48
194
  return_type: The return type of the tool's execution method. Defaults to "str".
195
+ return_description: Optional description of the return value.
196
+ return_type_details: Detailed description of the return type.
197
+ original_docstring: The full original docstring of the function, if applicable.
49
198
  need_validation: Flag to indicate if tool requires validation.
199
+ is_async: Flag to indicate if the tool is asynchronous (for documentation purposes).
200
+ toolbox_name: Optional name of the toolbox this tool belongs to.
50
201
  """
51
202
 
52
203
  model_config = ConfigDict(extra="allow", validate_assignment=True)
@@ -55,6 +206,11 @@ class ToolDefinition(BaseModel):
55
206
  description: str = Field(..., description="A brief description of what the tool does.")
56
207
  arguments: list[ToolArgument] = Field(default_factory=list, description="A list of arguments the tool accepts.")
57
208
  return_type: str = Field(default="str", description="The return type of the tool's execution method.")
209
+ return_description: str | None = Field(default=None, description="Description of the return value.")
210
+ return_type_details: str | None = Field(default=None, description="Detailed description of the return type.")
211
+ return_example: str | None = Field(default=None, description="Example of the return value.")
212
+ return_structure: str | None = Field(default=None, description="Structure of the return value.")
213
+ original_docstring: str | None = Field(default=None, description="The full original docstring of the function, if applicable.")
58
214
  need_validation: bool = Field(
59
215
  default=False,
60
216
  description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
@@ -71,6 +227,14 @@ class ToolDefinition(BaseModel):
71
227
  default=False,
72
228
  description="When True, provides access to the agent's conversation history. Useful for tools that need context from previous interactions.",
73
229
  )
230
+ is_async: bool = Field(
231
+ default=False,
232
+ description="Indicates if the tool is asynchronous (used for documentation).",
233
+ )
234
+ toolbox_name: str | None = Field(
235
+ default=None,
236
+ description="The name of the toolbox this tool belongs to, set during registration if applicable."
237
+ )
74
238
 
75
239
  def get_properties(self, exclude: list[str] | None = None) -> dict[str, Any]:
76
240
  """Return a dictionary of all non-None properties, excluding Tool class fields and specified fields.
@@ -87,9 +251,17 @@ class ToolDefinition(BaseModel):
87
251
  "description",
88
252
  "arguments",
89
253
  "return_type",
254
+ "return_description",
255
+ "return_type_details",
256
+ "return_example",
257
+ "return_structure",
258
+ "original_docstring",
90
259
  "need_validation",
260
+ "need_post_process",
91
261
  "need_variables",
92
262
  "need_caller_context_memory",
263
+ "is_async",
264
+ "toolbox_name",
93
265
  }
94
266
  properties = {}
95
267
 
@@ -113,52 +285,74 @@ class ToolDefinition(BaseModel):
113
285
  Returns:
114
286
  A detailed Markdown string representing the tool's configuration and usage.
115
287
  """
116
- # Tool name and description
117
288
  markdown = f"`{self.name}`:\n"
118
289
  markdown += f"- **Description**: {self.description}\n\n"
119
290
 
120
291
  properties_injectable = self.get_injectable_properties_in_execution()
292
+ if any(properties_injectable.get(arg.name) is not None for arg in self.arguments):
293
+ markdown += "- **Note**: Some arguments are injected from the tool's configuration and may not need to be provided explicitly.\n\n"
121
294
 
122
- # Parameters section
123
295
  if self.arguments:
124
296
  markdown += "- **Parameters**:\n"
125
297
  parameters = ""
126
298
  for arg in self.arguments:
127
- # Skip if parameter name matches an object property with non-None value
128
299
  if properties_injectable.get(arg.name) is not None:
129
300
  continue
130
-
301
+ type_info = f"{arg.arg_type}"
302
+ if arg.type_details and arg.type_details != arg.arg_type:
303
+ type_info += f" ({arg.type_details})"
131
304
  required_status = "required" if arg.required else "optional"
132
- # Prioritize example, then default, then create a generic description
133
305
  value_info = ""
306
+ if arg.default is not None:
307
+ value_info += f", default: `{arg.default}`"
134
308
  if arg.example is not None:
135
- value_info = f" (example: `{arg.example}`)"
136
- elif arg.default is not None:
137
- value_info = f" (default: `{arg.default}`)"
138
-
309
+ value_info += f", example: `{arg.example}`"
139
310
  parameters += (
140
- f" - `{arg.name}`: " f"({required_status}{value_info})\n" f" {arg.description or ''}\n"
311
+ f" - `{arg.name}`: ({type_info}, {required_status}{value_info})\n"
312
+ f" {arg.description or ''}\n"
141
313
  )
142
- if len(parameters) > 0:
143
- markdown += parameters + "\n\n"
314
+ if parameters:
315
+ markdown += parameters + "\n"
144
316
  else:
145
- markdown += "None\n\n"
317
+ markdown += " None\n\n"
146
318
 
147
- # Usage section with XML-style example
148
- markdown += "**Usage**:\n"
319
+ standard_fields = {
320
+ "name", "description", "arguments", "return_type", "return_description", "return_type_details",
321
+ "return_example", "return_structure", "original_docstring", "need_validation", "need_post_process", "need_variables", "need_caller_context_memory", "is_async",
322
+ "toolbox_name"
323
+ }
324
+ additional_fields = [f for f in self.model_fields if f not in standard_fields]
325
+ if additional_fields:
326
+ markdown += "- **Configuration**:\n"
327
+ for field in additional_fields:
328
+ field_info = self.model_fields[field]
329
+ field_type = type_hint_to_str(field_info.annotation)
330
+ field_desc = field_info.description or "No description provided."
331
+ if field in properties_injectable:
332
+ field_desc += f" Injects into '{field}' argument."
333
+ markdown += f" - `{field}`: ({field_type}) - {field_desc}\n"
334
+ markdown += "\n"
335
+
336
+ markdown += "- **Usage**: This tool can be invoked using the following XML-like syntax:\n"
149
337
  markdown += "```xml\n"
150
338
  markdown += f"<{self.name}>\n"
151
339
 
152
- # Generate example parameters
153
340
  for arg in self.arguments:
154
341
  if properties_injectable.get(arg.name) is not None:
155
342
  continue
156
- # Prioritize example, then default, then create a generic example
157
343
  example_value = arg.example or arg.default or f"Your {arg.name} here"
158
344
  markdown += f" <{arg.name}>{example_value}</{arg.name}>\n"
159
345
 
160
346
  markdown += f"</{self.name}>\n"
161
- markdown += "```\n"
347
+ markdown += "```\n\n"
348
+
349
+ markdown += f"- **Returns**: `{self.return_type}` - {self.return_description or 'The result of the tool execution.'}\n"
350
+ if self.return_type_details and self.return_type_details != self.return_type:
351
+ markdown += f" {self.return_type_details}\n"
352
+ if self.return_example:
353
+ markdown += f"- **Example Return Value**: `{self.return_example}`\n"
354
+ if self.return_structure:
355
+ markdown += f"- **Return Structure**: `{self.return_structure}`\n"
162
356
 
163
357
  return markdown
164
358
 
@@ -169,7 +363,6 @@ class ToolDefinition(BaseModel):
169
363
  List of ToolArgument instances that cannot be injected by the agent.
170
364
  """
171
365
  properties_injectable = self.get_injectable_properties_in_execution()
172
-
173
366
  return [arg for arg in self.arguments if properties_injectable.get(arg.name) is None]
174
367
 
175
368
  def get_injectable_properties_in_execution(self) -> dict[str, Any]:
@@ -178,63 +371,94 @@ class ToolDefinition(BaseModel):
178
371
  Returns:
179
372
  A dictionary of property names and values, excluding tool arguments and None values.
180
373
  """
181
- # This method is defined here in ToolDefinition and overridden in Tool
182
- # For ToolDefinition, it returns an empty dict since it has no execution context yet
183
374
  return {}
184
375
 
185
376
  def to_docstring(self) -> str:
186
377
  """Convert the tool definition into a Google-style docstring with function signature.
187
378
 
379
+ If an original_docstring is provided (e.g., from a function via create_tool), it is used directly.
380
+ Otherwise, constructs a detailed docstring from the tool's metadata.
381
+
188
382
  Returns:
189
383
  A string formatted as a valid Python docstring representing the tool's configuration,
190
- including the function signature and return type.
384
+ including the function signature, detailed argument types, and return type with descriptions.
191
385
  """
192
- # Construct the function signature
193
386
  signature_parts = []
194
387
  for arg in self.arguments:
195
- # Base argument: name and type
196
388
  arg_str = f"{arg.name}: {arg.arg_type}"
197
- # Add default value if present
198
389
  if arg.default is not None:
199
390
  arg_str += f" = {arg.default}"
200
391
  signature_parts.append(arg_str)
201
- signature = f"def {self.name}({', '.join(signature_parts)}) -> {self.return_type}:"
392
+ signature = f"{'async ' if self.is_async else ''}def {self.name}({', '.join(signature_parts)}) -> {self.return_type}:"
202
393
 
203
- # Start with the signature and description
204
- docstring = f'"""\n{signature}\n\n{self.description}\n\n'
205
-
206
- # Add Arguments section if there are any
207
- if self.arguments:
208
- docstring += "Args:\n"
209
- for arg in self.arguments:
210
- # Base argument line: name and type
211
- arg_line = f" {arg.name} ({arg.arg_type})"
212
-
213
- # Add optional/required status and default/example if present
214
- details = []
215
- if not arg.required:
216
- details.append("optional")
217
- if arg.default is not None:
218
- details.append(f"defaults to {arg.default}")
219
- if arg.example is not None:
220
- details.append(f"e.g., {arg.example}")
221
- if details:
222
- arg_line += f" [{', '.join(details)}]"
223
-
224
- # Add description if present
225
- if arg.description:
226
- arg_line += f": {arg.description}"
227
-
228
- docstring += f"{arg_line}\n"
229
-
230
- # Add Returns section
231
- docstring += f"Returns:\n {self.return_type}: The result of the tool execution.\n"
232
-
233
- # Close the docstring
234
- docstring += '"""'
235
-
394
+ if self.original_docstring:
395
+ # Use the full original docstring if available, ensuring proper indentation
396
+ docstring = f'"""\n{signature}\n\n{self.original_docstring.rstrip()}\n"""'
397
+ else:
398
+ # Fall back to constructing the docstring from metadata
399
+ docstring = f'"""\n{signature}\n\n{self.description}\n'
400
+ properties_injectable = self.get_injectable_properties_in_execution()
401
+ if properties_injectable:
402
+ docstring += "\n Note: Some arguments may be injected from the tool's configuration.\n"
403
+
404
+ if self.arguments:
405
+ docstring += "\nArgs:\n"
406
+ for arg in self.arguments:
407
+ arg_line = f" {arg.name} ({arg.arg_type})"
408
+ details = []
409
+ if not arg.required:
410
+ details.append("optional")
411
+ if arg.default is not None:
412
+ details.append(f"defaults to {arg.default}")
413
+ if arg.example is not None:
414
+ details.append(f"e.g., {arg.example}")
415
+ if details:
416
+ arg_line += f" [{', '.join(details)}]"
417
+ if arg.description:
418
+ arg_line += f": {arg.description}"
419
+ if arg.type_details and arg.type_details != arg.arg_type and arg.type_details != arg.description:
420
+ arg_line += f"\n {arg.type_details}"
421
+ docstring += f"{arg_line}\n"
422
+
423
+ if self.return_description:
424
+ docstring += f"\nReturns:\n {self.return_type}: {self.return_description.split(':')[0]}:\n"
425
+ if ':' in self.return_description:
426
+ fields = self.return_description.split(':', 1)[1].strip()
427
+ for line in fields.split('\n'):
428
+ if line.strip():
429
+ docstring += f" {line.strip()}\n"
430
+ else:
431
+ return_desc = self.return_type_details or "The result of the tool execution."
432
+ docstring += f"\nReturns:\n {self.return_type}: {return_desc}"
433
+ if self.return_structure:
434
+ docstring += f"\n Structure: {self.return_structure}"
435
+
436
+ if self.return_example:
437
+ docstring += f"\n Example: {self.return_example}"
438
+
439
+ docstring += "\n\nExamples:\n"
440
+ args_str = ", ".join([f"{arg.name}=\"{arg.example or '...'}\"" for arg in self.arguments if arg.required])
441
+ prefix = " result = " if not self.is_async else " result = await "
442
+ docstring += f"{prefix}{self.name}({args_str})\n"
443
+
444
+ standard_fields = {
445
+ "name", "description", "arguments", "return_type", "return_description", "return_type_details",
446
+ "return_example", "return_structure", "original_docstring", "need_validation", "need_post_process", "need_variables", "need_caller_context_memory", "is_async",
447
+ "toolbox_name"
448
+ }
449
+ additional_fields = [f for f in self.model_fields if f not in standard_fields]
450
+ if additional_fields:
451
+ docstring += "\nConfiguration:\n"
452
+ for field in additional_fields:
453
+ field_info = self.model_fields[field]
454
+ field_type = type_hint_to_str(field_info.annotation)
455
+ field_desc = field_info.description or "No description provided."
456
+ docstring += f" {field} ({field_type}): {field_desc}\n"
457
+
458
+ docstring += '\n"""'
236
459
  return docstring
237
460
 
461
+
238
462
  class Tool(ToolDefinition):
239
463
  """Extended class for tools with execution capabilities.
240
464
 
@@ -258,12 +482,12 @@ class Tool(ToolDefinition):
258
482
  if isinstance(arg, dict)
259
483
  else arg
260
484
  if isinstance(arg, ToolArgument)
261
- else ToolArgument(name=str(arg), type=type(arg).__name__)
485
+ else ToolArgument(name=str(arg), arg_type=type(arg).__name__)
262
486
  for arg in v
263
487
  ]
264
488
  return []
265
489
 
266
- def execute(self, **kwargs: Any) -> str:
490
+ def execute(self, **kwargs: Any) -> Any:
267
491
  """Execute the tool with provided arguments.
268
492
 
269
493
  If not implemented by a subclass, falls back to the asynchronous execute_async method.
@@ -272,15 +496,13 @@ class Tool(ToolDefinition):
272
496
  **kwargs: Keyword arguments for tool execution.
273
497
 
274
498
  Returns:
275
- A string representing the result of tool execution.
499
+ The result of tool execution, preserving the original type returned by the tool's logic.
276
500
  """
277
- # Check if execute is implemented in the subclass
278
501
  if self.__class__.execute is Tool.execute:
279
- # If not implemented, run the async version synchronously
280
502
  return asyncio.run(self.async_execute(**kwargs))
281
503
  raise NotImplementedError("This method should be implemented by subclasses.")
282
504
 
283
- async def async_execute(self, **kwargs: Any) -> str:
505
+ async def async_execute(self, **kwargs: Any) -> Any:
284
506
  """Asynchronous version of execute.
285
507
 
286
508
  By default, runs the synchronous execute method in a separate thread using asyncio.to_thread.
@@ -291,31 +513,63 @@ class Tool(ToolDefinition):
291
513
  **kwargs: Keyword arguments for tool execution.
292
514
 
293
515
  Returns:
294
- A string representing the result of tool execution.
516
+ The result of tool execution, preserving the original type returned by the tool's logic.
295
517
  """
296
- # Check if execute_async is implemented in the subclass
297
518
  if self.__class__.async_execute is Tool.async_execute:
298
519
  return await asyncio.to_thread(self.execute, **kwargs)
299
520
  raise NotImplementedError("This method should be implemented by subclasses.")
300
521
 
522
+ async def __call__(self, *args: Any, **kwargs: Any) -> Any:
523
+ """Make the tool callable, handling both synchronous and asynchronous executions.
524
+
525
+ The tool can be called with positional and/or keyword arguments, matching the arguments defined in the tool's metadata.
526
+
527
+ Args:
528
+ *args: Positional arguments to pass to the tool's execution method, mapped to argument names in the order defined by self.arguments.
529
+ **kwargs: Keyword arguments to pass to the tool's execution method.
530
+
531
+ Returns:
532
+ The result of the tool execution, preserving the original return type.
533
+
534
+ Raises:
535
+ TypeError: If too many positional arguments are provided or if an argument is specified both positionally and by keyword.
536
+
537
+ Usage:
538
+ For synchronous contexts: result = asyncio.run(tool(*args, **kwargs))
539
+ For asynchronous contexts: result = await tool(*args, **kwargs)
540
+ """
541
+ arg_names = [arg.name for arg in self.arguments]
542
+
543
+ for i, arg_value in enumerate(args):
544
+ if i >= len(arg_names):
545
+ raise TypeError(f"{self.name}() takes {len(arg_names)} positional arguments but {len(args)} were given")
546
+ arg_name = arg_names[i]
547
+ if arg_name in kwargs:
548
+ raise TypeError(f"{self.name}() got multiple values for argument '{arg_name}'")
549
+ kwargs[arg_name] = arg_value
550
+
551
+ if self.is_async:
552
+ return await self.async_execute(**kwargs)
553
+ else:
554
+ return await asyncio.to_thread(self.execute, **kwargs)
555
+
301
556
  def get_injectable_properties_in_execution(self) -> dict[str, Any]:
302
557
  """Get injectable properties excluding tool arguments.
303
558
 
304
559
  Returns:
305
560
  A dictionary of property names and values, excluding tool arguments and None values.
306
561
  """
307
- # Get argument names from tool definition
308
562
  argument_names = {arg.name for arg in self.arguments}
309
-
310
- # Get properties excluding arguments and filter out None values
311
563
  properties = self.get_properties(exclude=["arguments"])
312
564
  return {name: value for name, value in properties.items() if value is not None and name in argument_names}
313
565
 
566
+
314
567
  def create_tool(func: F) -> Tool:
315
- """Create a Tool instance from a Python function using AST analysis.
568
+ """Create a Tool instance from a Python function using AST analysis with enhanced return type metadata.
316
569
 
317
570
  Analyzes the function's source code to extract its name, docstring, and arguments,
318
- then constructs a Tool subclass with appropriate execution logic.
571
+ then constructs a Tool subclass with appropriate execution logic for both
572
+ synchronous and asynchronous functions.
319
573
 
320
574
  Args:
321
575
  func: The Python function (sync or async) to convert into a Tool.
@@ -329,32 +583,27 @@ def create_tool(func: F) -> Tool:
329
583
  if not callable(func):
330
584
  raise ValueError("Input must be a callable function")
331
585
 
332
- # Get source code and parse with AST
333
586
  try:
334
587
  source = inspect.getsource(func).strip()
335
588
  tree = ast.parse(source)
336
589
  except (OSError, TypeError, SyntaxError) as e:
337
590
  raise ValueError(f"Failed to parse function source: {e}")
338
591
 
339
- # Ensure root node is a function definition
340
592
  if not tree.body or not isinstance(tree.body[0], (ast.FunctionDef, ast.AsyncFunctionDef)):
341
593
  raise ValueError("Source must define a single function")
342
594
  func_def = tree.body[0]
343
595
 
344
- # Extract metadata
345
596
  name = func_def.name
346
597
  docstring = ast.get_docstring(func_def) or ""
347
598
  parsed_doc = parse_docstring(docstring)
348
599
  description = parsed_doc.short_description or f"Tool generated from {name}"
349
600
  param_docs = {p.arg_name: p.description for p in parsed_doc.params}
601
+ return_description = parsed_doc.returns.description if parsed_doc.returns else None
350
602
  is_async = isinstance(func_def, ast.AsyncFunctionDef)
351
603
 
352
- # Get type hints using typing module
353
604
  from typing import get_type_hints
354
605
  type_hints = get_type_hints(func)
355
- type_map = {int: "int", str: "string", float: "float", bool: "boolean"}
356
606
 
357
- # Process arguments
358
607
  args = func_def.args
359
608
  defaults = [None] * (len(args.args) - len(args.defaults)) + [
360
609
  ast.unparse(d) if isinstance(d, ast.AST) else str(d) for d in args.defaults
@@ -365,45 +614,77 @@ def create_tool(func: F) -> Tool:
365
614
  arg_name = arg.arg
366
615
  default = defaults[i]
367
616
  required = default is None
368
-
369
- # Determine argument type
370
- hint = type_hints.get(arg_name, str) # Default to str if no hint
371
- arg_type = type_map.get(hint, "string") # Fallback to string for unmapped types
372
-
373
- # Use docstring or default description
617
+ hint = type_hints.get(arg_name, str)
618
+ arg_type = type_hint_to_str(hint)
374
619
  description = param_docs.get(arg_name, f"Argument {arg_name}")
375
-
376
- # Create ToolArgument
377
620
  arguments.append(ToolArgument(
378
621
  name=arg_name,
379
622
  arg_type=arg_type,
380
623
  description=description,
381
624
  required=required,
382
625
  default=default,
383
- example=default if default else None
626
+ example=default if default else None,
627
+ type_details=get_type_description(hint)
384
628
  ))
385
629
 
386
- # Determine return type from type hints
387
630
  return_type = type_hints.get("return", str)
388
- return_type_str = type_map.get(return_type, "string")
631
+ return_type_str = type_hint_to_str(return_type)
632
+ return_type_details = get_type_description(return_type)
633
+ return_structure = get_type_schema(return_type)
389
634
 
390
- # Define Tool subclass
391
635
  class GeneratedTool(Tool):
392
636
  def __init__(self, *args: Any, **kwargs: Any):
393
- super().__init__(*args, name=name, description=description, arguments=arguments, return_type=return_type_str, **kwargs)
637
+ super().__init__(
638
+ *args,
639
+ name=name,
640
+ description=description,
641
+ arguments=arguments,
642
+ return_type=return_type_str,
643
+ return_description=return_description,
644
+ return_type_details=return_type_details,
645
+ return_structure=return_structure,
646
+ original_docstring=docstring, # Preserve full original docstring
647
+ is_async=is_async,
648
+ toolbox_name=None,
649
+ **kwargs
650
+ )
394
651
  self._func = func
395
652
 
396
- if is_async:
397
- async def async_execute(self, **kwargs: Any) -> str:
398
- result = await self._func(**kwargs)
399
- return str(result)
400
- else:
401
- def execute(self, **kwargs: Any) -> str:
402
- result = self._func(**kwargs)
403
- return str(result)
653
+ def execute(self, **kwargs: Any) -> Any:
654
+ """Execute the tool synchronously, handling both sync and async functions.
655
+
656
+ Args:
657
+ **kwargs: Keyword arguments for tool execution.
658
+
659
+ Returns:
660
+ The result of the function execution, preserving its original type.
661
+ """
662
+ injectable = self.get_injectable_properties_in_execution()
663
+ full_kwargs = {**injectable, **kwargs}
664
+ if self.is_async:
665
+ return asyncio.run(self.async_execute(**full_kwargs))
666
+ else:
667
+ return self._func(**full_kwargs)
668
+
669
+ async def async_execute(self, **kwargs: Any) -> Any:
670
+ """Execute the tool asynchronously, handling both sync and async functions.
671
+
672
+ Args:
673
+ **kwargs: Keyword arguments for tool execution.
674
+
675
+ Returns:
676
+ The result of the function execution, preserving its original type.
677
+ """
678
+ injectable = self.get_injectable_properties_in_execution()
679
+ full_kwargs = {**injectable, **kwargs}
680
+ if self.is_async:
681
+ return await self._func(**full_kwargs)
682
+ else:
683
+ return await asyncio.to_thread(self._func, **full_kwargs)
404
684
 
405
685
  return GeneratedTool()
406
686
 
687
+
407
688
  if __name__ == "__main__":
408
689
  # Basic tool with argument
409
690
  tool = Tool(
@@ -454,6 +735,9 @@ if __name__ == "__main__":
454
735
  Args:
455
736
  a: First number.
456
737
  b: Second number (optional).
738
+
739
+ Returns:
740
+ The sum of a and b.
457
741
  """
458
742
  return a + b
459
743
 
@@ -462,7 +746,9 @@ if __name__ == "__main__":
462
746
  print(sync_tool.to_markdown())
463
747
  print("Synchronous Tool Docstring:")
464
748
  print(sync_tool.to_docstring())
465
- print("Execution result:", sync_tool.execute(a=5, b=3))
749
+ print("Execution result (sync):", sync_tool.execute(a=5, b=3))
750
+ print("Execution result (async):", asyncio.run(sync_tool.async_execute(a=5, b=3)))
751
+ print("Execution result (callable):", asyncio.run(sync_tool(a=5, b=3)))
466
752
  print()
467
753
 
468
754
  # Test create_tool with asynchronous function
@@ -471,8 +757,15 @@ if __name__ == "__main__":
471
757
 
472
758
  Args:
473
759
  name: Name of the person.
760
+
761
+ Returns:
762
+ A greeting message.
763
+
764
+ Examples:
765
+ >>> await greet("Alice")
766
+ 'Hello, Alice'
474
767
  """
475
- await asyncio.sleep(0.1) # Simulate async work
768
+ await asyncio.sleep(0.1)
476
769
  return f"Hello, {name}"
477
770
 
478
771
  async_tool = create_tool(greet)
@@ -480,7 +773,33 @@ if __name__ == "__main__":
480
773
  print(async_tool.to_markdown())
481
774
  print("Asynchronous Tool Docstring:")
482
775
  print(async_tool.to_docstring())
483
- print("Execution result:", asyncio.run(async_tool.async_execute(name="Alice")))
776
+ print("Execution result (sync):", async_tool.execute(name="Alice"))
777
+ print("Execution result (async):", asyncio.run(async_tool.async_execute(name="Alice")))
778
+ print("Execution result (callable):", asyncio.run(async_tool(name="Alice")))
779
+ print()
780
+
781
+ # Comprehensive tool with complex types
782
+ from typing import Dict, List
783
+
784
+ def process_data(data: List[int], options: Dict[str, bool] = {}) -> Dict[str, int]:
785
+ """Process a list of integers with options.
786
+
787
+ Args:
788
+ data: List of integers to process.
789
+ options: Dictionary of options.
790
+
791
+ Returns:
792
+ A dictionary with results.
793
+ """
794
+ return {str(i): i for i in data}
795
+
796
+ complex_tool = create_tool(process_data)
797
+ print("Complex Tool Markdown:")
798
+ print(complex_tool.to_markdown())
799
+ print("Complex Tool Docstring:")
800
+ print(complex_tool.to_docstring())
801
+ print("Execution result (sync):", complex_tool.execute(data=[1, 2, 3]))
802
+ print("Execution result (callable):", asyncio.run(complex_tool(data=[1, 2, 3])))
484
803
  print()
485
804
 
486
805
  # Comprehensive tool for to_docstring demonstration with custom return type
@@ -492,9 +811,112 @@ if __name__ == "__main__":
492
811
  ToolArgument(name="y", arg_type="float", description="The second number", default="0.0", example="1.5"),
493
812
  ToolArgument(name="verbose", arg_type="boolean", description="Print extra info", default="False")
494
813
  ],
495
- return_type="int" # Custom return type
814
+ return_type="int",
815
+ return_description="The computed result of the operation.",
816
+ return_example="42",
817
+ return_structure="A single integer value."
496
818
  )
497
819
  print("Comprehensive Tool Markdown:")
498
820
  print(docstring_tool.to_markdown())
499
821
  print("Comprehensive Tool Docstring with Custom Return Type:")
500
- print(docstring_tool.to_docstring())
822
+ print(docstring_tool.to_docstring())
823
+
824
+ from dataclasses import dataclass
825
+
826
+ @dataclass
827
+ class Point:
828
+ x: int
829
+ y: int
830
+
831
+ async def distance(point1: Point, point2: Point) -> float:
832
+ """
833
+ Calculate the Euclidean distance between two points.
834
+
835
+ Args:
836
+ point1: First point with x and y coordinates.
837
+ point2: Second point with x and y coordinates.
838
+
839
+ Returns:
840
+ The Euclidean distance between two points.
841
+ """
842
+ return ((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2) ** 0.5
843
+
844
+ distance_tool = create_tool(distance)
845
+ print("Distance Tool Markdown:")
846
+ print(distance_tool.to_markdown())
847
+ print("Distance Tool Docstring:")
848
+ print(distance_tool.to_docstring())
849
+ print("Execution result (sync):", distance_tool.execute(point1=Point(x=1, y=2), point2=Point(x=4, y=6)))
850
+ print("Execution result (callable):", asyncio.run(distance_tool(point1=Point(x=1, y=2), point2=Point(x=4, y=6))))
851
+ print()
852
+
853
+ async def hello(name: str) -> str:
854
+ """
855
+ Greet a person.
856
+
857
+ Args:
858
+ name: Name of the person.
859
+
860
+ Returns:
861
+ A greeting message.
862
+ """
863
+ return f"Hello, {name}"
864
+
865
+ hello_tool = create_tool(hello)
866
+ print("Hello Tool Markdown:")
867
+ print(hello_tool.to_markdown())
868
+ print("Hello Tool Docstring:")
869
+ print(hello_tool.to_docstring())
870
+ print("Execution result (sync):", hello_tool.execute(name="Alice"))
871
+ print("Execution result (async):", asyncio.run(hello_tool.async_execute(name="Alice")))
872
+ print("Execution result (callable named arguments):", asyncio.run(hello_tool(name="Alice")))
873
+ print("Execution result (callable positional arguments):", asyncio.run(hello_tool("Alice")))
874
+ print()
875
+
876
+ def add(a: int, b: int = 0) -> int:
877
+ """
878
+ Add two numbers.
879
+
880
+ Args:
881
+ a: First number.
882
+ b: Second number (optional).
883
+
884
+ Returns:
885
+ The sum of a and b.
886
+ """
887
+ return a + b
888
+
889
+ add_tool = create_tool(add)
890
+ print("Add Tool Markdown:")
891
+ print(add_tool.to_markdown())
892
+ print("Add Tool Docstring:")
893
+ print(add_tool.to_docstring())
894
+ print("Execution result (sync):", add_tool.execute(a=5, b=3))
895
+ print("Execution result (async):", asyncio.run(add_tool.async_execute(a=5, b=3)))
896
+ print("Execution result (callable named arguments):", asyncio.run(add_tool(a=5, b=3)))
897
+ print("Execution result (callable positional arguments):", asyncio.run(add_tool(5, 3)))
898
+ print()
899
+
900
+ def subtract(a: int, b: int = 0) -> int:
901
+ """
902
+ Subtract two numbers.
903
+
904
+ Args:
905
+ a: First number.
906
+ b: Second number (optional).
907
+
908
+ Returns:
909
+ The difference of a and b.
910
+ """
911
+ return a - b
912
+
913
+ subtract_tool = create_tool(subtract)
914
+ print("Subtract Tool Markdown:")
915
+ print(subtract_tool.to_markdown())
916
+ print("Subtract Tool Docstring:")
917
+ print(subtract_tool.to_docstring())
918
+ print("Execution result (sync):", subtract_tool.execute(a=5, b=3))
919
+ print("Execution result (async):", asyncio.run(subtract_tool.async_execute(a=5, b=3)))
920
+ print("Execution result (callable named arguments):", asyncio.run(subtract_tool(a=5, b=3)))
921
+ print("Execution result (callable positional arguments):", asyncio.run(subtract_tool(5, 3)))
922
+ print()