quantalogic 0.80__py3-none-any.whl → 0.93__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 (55) hide show
  1. quantalogic/flow/__init__.py +16 -34
  2. quantalogic/main.py +11 -6
  3. quantalogic/tools/tool.py +8 -922
  4. quantalogic-0.93.dist-info/METADATA +475 -0
  5. {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/RECORD +8 -54
  6. quantalogic/codeact/TODO.md +0 -14
  7. quantalogic/codeact/__init__.py +0 -0
  8. quantalogic/codeact/agent.py +0 -478
  9. quantalogic/codeact/cli.py +0 -50
  10. quantalogic/codeact/cli_commands/__init__.py +0 -0
  11. quantalogic/codeact/cli_commands/create_toolbox.py +0 -45
  12. quantalogic/codeact/cli_commands/install_toolbox.py +0 -20
  13. quantalogic/codeact/cli_commands/list_executor.py +0 -15
  14. quantalogic/codeact/cli_commands/list_reasoners.py +0 -15
  15. quantalogic/codeact/cli_commands/list_toolboxes.py +0 -47
  16. quantalogic/codeact/cli_commands/task.py +0 -215
  17. quantalogic/codeact/cli_commands/tool_info.py +0 -24
  18. quantalogic/codeact/cli_commands/uninstall_toolbox.py +0 -43
  19. quantalogic/codeact/config.yaml +0 -21
  20. quantalogic/codeact/constants.py +0 -9
  21. quantalogic/codeact/events.py +0 -85
  22. quantalogic/codeact/examples/README.md +0 -342
  23. quantalogic/codeact/examples/agent_sample.yaml +0 -29
  24. quantalogic/codeact/executor.py +0 -186
  25. quantalogic/codeact/history_manager.py +0 -94
  26. quantalogic/codeact/llm_util.py +0 -57
  27. quantalogic/codeact/plugin_manager.py +0 -92
  28. quantalogic/codeact/prompts/error_format.j2 +0 -11
  29. quantalogic/codeact/prompts/generate_action.j2 +0 -77
  30. quantalogic/codeact/prompts/generate_program.j2 +0 -52
  31. quantalogic/codeact/prompts/response_format.j2 +0 -11
  32. quantalogic/codeact/react_agent.py +0 -318
  33. quantalogic/codeact/reasoner.py +0 -185
  34. quantalogic/codeact/templates/toolbox/README.md.j2 +0 -10
  35. quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +0 -16
  36. quantalogic/codeact/templates/toolbox/tools.py.j2 +0 -6
  37. quantalogic/codeact/templates.py +0 -7
  38. quantalogic/codeact/tools_manager.py +0 -258
  39. quantalogic/codeact/utils.py +0 -62
  40. quantalogic/codeact/xml_utils.py +0 -126
  41. quantalogic/flow/flow.py +0 -1070
  42. quantalogic/flow/flow_extractor.py +0 -783
  43. quantalogic/flow/flow_generator.py +0 -322
  44. quantalogic/flow/flow_manager.py +0 -676
  45. quantalogic/flow/flow_manager_schema.py +0 -287
  46. quantalogic/flow/flow_mermaid.py +0 -365
  47. quantalogic/flow/flow_validator.py +0 -479
  48. quantalogic/flow/flow_yaml.linkedin.md +0 -31
  49. quantalogic/flow/flow_yaml.md +0 -767
  50. quantalogic/flow/templates/prompt_check_inventory.j2 +0 -1
  51. quantalogic/flow/templates/system_check_inventory.j2 +0 -1
  52. quantalogic-0.80.dist-info/METADATA +0 -900
  53. {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/LICENSE +0 -0
  54. {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/WHEEL +0 -0
  55. {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/entry_points.txt +0 -0
quantalogic/tools/tool.py CHANGED
@@ -1,922 +1,8 @@
1
- """Module for defining tool arguments and base tool classes.
2
-
3
- This module provides base classes and data models for creating configurable tools
4
- with type-validated arguments and execution methods.
5
- """
6
-
7
- import ast
8
- import asyncio
9
- import inspect
10
- from typing import Any, Callable, TypeVar, Union, get_args, get_origin
11
-
12
- from docstring_parser import parse as parse_docstring
13
- from pydantic import BaseModel, ConfigDict, Field, field_validator
14
-
15
- # Type variable for create_tool to preserve function signature
16
- F = TypeVar('F', bound=Callable[..., Any])
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
-
161
- class ToolArgument(BaseModel):
162
- """Represents an argument for a tool with validation and description.
163
-
164
- Attributes:
165
- name: The name of the argument.
166
- arg_type: The type of the argument, e.g., 'string', 'int', 'list[int]', 'dict[str, float]'.
167
- description: Optional description of the argument.
168
- required: Indicates if the argument is mandatory.
169
- default: Optional default value for the argument.
170
- example: Optional example value to illustrate the argument's usage.
171
- type_details: Detailed description of the argument's type.
172
- """
173
-
174
- name: str = Field(..., description="The name of the argument.")
175
- arg_type: str = Field(
176
- ..., description="The type of the argument, e.g., 'string', 'int', 'list[int]', 'dict[str, float]', etc."
177
- )
178
- description: str | None = Field(default=None, description="A brief description of the argument.")
179
- required: bool = Field(default=False, description="Indicates if the argument is required.")
180
- default: str | None = Field(
181
- default=None, description="The default value for the argument. This parameter is required."
182
- )
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
-
186
-
187
- class ToolDefinition(BaseModel):
188
- """Base class for defining tool configurations without execution logic.
189
-
190
- Attributes:
191
- name: Unique name of the tool.
192
- description: Brief description of the tool's functionality.
193
- arguments: List of arguments the tool accepts.
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.
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.
201
- """
202
-
203
- model_config = ConfigDict(extra="allow", validate_assignment=True)
204
-
205
- name: str = Field(..., description="The unique name of the tool.")
206
- description: str = Field(..., description="A brief description of what the tool does.")
207
- arguments: list[ToolArgument] = Field(default_factory=list, description="A list of arguments the tool accepts.")
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.")
214
- need_validation: bool = Field(
215
- default=False,
216
- description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
217
- )
218
- need_post_process: bool = Field(
219
- default=True,
220
- description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
221
- )
222
- need_variables: bool = Field(
223
- default=False,
224
- description="When True, provides access to the agent's variable store. Required for tools that need to interpolate variables (e.g., Jinja templates).",
225
- )
226
- need_caller_context_memory: bool = Field(
227
- default=False,
228
- description="When True, provides access to the agent's conversation history. Useful for tools that need context from previous interactions.",
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
- )
238
-
239
- def get_properties(self, exclude: list[str] | None = None) -> dict[str, Any]:
240
- """Return a dictionary of all non-None properties, excluding Tool class fields and specified fields.
241
-
242
- Args:
243
- exclude: Optional list of field names to exclude from the result
244
-
245
- Returns:
246
- Dictionary of property names and values, excluding Tool class fields and specified fields.
247
- """
248
- exclude = exclude or []
249
- tool_fields = {
250
- "name",
251
- "description",
252
- "arguments",
253
- "return_type",
254
- "return_description",
255
- "return_type_details",
256
- "return_example",
257
- "return_structure",
258
- "original_docstring",
259
- "need_validation",
260
- "need_post_process",
261
- "need_variables",
262
- "need_caller_context_memory",
263
- "is_async",
264
- "toolbox_name",
265
- }
266
- properties = {}
267
-
268
- for name, value in self.__dict__.items():
269
- if name not in tool_fields and name not in exclude and value is not None and not name.startswith("_"):
270
- properties[name] = value
271
-
272
- return properties
273
-
274
- def to_json(self) -> str:
275
- """Convert the tool to a JSON string representation.
276
-
277
- Returns:
278
- A JSON string of the tool's configuration.
279
- """
280
- return self.model_dump_json()
281
-
282
- def to_markdown(self) -> str:
283
- """Create a comprehensive Markdown representation of the tool.
284
-
285
- Returns:
286
- A detailed Markdown string representing the tool's configuration and usage.
287
- """
288
- markdown = f"`{self.name}`:\n"
289
- markdown += f"- **Description**: {self.description}\n\n"
290
-
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"
294
-
295
- if self.arguments:
296
- markdown += "- **Parameters**:\n"
297
- parameters = ""
298
- for arg in self.arguments:
299
- if properties_injectable.get(arg.name) is not None:
300
- continue
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})"
304
- required_status = "required" if arg.required else "optional"
305
- value_info = ""
306
- if arg.default is not None:
307
- value_info += f", default: `{arg.default}`"
308
- if arg.example is not None:
309
- value_info += f", example: `{arg.example}`"
310
- parameters += (
311
- f" - `{arg.name}`: ({type_info}, {required_status}{value_info})\n"
312
- f" {arg.description or ''}\n"
313
- )
314
- if parameters:
315
- markdown += parameters + "\n"
316
- else:
317
- markdown += " None\n\n"
318
-
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"
337
- markdown += "```xml\n"
338
- markdown += f"<{self.name}>\n"
339
-
340
- for arg in self.arguments:
341
- if properties_injectable.get(arg.name) is not None:
342
- continue
343
- example_value = arg.example or arg.default or f"Your {arg.name} here"
344
- markdown += f" <{arg.name}>{example_value}</{arg.name}>\n"
345
-
346
- markdown += f"</{self.name}>\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"
356
-
357
- return markdown
358
-
359
- def get_non_injectable_arguments(self) -> list[ToolArgument]:
360
- """Get arguments that cannot be injected from properties.
361
-
362
- Returns:
363
- List of ToolArgument instances that cannot be injected by the agent.
364
- """
365
- properties_injectable = self.get_injectable_properties_in_execution()
366
- return [arg for arg in self.arguments if properties_injectable.get(arg.name) is None]
367
-
368
- def get_injectable_properties_in_execution(self) -> dict[str, Any]:
369
- """Get injectable properties excluding tool arguments.
370
-
371
- Returns:
372
- A dictionary of property names and values, excluding tool arguments and None values.
373
- """
374
- return {}
375
-
376
- def to_docstring(self) -> str:
377
- """Convert the tool definition into a Google-style docstring with function signature.
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
-
382
- Returns:
383
- A string formatted as a valid Python docstring representing the tool's configuration,
384
- including the function signature, detailed argument types, and return type with descriptions.
385
- """
386
- signature_parts = []
387
- for arg in self.arguments:
388
- arg_str = f"{arg.name}: {arg.arg_type}"
389
- if arg.default is not None:
390
- arg_str += f" = {arg.default}"
391
- signature_parts.append(arg_str)
392
- signature = f"{'async ' if self.is_async else ''}def {self.name}({', '.join(signature_parts)}) -> {self.return_type}:"
393
-
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"""'
459
- return docstring
460
-
461
-
462
- class Tool(ToolDefinition):
463
- """Extended class for tools with execution capabilities.
464
-
465
- Inherits from ToolDefinition and adds execution functionality.
466
- """
467
-
468
- @field_validator("arguments", mode="before")
469
- @classmethod
470
- def validate_arguments(cls, v: Any) -> list[ToolArgument]:
471
- """Validate and convert arguments to ToolArgument instances.
472
-
473
- Args:
474
- v: Input arguments to validate.
475
-
476
- Returns:
477
- A list of validated ToolArgument instances.
478
- """
479
- if isinstance(v, list):
480
- return [
481
- ToolArgument(**arg)
482
- if isinstance(arg, dict)
483
- else arg
484
- if isinstance(arg, ToolArgument)
485
- else ToolArgument(name=str(arg), arg_type=type(arg).__name__)
486
- for arg in v
487
- ]
488
- return []
489
-
490
- def execute(self, **kwargs: Any) -> Any:
491
- """Execute the tool with provided arguments.
492
-
493
- If not implemented by a subclass, falls back to the asynchronous execute_async method.
494
-
495
- Args:
496
- **kwargs: Keyword arguments for tool execution.
497
-
498
- Returns:
499
- The result of tool execution, preserving the original type returned by the tool's logic.
500
- """
501
- if self.__class__.execute is Tool.execute:
502
- return asyncio.run(self.async_execute(**kwargs))
503
- raise NotImplementedError("This method should be implemented by subclasses.")
504
-
505
- async def async_execute(self, **kwargs: Any) -> Any:
506
- """Asynchronous version of execute.
507
-
508
- By default, runs the synchronous execute method in a separate thread using asyncio.to_thread.
509
- Subclasses can override this method to provide a native asynchronous implementation for
510
- operations that benefit from async I/O (e.g., network requests).
511
-
512
- Args:
513
- **kwargs: Keyword arguments for tool execution.
514
-
515
- Returns:
516
- The result of tool execution, preserving the original type returned by the tool's logic.
517
- """
518
- if self.__class__.async_execute is Tool.async_execute:
519
- return await asyncio.to_thread(self.execute, **kwargs)
520
- raise NotImplementedError("This method should be implemented by subclasses.")
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
-
556
- def get_injectable_properties_in_execution(self) -> dict[str, Any]:
557
- """Get injectable properties excluding tool arguments.
558
-
559
- Returns:
560
- A dictionary of property names and values, excluding tool arguments and None values.
561
- """
562
- argument_names = {arg.name for arg in self.arguments}
563
- properties = self.get_properties(exclude=["arguments"])
564
- return {name: value for name, value in properties.items() if value is not None and name in argument_names}
565
-
566
-
567
- def create_tool(func: F) -> Tool:
568
- """Create a Tool instance from a Python function using AST analysis with enhanced return type metadata.
569
-
570
- Analyzes the function's source code to extract its name, docstring, and arguments,
571
- then constructs a Tool subclass with appropriate execution logic for both
572
- synchronous and asynchronous functions.
573
-
574
- Args:
575
- func: The Python function (sync or async) to convert into a Tool.
576
-
577
- Returns:
578
- A Tool subclass instance configured based on the function.
579
-
580
- Raises:
581
- ValueError: If the input is not a valid function or lacks a function definition.
582
- """
583
- if not callable(func):
584
- raise ValueError("Input must be a callable function")
585
-
586
- try:
587
- source = inspect.getsource(func).strip()
588
- tree = ast.parse(source)
589
- except (OSError, TypeError, SyntaxError) as e:
590
- raise ValueError(f"Failed to parse function source: {e}")
591
-
592
- if not tree.body or not isinstance(tree.body[0], (ast.FunctionDef, ast.AsyncFunctionDef)):
593
- raise ValueError("Source must define a single function")
594
- func_def = tree.body[0]
595
-
596
- name = func_def.name
597
- docstring = ast.get_docstring(func_def) or ""
598
- parsed_doc = parse_docstring(docstring)
599
- description = parsed_doc.short_description or f"Tool generated from {name}"
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
602
- is_async = isinstance(func_def, ast.AsyncFunctionDef)
603
-
604
- from typing import get_type_hints
605
- type_hints = get_type_hints(func)
606
-
607
- args = func_def.args
608
- defaults = [None] * (len(args.args) - len(args.defaults)) + [
609
- ast.unparse(d) if isinstance(d, ast.AST) else str(d) for d in args.defaults
610
- ]
611
- arguments: list[ToolArgument] = []
612
-
613
- for i, arg in enumerate(args.args):
614
- arg_name = arg.arg
615
- default = defaults[i]
616
- required = default is None
617
- hint = type_hints.get(arg_name, str)
618
- arg_type = type_hint_to_str(hint)
619
- description = param_docs.get(arg_name, f"Argument {arg_name}")
620
- arguments.append(ToolArgument(
621
- name=arg_name,
622
- arg_type=arg_type,
623
- description=description,
624
- required=required,
625
- default=default,
626
- example=default if default else None,
627
- type_details=get_type_description(hint)
628
- ))
629
-
630
- return_type = type_hints.get("return", str)
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)
634
-
635
- class GeneratedTool(Tool):
636
- def __init__(self, *args: Any, **kwargs: Any):
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
- )
651
- self._func = func
652
-
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)
684
-
685
- return GeneratedTool()
686
-
687
-
688
- if __name__ == "__main__":
689
- # Basic tool with argument
690
- tool = Tool(
691
- name="my_tool",
692
- description="A simple tool",
693
- arguments=[ToolArgument(name="arg1", arg_type="string")]
694
- )
695
- print("Basic Tool Markdown:")
696
- print(tool.to_markdown())
697
- print("Basic Tool Docstring:")
698
- print(tool.to_docstring())
699
- print()
700
-
701
- # Tool with injectable field (undefined)
702
- class MyTool(Tool):
703
- field1: str | None = Field(default=None, description="Field 1 description")
704
-
705
- tool_with_fields = MyTool(
706
- name="my_tool1",
707
- description="A simple tool with a field",
708
- arguments=[ToolArgument(name="field1", arg_type="string")]
709
- )
710
- print("Tool with Undefined Field Markdown:")
711
- print(tool_with_fields.to_markdown())
712
- print("Injectable Properties (should be empty):", tool_with_fields.get_injectable_properties_in_execution())
713
- print("Tool with Undefined Field Docstring:")
714
- print(tool_with_fields.to_docstring())
715
- print()
716
-
717
- # Tool with defined injectable field
718
- tool_with_fields_defined = MyTool(
719
- name="my_tool2",
720
- description="A simple tool with a defined field",
721
- field1="field1_value",
722
- arguments=[ToolArgument(name="field1", arg_type="string")]
723
- )
724
- print("Tool with Defined Field Markdown:")
725
- print(tool_with_fields_defined.to_markdown())
726
- print("Injectable Properties (should include field1):", tool_with_fields_defined.get_injectable_properties_in_execution())
727
- print("Tool with Defined Field Docstring:")
728
- print(tool_with_fields_defined.to_docstring())
729
- print()
730
-
731
- # Test create_tool with synchronous function
732
- def add(a: int, b: int = 0) -> int:
733
- """Add two numbers.
734
-
735
- Args:
736
- a: First number.
737
- b: Second number (optional).
738
-
739
- Returns:
740
- The sum of a and b.
741
- """
742
- return a + b
743
-
744
- sync_tool = create_tool(add)
745
- print("Synchronous Tool Markdown:")
746
- print(sync_tool.to_markdown())
747
- print("Synchronous Tool Docstring:")
748
- print(sync_tool.to_docstring())
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)))
752
- print()
753
-
754
- # Test create_tool with asynchronous function
755
- async def greet(name: str) -> str:
756
- """Greet a person.
757
-
758
- Args:
759
- name: Name of the person.
760
-
761
- Returns:
762
- A greeting message.
763
-
764
- Examples:
765
- >>> await greet("Alice")
766
- 'Hello, Alice'
767
- """
768
- await asyncio.sleep(0.1)
769
- return f"Hello, {name}"
770
-
771
- async_tool = create_tool(greet)
772
- print("Asynchronous Tool Markdown:")
773
- print(async_tool.to_markdown())
774
- print("Asynchronous Tool Docstring:")
775
- print(async_tool.to_docstring())
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])))
803
- print()
804
-
805
- # Comprehensive tool for to_docstring demonstration with custom return type
806
- docstring_tool = Tool(
807
- name="sample_tool",
808
- description="A sample tool for testing docstring generation.",
809
- arguments=[
810
- ToolArgument(name="x", arg_type="int", description="The first number", required=True),
811
- ToolArgument(name="y", arg_type="float", description="The second number", default="0.0", example="1.5"),
812
- ToolArgument(name="verbose", arg_type="boolean", description="Print extra info", default="False")
813
- ],
814
- return_type="int",
815
- return_description="The computed result of the operation.",
816
- return_example="42",
817
- return_structure="A single integer value."
818
- )
819
- print("Comprehensive Tool Markdown:")
820
- print(docstring_tool.to_markdown())
821
- print("Comprehensive Tool Docstring with Custom Return Type:")
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()
1
+ from quantalogic_toolbox.tool import Tool, ToolArgument, create_tool
2
+
3
+ __all__ = [
4
+ "ToolArgument",
5
+ "Tool",
6
+ "create_tool",
7
+ ]
8
+