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.
- quantalogic/agent.py +0 -1
- quantalogic/codeact/TODO.md +14 -0
- quantalogic/codeact/agent.py +400 -421
- quantalogic/codeact/cli.py +42 -224
- quantalogic/codeact/cli_commands/__init__.py +0 -0
- quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
- quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
- quantalogic/codeact/cli_commands/list_executor.py +15 -0
- quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
- quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
- quantalogic/codeact/cli_commands/task.py +215 -0
- quantalogic/codeact/cli_commands/tool_info.py +24 -0
- quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
- quantalogic/codeact/config.yaml +21 -0
- quantalogic/codeact/constants.py +1 -1
- quantalogic/codeact/events.py +12 -5
- quantalogic/codeact/examples/README.md +342 -0
- quantalogic/codeact/examples/agent_sample.yaml +29 -0
- quantalogic/codeact/executor.py +186 -0
- quantalogic/codeact/history_manager.py +94 -0
- quantalogic/codeact/llm_util.py +3 -22
- quantalogic/codeact/plugin_manager.py +92 -0
- quantalogic/codeact/prompts/generate_action.j2 +65 -14
- quantalogic/codeact/prompts/generate_program.j2 +32 -19
- quantalogic/codeact/react_agent.py +318 -0
- quantalogic/codeact/reasoner.py +185 -0
- quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
- quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
- quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
- quantalogic/codeact/templates.py +7 -0
- quantalogic/codeact/tools_manager.py +242 -119
- quantalogic/codeact/utils.py +16 -89
- quantalogic/codeact/xml_utils.py +126 -0
- quantalogic/flow/flow.py +151 -41
- quantalogic/flow/flow_extractor.py +61 -1
- quantalogic/flow/flow_generator.py +34 -6
- quantalogic/flow/flow_manager.py +64 -25
- quantalogic/flow/flow_manager_schema.py +32 -0
- quantalogic/tools/action_gen.py +1 -1
- quantalogic/tools/tool.py +531 -109
- {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -3
- {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/RECORD +45 -22
- {quantalogic-0.61.3.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
- quantalogic-0.80.dist-info/entry_points.txt +3 -0
- quantalogic-0.61.3.dist-info/entry_points.txt +0 -6
- {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
|
8
|
+
import asyncio
|
9
9
|
import inspect
|
10
|
-
from typing import Any, Callable,
|
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
|
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:
|
32
|
-
..., description="The type 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."
|
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
|
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}`:
|
311
|
+
f" - `{arg.name}`: ({type_info}, {required_status}{value_info})\n"
|
312
|
+
f" {arg.description or ''}\n"
|
141
313
|
)
|
142
|
-
if
|
143
|
-
markdown += parameters + "\n
|
314
|
+
if parameters:
|
315
|
+
markdown += parameters + "\n"
|
144
316
|
else:
|
145
|
-
markdown += "None\n\n"
|
317
|
+
markdown += " None\n\n"
|
146
318
|
|
147
|
-
|
148
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
docstring
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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),
|
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) ->
|
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
|
-
|
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) ->
|
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
|
-
|
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
|
-
|
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 =
|
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__(
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
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)
|
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:",
|
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"
|
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()
|