quantalogic 0.61.2__py3-none-any.whl → 0.80__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. quantalogic/agent.py +0 -1
  2. quantalogic/codeact/TODO.md +14 -0
  3. quantalogic/codeact/agent.py +400 -421
  4. quantalogic/codeact/cli.py +42 -224
  5. quantalogic/codeact/cli_commands/__init__.py +0 -0
  6. quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
  7. quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
  8. quantalogic/codeact/cli_commands/list_executor.py +15 -0
  9. quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
  10. quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
  11. quantalogic/codeact/cli_commands/task.py +215 -0
  12. quantalogic/codeact/cli_commands/tool_info.py +24 -0
  13. quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
  14. quantalogic/codeact/config.yaml +21 -0
  15. quantalogic/codeact/constants.py +1 -1
  16. quantalogic/codeact/events.py +12 -5
  17. quantalogic/codeact/examples/README.md +342 -0
  18. quantalogic/codeact/examples/agent_sample.yaml +29 -0
  19. quantalogic/codeact/executor.py +186 -0
  20. quantalogic/codeact/history_manager.py +94 -0
  21. quantalogic/codeact/llm_util.py +3 -22
  22. quantalogic/codeact/plugin_manager.py +92 -0
  23. quantalogic/codeact/prompts/generate_action.j2 +65 -14
  24. quantalogic/codeact/prompts/generate_program.j2 +32 -19
  25. quantalogic/codeact/react_agent.py +318 -0
  26. quantalogic/codeact/reasoner.py +185 -0
  27. quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
  28. quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
  29. quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
  30. quantalogic/codeact/templates.py +7 -0
  31. quantalogic/codeact/tools_manager.py +242 -119
  32. quantalogic/codeact/utils.py +16 -89
  33. quantalogic/codeact/xml_utils.py +126 -0
  34. quantalogic/flow/flow.py +151 -41
  35. quantalogic/flow/flow_extractor.py +61 -1
  36. quantalogic/flow/flow_generator.py +34 -6
  37. quantalogic/flow/flow_manager.py +64 -25
  38. quantalogic/flow/flow_manager_schema.py +32 -0
  39. quantalogic/tools/action_gen.py +1 -1
  40. quantalogic/tools/action_gen_safe.py +340 -0
  41. quantalogic/tools/tool.py +531 -109
  42. quantalogic/tools/write_file_tool.py +7 -8
  43. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -2
  44. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/RECORD +47 -42
  45. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
  46. quantalogic-0.80.dist-info/entry_points.txt +3 -0
  47. quantalogic/python_interpreter/__init__.py +0 -23
  48. quantalogic/python_interpreter/assignment_visitors.py +0 -63
  49. quantalogic/python_interpreter/base_visitors.py +0 -20
  50. quantalogic/python_interpreter/class_visitors.py +0 -22
  51. quantalogic/python_interpreter/comprehension_visitors.py +0 -172
  52. quantalogic/python_interpreter/context_visitors.py +0 -59
  53. quantalogic/python_interpreter/control_flow_visitors.py +0 -88
  54. quantalogic/python_interpreter/exception_visitors.py +0 -109
  55. quantalogic/python_interpreter/exceptions.py +0 -39
  56. quantalogic/python_interpreter/execution.py +0 -202
  57. quantalogic/python_interpreter/function_utils.py +0 -386
  58. quantalogic/python_interpreter/function_visitors.py +0 -209
  59. quantalogic/python_interpreter/import_visitors.py +0 -28
  60. quantalogic/python_interpreter/interpreter_core.py +0 -358
  61. quantalogic/python_interpreter/literal_visitors.py +0 -74
  62. quantalogic/python_interpreter/misc_visitors.py +0 -148
  63. quantalogic/python_interpreter/operator_visitors.py +0 -108
  64. quantalogic/python_interpreter/scope.py +0 -10
  65. quantalogic/python_interpreter/visit_handlers.py +0 -110
  66. quantalogic-0.61.2.dist-info/entry_points.txt +0 -6
  67. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/LICENSE +0 -0
@@ -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,10 @@
1
+ # {{ name }}
2
+
3
+ A custom toolbox for Quantalogic.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ poetry install
9
+ ```
10
+
@@ -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,6 @@
1
+ from quantalogic.tools import create_tool
2
+
3
+ @create_tool
4
+ async def echo_tool(message: str) -> str:
5
+ """An example tool that echoes the input."""
6
+ return f"Echo: {message}"
@@ -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)