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.
- quantalogic/agent.py +0 -1
- quantalogic/codeact/TODO.md +14 -0
- quantalogic/codeact/agent.py +400 -421
- quantalogic/codeact/cli.py +42 -224
- quantalogic/codeact/cli_commands/__init__.py +0 -0
- quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
- quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
- quantalogic/codeact/cli_commands/list_executor.py +15 -0
- quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
- quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
- quantalogic/codeact/cli_commands/task.py +215 -0
- quantalogic/codeact/cli_commands/tool_info.py +24 -0
- quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
- quantalogic/codeact/config.yaml +21 -0
- quantalogic/codeact/constants.py +1 -1
- quantalogic/codeact/events.py +12 -5
- quantalogic/codeact/examples/README.md +342 -0
- quantalogic/codeact/examples/agent_sample.yaml +29 -0
- quantalogic/codeact/executor.py +186 -0
- quantalogic/codeact/history_manager.py +94 -0
- quantalogic/codeact/llm_util.py +3 -22
- quantalogic/codeact/plugin_manager.py +92 -0
- quantalogic/codeact/prompts/generate_action.j2 +65 -14
- quantalogic/codeact/prompts/generate_program.j2 +32 -19
- quantalogic/codeact/react_agent.py +318 -0
- quantalogic/codeact/reasoner.py +185 -0
- quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
- quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
- quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
- quantalogic/codeact/templates.py +7 -0
- quantalogic/codeact/tools_manager.py +242 -119
- quantalogic/codeact/utils.py +16 -89
- quantalogic/codeact/xml_utils.py +126 -0
- quantalogic/flow/flow.py +151 -41
- quantalogic/flow/flow_extractor.py +61 -1
- quantalogic/flow/flow_generator.py +34 -6
- quantalogic/flow/flow_manager.py +64 -25
- quantalogic/flow/flow_manager_schema.py +32 -0
- quantalogic/tools/action_gen.py +1 -1
- quantalogic/tools/action_gen_safe.py +340 -0
- quantalogic/tools/tool.py +531 -109
- quantalogic/tools/write_file_tool.py +7 -8
- {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -2
- {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/RECORD +47 -42
- {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
- quantalogic-0.80.dist-info/entry_points.txt +3 -0
- quantalogic/python_interpreter/__init__.py +0 -23
- quantalogic/python_interpreter/assignment_visitors.py +0 -63
- quantalogic/python_interpreter/base_visitors.py +0 -20
- quantalogic/python_interpreter/class_visitors.py +0 -22
- quantalogic/python_interpreter/comprehension_visitors.py +0 -172
- quantalogic/python_interpreter/context_visitors.py +0 -59
- quantalogic/python_interpreter/control_flow_visitors.py +0 -88
- quantalogic/python_interpreter/exception_visitors.py +0 -109
- quantalogic/python_interpreter/exceptions.py +0 -39
- quantalogic/python_interpreter/execution.py +0 -202
- quantalogic/python_interpreter/function_utils.py +0 -386
- quantalogic/python_interpreter/function_visitors.py +0 -209
- quantalogic/python_interpreter/import_visitors.py +0 -28
- quantalogic/python_interpreter/interpreter_core.py +0 -358
- quantalogic/python_interpreter/literal_visitors.py +0 -74
- quantalogic/python_interpreter/misc_visitors.py +0 -148
- quantalogic/python_interpreter/operator_visitors.py +0 -108
- quantalogic/python_interpreter/scope.py +0 -10
- quantalogic/python_interpreter/visit_handlers.py +0 -110
- quantalogic-0.61.2.dist-info/entry_points.txt +0 -6
- {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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
"""
|
51
|
-
def __init__(self, model: str =
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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 []
|
quantalogic/codeact/utils.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
import ast
|
2
2
|
import inspect
|
3
3
|
from functools import wraps
|
4
|
-
from typing import
|
4
|
+
from typing import Callable, List, Union
|
5
5
|
|
6
6
|
from loguru import logger
|
7
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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")
|