quantalogic 0.61.2__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.
Files changed (67) hide show
  1. quantalogic/agent.py +0 -1
  2. quantalogic/codeact/TODO.md +14 -0
  3. quantalogic/codeact/agent.py +400 -421
  4. quantalogic/codeact/cli.py +42 -224
  5. quantalogic/codeact/cli_commands/__init__.py +0 -0
  6. quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
  7. quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
  8. quantalogic/codeact/cli_commands/list_executor.py +15 -0
  9. quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
  10. quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
  11. quantalogic/codeact/cli_commands/task.py +215 -0
  12. quantalogic/codeact/cli_commands/tool_info.py +24 -0
  13. quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
  14. quantalogic/codeact/config.yaml +21 -0
  15. quantalogic/codeact/constants.py +1 -1
  16. quantalogic/codeact/events.py +12 -5
  17. quantalogic/codeact/examples/README.md +342 -0
  18. quantalogic/codeact/examples/agent_sample.yaml +29 -0
  19. quantalogic/codeact/executor.py +186 -0
  20. quantalogic/codeact/history_manager.py +94 -0
  21. quantalogic/codeact/llm_util.py +3 -22
  22. quantalogic/codeact/plugin_manager.py +92 -0
  23. quantalogic/codeact/prompts/generate_action.j2 +65 -14
  24. quantalogic/codeact/prompts/generate_program.j2 +32 -19
  25. quantalogic/codeact/react_agent.py +318 -0
  26. quantalogic/codeact/reasoner.py +185 -0
  27. quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
  28. quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
  29. quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
  30. quantalogic/codeact/templates.py +7 -0
  31. quantalogic/codeact/tools_manager.py +242 -119
  32. quantalogic/codeact/utils.py +16 -89
  33. quantalogic/codeact/xml_utils.py +126 -0
  34. quantalogic/flow/flow.py +151 -41
  35. quantalogic/flow/flow_extractor.py +61 -1
  36. quantalogic/flow/flow_generator.py +34 -6
  37. quantalogic/flow/flow_manager.py +64 -25
  38. quantalogic/flow/flow_manager_schema.py +32 -0
  39. quantalogic/tools/action_gen.py +1 -1
  40. quantalogic/tools/action_gen_safe.py +340 -0
  41. quantalogic/tools/tool.py +531 -109
  42. quantalogic/tools/write_file_tool.py +7 -8
  43. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -2
  44. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/RECORD +47 -42
  45. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
  46. quantalogic-0.80.dist-info/entry_points.txt +3 -0
  47. quantalogic/python_interpreter/__init__.py +0 -23
  48. quantalogic/python_interpreter/assignment_visitors.py +0 -63
  49. quantalogic/python_interpreter/base_visitors.py +0 -20
  50. quantalogic/python_interpreter/class_visitors.py +0 -22
  51. quantalogic/python_interpreter/comprehension_visitors.py +0 -172
  52. quantalogic/python_interpreter/context_visitors.py +0 -59
  53. quantalogic/python_interpreter/control_flow_visitors.py +0 -88
  54. quantalogic/python_interpreter/exception_visitors.py +0 -109
  55. quantalogic/python_interpreter/exceptions.py +0 -39
  56. quantalogic/python_interpreter/execution.py +0 -202
  57. quantalogic/python_interpreter/function_utils.py +0 -386
  58. quantalogic/python_interpreter/function_visitors.py +0 -209
  59. quantalogic/python_interpreter/import_visitors.py +0 -28
  60. quantalogic/python_interpreter/interpreter_core.py +0 -358
  61. quantalogic/python_interpreter/literal_visitors.py +0 -74
  62. quantalogic/python_interpreter/misc_visitors.py +0 -148
  63. quantalogic/python_interpreter/operator_visitors.py +0 -108
  64. quantalogic/python_interpreter/scope.py +0 -10
  65. quantalogic/python_interpreter/visit_handlers.py +0 -110
  66. quantalogic-0.61.2.dist-info/entry_points.txt +0 -6
  67. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/LICENSE +0 -0
@@ -1,135 +1,258 @@
1
+ """Tool management module for defining and retrieving agent tools."""
2
+
1
3
  import asyncio
4
+ import importlib
5
+ import importlib.metadata
6
+ import inspect
7
+ import os
2
8
  from contextlib import AsyncExitStack
3
- from typing import List
9
+ from typing import Any, Dict, List, Optional
4
10
 
5
11
  import litellm
6
12
  from loguru import logger
7
13
 
