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.
- quantalogic/flow/__init__.py +16 -34
- quantalogic/main.py +11 -6
- quantalogic/tools/tool.py +8 -922
- quantalogic-0.93.dist-info/METADATA +475 -0
- {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/RECORD +8 -54
- quantalogic/codeact/TODO.md +0 -14
- quantalogic/codeact/__init__.py +0 -0
- quantalogic/codeact/agent.py +0 -478
- quantalogic/codeact/cli.py +0 -50
- quantalogic/codeact/cli_commands/__init__.py +0 -0
- quantalogic/codeact/cli_commands/create_toolbox.py +0 -45
- quantalogic/codeact/cli_commands/install_toolbox.py +0 -20
- quantalogic/codeact/cli_commands/list_executor.py +0 -15
- quantalogic/codeact/cli_commands/list_reasoners.py +0 -15
- quantalogic/codeact/cli_commands/list_toolboxes.py +0 -47
- quantalogic/codeact/cli_commands/task.py +0 -215
- quantalogic/codeact/cli_commands/tool_info.py +0 -24
- quantalogic/codeact/cli_commands/uninstall_toolbox.py +0 -43
- quantalogic/codeact/config.yaml +0 -21
- quantalogic/codeact/constants.py +0 -9
- quantalogic/codeact/events.py +0 -85
- quantalogic/codeact/examples/README.md +0 -342
- quantalogic/codeact/examples/agent_sample.yaml +0 -29
- quantalogic/codeact/executor.py +0 -186
- quantalogic/codeact/history_manager.py +0 -94
- quantalogic/codeact/llm_util.py +0 -57
- quantalogic/codeact/plugin_manager.py +0 -92
- quantalogic/codeact/prompts/error_format.j2 +0 -11
- quantalogic/codeact/prompts/generate_action.j2 +0 -77
- quantalogic/codeact/prompts/generate_program.j2 +0 -52
- quantalogic/codeact/prompts/response_format.j2 +0 -11
- quantalogic/codeact/react_agent.py +0 -318
- quantalogic/codeact/reasoner.py +0 -185
- quantalogic/codeact/templates/toolbox/README.md.j2 +0 -10
- quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +0 -16
- quantalogic/codeact/templates/toolbox/tools.py.j2 +0 -6
- quantalogic/codeact/templates.py +0 -7
- quantalogic/codeact/tools_manager.py +0 -258
- quantalogic/codeact/utils.py +0 -62
- quantalogic/codeact/xml_utils.py +0 -126
- quantalogic/flow/flow.py +0 -1070
- quantalogic/flow/flow_extractor.py +0 -783
- quantalogic/flow/flow_generator.py +0 -322
- quantalogic/flow/flow_manager.py +0 -676
- quantalogic/flow/flow_manager_schema.py +0 -287
- quantalogic/flow/flow_mermaid.py +0 -365
- quantalogic/flow/flow_validator.py +0 -479
- quantalogic/flow/flow_yaml.linkedin.md +0 -31
- quantalogic/flow/flow_yaml.md +0 -767
- quantalogic/flow/templates/prompt_check_inventory.j2 +0 -1
- quantalogic/flow/templates/system_check_inventory.j2 +0 -1
- quantalogic-0.80.dist-info/METADATA +0 -900
- {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/LICENSE +0 -0
- {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/WHEEL +0 -0
- {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/entry_points.txt +0 -0
quantalogic/codeact/reasoner.py
DELETED
@@ -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,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"
|
quantalogic/codeact/templates.py
DELETED
@@ -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 []
|
quantalogic/codeact/utils.py
DELETED
@@ -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
|
quantalogic/codeact/xml_utils.py
DELETED
@@ -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")
|