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/agent.py
DELETED
@@ -1,478 +0,0 @@
|
|
1
|
-
"""High-level interface for the Quantalogic Agent with modular configuration."""
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
import os
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
8
|
-
|
9
|
-
import yaml
|
10
|
-
from jinja2 import Environment
|
11
|
-
from loguru import logger
|
12
|
-
from lxml import etree
|
13
|
-
|
14
|
-
from quantalogic.tools import Tool
|
15
|
-
|
16
|
-
from .constants import MAX_HISTORY_TOKENS, MAX_TOKENS
|
17
|
-
from .executor import BaseExecutor, Executor
|
18
|
-
from .llm_util import litellm_completion
|
19
|
-
from .plugin_manager import PluginManager
|
20
|
-
from .react_agent import ReActAgent
|
21
|
-
from .reasoner import BaseReasoner, Reasoner
|
22
|
-
from .templates import jinja_env as default_jinja_env
|
23
|
-
from .tools_manager import RetrieveStepTool, get_default_tools
|
24
|
-
from .utils import process_tools
|
25
|
-
|
26
|
-
|
27
|
-
@dataclass
|
28
|
-
class AgentConfig:
|
29
|
-
"""Comprehensive configuration for the Agent, loadable from a YAML file or direct arguments."""
|
30
|
-
model: str = "gemini/gemini-2.0-flash"
|
31
|
-
max_iterations: int = 5
|
32
|
-
tools: Optional[List[Union[Tool, Callable]]] = None
|
33
|
-
max_history_tokens: int = MAX_HISTORY_TOKENS
|
34
|
-
toolbox_directory: str = "toolboxes"
|
35
|
-
enabled_toolboxes: Optional[List[str]] = None
|
36
|
-
reasoner_name: str = "default"
|
37
|
-
executor_name: str = "default"
|
38
|
-
personality: Optional[str] = None
|
39
|
-
backstory: Optional[str] = None
|
40
|
-
sop: Optional[str] = None
|
41
|
-
jinja_env: Optional[Environment] = None
|
42
|
-
name: Optional[str] = None
|
43
|
-
tools_config: Optional[List[Dict[str, Any]]] = None
|
44
|
-
reasoner: Optional[Dict[str, Any]] = field(default_factory=lambda: {"name": "default"})
|
45
|
-
executor: Optional[Dict[str, Any]] = field(default_factory=lambda: {"name": "default"})
|
46
|
-
profile: Optional[str] = None
|
47
|
-
customizations: Optional[Dict[str, Any]] = None
|
48
|
-
agent_tool_model: str = "gemini/gemini-2.0-flash" # Configurable model for AgentTool
|
49
|
-
agent_tool_timeout: int = 30 # Configurable timeout for AgentTool
|
50
|
-
|
51
|
-
def __init__(
|
52
|
-
self,
|
53
|
-
model: str = "gemini/gemini-2.0-flash",
|
54
|
-
max_iterations: int = 5,
|
55
|
-
tools: Optional[List[Union[Tool, Callable]]] = None,
|
56
|
-
max_history_tokens: int = MAX_HISTORY_TOKENS,
|
57
|
-
toolbox_directory: str = "toolboxes",
|
58
|
-
enabled_toolboxes: Optional[List[str]] = None,
|
59
|
-
reasoner_name: str = "default",
|
60
|
-
executor_name: str = "default",
|
61
|
-
personality: Optional[str] = None,
|
62
|
-
backstory: Optional[str] = None,
|
63
|
-
sop: Optional[str] = None,
|
64
|
-
jinja_env: Optional[Environment] = None,
|
65
|
-
config_file: Optional[str] = None,
|
66
|
-
name: Optional[str] = None,
|
67
|
-
tools_config: Optional[List[Dict[str, Any]]] = None,
|
68
|
-
reasoner: Optional[Dict[str, Any]] = None,
|
69
|
-
executor: Optional[Dict[str, Any]] = None,
|
70
|
-
profile: Optional[str] = None,
|
71
|
-
customizations: Optional[Dict[str, Any]] = None,
|
72
|
-
agent_tool_model: str = "gemini/gemini-2.0-flash",
|
73
|
-
agent_tool_timeout: int = 30
|
74
|
-
) -> None:
|
75
|
-
"""Initialize configuration from arguments or a YAML file."""
|
76
|
-
if config_file:
|
77
|
-
try:
|
78
|
-
with open(Path(__file__).parent / config_file) as f:
|
79
|
-
config: Dict = yaml.safe_load(f) or {}
|
80
|
-
self._load_from_config(config, model, max_iterations, max_history_tokens, toolbox_directory,
|
81
|
-
tools, enabled_toolboxes, reasoner_name, executor_name, personality,
|
82
|
-
backstory, sop, jinja_env, name, tools_config, reasoner, executor,
|
83
|
-
profile, customizations, agent_tool_model, agent_tool_timeout)
|
84
|
-
except FileNotFoundError as e:
|
85
|
-
logger.warning(f"Config file {config_file} not found: {e}. Using defaults.")
|
86
|
-
self._set_defaults(model, max_iterations, max_history_tokens, toolbox_directory,
|
87
|
-
tools, enabled_toolboxes, reasoner_name, executor_name, personality,
|
88
|
-
backstory, sop, jinja_env, name, tools_config, reasoner, executor,
|
89
|
-
profile, customizations, agent_tool_model, agent_tool_timeout)
|
90
|
-
except yaml.YAMLError as e:
|
91
|
-
logger.error(f"Error parsing YAML config {config_file}: {e}. Falling back to defaults.")
|
92
|
-
self._set_defaults(model, max_iterations, max_history_tokens, toolbox_directory,
|
93
|
-
tools, enabled_toolboxes, reasoner_name, executor_name, personality,
|
94
|
-
backstory, sop, jinja_env, name, tools_config, reasoner, executor,
|
95
|
-
profile, customizations, agent_tool_model, agent_tool_timeout)
|
96
|
-
else:
|
97
|
-
self._set_defaults(model, max_iterations, max_history_tokens, toolbox_directory,
|
98
|
-
tools, enabled_toolboxes, reasoner_name, executor_name, personality,
|
99
|
-
backstory, sop, jinja_env, name, tools_config, reasoner, executor,
|
100
|
-
profile, customizations, agent_tool_model, agent_tool_timeout)
|
101
|
-
self.__post_init__()
|
102
|
-
|
103
|
-
def _load_from_config(self, config: Dict, *args) -> None:
|
104
|
-
"""Load configuration from a dictionary, overriding with explicit arguments if provided."""
|
105
|
-
model, max_iterations, max_history_tokens, toolbox_directory, tools, enabled_toolboxes, \
|
106
|
-
reasoner_name, executor_name, personality, backstory, sop, jinja_env, name, tools_config, \
|
107
|
-
reasoner, executor, profile, customizations, agent_tool_model, agent_tool_timeout = args
|
108
|
-
|
109
|
-
self.model = config.get("model", model)
|
110
|
-
self.max_iterations = config.get("max_iterations", max_iterations)
|
111
|
-
self.max_history_tokens = config.get("max_history_tokens", max_history_tokens)
|
112
|
-
self.toolbox_directory = config.get("toolbox_directory", toolbox_directory)
|
113
|
-
self.tools = tools if tools is not None else config.get("tools")
|
114
|
-
self.enabled_toolboxes = config.get("enabled_toolboxes", enabled_toolboxes)
|
115
|
-
self.reasoner = config.get("reasoner", {"name": config.get("reasoner_name", reasoner_name)})
|
116
|
-
self.executor = config.get("executor", {"name": config.get("executor_name", executor_name)})
|
117
|
-
self.personality = config.get("personality", personality)
|
118
|
-
self.backstory = config.get("backstory", backstory)
|
119
|
-
self.sop = config.get("sop", sop)
|
120
|
-
self.jinja_env = jinja_env or default_jinja_env
|
121
|
-
self.name = config.get("name", name)
|
122
|
-
self.tools_config = config.get("tools_config", tools_config)
|
123
|
-
self.profile = config.get("profile", profile)
|
124
|
-
self.customizations = config.get("customizations", customizations)
|
125
|
-
self.agent_tool_model = config.get("agent_tool_model", agent_tool_model)
|
126
|
-
self.agent_tool_timeout = config.get("agent_tool_timeout", agent_tool_timeout)
|
127
|
-
|
128
|
-
def _set_defaults(self, model, max_iterations, max_history_tokens, toolbox_directory,
|
129
|
-
tools, enabled_toolboxes, reasoner_name, executor_name, personality,
|
130
|
-
backstory, sop, jinja_env, name, tools_config, reasoner, executor,
|
131
|
-
profile, customizations, agent_tool_model, agent_tool_timeout) -> None:
|
132
|
-
"""Set default values for all configuration fields."""
|
133
|
-
self.model = model
|
134
|
-
self.max_iterations = max_iterations
|
135
|
-
self.max_history_tokens = max_history_tokens
|
136
|
-
self.toolbox_directory = toolbox_directory
|
137
|
-
self.tools = tools
|
138
|
-
self.enabled_toolboxes = enabled_toolboxes
|
139
|
-
self.reasoner = reasoner if reasoner is not None else {"name": reasoner_name}
|
140
|
-
self.executor = executor if executor is not None else {"name": executor_name}
|
141
|
-
self.personality = personality
|
142
|
-
self.backstory = backstory
|
143
|
-
self.sop = sop
|
144
|
-
self.jinja_env = jinja_env or default_jinja_env
|
145
|
-
self.name = name
|
146
|
-
self.tools_config = tools_config
|
147
|
-
self.profile = profile
|
148
|
-
self.customizations = customizations
|
149
|
-
self.agent_tool_model = agent_tool_model
|
150
|
-
self.agent_tool_timeout = agent_tool_timeout
|
151
|
-
|
152
|
-
def __post_init__(self) -> None:
|
153
|
-
"""Apply profile defaults and customizations after initialization."""
|
154
|
-
profiles = {
|
155
|
-
"math_expert": {
|
156
|
-
"personality": {"traits": ["precise", "logical"]},
|
157
|
-
"tools_config": [{"name": "math_tools", "enabled": True}],
|
158
|
-
"sop": "Focus on accuracy and clarity in mathematical solutions."
|
159
|
-
},
|
160
|
-
"creative_writer": {
|
161
|
-
"personality": {"traits": ["creative", "expressive"]},
|
162
|
-
"tools_config": [{"name": "text_tools", "enabled": True}],
|
163
|
-
"sop": "Generate engaging and imaginative content."
|
164
|
-
}
|
165
|
-
}
|
166
|
-
if self.profile and self.profile in profiles:
|
167
|
-
base_config = profiles[self.profile]
|
168
|
-
for key, value in base_config.items():
|
169
|
-
if not getattr(self, key) or (key == "personality" and isinstance(getattr(self, key), str)):
|
170
|
-
setattr(self, key, value)
|
171
|
-
if self.customizations:
|
172
|
-
for key, value in self.customizations.items():
|
173
|
-
if hasattr(self, key):
|
174
|
-
current = getattr(self, key)
|
175
|
-
if isinstance(current, dict):
|
176
|
-
current.update(value)
|
177
|
-
elif current is None or (key in ["personality", "backstory"] and isinstance(current, str)):
|
178
|
-
setattr(self, key, value)
|
179
|
-
|
180
|
-
class Agent:
|
181
|
-
"""High-level interface for the Quantalogic Agent with unified configuration."""
|
182
|
-
def __init__(
|
183
|
-
self,
|
184
|
-
config: Union[AgentConfig, str, None] = None
|
185
|
-
) -> None:
|
186
|
-
"""Initialize the agent with a configuration."""
|
187
|
-
try:
|
188
|
-
if isinstance(config, str):
|
189
|
-
config = AgentConfig(config_file=config)
|
190
|
-
elif config is None:
|
191
|
-
config = AgentConfig()
|
192
|
-
elif not isinstance(config, AgentConfig):
|
193
|
-
raise ValueError("Config must be an AgentConfig instance or a string path to a config file.")
|
194
|
-
except Exception as e:
|
195
|
-
logger.error(f"Failed to initialize config: {e}. Using default configuration.")
|
196
|
-
config = AgentConfig()
|
197
|
-
|
198
|
-
self.config = config
|
199
|
-
self.plugin_manager = PluginManager()
|
200
|
-
try:
|
201
|
-
self.plugin_manager.load_plugins()
|
202
|
-
except Exception as e:
|
203
|
-
logger.error(f"Failed to load plugins: {e}")
|
204
|
-
self.model: str = config.model
|
205
|
-
self.default_tools: List[Tool] = self._get_tools()
|
206
|
-
self.max_iterations: int = config.max_iterations
|
207
|
-
self.personality = config.personality
|
208
|
-
self.backstory = config.backstory
|
209
|
-
self.sop: Optional[str] = config.sop
|
210
|
-
self.name: Optional[str] = config.name
|
211
|
-
self.max_history_tokens: int = config.max_history_tokens
|
212
|
-
self.jinja_env: Environment = config.jinja_env
|
213
|
-
self._observers: List[Tuple[Callable, List[str]]] = []
|
214
|
-
self.last_solve_context_vars: Dict = {}
|
215
|
-
self.default_reasoner_name: str = config.reasoner.get("name", config.reasoner_name)
|
216
|
-
self.default_executor_name: str = config.executor.get("name", config.executor_name)
|
217
|
-
|
218
|
-
def _get_tools(self) -> List[Tool]:
|
219
|
-
"""Load tools, applying tools_config if provided."""
|
220
|
-
try:
|
221
|
-
base_tools = (
|
222
|
-
process_tools(self.config.tools)
|
223
|
-
if self.config.tools is not None
|
224
|
-
else get_default_tools(self.model, enabled_toolboxes=self.config.enabled_toolboxes)
|
225
|
-
)
|
226
|
-
if not self.config.tools_config:
|
227
|
-
return base_tools
|
228
|
-
|
229
|
-
self._resolve_secrets(self.config.tools_config)
|
230
|
-
filtered_tools = []
|
231
|
-
processed_names = set()
|
232
|
-
for tool_conf in self.config.tools_config:
|
233
|
-
tool_name = tool_conf.get("name")
|
234
|
-
if tool_conf.get("enabled", True):
|
235
|
-
tool = next((t for t in base_tools if t.name == tool_name or t.toolbox_name == tool_name), None)
|
236
|
-
if tool and tool.name not in processed_names:
|
237
|
-
for key, value in tool_conf.items():
|
238
|
-
if key not in ["name", "enabled"]:
|
239
|
-
setattr(tool, key, value)
|
240
|
-
filtered_tools.append(tool)
|
241
|
-
processed_names.add(tool.name)
|
242
|
-
for tool in base_tools:
|
243
|
-
if tool.name not in processed_names:
|
244
|
-
filtered_tools.append(tool)
|
245
|
-
logger.info(f"Loaded {len(filtered_tools)} tools successfully.")
|
246
|
-
return filtered_tools
|
247
|
-
except Exception as e:
|
248
|
-
logger.error(f"Error loading tools: {e}. Returning empty toolset.")
|
249
|
-
return []
|
250
|
-
|
251
|
-
def _resolve_secrets(self, config_dict: List[Dict[str, Any]]) -> None:
|
252
|
-
"""Resolve environment variable placeholders in tools_config."""
|
253
|
-
try:
|
254
|
-
for item in config_dict:
|
255
|
-
for key, value in item.items():
|
256
|
-
if isinstance(value, str) and "{{ env." in value:
|
257
|
-
env_var = value.split("{{ env.")[1].split("}}")[0]
|
258
|
-
item[key] = os.getenv(env_var, value)
|
259
|
-
elif isinstance(value, dict):
|
260
|
-
self._resolve_secrets([value])
|
261
|
-
except Exception as e:
|
262
|
-
logger.error(f"Error resolving secrets in tools_config: {e}")
|
263
|
-
|
264
|
-
def _build_system_prompt(self) -> str:
|
265
|
-
"""Build a system prompt based on name, personality, backstory, and SOP."""
|
266
|
-
try:
|
267
|
-
prompt = f"I am {self.name}, an AI assistant." if self.name else "You are an AI assistant."
|
268
|
-
if self.personality:
|
269
|
-
if isinstance(self.personality, str):
|
270
|
-
prompt += f" I have a {self.personality} personality."
|
271
|
-
elif isinstance(self.personality, dict):
|
272
|
-
traits = self.personality.get("traits", [])
|
273
|
-
if traits:
|
274
|
-
prompt += f" I have the following personality traits: {', '.join(traits)}."
|
275
|
-
tone = self.personality.get("tone")
|
276
|
-
if tone:
|
277
|
-
prompt += f" My tone is {tone}."
|
278
|
-
humor = self.personality.get("humor_level")
|
279
|
-
if humor:
|
280
|
-
prompt += f" My humor level is {humor}."
|
281
|
-
if self.backstory:
|
282
|
-
if isinstance(self.backstory, str):
|
283
|
-
prompt += f" My backstory is: {self.backstory}"
|
284
|
-
elif isinstance(self.backstory, dict):
|
285
|
-
origin = self.backstory.get("origin")
|
286
|
-
if origin:
|
287
|
-
prompt += f" I was created by {origin}."
|
288
|
-
purpose = self.backstory.get("purpose")
|
289
|
-
if purpose:
|
290
|
-
prompt += f" My purpose is {purpose}."
|
291
|
-
experience = self.backstory.get("experience")
|
292
|
-
if experience:
|
293
|
-
prompt += f" My experience includes: {experience}"
|
294
|
-
if self.sop:
|
295
|
-
prompt += f" Follow this standard operating procedure: {self.sop}"
|
296
|
-
return prompt
|
297
|
-
except Exception as e:
|
298
|
-
logger.error(f"Error building system prompt: {e}. Using default.")
|
299
|
-
return "You are an AI assistant."
|
300
|
-
|
301
|
-
async def chat(
|
302
|
-
self,
|
303
|
-
message: str,
|
304
|
-
use_tools: bool = False,
|
305
|
-
tools: Optional[List[Union[Tool, Callable]]] = None,
|
306
|
-
timeout: int = 30,
|
307
|
-
max_tokens: int = MAX_TOKENS,
|
308
|
-
temperature: float = 0.7,
|
309
|
-
streaming: bool = False,
|
310
|
-
reasoner_name: Optional[str] = None,
|
311
|
-
executor_name: Optional[str] = None
|
312
|
-
) -> str:
|
313
|
-
"""Single-step interaction with optional custom tools and streaming."""
|
314
|
-
system_prompt: str = self._build_system_prompt()
|
315
|
-
try:
|
316
|
-
if use_tools:
|
317
|
-
chat_tools: List[Tool] = process_tools(tools) if tools is not None else self.default_tools
|
318
|
-
reasoner_name = reasoner_name or self.default_reasoner_name
|
319
|
-
executor_name = executor_name or self.default_executor_name
|
320
|
-
reasoner_cls = self.plugin_manager.reasoners.get(reasoner_name, Reasoner)
|
321
|
-
executor_cls = self.plugin_manager.executors.get(executor_name, Executor)
|
322
|
-
reasoner_config = self.config.reasoner.get("config", {})
|
323
|
-
executor_config = self.config.executor.get("config", {})
|
324
|
-
chat_agent = ReActAgent(
|
325
|
-
model=self.model,
|
326
|
-
tools=chat_tools,
|
327
|
-
max_iterations=1,
|
328
|
-
max_history_tokens=self.max_history_tokens,
|
329
|
-
reasoner=reasoner_cls(self.model, chat_tools, **reasoner_config),
|
330
|
-
executor=executor_cls(chat_tools, self._notify_observers, **executor_config)
|
331
|
-
)
|
332
|
-
chat_agent.executor.register_tool(RetrieveStepTool(chat_agent.history_manager.store))
|
333
|
-
for observer, event_types in self._observers:
|
334
|
-
chat_agent.add_observer(observer, event_types)
|
335
|
-
history: List[Dict] = await chat_agent.solve(message, system_prompt=system_prompt, streaming=streaming)
|
336
|
-
return self._extract_response(history)
|
337
|
-
else:
|
338
|
-
response: str = await litellm_completion(
|
339
|
-
model=self.model,
|
340
|
-
messages=[
|
341
|
-
{"role": "system", "content": system_prompt},
|
342
|
-
{"role": "user", "content": message}
|
343
|
-
],
|
344
|
-
max_tokens=max_tokens,
|
345
|
-
temperature=temperature,
|
346
|
-
stream=streaming,
|
347
|
-
notify_event=self._notify_observers if streaming else None
|
348
|
-
)
|
349
|
-
return response.strip()
|
350
|
-
except Exception as e:
|
351
|
-
logger.error(f"Chat failed: {e}")
|
352
|
-
return f"Error: Unable to process chat request due to {str(e)}"
|
353
|
-
|
354
|
-
def sync_chat(self, message: str, timeout: int = 30) -> str:
|
355
|
-
"""Synchronous wrapper for chat."""
|
356
|
-
try:
|
357
|
-
return asyncio.run(self.chat(message, timeout=timeout))
|
358
|
-
except Exception as e:
|
359
|
-
logger.error(f"Synchronous chat failed: {e}")
|
360
|
-
return f"Error: {str(e)}"
|
361
|
-
|
362
|
-
async def solve(
|
363
|
-
self,
|
364
|
-
task: str,
|
365
|
-
success_criteria: Optional[str] = None,
|
366
|
-
max_iterations: Optional[int] = None,
|
367
|
-
tools: Optional[List[Union[Tool, Callable]]] = None,
|
368
|
-
timeout: int = 300,
|
369
|
-
streaming: bool = False,
|
370
|
-
reasoner_name: Optional[str] = None,
|
371
|
-
executor_name: Optional[str] = None
|
372
|
-
) -> List[Dict]:
|
373
|
-
"""Multi-step task solving with optional custom tools, max_iterations, and streaming."""
|
374
|
-
system_prompt: str = self._build_system_prompt()
|
375
|
-
try:
|
376
|
-
solve_tools: List[Tool] = process_tools(tools) if tools is not None else self.default_tools
|
377
|
-
reasoner_name = reasoner_name or self.default_reasoner_name
|
378
|
-
executor_name = executor_name or self.default_executor_name
|
379
|
-
reasoner_cls = self.plugin_manager.reasoners.get(reasoner_name, Reasoner)
|
380
|
-
executor_cls = self.plugin_manager.executors.get(executor_name, Executor)
|
381
|
-
reasoner_config = self.config.reasoner.get("config", {})
|
382
|
-
executor_config = self.config.executor.get("config", {})
|
383
|
-
solve_agent = ReActAgent(
|
384
|
-
model=self.model,
|
385
|
-
tools=solve_tools,
|
386
|
-
max_iterations=max_iterations if max_iterations is not None else self.max_iterations,
|
387
|
-
max_history_tokens=self.max_history_tokens,
|
388
|
-
reasoner=reasoner_cls(self.model, solve_tools, **reasoner_config),
|
389
|
-
executor=executor_cls(solve_tools, self._notify_observers, **executor_config)
|
390
|
-
)
|
391
|
-
solve_agent.executor.register_tool(RetrieveStepTool(solve_agent.history_manager.store))
|
392
|
-
for observer, event_types in self._observers:
|
393
|
-
solve_agent.add_observer(observer, event_types)
|
394
|
-
|
395
|
-
history: List[Dict] = await solve_agent.solve(
|
396
|
-
task,
|
397
|
-
success_criteria,
|
398
|
-
system_prompt=system_prompt,
|
399
|
-
max_iterations=max_iterations,
|
400
|
-
streaming=streaming
|
401
|
-
)
|
402
|
-
self.last_solve_context_vars = solve_agent.context_vars.copy()
|
403
|
-
return history
|
404
|
-
except Exception as e:
|
405
|
-
logger.error(f"Solve failed: {e}")
|
406
|
-
return [{"error": f"Failed to solve task: {str(e)}"}]
|
407
|
-
|
408
|
-
def sync_solve(self, task: str, success_criteria: Optional[str] = None, timeout: int = 300) -> List[Dict]:
|
409
|
-
"""Synchronous wrapper for solve."""
|
410
|
-
try:
|
411
|
-
return asyncio.run(self.solve(task, success_criteria, timeout=timeout))
|
412
|
-
except Exception as e:
|
413
|
-
logger.error(f"Synchronous solve failed: {e}")
|
414
|
-
return [{"error": f"Failed to solve task synchronously: {str(e)}"}]
|
415
|
-
|
416
|
-
def add_observer(self, observer: Callable, event_types: List[str]) -> 'Agent':
|
417
|
-
"""Add an observer to be applied to agents created in chat and solve."""
|
418
|
-
self._observers.append((observer, event_types))
|
419
|
-
return self
|
420
|
-
|
421
|
-
def register_tool(self, tool: Tool) -> None:
|
422
|
-
"""Register a new tool dynamically at runtime."""
|
423
|
-
try:
|
424
|
-
if tool.name in [t.name for t in self.default_tools]:
|
425
|
-
raise ValueError(f"Tool '{tool.name}' is already registered")
|
426
|
-
self.default_tools.append(tool)
|
427
|
-
self.plugin_manager.tools.register(tool)
|
428
|
-
except Exception as e:
|
429
|
-
logger.error(f"Failed to register tool {tool.name}: {e}")
|
430
|
-
|
431
|
-
def register_reasoner(self, reasoner: BaseReasoner, name: str) -> None:
|
432
|
-
"""Register a new reasoner dynamically at runtime."""
|
433
|
-
try:
|
434
|
-
self.plugin_manager.reasoners[name] = reasoner.__class__
|
435
|
-
except Exception as e:
|
436
|
-
logger.error(f"Failed to register reasoner {name}: {e}")
|
437
|
-
|
438
|
-
def register_executor(self, executor: BaseExecutor, name: str) -> None:
|
439
|
-
"""Register a new executor dynamically at runtime."""
|
440
|
-
try:
|
441
|
-
self.plugin_manager.executors[name] = executor.__class__
|
442
|
-
except Exception as e:
|
443
|
-
logger.error(f"Failed to register executor {name}: {e}")
|
444
|
-
|
445
|
-
def list_tools(self) -> List[str]:
|
446
|
-
"""Return a list of available tool names."""
|
447
|
-
return [tool.name for tool in self.default_tools]
|
448
|
-
|
449
|
-
def get_context_vars(self) -> Dict:
|
450
|
-
"""Return the context variables from the last solve call."""
|
451
|
-
return self.last_solve_context_vars
|
452
|
-
|
453
|
-
def _extract_response(self, history: List[Dict]) -> str:
|
454
|
-
"""Extract a clean response from the history."""
|
455
|
-
if not history:
|
456
|
-
return "No response generated."
|
457
|
-
last_result: str = history[-1].get("result", "")
|
458
|
-
try:
|
459
|
-
root = etree.fromstring(last_result)
|
460
|
-
if root.findtext("Status") == "Success":
|
461
|
-
value: str = root.findtext("Value") or ""
|
462
|
-
final_answer: Optional[str] = root.findtext("FinalAnswer")
|
463
|
-
return final_answer.strip() if final_answer else value.strip()
|
464
|
-
else:
|
465
|
-
return f"Error: {root.findtext('Value') or 'Unknown error'}"
|
466
|
-
except etree.XMLSyntaxError as e:
|
467
|
-
logger.error(f"Failed to parse response XML: {e}")
|
468
|
-
return last_result
|
469
|
-
|
470
|
-
async def _notify_observers(self, event: object) -> None:
|
471
|
-
"""Notify all subscribed observers of an event."""
|
472
|
-
try:
|
473
|
-
await asyncio.gather(
|
474
|
-
*(observer(event) for observer, types in self._observers if event.event_type in types),
|
475
|
-
return_exceptions=True
|
476
|
-
)
|
477
|
-
except Exception as e:
|
478
|
-
logger.error(f"Error notifying observers: {e}")
|
quantalogic/codeact/cli.py
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
"""Command-line interface entry point for Quantalogic Agent."""
|
2
|
-
|
3
|
-
import importlib
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
import typer
|
7
|
-
from loguru import logger
|
8
|
-
from rich.console import Console
|
9
|
-
|
10
|
-
from quantalogic.codeact.plugin_manager import PluginManager
|
11
|
-
|
12
|
-
app = typer.Typer(no_args_is_help=True)
|
13
|
-
console = Console()
|
14
|
-
|
15
|
-
# Initialize PluginManager at module level to avoid duplicate loading
|
16
|
-
plugin_manager = PluginManager()
|
17
|
-
plugin_manager.load_plugins() # This is now synchronous
|
18
|
-
|
19
|
-
# Dynamically load CLI commands from cli_commands directory
|
20
|
-
cli_commands_dir = Path(__file__).parent / "cli_commands"
|
21
|
-
for file in cli_commands_dir.glob("*.py"):
|
22
|
-
if file.stem != "__init__":
|
23
|
-
module_name = f"quantalogic.codeact.cli_commands.{file.stem}"
|
24
|
-
try:
|
25
|
-
module = importlib.import_module(module_name)
|
26
|
-
command_name = file.stem.replace("_", "-")
|
27
|
-
# Handle Typer app instances (e.g., list_toolboxes.py)
|
28
|
-
if hasattr(module, "app") and isinstance(module.app, typer.Typer):
|
29
|
-
if len(module.app.registered_commands) == 1:
|
30
|
-
# If there's exactly one command, register it directly
|
31
|
-
command = module.app.registered_commands[0]
|
32
|
-
app.command(name=command_name)(command.callback)
|
33
|
-
logger.debug(f"Loaded direct command: {command_name}")
|
34
|
-
else:
|
35
|
-
# Otherwise, treat it as a subcommand group
|
36
|
-
app.add_typer(module.app, name=command_name)
|
37
|
-
logger.debug(f"Loaded command group: {command_name}")
|
38
|
-
# Handle direct function commands (e.g., task.py)
|
39
|
-
elif hasattr(module, file.stem) and callable(getattr(module, file.stem)):
|
40
|
-
app.command(name=command_name)(getattr(module, file.stem))
|
41
|
-
logger.debug(f"Loaded direct command: {command_name}")
|
42
|
-
except ImportError as e:
|
43
|
-
logger.error(f"Failed to load command module {module_name}: {e}")
|
44
|
-
|
45
|
-
# Load plugin CLI commands dynamically using the module-level plugin_manager
|
46
|
-
for cmd_name, cmd_func in plugin_manager.cli_commands.items():
|
47
|
-
app.command(name=cmd_name)(cmd_func)
|
48
|
-
|
49
|
-
if __name__ == "__main__":
|
50
|
-
app()
|
File without changes
|
@@ -1,45 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
|
3
|
-
import typer
|
4
|
-
from jinja2 import Environment, FileSystemLoader
|
5
|
-
from loguru import logger
|
6
|
-
from rich.console import Console
|
7
|
-
|
8
|
-
app = typer.Typer()
|
9
|
-
|
10
|
-
console = Console()
|
11
|
-
|
12
|
-
@app.command()
|
13
|
-
def create_toolbox(
|
14
|
-
name: str = typer.Argument(..., help="Name of the new toolbox (e.g., 'my-toolbox')")
|
15
|
-
) -> None:
|
16
|
-
"""Create a starter toolbox project with the given name using Jinja2 templates."""
|
17
|
-
toolbox_dir = Path(name)
|
18
|
-
if toolbox_dir.exists():
|
19
|
-
logger.error(f"Directory '{name}' already exists.")
|
20
|
-
console.print(f"[red]Error: Directory '{name}' already exists.[/red]")
|
21
|
-
raise typer.Exit(code=1)
|
22
|
-
|
23
|
-
# Set up Jinja2 environment
|
24
|
-
template_dir = Path(__file__).parent.parent / "templates" / "toolbox"
|
25
|
-
env = Environment(loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True)
|
26
|
-
|
27
|
-
# Create directory structure
|
28
|
-
toolbox_dir.mkdir()
|
29
|
-
package_name = name.replace("-", "_")
|
30
|
-
package_dir = toolbox_dir / package_name
|
31
|
-
package_dir.mkdir()
|
32
|
-
(package_dir / "__init__.py").touch()
|
33
|
-
|
34
|
-
# Render and write template files
|
35
|
-
context = {"name": name, "package_name": package_name}
|
36
|
-
for template_name in ["pyproject.toml.j2", "tools.py.j2", "README.md.j2"]:
|
37
|
-
template = env.get_template(template_name)
|
38
|
-
content = template.render(**context)
|
39
|
-
output_file = toolbox_dir / template_name.replace(".j2", "")
|
40
|
-
if template_name == "tools.py.j2":
|
41
|
-
output_file = package_dir / "tools.py"
|
42
|
-
output_file.write_text(content.strip())
|
43
|
-
|
44
|
-
logger.info(f"Created starter toolbox project: {name}")
|
45
|
-
console.print(f"[green]Created starter toolbox project: {name}[/green]")
|
@@ -1,20 +0,0 @@
|
|
1
|
-
import subprocess
|
2
|
-
|
3
|
-
import typer
|
4
|
-
from rich.console import Console
|
5
|
-
|
6
|
-
app = typer.Typer()
|
7
|
-
|
8
|
-
console = Console()
|
9
|
-
|
10
|
-
@app.command()
|
11
|
-
def install_toolbox(
|
12
|
-
toolbox_name: str = typer.Argument(..., help="Name of the toolbox to install (PyPI package or local wheel file)")
|
13
|
-
) -> None:
|
14
|
-
"""Install a toolbox using uv pip install."""
|
15
|
-
try:
|
16
|
-
subprocess.run(["uv", "pip", "install", toolbox_name], check=True)
|
17
|
-
console.print(f"[green]Toolbox '{toolbox_name}' installed successfully[/green]")
|
18
|
-
except subprocess.CalledProcessError as e:
|
19
|
-
console.print(f"[red]Failed to install toolbox: {e}[/red]")
|
20
|
-
raise typer.Exit(code=1)
|
@@ -1,15 +0,0 @@
|
|
1
|
-
import typer
|
2
|
-
from rich.console import Console
|
3
|
-
|
4
|
-
from quantalogic.codeact.cli import plugin_manager # Import shared plugin_manager from cli.py
|
5
|
-
|
6
|
-
app = typer.Typer()
|
7
|
-
|
8
|
-
console = Console()
|
9
|
-
|
10
|
-
@app.command()
|
11
|
-
def list_executors() -> None:
|
12
|
-
"""List all available executors."""
|
13
|
-
console.print("[bold cyan]Available Executors:[/bold cyan]")
|
14
|
-
for name in plugin_manager.executors.keys():
|
15
|
-
console.print(f"- {name}")
|
@@ -1,15 +0,0 @@
|
|
1
|
-
import typer
|
2
|
-
from rich.console import Console
|
3
|
-
|
4
|
-
from quantalogic.codeact.cli import plugin_manager # Import shared plugin_manager from cli.py
|
5
|
-
|
6
|
-
app = typer.Typer()
|
7
|
-
|
8
|
-
console = Console()
|
9
|
-
|
10
|
-
@app.command()
|
11
|
-
def list_reasoners() -> None:
|
12
|
-
"""List all available reasoners."""
|
13
|
-
console.print("[bold cyan]Available Reasoners:[/bold cyan]")
|
14
|
-
for name in plugin_manager.reasoners.keys():
|
15
|
-
console.print(f"- {name}")
|