8
- from quantalogic.tools import (
9
- EditWholeContentTool,
10
- ExecuteBashCommandTool,
11
- GrepAppTool,
12
- InputQuestionTool,
13
- JinjaTool,
14
- ListDirectoryTool,
15
- ReadFileBlockTool,
16
- ReadFileTool,
17
- ReadHTMLTool,
18
- ReplaceInFileTool,
19
- RipgrepTool,
20
- SearchDefinitionNamesTool,
21
- TaskCompleteTool,
22
- Tool,
23
- ToolArgument,
24
- WriteFileTool,
25
- create_tool,
26
- )
27
-
28
- from .utils import log_async_tool, log_tool_method
29
-
30
-
31
- @create_tool
32
- @log_async_tool("Adding")
33
- async def add_tool(a: int, b: int) -> str:
34
- """Adds two numbers and returns the sum as a string."""
35
- return str(a + b)
36
-
37
- @create_tool
38
- @log_async_tool("Multiplying")
39
- async def multiply_tool(x: int, y: int) -> str:
40
- """Multiplies two numbers and returns the product as a string."""
41
- return str(x * y)
42
-
43
- @create_tool
44
- @log_async_tool("Concatenating")
45
- async def concat_tool(s1: str, s2: str) -> str:
46
- """Concatenates two strings and returns the result."""
47
- return s1 + s2
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
+
48
85
 
49
86
  class AgentTool(Tool):
50
- """Tool for generating text using a language model."""
51
- def __init__(self, model: str = "gemini/gemini-2.0-flash"):
52
- super().__init__(
53
- name="agent_tool",
54
- description="Generates text using a language model.",
55
- arguments=[
56
- ToolArgument(name="system_prompt", arg_type="string",
57
- description="System prompt to guide the model", required=True),
58
- ToolArgument(name="prompt", arg_type="string",
59
- description="User prompt to generate a response", required=True),
60
- ToolArgument(name="temperature", arg_type="float",
61
- description="Temperature for generation (0 to 1)", required=True)
62
- ],
63
- return_type="string"
64
- )
65
- self.model = model
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
66
123
 
67
124
  @log_tool_method
68
125
  async def async_execute(self, **kwargs) -> str:
69
- system_prompt = kwargs["system_prompt"]
70
- prompt = kwargs["prompt"]
71
- temperature = float(kwargs["temperature"])
72
-
73
- if not 0 <= temperature <= 1:
74
- raise ValueError("Temperature must be between 0 and 1")
75
-
76
- logger.info(f"Generating with {self.model}, temp={temperature}")
77
- async with AsyncExitStack() as stack:
78
- await stack.enter_async_context(asyncio.timeout(30))
79
- response = await litellm.acompletion(
80
- model=self.model,
81
- messages=[
82
- {"role": "system", "content": system_prompt},
83
- {"role": "user", "content": prompt}
84
- ],
85
- temperature=temperature,
86
- max_tokens=1000
87
- )
88
- return response.choices[0].message.content.strip()
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
+
89
162
 
90
163
  class RetrieveStepTool(Tool):
91
- """Tool to retrieve information from a specific previous step."""
92
- def __init__(self, history_store: List[dict]):
93
- super().__init__(
94
- name="retrieve_step",
95
- description="Retrieve the thought, action, and result from a specific step.",
96
- arguments=[
97
- ToolArgument(name="step_number", arg_type="int",
98
- description="The step number to retrieve (1-based)", required=True)
99
- ],
100
- return_type="string"
101
- )
102
- self.history_store = history_store
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
103
181
 
104
182
  @log_tool_method
105
183
  async def async_execute(self, **kwargs) -> str:
106
- step_number = kwargs["step_number"]
107
- if step_number < 1 or step_number > len(self.history_store):
108
- return f"Error: Step {step_number} does not exist."
109
- step = self.history_store[step_number - 1]
110
- return (
111
- f"Step {step_number}:\n"
112
- f"Thought: {step['thought']}\n"
113
- f"Action: {step['action']}\n"
114
- f"Result: {step['result']}"
115
- )
116
-
117
- def get_default_tools(model: str) -> List[Tool]:
118
- """Return list of default tools."""
119
- return [
120
- #EditWholeContentTool(),
121
- #ExecuteBashCommandTool(),
122
- GrepAppTool(),
123
- InputQuestionTool(),
124
- JinjaTool(),
125
- ListDirectoryTool(),
126
- ReadFileBlockTool(),
127
- ReadFileTool(),
128
- ReadHTMLTool(),
129
- #ReplaceInFileTool(),
130
- # RipgrepTool(),
131
- # SearchDefinitionNamesTool(),
132
- #TaskCompleteTool(),
133
- WriteFileTool(),
134
- AgentTool(model=model),
135
- ]
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,10 +1,11 @@
1
1
  import ast
2
2
  import inspect
3
3
  from functools import wraps
4
- from typing import Any, Callable, Tuple
4
+ from typing import Callable, List, Union
5
5
 
