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
@@ -0,0 +1,318 @@
|
|
1
|
+
"""Core implementation of the ReAct framework for reasoning and acting."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import time
|
5
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
6
|
+
|
7
|
+
from loguru import logger
|
8
|
+
from lxml import etree
|
9
|
+
|
10
|
+
from quantalogic.tools import Tool
|
11
|
+
|
12
|
+
from .events import (
|
13
|
+
ActionExecutedEvent,
|
14
|
+
ActionGeneratedEvent,
|
15
|
+
ErrorOccurredEvent,
|
16
|
+
StepCompletedEvent,
|
17
|
+
StepStartedEvent,
|
18
|
+
TaskCompletedEvent,
|
19
|
+
TaskStartedEvent,
|
20
|
+
ThoughtGeneratedEvent,
|
21
|
+
)
|
22
|
+
from .executor import BaseExecutor, Executor
|
23
|
+
from .history_manager import HistoryManager
|
24
|
+
from .llm_util import litellm_completion
|
25
|
+
from .reasoner import BaseReasoner, Reasoner
|
26
|
+
from .tools_manager import ToolRegistry
|
27
|
+
from .xml_utils import XMLResultHandler
|
28
|
+
|
29
|
+
|
30
|
+
class ReActAgent:
|
31
|
+
"""Implements the ReAct framework for reasoning and acting with enhanced memory management."""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
model: str,
|
36
|
+
tools: List[Tool],
|
37
|
+
max_iterations: int = 5,
|
38
|
+
max_history_tokens: int = 2000,
|
39
|
+
system_prompt: str = "", # New parameter for persistent context
|
40
|
+
task_description: str = "", # New parameter for persistent context
|
41
|
+
reasoner: Optional[BaseReasoner] = None,
|
42
|
+
executor: Optional[BaseExecutor] = None,
|
43
|
+
tool_registry: Optional[ToolRegistry] = None,
|
44
|
+
history_manager: Optional[HistoryManager] = None,
|
45
|
+
error_handler: Optional[Callable[[Exception, int], bool]] = None
|
46
|
+
) -> None:
|
47
|
+
"""
|
48
|
+
Initialize the ReActAgent with tools, reasoning, execution, and memory components.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
model (str): Language model identifier.
|
52
|
+
tools (List[Tool]): List of available tools.
|
53
|
+
max_iterations (int): Maximum reasoning steps (default: 5).
|
54
|
+
max_history_tokens (int): Max tokens for history (default: 2000).
|
55
|
+
system_prompt (str): Persistent system instructions (default: "").
|
56
|
+
task_description (str): Persistent task context (default: "").
|
57
|
+
reasoner (Optional[BaseReasoner]): Custom reasoner instance.
|
58
|
+
executor (Optional[BaseExecutor]): Custom executor instance.
|
59
|
+
tool_registry (Optional[ToolRegistry]): Custom tool registry.
|
60
|
+
history_manager (Optional[HistoryManager]): Custom history manager.
|
61
|
+
error_handler (Optional[Callable[[Exception, int], bool]]): Error handler callback.
|
62
|
+
"""
|
63
|
+
self.tool_registry = tool_registry or ToolRegistry()
|
64
|
+
for tool in tools:
|
65
|
+
self.tool_registry.register(tool)
|
66
|
+
self.reasoner: BaseReasoner = reasoner or Reasoner(model, self.tool_registry.get_tools())
|
67
|
+
self.executor: BaseExecutor = executor or Executor(self.tool_registry.get_tools(), notify_event=self._notify_observers)
|
68
|
+
self.max_iterations: int = max_iterations
|
69
|
+
self.max_history_tokens: int = max_history_tokens
|
70
|
+
self.history_manager: HistoryManager = history_manager or HistoryManager(
|
71
|
+
max_tokens=max_history_tokens,
|
72
|
+
system_prompt=system_prompt,
|
73
|
+
task_description=task_description
|
74
|
+
)
|
75
|
+
self.context_vars: Dict = {}
|
76
|
+
self._observers: List[Tuple[Callable, List[str]]] = []
|
77
|
+
self.error_handler = error_handler or (lambda e, step: False) # Default: no retry
|
78
|
+
|
79
|
+
def add_observer(self, observer: Callable, event_types: List[str]) -> 'ReActAgent':
|
80
|
+
"""Add an observer for specific event types."""
|
81
|
+
self._observers.append((observer, event_types))
|
82
|
+
return self
|
83
|
+
|
84
|
+
async def _notify_observers(self, event: object) -> None:
|
85
|
+
"""Notify all subscribed observers of an event."""
|
86
|
+
await asyncio.gather(
|
87
|
+
*(observer(event) for observer, types in self._observers if event.event_type in types),
|
88
|
+
return_exceptions=True
|
89
|
+
)
|
90
|
+
|
91
|
+
async def generate_action(
|
92
|
+
self,
|
93
|
+
task: str,
|
94
|
+
history: List[Dict],
|
95
|
+
step: int,
|
96
|
+
max_iterations: int,
|
97
|
+
system_prompt: Optional[str] = None,
|
98
|
+
streaming: bool = False
|
99
|
+
) -> str:
|
100
|
+
"""
|
101
|
+
Generate an action using the Reasoner, passing available variables.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
task (str): The task to address.
|
105
|
+
history (List[Dict]): Stored step history.
|
106
|
+
step (int): Current step number.
|
107
|
+
max_iterations (int): Maximum allowed steps.
|
108
|
+
system_prompt (Optional[str]): Override system prompt (optional).
|
109
|
+
streaming (bool): Whether to stream the response.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
str: Generated action in XML format.
|
113
|
+
"""
|
114
|
+
history_str: str = self.history_manager.format_history(max_iterations)
|
115
|
+
available_vars: List[str] = list(self.context_vars.keys())
|
116
|
+
start: float = time.perf_counter()
|
117
|
+
response: str = await self.reasoner.generate_action(
|
118
|
+
task,
|
119
|
+
history_str,
|
120
|
+
step,
|
121
|
+
max_iterations,
|
122
|
+
system_prompt or self.history_manager.system_prompt,
|
123
|
+
self._notify_observers,
|
124
|
+
streaming=streaming,
|
125
|
+
available_vars=available_vars
|
126
|
+
)
|
127
|
+
thought, code = XMLResultHandler.parse_action_response(response)
|
128
|
+
gen_time: float = time.perf_counter() - start
|
129
|
+
await self._notify_observers(ThoughtGeneratedEvent(
|
130
|
+
event_type="ThoughtGenerated", step_number=step, thought=thought, generation_time=gen_time
|
131
|
+
))
|
132
|
+
await self._notify_observers(ActionGeneratedEvent(
|
133
|
+
event_type="ActionGenerated", step_number=step, action_code=code, generation_time=gen_time
|
134
|
+
))
|
135
|
+
if not response.endswith("</Code>"):
|
136
|
+
logger.warning(f"Response might be truncated at step {step}")
|
137
|
+
return response
|
138
|
+
|
139
|
+
async def execute_action(self, code: str, step: int, timeout: int = 300) -> str:
|
140
|
+
"""
|
141
|
+
Execute an action using the Executor.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
code (str): Code to execute.
|
145
|
+
step (int): Current step number.
|
146
|
+
timeout (int): Execution timeout in seconds (default: 300).
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
str: Execution result in XML format.
|
150
|
+
"""
|
151
|
+
start: float = time.perf_counter()
|
152
|
+
result_xml: str = await self.executor.execute_action(code, self.context_vars, step, timeout)
|
153
|
+
execution_time: float = time.perf_counter() - start
|
154
|
+
await self._notify_observers(ActionExecutedEvent(
|
155
|
+
event_type="ActionExecuted", step_number=step, result_xml=result_xml, execution_time=execution_time
|
156
|
+
))
|
157
|
+
return result_xml
|
158
|
+
|
159
|
+
async def is_task_complete(self, task: str, history: List[Dict], result: str, success_criteria: Optional[str]) -> Tuple[bool, str]:
|
160
|
+
"""
|
161
|
+
Check if the task is complete based on the result.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
task (str): The task being solved.
|
165
|
+
history (List[Dict]): Step history.
|
166
|
+
result (str): Result of the latest action.
|
167
|
+
success_criteria (Optional[str]): Optional success criteria.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Tuple[bool, str]: (is_complete, final_answer).
|
171
|
+
"""
|
172
|
+
try:
|
173
|
+
root = etree.fromstring(result)
|
174
|
+
if root.findtext("Completed") == "true":
|
175
|
+
final_answer: str = root.findtext("FinalAnswer") or ""
|
176
|
+
verification: str = await litellm_completion(
|
177
|
+
model=self.reasoner.model,
|
178
|
+
messages=[{
|
179
|
+
"role": "user",
|
180
|
+
"content": f"Does '{final_answer}' solve '{task}' given history:\n{self.history_manager.format_history(self.max_iterations)}?"
|
181
|
+
}],
|
182
|
+
max_tokens=100,
|
183
|
+
temperature=0.1,
|
184
|
+
stream=False
|
185
|
+
)
|
186
|
+
if verification and "yes" in verification.lower():
|
187
|
+
return True, final_answer
|
188
|
+
return True, final_answer
|
189
|
+
except etree.XMLSyntaxError:
|
190
|
+
pass
|
191
|
+
|
192
|
+
if success_criteria and (result_value := XMLResultHandler.extract_result_value(result)) and success_criteria in result_value:
|
193
|
+
return True, result_value
|
194
|
+
return False, ""
|
195
|
+
|
196
|
+
async def _run_step(self, task: str, step: int, max_iters: int,
|
197
|
+
system_prompt: Optional[str], streaming: bool) -> Dict:
|
198
|
+
"""
|
199
|
+
Execute a single step of the ReAct loop with retry logic.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
task (str): The task to address.
|
203
|
+
step (int): Current step number.
|
204
|
+
max_iters (int): Maximum allowed steps.
|
205
|
+
system_prompt (Optional[str]): System prompt override.
|
206
|
+
streaming (bool): Whether to stream responses.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Dict: Step data (step_number, thought, action, result).
|
210
|
+
"""
|
211
|
+
await self._notify_observers(StepStartedEvent(
|
212
|
+
event_type="StepStarted",
|
213
|
+
step_number=step,
|
214
|
+
system_prompt=self.history_manager.system_prompt,
|
215
|
+
task_description=self.history_manager.task_description
|
216
|
+
))
|
217
|
+
for attempt in range(3):
|
218
|
+
try:
|
219
|
+
response: str = await self.generate_action(task, self.history_manager.store, step, max_iters, system_prompt, streaming)
|
220
|
+
thought, code = XMLResultHandler.parse_action_response(response)
|
221
|
+
result: str = await self.execute_action(code, step)
|
222
|
+
step_data = {"step_number": step, "thought": thought, "action": code, "result": result}
|
223
|
+
self.history_manager.add_step(step_data)
|
224
|
+
return step_data
|
225
|
+
except Exception as e:
|
226
|
+
if not self.error_handler(e, step) or attempt == 2:
|
227
|
+
await self._notify_observers(ErrorOccurredEvent(
|
228
|
+
event_type="ErrorOccurred", error_message=str(e), step_number=step
|
229
|
+
))
|
230
|
+
raise
|
231
|
+
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
232
|
+
|
233
|
+
async def _finalize_step(self, task: str, step_data: Dict,
|
234
|
+
success_criteria: Optional[str]) -> Tuple[bool, Dict]:
|
235
|
+
"""
|
236
|
+
Check completion and notify observers for a step.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
task (str): The task being solved.
|
240
|
+
step_data (Dict): Current step data.
|
241
|
+
success_criteria (Optional[str]): Optional success criteria.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Tuple[bool, Dict]: (is_complete, updated_step_data).
|
245
|
+
"""
|
246
|
+
is_complete, final_answer = await self.is_task_complete(task, self.history_manager.store, step_data["result"], success_criteria)
|
247
|
+
if is_complete:
|
248
|
+
try:
|
249
|
+
root = etree.fromstring(step_data["result"])
|
250
|
+
if root.find("FinalAnswer") is None:
|
251
|
+
final_answer_elem = etree.Element("FinalAnswer")
|
252
|
+
final_answer_elem.text = etree.CDATA(final_answer)
|
253
|
+
root.append(final_answer_elem)
|
254
|
+
step_data["result"] = etree.tostring(root, pretty_print=True, encoding="unicode")
|
255
|
+
except etree.XMLSyntaxError as e:
|
256
|
+
logger.error(f"Failed to parse result XML for appending FinalAnswer: {e}")
|
257
|
+
if "<FinalAnswer>" not in step_data["result"]:
|
258
|
+
step_data["result"] += f"\n<FinalAnswer><![CDATA[\n{final_answer}\n]]></FinalAnswer>"
|
259
|
+
await self._notify_observers(StepCompletedEvent(
|
260
|
+
event_type="StepCompleted", step_number=step_data["step_number"],
|
261
|
+
thought=step_data["thought"], action=step_data["action"], result=step_data["result"],
|
262
|
+
is_complete=is_complete, final_answer=final_answer if is_complete else None
|
263
|
+
))
|
264
|
+
return is_complete, step_data
|
265
|
+
|
266
|
+
async def solve(
|
267
|
+
self,
|
268
|
+
task: str,
|
269
|
+
success_criteria: Optional[str] = None,
|
270
|
+
system_prompt: Optional[str] = None,
|
271
|
+
max_iterations: Optional[int] = None,
|
272
|
+
streaming: bool = False
|
273
|
+
) -> List[Dict]:
|
274
|
+
"""
|
275
|
+
Solve a task using the ReAct framework with persistent memory.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
task (str): The task to solve.
|
279
|
+
success_criteria (Optional[str]): Criteria for success.
|
280
|
+
system_prompt (Optional[str]): System prompt override.
|
281
|
+
max_iterations (Optional[int]): Override for max steps.
|
282
|
+
streaming (bool): Whether to stream responses.
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
List[Dict]: History of steps taken.
|
286
|
+
"""
|
287
|
+
max_iters: int = max_iterations if max_iterations is not None else self.max_iterations
|
288
|
+
self.history_manager.store.clear() # Reset history for new task
|
289
|
+
if system_prompt is not None:
|
290
|
+
self.history_manager.system_prompt = system_prompt
|
291
|
+
self.history_manager.task_description = task
|
292
|
+
await self._notify_observers(TaskStartedEvent(
|
293
|
+
event_type="TaskStarted",
|
294
|
+
task_description=task,
|
295
|
+
system_prompt=self.history_manager.system_prompt
|
296
|
+
))
|
297
|
+
|
298
|
+
for step in range(1, max_iters + 1):
|
299
|
+
try:
|
300
|
+
step_data: Dict = await self._run_step(task, step, max_iters, system_prompt, streaming)
|
301
|
+
is_complete, step_data = await self._finalize_step(task, step_data, success_criteria)
|
302
|
+
if is_complete:
|
303
|
+
await self._notify_observers(TaskCompletedEvent(
|
304
|
+
event_type="TaskCompleted", final_answer=step_data["result"], reason="success"
|
305
|
+
))
|
306
|
+
break
|
307
|
+
except Exception as e:
|
308
|
+
await self._notify_observers(ErrorOccurredEvent(
|
309
|
+
event_type="ErrorOccurred", error_message=str(e), step_number=step
|
310
|
+
))
|
311
|
+
break
|
312
|
+
|
313
|
+
if not any("<FinalAnswer>" in step["result"] for step in self.history_manager.store):
|
314
|
+
await self._notify_observers(TaskCompletedEvent(
|
315
|
+
event_type="TaskCompleted", final_answer=None,
|
316
|
+
reason="max_iterations_reached" if len(self.history_manager.store) == max_iters else "error"
|
317
|
+
))
|
318
|
+
return self.history_manager.store
|
@@ -0,0 +1,185 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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"
|
@@ -0,0 +1,7 @@
|
|
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)
|