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
@@ -1,185 +0,0 @@
1
- import ast
2
- import asyncio
3
- from abc import ABC, abstractmethod
4
- from typing import Any, Callable, Dict, List, Optional
5
-
6
- from quantalogic.tools import Tool
7
-
8
- from .constants import MAX_GENERATE_PROGRAM_TOKENS
9
- from .events import PromptGeneratedEvent
10
- from .llm_util import litellm_completion
11
- from .templates import jinja_env
12
- from .xml_utils import XMLResultHandler, validate_xml
13
-
14
-
15
- async def generate_program(
16
- task_description: str,
17
- tools: List[Tool],
18
- model: str,
19
- max_tokens: int,
20
- step: int,
21
- notify_event: Callable,
22
- streaming: bool = False
23
- ) -> str:
24
- """Generate a Python program using the specified model with streaming support and retries."""
25
- tools_by_toolbox = {}
26
- for tool in tools:
27
- toolbox_name = tool.toolbox_name if tool.toolbox_name else "default"
28
- if toolbox_name not in tools_by_toolbox:
29
- tools_by_toolbox[toolbox_name] = []
30
- tools_by_toolbox[toolbox_name].append(tool.to_docstring())
31
-
32
- prompt = jinja_env.get_template("generate_program.j2").render(
33
- task_description=task_description,
34
- tools_by_toolbox=tools_by_toolbox
35
- )
36
- await notify_event(PromptGeneratedEvent(
37
- event_type="PromptGenerated", step_number=step, prompt=prompt
38
- ))
39
-
40
- for attempt in range(3):
41
- try:
42
- response = await litellm_completion(
43
- model=model,
44
- messages=[
45
- {"role": "system", "content": "You are a Python code generator."},
46
- {"role": "user", "content": prompt}
47
- ],
48
- max_tokens=max_tokens,
49
- temperature=0.3,
50
- stream=streaming,
51
- step=step,
52
- notify_event=notify_event
53
- )
54
- code = response.strip()
55
- return code[9:-3].strip() if code.startswith("```python") and code.endswith("```") else code
56
- except Exception as e:
57
- if attempt < 2:
58
- await asyncio.sleep(2 ** attempt)
59
- else:
60
- raise Exception(f"Code generation failed with {model} after 3 attempts: {e}")
61
-
62
-
63
- class PromptStrategy(ABC):
64
- """Abstract base class for prompt generation strategies."""
65
- @abstractmethod
66
- async def generate_prompt(self, task: str, history_str: str, step: int, max_iterations: int, available_vars: List[str]) -> str:
67
- pass
68
-
69
-
70
- class DefaultPromptStrategy(PromptStrategy):
71
- """Default strategy using Jinja2 templates."""
72
- async def generate_prompt(self, task: str, history_str: str, step: int, max_iterations: int, available_vars: List[str]) -> str:
73
- return jinja_env.get_template("generate_action.j2").render(
74
- task=task,
75
- history_str=history_str,
76
- current_step=step,
77
- max_iterations=max_iterations,
78
- available_vars=available_vars
79
- )
80
-
81
-
82
- class BaseReasoner(ABC):
83
- """Abstract base class for reasoning components."""
84
- @abstractmethod
85
- async def generate_action(
86
- self,
87
- task: str,
88
- history_str: str,
89
- step: int,
90
- max_iterations: int,
91
- system_prompt: Optional[str],
92
- notify_event: Callable,
93
- streaming: bool,
94
- available_vars: List[str]
95
- ) -> str:
96
- pass
97
-
98
-
99
- class Reasoner(BaseReasoner):
100
- """Handles action generation using the language model."""
101
- def __init__(self, model: str, tools: List[Tool], config: Optional[Dict[str, Any]] = None, prompt_strategy: Optional[PromptStrategy] = None):
102
- self.model = model
103
- self.tools = tools
104
- self.config = config or {}
105
- self.prompt_strategy = prompt_strategy or DefaultPromptStrategy()
106
-
107
- async def generate_action(
108
- self,
109
- task: str,
110
- history_str: str,
111
- step: int,
112
- max_iterations: int,
113
- system_prompt: Optional[str] = None,
114
- notify_event: Callable = None,
115
- streaming: bool = False,
116
- available_vars: List[str] = None
117
- ) -> str:
118
- """Generate an action based on task and history with streaming support."""
119
- try:
120
- # Prepare type hints for available variables based on tool return types
121
- available_var_types = {}
122
- for var_name in available_vars or []:
123
- # Infer type from tool documentation if possible (simplified heuristic)
124
- if "plan" in var_name.lower():
125
- available_var_types[var_name] = "PlanResult (has attributes: task_id, task_description, subtasks)"
126
- else:
127
- available_var_types[var_name] = "Unknown (check history or assume str)"
128
-
129
- task_prompt = await self.prompt_strategy.generate_prompt(
130
- task if not system_prompt else f"{system_prompt}\nTask: {task}",
131
- history_str,
132
- step,
133
- max_iterations,
134
- available_vars or [] # Default to empty list if None
135
- )
136
- await notify_event(PromptGeneratedEvent(
137
- event_type="PromptGenerated", step_number=step, prompt=task_prompt
138
- ))
139
- program = await generate_program(
140
- task_prompt + f"\n\nVariable Types in context_vars:\n{available_var_types}",
141
- self.tools,
142
- self.model,
143
- self.config.get("max_tokens", MAX_GENERATE_PROGRAM_TOKENS),
144
- step,
145
- notify_event,
146
- streaming=streaming
147
- )
148
- program = self._clean_code(program)
149
- response = jinja_env.get_template("response_format.j2").render(
150
- task=task,
151
- included_stepsincluded_stepshistory_str=history_str,
152
- program=program,
153
- current_step=step,
154
- max_iterations=max_iterations
155
- )
156
- if not validate_xml(response):
157
- raise ValueError("Invalid XML generated")
158
- return response
159
- except Exception as e:
160
- return XMLResultHandler.format_error_result(str(e))
161
-
162
- def _clean_code(self, code: str) -> str:
163
- lines = code.splitlines()
164
- in_code_block = False
165
- code_lines = []
166
-
167
- for line in lines:
168
- if line.startswith('```python'):
169
- in_code_block = True
170
- code_part = line[len('```python'):].strip()
171
- if code_part:
172
- code_lines.append(code_part)
173
- continue
174
- if in_code_block:
175
- if line.startswith('```'):
176
- break
177
- code_lines.append(line)
178
-
179
- final_code = '\n'.join(code_lines) if in_code_block else code
180
-
181
- try:
182
- ast.parse(final_code)
183
- return final_code
184
- except SyntaxError as e:
185
- raise ValueError(f'Invalid Python code: {e}') from e
@@ -1,10 +0,0 @@
1
- # {{ name }}
2
-
3
- A custom toolbox for Quantalogic.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- poetry install
9
- ```
10
-
@@ -1,16 +0,0 @@
1
- [tool.poetry]
2
- name = "{{ name }}"
3
- version = "0.1.0"
4
- description = "A custom toolbox for Quantalogic"
5
- authors = ["Your Name <you@example.com>"]
6
-
7
- [tool.poetry.dependencies]
8
- python = "^3.10"
9
- quantalogic = ">0.60.0"
10
-
11
- [build-system]
12
- requires = ["poetry-core>=1.0.0"]
13
- build-backend = "poetry.core.masonry.api"
14
-
15
- [tool.poetry.plugins."quantalogic.tools"]
16
- "{{ name }}" = "{{ package_name }}.tools"
@@ -1,6 +0,0 @@
1
- from quantalogic.tools import create_tool
2
-
3
- @create_tool
4
- async def echo_tool(message: str) -> str:
5
- """An example tool that echoes the input."""
6
- return f"Echo: {message}"
@@ -1,7 +0,0 @@
1
- from jinja2 import Environment, FileSystemLoader
2
-
3
- from .constants import TEMPLATE_DIR
4
-
5
- # Centralized Jinja2 environment for template rendering
6
- # Note: This is the default environment; custom environments can be passed to Agent
7
- jinja_env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), trim_blocks=True, lstrip_blocks=True)
@@ -1,258 +0,0 @@
1
- """Tool management module for defining and retrieving agent tools."""
2
-
3
- import asyncio
4
- import importlib
5
- import importlib.metadata
6
- import inspect
7
- import os
8
- from contextlib import AsyncExitStack
9
- from typing import Any, Dict, List, Optional
10
-
11
- import litellm
12
- from loguru import logger
13
-
14
- from quantalogic.tools import Tool, ToolArgument
15
-
16
- from .utils import log_tool_method
17
-
18
-
19
- class ToolRegistry:
20
- """Manages tool registration with dependency and conflict checking."""
21
- def __init__(self):
22
- self.tools: Dict[tuple[str, str], Tool] = {}
23
-
24
- def register(self, tool: Tool) -> None:
25
- """Register a tool, checking for conflicts within the same toolbox."""
26
- try:
27
- key = (tool.toolbox_name or "default", tool.name)
28
- if key in self.tools:
29
- logger.warning(f"Tool '{tool.name}' in toolbox '{tool.toolbox_name or 'default'}' is already registered. Skipping.")
30
- return
31
- self.tools[key] = tool
32
- logger.debug(f"Tool registered: {tool.name} in toolbox {tool.toolbox_name or 'default'}")
33
- except Exception as e:
34
- logger.error(f"Failed to register tool {tool.name}: {e}")
35
- raise
36
-
37
- def get_tools(self) -> List[Tool]:
38
- """Return all registered tools."""
39
- try:
40
- logger.debug(f"Returning {len(self.tools)} tools: {list(self.tools.keys())}")
41
- return list(self.tools.values())
42
- except Exception as e:
43
- logger.error(f"Error retrieving tools: {e}")
44
- return []
45
-
46
- def register_tools_from_module(self, module, toolbox_name: str) -> None:
47
- """Register all @create_tool generated Tool instances from a module with toolbox name."""
48
- try:
49
- tools_found = False
50
- for name, obj in inspect.getmembers(module):
51
- if isinstance(obj, Tool) and hasattr(obj, '_func'):
52
- obj.toolbox_name = toolbox_name
53
- self.register(obj)
54
- logger.debug(f"Registered tool: {obj.name} from {module.__name__} in toolbox {toolbox_name}")
55
- tools_found = True
56
- if not tools_found:
57
- logger.warning(f"No @create_tool generated tools found in {module.__name__}")
58
- except Exception as e:
59
- logger.error(f"Failed to register tools from module {module.__name__}: {e}")
60
- raise
61
-
62
- def load_toolboxes(self, toolbox_names: Optional[List[str]] = None) -> None:
63
- """Load toolboxes from registered entry points, optionally filtering by name."""
64
- try:
65
- entry_points = importlib.metadata.entry_points(group="quantalogic.tools")
66
- except Exception as e:
67
- logger.error(f"Failed to retrieve entry points: {e}")
68
- entry_points = []
69
-
70
- try:
71
- if toolbox_names is not None:
72
- entry_points = [ep for ep in entry_points if ep.name in toolbox_names]
73
-
74
- logger.debug(f"Found {len(entry_points)} toolbox entry points")
75
- for ep in entry_points:
76
- try:
77
- module = ep.load()
78
- self.register_tools_from_module(module, toolbox_name=ep.name)
79
- logger.info(f"Successfully loaded toolbox: {ep.name}")
80
- except ImportError as e:
81
- logger.error(f"Failed to import toolbox {ep.name}: {e}")
82
- except Exception as e:
83
- logger.error(f"Error loading toolboxes: {e}")
84
-
85
-
86
- class AgentTool(Tool):
87
- """A specialized tool for generating text using language models, designed for AI agent workflows."""
88
- def __init__(self, model: str = None, timeout: int = None, config: Optional[Dict[str, Any]] = None) -> None:
89
- """Initialize the AgentTool with configurable model and timeout."""
90
- try:
91
- super().__init__(
92
- name="agent_tool",
93
- description="Generates text using a language model. This is a stateless agent - all necessary context must be explicitly provided in either the system prompt or user prompt.",
94
- arguments=[
95
- ToolArgument(name="system_prompt", arg_type="string", description="System prompt to guide the model", required=True),
96
- ToolArgument(name="prompt", arg_type="string", description="User prompt to generate a response", required=True),
97
- ToolArgument(name="temperature", arg_type="float", description="Temperature for generation (0 to 1)", required=True)
98
- ],
99
- return_type="string"
100
- )
101
- self.config = config or {}
102
- self.model: str = self._resolve_model(model)
103
- self.timeout: int = self._resolve_timeout(timeout)
104
- except Exception as e:
105
- logger.error(f"Failed to initialize AgentTool: {e}")
106
- raise
107
-
108
- def _resolve_model(self, model: Optional[str]) -> str:
109
- """Resolve the model from config, argument, or environment variable."""
110
- try:
111
- return self.config.get("model", model) or os.getenv("AGENT_MODEL", "gemini/gemini-2.0-flash")
112
- except Exception as e:
113
- logger.error(f"Error resolving model: {e}. Using default.")
114
- return "gemini/gemini-2.0-flash"
115
-
116
- def _resolve_timeout(self, timeout: Optional[int]) -> int:
117
- """Resolve the timeout from config, argument, or environment variable."""
118
- try:
119
- return self.config.get("timeout", timeout) or int(os.getenv("AGENT_TIMEOUT", "30"))
120
- except (ValueError, TypeError) as e:
121
- logger.error(f"Error resolving timeout: {e}. Using default.")
122
- return 30
123
-
124
- @log_tool_method
125
- async def async_execute(self, **kwargs) -> str:
126
- """Execute the tool asynchronously with error handling."""
127
- try:
128
- system_prompt: str = kwargs["system_prompt"]
129
- prompt: str = kwargs["prompt"]
130
- temperature: float = float(kwargs["temperature"])
131
-
132
- if not 0 <= temperature <= 1:
133
- raise ValueError("Temperature must be between 0 and 1")
134
-
135
- logger.info(f"Generating with {self.model}, temp={temperature}, timeout={self.timeout}s")
136
- async with AsyncExitStack() as stack:
137
- await stack.enter_async_context(asyncio.timeout(self.timeout))
138
- try:
139
- response = await litellm.acompletion(
140
- model=self.model,
141
- messages=[
142
- {"role": "system", "content": system_prompt},
143
- {"role": "user", "content": prompt}
144
- ],
145
- temperature=temperature,
146
- max_tokens=1000
147
- )
148
- result = response.choices[0].message.content.strip()
149
- logger.info(f"AgentTool generated text successfully: {result[:50]}...")
150
- return result
151
- except Exception as e:
152
- error_msg = f"Error: Unable to generate text due to {str(e)}"
153
- logger.error(f"AgentTool failed: {e}")
154
- return error_msg
155
- except TimeoutError:
156
- logger.error(f"AgentTool execution timed out after {self.timeout}s")
157
- return "Error: Execution timed out"
158
- except Exception as e:
159
- logger.error(f"Unexpected error in AgentTool execution: {e}")
160
- return f"Error: {str(e)}"
161
-
162
-
163
- class RetrieveStepTool(Tool):
164
- """Tool to retrieve information from a specific previous step with indexed access."""
165
- def __init__(self, history_store: List[dict], config: Optional[Dict[str, Any]] = None) -> None:
166
- """Initialize the RetrieveStepTool with history store."""
167
- try:
168
- super().__init__(
169
- name="retrieve_step",
170
- description="Retrieve the thought, action, and result from a specific step.",
171
- arguments=[
172
- ToolArgument(name="step_number", arg_type="int", description="The step number to retrieve (1-based)", required=True)
173
- ],
174
- return_type="string"
175
- )
176
- self.config = config or {}
177
- self.history_index: Dict[int, dict] = {i + 1: step for i, step in enumerate(history_store)}
178
- except Exception as e:
179
- logger.error(f"Failed to initialize RetrieveStepTool: {e}")
180
- raise
181
-
182
- @log_tool_method
183
- async def async_execute(self, **kwargs) -> str:
184
- """Execute the tool to retrieve step information."""
185
- try:
186
- step_number: int = kwargs["step_number"]
187
- if step_number not in self.history_index:
188
- error_msg = f"Error: Step {step_number} is out of range (1-{len(self.history_index)})"
189
- logger.error(error_msg)
190
- raise ValueError(error_msg)
191
- step: dict = self.history_index[step_number]
192
- result = (
193
- f"Step {step_number}:\n"
194
- f"Thought: {step['thought']}\n"
195
- f"Action: {step['action']}\n"
196
- f"Result: {step['result']}"
197
- )
198
- logger.info(f"Retrieved step {step_number} successfully")
199
- return result
200
- except Exception as e:
201
- logger.error(f"Error retrieving step {kwargs.get('step_number', 'unknown')}: {e}")
202
- return f"Error: {str(e)}"
203
-
204
-
205
- def get_default_tools(
206
- model: str,
207
- history_store: Optional[List[dict]] = None,
208
- enabled_toolboxes: Optional[List[str]] = None,
209
- tools_config: Optional[List[Dict[str, Any]]] = None
210
- ) -> List[Tool]:
211
- """Dynamically load default tools using the pre-loaded registry from PluginManager."""
212
- from .cli import plugin_manager # Import shared singleton plugin_manager
213
-
214
- try:
215
- # Ensure plugins are loaded
216
- plugin_manager.load_plugins()
217
- registry = plugin_manager.tools
218
-
219
- # Register static tools if not already present
220
- static_tools: List[Tool] = [AgentTool(model=model)]
221
- if history_store is not None:
222
- static_tools.append(RetrieveStepTool(history_store))
223
-
224
- for tool in static_tools:
225
- try:
226
- registry.register(tool)
227
- except ValueError as e:
228
- logger.debug(f"Static tool {tool.name} already registered: {e}")
229
-
230
- # Filter tools based on enabled_toolboxes
231
- if enabled_toolboxes:
232
- tools = [t for t in registry.get_tools() if t.toolbox_name in enabled_toolboxes]
233
- else:
234
- tools = registry.get_tools()
235
-
236
- # Apply tools_config if provided
237
- if tools_config:
238
- filtered_tools = []
239
- processed_names = set()
240
- for tool_conf in tools_config:
241
- if tool_conf.get("enabled", True):
242
- tool = next((t for t in tools if t.name == tool_conf["name"] or t.toolbox_name == tool_conf["name"]), None)
243
- if tool and tool.name not in processed_names:
244
- for key, value in tool_conf.items():
245
- if key not in ["name", "enabled"]:
246
- setattr(tool, key, value)
247
- filtered_tools.append(tool)
248
- processed_names.add(tool.name)
249
- for tool in tools:
250
- if tool.name not in processed_names:
251
- filtered_tools.append(tool)
252
- tools = filtered_tools
253
-
254
- logger.info(f"Loaded {len(tools)} default tools: {[(tool.toolbox_name or 'default', tool.name) for tool in tools]}")
255
- return tools
256
- except Exception as e:
257
- logger.error(f"Failed to load default tools: {e}")
258
- return []
@@ -1,62 +0,0 @@
1
- import ast
2
- import inspect
3
- from functools import wraps
4
- from typing import Callable, List, Union
5
-
6
- from loguru import logger
7
-
8
- from quantalogic.tools import Tool, create_tool
9
-
10
-
11
- def log_async_tool(verb: str):
12
- """Decorator factory for consistent async tool logging."""
13
- def decorator(func: Callable) -> Callable:
14
- @wraps(func)
15
- async def wrapper(*args, **kwargs):
16
- logger.info(f"Starting tool: {func.__name__}")
17
- sig = inspect.signature(func)
18
- bound_args = sig.bind(*args, **kwargs)
19
- bound_args.apply_defaults()
20
- logger.info(f"{verb} {', '.join(f'{k}={v}' for k, v in bound_args.arguments.items())}")
21
- result = await func(*args, **kwargs)
22
- logger.info(f"Finished tool: {func.__name__}")
23
- return result
24
- return wrapper
25
- return decorator
26
-
27
- def log_tool_method(func: Callable) -> Callable:
28
- """Decorator for logging Tool class methods."""
29
- @wraps(func)
30
- async def wrapper(self, **kwargs):
31
- logger.info(f"Starting tool: {self.name}")
32
- try:
33
- result = await func(self, **kwargs)
34
- logger.info(f"Finished tool: {self.name}")
35
- return result
36
- except Exception as e:
37
- logger.error(f"Tool {self.name} failed: {e}")
38
- raise
39
- return wrapper
40
-
41
- def validate_code(code: str) -> bool:
42
- """Check if code has an async main() function."""
43
- try:
44
- tree = ast.parse(code)
45
- return any(isinstance(node, ast.AsyncFunctionDef) and node.name == "main"
46
- for node in ast.walk(tree))
47
- except SyntaxError:
48
- return False
49
-
50
- def process_tools(tools: List[Union[Tool, Callable]]) -> List[Tool]:
51
- """Convert a list of tools or callables into Tool instances."""
52
- processed_tools: List[Tool] = []
53
- for tool in tools:
54
- if isinstance(tool, Tool):
55
- processed_tools.append(tool)
56
- elif callable(tool):
57
- if not inspect.iscoroutinefunction(tool):
58
- raise ValueError(f"Callable '{tool.__name__}' must be an async function to be used as a tool.")
59
- processed_tools.append(create_tool(tool))
60
- else:
61
- raise ValueError(f"Invalid item type: {type(tool)}. Expected Tool or async function.")
62
- return processed_tools
@@ -1,126 +0,0 @@
1
- from typing import Any, Tuple
2
-
3
- from loguru import logger
4
- from lxml import etree
5
-
6
-
7
- def validate_xml(xml_string: str) -> bool:
8
- """Validate XML string using strict parser."""
9
- try:
10
- etree.fromstring(xml_string)
11
- return True
12
- except etree.XMLSyntaxError as e:
13
- logger.error(f"XML validation failed: {e}")
14
- return False
15
-
16
- def format_xml_element(tag: str, value: Any, **attribs) -> etree.Element:
17
- """Create an XML element with optional CDATA and attributes."""
18
- elem = etree.Element(tag, **attribs)
19
- elem.text = etree.CDATA(str(value)) if value is not None else None
20
- return elem
21
-
22
- class XMLResultHandler:
23
- """Utility class for handling all XML formatting and parsing operations."""
24
-
25
- _parser = etree.XMLParser(recover=True, remove_comments=True, resolve_entities=False)
26
-
27
- @staticmethod
28
- def format_execution_result(result) -> str:
29
- """Format execution result as XML with proper variable handling."""
30
- root = etree.Element("ExecutionResult")
31
- root.append(format_xml_element("Status", "Success" if not result.error else "Error"))
32
- root.append(format_xml_element("Value", result.result or result.error))
33
- root.append(format_xml_element("ExecutionTime", f"{result.execution_time:.2f} seconds"))
34
-
35
- completed = result.result and result.result.startswith("Task completed:")
36
- root.append(format_xml_element("Completed", str(completed).lower()))
37
-
38
- if completed:
39
- final_answer = result.result[len("Task completed:"):].strip()
40
- root.append(format_xml_element("FinalAnswer", final_answer))
41
-
42
- if result.local_variables:
43
- vars_elem = etree.SubElement(root, "Variables")
44
- for k, v in result.local_variables.items():
45
- if not callable(v) and not k.startswith("__"):
46
- var_elem = etree.SubElement(vars_elem, "Variable", name=k)
47
- var_value = str(v)[:5000] + ("... (truncated)" if len(str(v)) > 5000 else "")
48
- var_elem.text = etree.CDATA(var_value)
49
-
50
- xml_str = etree.tostring(root, pretty_print=True, encoding="unicode")
51
- if not validate_xml(xml_str):
52
- logger.error(f"Generated invalid XML: {xml_str}")
53
- raise ValueError("Generated XML is invalid")
54
- return xml_str
55
-
56
- @staticmethod
57
- def format_result_summary(result_xml: str) -> str:
58
- """Format XML result into a readable summary with error resilience."""
59
- try:
60
- root = etree.fromstring(result_xml, parser=XMLResultHandler._parser)
61
- if root is None:
62
- raise ValueError("Empty XML document")
63
-
64
- lines = [
65
- f"- Status: {root.findtext('Status', 'N/A')}",
66
- f"- Value: {root.findtext('Value', 'N/A')}",
67
- f"- Execution Time: {root.findtext('ExecutionTime', 'N/A')}",
68
- f"- Completed: {root.findtext('Completed', 'N/A').capitalize()}"
69
- ]
70
-
71
- if final_answer := root.findtext("FinalAnswer"):
72
- lines.append(f"- Final Answer: {final_answer}")
73
-
74
- if (vars_elem := root.find("Variables")) is not None:
75
- lines.append("- Variables:")
76
- lines.extend(f" - {var.get('name', 'unknown')}: {var.text.strip() or 'N/A'}"
77
- for var in vars_elem.findall("Variable"))
78
- return "\n".join(lines)
79
- except (etree.XMLSyntaxError, ValueError) as e:
80
- logger.error(f"Failed to parse XML result: {e}")
81
- return f"Raw Result (Error: {str(e)}):\n{result_xml}"
82
-
83
- @staticmethod
84
- def parse_action_response(response: str) -> Tuple[str, str]:
85
- """Parse XML response to extract thought and code with robust error handling."""
86
- try:
87
- root = etree.fromstring(response, parser=XMLResultHandler._parser)
88
- if root is None:
89
- raise ValueError("Empty XML document")
90
-
91
- # Log XML parsing warnings
92
- if XMLResultHandler._parser.error_log:
93
- for error in XMLResultHandler._parser.error_log:
94
- logger.warning(f"XML parse warning: {error.message} (line {error.line})")
95
-
96
- return (
97
- root.findtext("Thought") or "",
98
- root.findtext("Code") or ""
99
- )
100
- except etree.XMLSyntaxError as e:
101
- logger.error(f"Critical XML parsing error: {e}")
102
- raise ValueError(f"Malformed XML structure: {e}") from e
103
-
104
- @staticmethod
105
- def extract_result_value(result: str) -> str:
106
- """Safely extract the value from the result XML."""
107
- try:
108
- root = etree.fromstring(result, parser=XMLResultHandler._parser)
109
- return root.findtext("Value") or "" if root is not None else ""
110
- except etree.XMLSyntaxError as e:
111
- logger.warning(f"XML extraction error: {e}")
112
- return ""
113
-
114
- @staticmethod
115
- def format_error_result(error_msg: str) -> str:
116
- """Format an error result as XML with proper escaping."""
117
- root = etree.Element("Action")
118
- root.append(format_xml_element("Thought", f"Failed to generate valid action: {error_msg}"))
119
- error_elem = etree.SubElement(root, "Error")
120
- error_elem.append(format_xml_element("Message", error_msg))
121
- root.append(format_xml_element("Code", """
122
- import asyncio
123
- async def main():
124
- print("Error: Action generation failed")
125
- """))
126
- return etree.tostring(root, pretty_print=True, encoding="unicode")