6
6
  from loguru import logger
7
- from lxml import etree
7
+
8
+ from quantalogic.tools import Tool, create_tool
8
9
 
9
10
 
10
11
  def log_async_tool(verb: str):
@@ -23,7 +24,6 @@ def log_async_tool(verb: str):
23
24
  return wrapper
24
25
  return decorator
25
26
 
26
-
27
27
  def log_tool_method(func: Callable) -> Callable:
28
28
  """Decorator for logging Tool class methods."""
29
29
  @wraps(func)
@@ -38,17 +38,6 @@ def log_tool_method(func: Callable) -> Callable:
38
38
  raise
39
39
  return wrapper
40
40
 
41
-
42
- def validate_xml(xml_string: str) -> bool:
43
- """Validate XML string."""
44
- try:
45
- etree.fromstring(xml_string)
46
- return True
47
- except etree.XMLSyntaxError as e:
48
- logger.error(f"XML validation failed: {e}")
49
- return False
50
-
51
-
52
41
  def validate_code(code: str) -> bool:
53
42
  """Check if code has an async main() function."""
54
43
  try:
@@ -58,78 +47,16 @@ def validate_code(code: str) -> bool:
58
47
  except SyntaxError:
59
48
  return False
60
49
 
61
-
62
- def format_xml_element(tag: str, value: Any, **attribs) -> etree.Element:
63
- """Create an XML element with optional CDATA and attributes."""
64
- elem = etree.Element(tag, **attribs)
65
- elem.text = etree.CDATA(str(value)) if value is not None else None
66
- return elem
67
-
68
-
69
- class XMLResultHandler:
70
- """Utility class for handling XML formatting and parsing."""
71
- @staticmethod
72
- def format_execution_result(result) -> str:
73
- """Format execution result as XML."""
74
- root = etree.Element("ExecutionResult")
75
- root.append(format_xml_element("Status", "Success" if not result.error else "Error"))
76
- root.append(format_xml_element("Value", result.result or result.error))
77
- root.append(format_xml_element("ExecutionTime", f"{result.execution_time:.2f} seconds"))
78
-
79
- completed = result.result and result.result.startswith("Task completed:")
80
- root.append(format_xml_element("Completed", str(completed).lower()))
81
-
82
- if completed:
83
- final_answer = result.result[len("Task completed:"):].strip()
84
- root.append(format_xml_element("FinalAnswer", final_answer))
85
-
86
- if result.local_variables:
87
- vars_elem = etree.SubElement(root, "Variables")
88
- for k, v in result.local_variables.items():
89
- if not callable(v) and not k.startswith("__"):
90
- vars_elem.append(format_xml_element("Variable", str(v)[:5000] +
91
- ("... (truncated)" if len(str(v)) > 5000 else ""),
92
- name=k))
93
- return etree.tostring(root, pretty_print=True, encoding="unicode")
94
-
95
- @staticmethod
96
- def format_result_summary(result_xml: str) -> str:
97
- """Format XML result into a readable summary."""
98
- try:
99
- root = etree.fromstring(result_xml)
100
- lines = [
101
- f"- Status: {root.findtext('Status', 'N/A')}",
102
- f"- Value: {root.findtext('Value', 'N/A')}",
103
- f"- Execution Time: {root.findtext('ExecutionTime', 'N/A')}",
104
- f"- Completed: {root.findtext('Completed', 'N/A').capitalize()}"
105
- ]
106
- if final_answer := root.findtext("FinalAnswer"):
107
- lines.append(f"- Final Answer: {final_answer}")
108
-
109
- if (vars_elem := root.find("Variables")) is not None:
110
- lines.append("- Variables:")
111
- lines.extend(f" - {var.get('name', 'unknown')}: {var.text.strip() or 'N/A'}"
112
- for var in vars_elem.findall("Variable"))
113
- return "\n".join(lines)
114
- except etree.XMLSyntaxError:
115
- logger.error(f"Failed to parse XML: {result_xml}")
116
- return result_xml
117
-
118
- @staticmethod
119
- def parse_response(response: str) -> Tuple[str, str]:
120
- """Parse XML response to extract thought and code."""
121
- try:
122
- root = etree.fromstring(response)
123
- thought = root.findtext("Thought") or ""
124
- code = root.findtext("Code") or ""
125
- return thought, code
126
- except etree.XMLSyntaxError as e:
127
- raise ValueError(f"Failed to parse XML: {e}")
128
-
129
- @staticmethod
130
- def extract_result_value(result: str) -> str:
131
- """Extract the value from the result XML."""
132
- try:
133
- return etree.fromstring(result).findtext("Value") or ""
134
- except etree.XMLSyntaxError:
135
- return ""
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
@@ -0,0 +1,126 @@
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")