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.
@@ -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
- from typing import Any, Literal
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")))