quantalogic 0.50.29__py3-none-any.whl → 0.52.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/flow/__init__.py +17 -0
- quantalogic/flow/flow.py +9 -7
- quantalogic/flow/flow_extractor.py +32 -100
- quantalogic/flow/flow_generator.py +10 -3
- quantalogic/flow/flow_manager.py +88 -33
- quantalogic/flow/flow_manager_schema.py +3 -4
- quantalogic/flow/flow_mermaid.py +240 -0
- quantalogic/flow/flow_validator.py +335 -0
- quantalogic/flow/flow_yaml.md +393 -322
- quantalogic/tools/__init__.py +3 -2
- quantalogic/tools/tool.py +129 -3
- quantalogic-0.52.0.dist-info/METADATA +787 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.52.0.dist-info}/RECORD +16 -14
- quantalogic-0.50.29.dist-info/METADATA +0 -554
- {quantalogic-0.50.29.dist-info → quantalogic-0.52.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.52.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.52.0.dist-info}/entry_points.txt +0 -0
quantalogic/tools/__init__.py
CHANGED
@@ -28,7 +28,7 @@ from .sequence_tool import SequenceTool
|
|
28
28
|
from .serpapi_search_tool import SerpApiSearchTool
|
29
29
|
from .sql_query_tool import SQLQueryTool
|
30
30
|
from .task_complete_tool import TaskCompleteTool
|
31
|
-
from .tool import Tool, ToolArgument
|
31
|
+
from .tool import Tool, ToolArgument, create_tool
|
32
32
|
from .unified_diff_tool import UnifiedDiffTool
|
33
33
|
from .wikipedia_search_tool import WikipediaSearchTool
|
34
34
|
from .write_file_tool import WriteFileTool
|
@@ -65,5 +65,6 @@ __all__ = [
|
|
65
65
|
'ToolArgument',
|
66
66
|
'UnifiedDiffTool',
|
67
67
|
'WikipediaSearchTool',
|
68
|
-
'WriteFileTool'
|
68
|
+
'WriteFileTool',
|
69
|
+
"create_tool"
|
69
70
|
]
|
quantalogic/tools/tool.py
CHANGED
@@ -4,11 +4,16 @@ This module provides base classes and data models for creating configurable tool
|
|
4
4
|
with type-validated arguments and execution methods.
|
5
5
|
"""
|
6
6
|
|
7
|
+
import ast
|
7
8
|
import asyncio # Added for asynchronous support
|
8
|
-
|
9
|
+
import inspect
|
10
|
+
from typing import Any, Callable, Literal, TypeVar
|
9
11
|
|
12
|
+
from docstring_parser import parse as parse_docstring
|
10
13
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
11
14
|
|
15
|
+
# Type variable for create_tool to preserve function signature
|
16
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
12
17
|
|
13
18
|
class ToolArgument(BaseModel):
|
14
19
|
"""Represents an argument for a tool with validation and description.
|
@@ -204,7 +209,7 @@ class Tool(ToolDefinition):
|
|
204
209
|
]
|
205
210
|
return []
|
206
211
|
|
207
|
-
def execute(self, **kwargs) -> str:
|
212
|
+
def execute(self, **kwargs: Any) -> str:
|
208
213
|
"""Execute the tool with provided arguments.
|
209
214
|
|
210
215
|
If not implemented by a subclass, falls back to the asynchronous execute_async method.
|
@@ -221,7 +226,7 @@ class Tool(ToolDefinition):
|
|
221
226
|
return asyncio.run(self.async_execute(**kwargs))
|
222
227
|
raise NotImplementedError("This method should be implemented by subclasses.")
|
223
228
|
|
224
|
-
async def async_execute(self, **kwargs) -> str:
|
229
|
+
async def async_execute(self, **kwargs: Any) -> str:
|
225
230
|
"""Asynchronous version of execute.
|
226
231
|
|
227
232
|
By default, runs the synchronous execute method in a separate thread using asyncio.to_thread.
|
@@ -253,6 +258,96 @@ class Tool(ToolDefinition):
|
|
253
258
|
return {name: value for name, value in properties.items() if value is not None and name in argument_names}
|
254
259
|
|
255
260
|
|
261
|
+
def create_tool(func: F) -> Tool:
|
262
|
+
"""Create a Tool instance from a Python function using AST analysis.
|
263
|
+
|
264
|
+
Analyzes the function's source code to extract its name, docstring, and arguments,
|
265
|
+
then constructs a Tool subclass with appropriate execution logic.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
func: The Python function (sync or async) to convert into a Tool.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
A Tool subclass instance configured based on the function.
|
272
|
+
|
273
|
+
Raises:
|
274
|
+
ValueError: If the input is not a valid function or lacks a function definition.
|
275
|
+
"""
|
276
|
+
if not callable(func):
|
277
|
+
raise ValueError("Input must be a callable function")
|
278
|
+
|
279
|
+
# Get source code and parse with AST
|
280
|
+
try:
|
281
|
+
source = inspect.getsource(func).strip()
|
282
|
+
tree = ast.parse(source)
|
283
|
+
except (OSError, TypeError, SyntaxError) as e:
|
284
|
+
raise ValueError(f"Failed to parse function source: {e}")
|
285
|
+
|
286
|
+
# Ensure root node is a function definition
|
287
|
+
if not tree.body or not isinstance(tree.body[0], (ast.FunctionDef, ast.AsyncFunctionDef)):
|
288
|
+
raise ValueError("Source must define a single function")
|
289
|
+
func_def = tree.body[0]
|
290
|
+
|
291
|
+
# Extract metadata
|
292
|
+
name = func_def.name
|
293
|
+
docstring = ast.get_docstring(func_def) or ""
|
294
|
+
parsed_doc = parse_docstring(docstring)
|
295
|
+
description = parsed_doc.short_description or f"Tool generated from {name}"
|
296
|
+
param_docs = {p.arg_name: p.description for p in parsed_doc.params}
|
297
|
+
is_async = isinstance(func_def, ast.AsyncFunctionDef)
|
298
|
+
|
299
|
+
# Get type hints using typing module
|
300
|
+
from typing import get_type_hints
|
301
|
+
type_hints = get_type_hints(func)
|
302
|
+
type_map = {int: "int", str: "string", float: "float", bool: "boolean"}
|
303
|
+
|
304
|
+
# Process arguments
|
305
|
+
args = func_def.args
|
306
|
+
defaults = [None] * (len(args.args) - len(args.defaults)) + [
|
307
|
+
ast.unparse(d) if isinstance(d, ast.AST) else str(d) for d in args.defaults
|
308
|
+
]
|
309
|
+
arguments: list[ToolArgument] = []
|
310
|
+
|
311
|
+
for i, arg in enumerate(args.args):
|
312
|
+
arg_name = arg.arg
|
313
|
+
default = defaults[i]
|
314
|
+
required = default is None
|
315
|
+
|
316
|
+
# Determine argument type
|
317
|
+
hint = type_hints.get(arg_name, str) # Default to str if no hint
|
318
|
+
arg_type = type_map.get(hint, "string") # Fallback to string for unmapped types
|
319
|
+
|
320
|
+
# Use docstring or default description
|
321
|
+
description = param_docs.get(arg_name, f"Argument {arg_name}")
|
322
|
+
|
323
|
+
# Create ToolArgument
|
324
|
+
arguments.append(ToolArgument(
|
325
|
+
name=arg_name,
|
326
|
+
arg_type=arg_type,
|
327
|
+
description=description,
|
328
|
+
required=required,
|
329
|
+
default=default,
|
330
|
+
example=default if default else None
|
331
|
+
))
|
332
|
+
|
333
|
+
# Define Tool subclass
|
334
|
+
class GeneratedTool(Tool):
|
335
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
336
|
+
super().__init__(*args, name=name, description=description, arguments=arguments, **kwargs)
|
337
|
+
self._func = func
|
338
|
+
|
339
|
+
if is_async:
|
340
|
+
async def async_execute(self, **kwargs: Any) -> str:
|
341
|
+
result = await self._func(**kwargs)
|
342
|
+
return str(result)
|
343
|
+
else:
|
344
|
+
def execute(self, **kwargs: Any) -> str:
|
345
|
+
result = self._func(**kwargs)
|
346
|
+
return str(result)
|
347
|
+
|
348
|
+
return GeneratedTool()
|
349
|
+
|
350
|
+
|
256
351
|
if __name__ == "__main__":
|
257
352
|
tool = Tool(name="my_tool", description="A simple tool", arguments=[ToolArgument(name="arg1", arg_type="string")])
|
258
353
|
print(tool.to_markdown())
|
@@ -274,3 +369,34 @@ if __name__ == "__main__":
|
|
274
369
|
)
|
275
370
|
print(tool_with_fields_defined.to_markdown())
|
276
371
|
print(tool_with_fields_defined.get_injectable_properties_in_execution())
|
372
|
+
|
373
|
+
# Test create_tool with synchronous function
|
374
|
+
def add(a: int, b: int = 0) -> int:
|
375
|
+
"""Add two numbers.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
a: First number.
|
379
|
+
b: Second number (optional).
|
380
|
+
"""
|
381
|
+
return a + b
|
382
|
+
|
383
|
+
# Test create_tool with asynchronous function
|
384
|
+
async def greet(name: str) -> str:
|
385
|
+
"""Greet a person.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
name: Name of the person.
|
389
|
+
"""
|
390
|
+
await asyncio.sleep(0.1) # Simulate async work
|
391
|
+
return f"Hello, {name}"
|
392
|
+
|
393
|
+
# Create and test tools
|
394
|
+
sync_tool = create_tool(add)
|
395
|
+
print("\nSynchronous Tool:")
|
396
|
+
print(sync_tool.to_markdown())
|
397
|
+
print("Execution result:", sync_tool.execute(a=5, b=3))
|
398
|
+
|
399
|
+
async_tool = create_tool(greet)
|
400
|
+
print("\nAsynchronous Tool:")
|
401
|
+
print(async_tool.to_markdown())
|
402
|
+
print("Execution result:", asyncio.run(async_tool.async_execute(name="Alice")))
|