quantalogic 0.51.0__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")))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: quantalogic
3
- Version: 0.51.0
3
+ Version: 0.52.0
4
4
  Summary: QuantaLogic ReAct Agents
5
5
  Author: Raphaël MANSUY
6
6
  Author-email: raphael.mansuy@gmail.com
@@ -88,6 +88,8 @@ At [QuantaLogic](https://www.quantalogic.app), we spotted a black hole: amazing
88
88
  - [Quick Start](#quick-start)
89
89
  - [ReAct Framework: Dynamic Agents](#react-framework-dynamic-agents)
90
90
  - [Flow Module: Structured Workflows](#flow-module-structured-workflows)
91
+ - 📘 **[Workflow YAML DSL Specification](./quantalogic/flow/flow_yaml.md)**: Comprehensive guide to defining powerful, structured workflows using our Domain-Specific Language
92
+ - 📚 **[Flow YAML Documentation](https://quantalogic.github.io/quantalogic/flow/flow_yaml)**: Dive into the official documentation for a deeper understanding of Flow YAML and its applications
91
93
  - [ReAct vs. Flow: Pick Your Power](#react-vs-flow-pick-your-power)
92
94
  - [Using the CLI](#using-the-cli)
93
95
  - [Examples That Spark Joy](#examples-that-spark-joy)
@@ -357,7 +359,15 @@ Perfect for coding, debugging, or answering wild questions on the fly.
357
359
 
358
360
  ## Flow Module: Structured Workflows
359
361
 
360
- The **Flow module** is your architect—building workflows that hum with precision. Its all about nodes, transitions, and a steady rhythm, ideal for repeatable missions.
362
+ The **Flow module** is your architect—building workflows that hum with precision. It's all about nodes, transitions, and a steady rhythm, ideal for repeatable missions.
363
+
364
+ 🔍 **Want to dive deeper?** Check out our comprehensive [Workflow YAML DSL Specification](./quantalogic/flow/flow_yaml.md), a detailed guide that walks you through defining powerful, structured workflows. From basic node configurations to complex transition logic, this documentation is your roadmap to mastering workflow design with QuantaLogic.
365
+
366
+ 📚 **For a deeper understanding of Flow YAML and its applications, please refer to the official [Flow YAML Documentation](https://quantalogic.github.io/quantalogic/flow/flow_yaml).**
367
+
368
+ The Flow YAML documentation provides a comprehensive overview of the Flow YAML language, including its syntax, features, and best practices. It's a valuable resource for anyone looking to create complex workflows with QuantaLogic.
369
+
370
+ Additionally, the Flow YAML documentation includes a number of examples and tutorials to help you get started with creating your own workflows. These examples cover a range of topics, from simple workflows to more complex scenarios, and are designed to help you understand how to use Flow YAML to create powerful and flexible workflows.
361
371
 
362
372
  ### The Building Blocks
363
373
  - **Nodes**: Tasks like functions or LLM calls.
@@ -629,6 +639,83 @@ mypy quantalogic # Check types
629
639
  ruff check quantalogic # Lint it
630
640
  ```
631
641
 
642
+ ### Create Custom Tools
643
+ The `create_tool()` function transforms any Python function into a reusable Tool:
644
+
645
+ ```python
646
+ from quantalogic.tools import create_tool
647
+
648
+ def weather_lookup(city: str, country: str = "US") -> dict:
649
+ """Retrieve current weather for a given location.
650
+
651
+ Args:
652
+ city: Name of the city to look up
653
+ country: Two-letter country code (default: US)
654
+
655
+ Returns:
656
+ Dictionary with weather information
657
+ """
658
+ # Implement weather lookup logic here
659
+ return {"temperature": 22, "condition": "Sunny"}
660
+
661
+ # Convert the function to a Tool
662
+ weather_tool = create_tool(weather_lookup)
663
+
664
+ # Now you can use it as a Tool
665
+ print(weather_tool.to_markdown()) # Generate tool documentation
666
+ result = weather_tool.execute(city="New York") # Execute as a tool
667
+ ```
668
+
669
+ #### Using Custom Tools with ReAct Agent
670
+
671
+ Here's how to integrate custom tools with a ReAct Agent:
672
+
673
+ ```python
674
+ from quantalogic import Agent
675
+ from quantalogic.tools import create_tool, PythonTool
676
+
677
+ # Create a custom stock price lookup tool
678
+ def get_stock_price(symbol: str) -> str:
679
+ """Get the current price of a stock by its ticker symbol.
680
+
681
+ Args:
682
+ symbol: Stock ticker symbol (e.g., AAPL, MSFT)
683
+
684
+ Returns:
685
+ Current stock price information
686
+ """
687
+ # In a real implementation, you would fetch from an API
688
+ prices = {"AAPL": 185.92, "MSFT": 425.27, "GOOGL": 175.43}
689
+ if symbol in prices:
690
+ return f"{symbol} is currently trading at ${prices[symbol]}"
691
+ return f"Could not find price for {symbol}"
692
+
693
+ # Create an agent with standard and custom tools
694
+ agent = Agent(
695
+ model_name="gpt-4o",
696
+ tools=[
697
+ PythonTool(), # Standard Python execution tool
698
+ create_tool(get_stock_price) # Custom stock price tool
699
+ ]
700
+ )
701
+
702
+ # The agent can now use both tools to solve tasks
703
+ result = agent.solve_task(
704
+ "Write a Python function to calculate investment growth, "
705
+ "then analyze Apple stock's current price"
706
+ )
707
+
708
+ print(result)
709
+ ```
710
+
711
+ In this example, the agent can seamlessly use both the standard `PythonTool` and your custom stock price lookup tool to complete the task.
712
+
713
+ Key features of `create_tool()`:
714
+ - 🔧 Automatically converts functions to Tools
715
+ - 📝 Extracts metadata from function signature and docstring
716
+ - 🔍 Supports both synchronous and asynchronous functions
717
+ - 🛠️ Generates tool documentation and validation
718
+
632
719
  ---
633
720
 
634
721
  ## Contributing
@@ -9,13 +9,15 @@ quantalogic/console_print_token.py,sha256=5IRVoPhwWZtSc4LpNoAsCQhCB_RnAW9chycGgy
9
9
  quantalogic/create_custom_agent.py,sha256=1ZMsbpQGHFueJJpfJIuYCWvR3LUsEtDYqDbr6OcwlWw,20850
10
10
  quantalogic/docs_cli.py,sha256=Ie6NwKQuxLKwVQ-cjhFMCttXeiHDjGhNY4hSmMtc0Qg,1664
11
11
  quantalogic/event_emitter.py,sha256=e_1r6hvx5GmW84iuRkoqcjpjRiYHBk4hzujd5ZoUC6U,16777
12
- quantalogic/flow/__init__.py,sha256=asLVwbDH6zVFhILschBOuZZWyKvMGdqhQbB1rd2RXHo,590
12
+ quantalogic/flow/__init__.py,sha256=MD5FAdD6jnVnTPMIOmToKjFxHBQvLmOCT0VeaWhASBc,1295
13
13
  quantalogic/flow/flow.py,sha256=P4T6N6IDOHA5OKqoNjLIZ9BQ9Vk8LwhrqcYjz3wjMB8,25297
14
- quantalogic/flow/flow_extractor.py,sha256=4-q1gBBvSIova7DPTzr4x6e7Dg9SmueowPWqoeDHUwU,29926
15
- quantalogic/flow/flow_generator.py,sha256=rd-aJMngzJ3Jv8b7rkU6FlLRYPKBYTQ46dqsrzYjQFE,3987
16
- quantalogic/flow/flow_manager.py,sha256=622rikNGT0AVryWzhiEu15oTyZ8_HqIp_tOl2M6yTjE,20650
17
- quantalogic/flow/flow_manager_schema.py,sha256=5ytHEh9Qa31yKA4FPRh1lGJYOBGp33ag7crqsJexWG8,7288
18
- quantalogic/flow/flow_yaml.md,sha256=ZKhKW7Rp5czJWe99MnOHaXLs3U5OsgxUK6K8ezqnSzk,15776
14
+ quantalogic/flow/flow_extractor.py,sha256=tkhisbnEhXXGk996tVU9_QNgB1rAW7z7yKtWxXG_G5Y,26804
15
+ quantalogic/flow/flow_generator.py,sha256=FMEVVXOt3Q6PpTjcA3zO87IGgtvGIWaQEDfEZKMQ_xw,4219
16
+ quantalogic/flow/flow_manager.py,sha256=2-HzQCzNGbrP4aMRCg3c1kvT9KBLfU470G-WhYfKWeo,20976
17
+ quantalogic/flow/flow_manager_schema.py,sha256=07lAiefER0aWEA2tkBkWT7iEX9EOLRwP81m5UMahiQk,7249
18
+ quantalogic/flow/flow_mermaid.py,sha256=iehCtKYP7mrVDjKewD2f_XUzxJK7xfan1ynSpuMrexw,10305
19
+ quantalogic/flow/flow_validator.py,sha256=rawAfus7WMKt9Jfw1OD3qsB1CkCXDHU2cJl_gvSUgPs,15561
20
+ quantalogic/flow/flow_yaml.md,sha256=J779AkqI2cfn0cb2rNmICnSW38RtK5UcpOnn6AAeOMY,15195
19
21
  quantalogic/generative_model.py,sha256=os30wdVRq3OsSf8M7TjoaGqJweL99UINQtSGCwoE91k,15913
20
22
  quantalogic/get_model_info.py,sha256=RgblwjjP7G97v_AzoGbXxXBIO082jVCRmvRwxnEpW_s,2991
21
23
  quantalogic/interactive_text_editor.py,sha256=CzefvRiLscFfOKBS4gmrI10Gn3SF_eS5zbiLVQ9Gugw,16334
@@ -47,7 +49,7 @@ quantalogic/server/templates/index.html,sha256=nDnXJoQEm1vXbhXtgaYk0G5VXj0wwzE6K
47
49
  quantalogic/task_file_reader.py,sha256=oPcB4vTxJ__Y8o7VVABIPOkVw3tGDMdQYwdK27PERlE,1440
48
50
  quantalogic/task_runner.py,sha256=c2QVGKZfHA0wZIBUvehpjMvtRaKZuhuYQerjIiCC9iY,10053
49
51
  quantalogic/tool_manager.py,sha256=vNA7aBKgdU3wpw_goom6i9rg_64pNZapNxvg4cUhhCI,6983
50
- quantalogic/tools/__init__.py,sha256=tW9Qx2E40OcgNhVBSXTmluSm9tU1rWoZ4EndHZ8uvGw,2242
52
+ quantalogic/tools/__init__.py,sha256=lpfUu6S61dr4WYW4izUkpob7s5NLhkmmR5Zdi9iYrIc,2274
51
53
  quantalogic/tools/agent_tool.py,sha256=MXCXxWHRch7VK4UWhtRP1jeI8Np9Ne2CUGo8vm1oZiM,3064
52
54
  quantalogic/tools/composio/__init__.py,sha256=Yo9ygNx0qQILVhIfRgqpO8fgnCgp5WoZMd3Hm5D20GY,429
53
55
  quantalogic/tools/composio/composio.py,sha256=icVHA_Scr1pViBhahGGBGBRBl9JSB3hGSqpgQzAIUH8,17627
@@ -130,7 +132,7 @@ quantalogic/tools/sequence_tool.py,sha256=Hb2FSjWWACvXZX7rmJXPk5lnnnqaDeRTbhQQRt
130
132
  quantalogic/tools/serpapi_search_tool.py,sha256=sX-Noch77kGP2XiwislPNFyy3_4TH6TwMK6C81L3q9Y,5316
131
133
  quantalogic/tools/sql_query_tool.py,sha256=jEDZLlxOsB2bzsWlEqsqvTKiyovnRuk0XvgtwW7-WSQ,6055
132
134
  quantalogic/tools/task_complete_tool.py,sha256=L8tuyVoN07Q2hOsxx17JTW0C5Jd_N-C0i_0PtCUQUKU,929
133
- quantalogic/tools/tool.py,sha256=2XlveQf7NVuDlPz-IgE0gURye8rIp9iH3YhWhoOsnog,11232
135
+ quantalogic/tools/tool.py,sha256=FLF4Y1nqjArVUYakD0w3WcDxkRJkbSBQzTG6Sp72Z4Q,15592
134
136
  quantalogic/tools/unified_diff_tool.py,sha256=o7OiYnCM5MjbPlQTpB2OmkMQRI9zjdToQmgVkhiTvOI,14148
135
137
  quantalogic/tools/utilities/__init__.py,sha256=M54fNYmXlTzG-nTnBVQamxSCuu8X4WzRM4bQQ-NvIC4,606
136
138
  quantalogic/tools/utilities/csv_processor_tool.py,sha256=Mu_EPVj6iYAclNaVX_vbkekxcNwPYwy7dW1SCY22EwY,9023
@@ -161,8 +163,8 @@ quantalogic/version_check.py,sha256=JyQFTNMDWtpHCLnN-BiakzB2cyXf6kUFsTjvmSruZi4,
161
163
  quantalogic/welcome_message.py,sha256=o4tHdgabNuIV9kbIDPgS3_2yzJhayK30oKad2UouYDc,3020
162
164
  quantalogic/xml_parser.py,sha256=AKuMdJC3QAX3Z_tErHVlZ-msjPemWaZUFiTwkHz76jg,11614
163
165
  quantalogic/xml_tool_parser.py,sha256=Vz4LEgDbelJynD1siLOVkJ3gLlfHsUk65_gCwbYJyGc,3784
164
- quantalogic-0.51.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
165
- quantalogic-0.51.0.dist-info/METADATA,sha256=RZgwZd1kGVzEaJQFX-jt821N3p6T32gtyYum_TQ9qCE,24260
166
- quantalogic-0.51.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
167
- quantalogic-0.51.0.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
168
- quantalogic-0.51.0.dist-info/RECORD,,
166
+ quantalogic-0.52.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
167
+ quantalogic-0.52.0.dist-info/METADATA,sha256=grXQsXOLmrDRG-vM0vc_q75yfXbrIUNtouVAd3__8rA,28120
168
+ quantalogic-0.52.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
169
+ quantalogic-0.52.0.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
170
+ quantalogic-0.52.0.dist-info/RECORD,,