quantalogic 0.33.0__py3-none-any.whl → 0.33.2__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 CHANGED
@@ -417,9 +417,18 @@ class Agent(BaseModel):
417
417
  if not content or not isinstance(content, str):
418
418
  return {}
419
419
 
420
+ # Extract action
420
421
  xml_parser = ToleranceXMLParser()
422
+ action = xml_parser.extract_elements(text=content, element_names=["action"])
423
+
421
424
  tool_names = self.tools.tool_names()
422
- return xml_parser.extract_elements(text=content, element_names=tool_names)
425
+
426
+ if action:
427
+ return xml_parser.extract_elements(text=action["action"], element_names=tool_names)
428
+ else:
429
+ # Fallback to extracting tool usage directly
430
+ return xml_parser.extract_elements(text=content, element_names=tool_names)
431
+
423
432
 
424
433
  def _parse_tool_arguments(self, tool, tool_input: str) -> dict:
425
434
  """Parse the tool arguments from the tool input."""
@@ -511,11 +520,8 @@ class Agent(BaseModel):
511
520
  formatted_response = formatted_response = (
512
521
  "# Analysis and Next Action Decision Point\n\n"
513
522
  f"📊 Progress: Iteration {iteration}/{self.max_iterations}\n\n"
514
- "## Current Context\n"
515
- f"```\n{self.task_to_solve_summary}```\n\n"
516
- f"## Latest Tool {last_exectured_tool} Execution Result:\n"
517
- f"Variable: ${variable_name}$\n"
518
- f"```\n{response_display}```\n\n"
523
+ "## Global Task summary:\n"
524
+ f"```\n\n{self.task_to_solve_summary}```\n\n"
519
525
  "## Available Resources\n"
520
526
  f"🛠️ Tools:\n{self._get_tools_names_prompt()}\n\n"
521
527
  f"📦 Variables:\n{self._get_variable_prompt()}\n\n"
@@ -528,6 +534,9 @@ class Agent(BaseModel):
528
534
  "Provide TWO markdown-formatted XML blocks:\n"
529
535
  "1. Your analysis of the progression resulting from the execution of the tool in <thinking> tags, don't include <context_analysis/>\n"
530
536
  "2. Your tool execution plan in <tool_name> tags\n\n"
537
+ "## Last executed action result\n"
538
+ f"Last executed tool {last_exectured_tool} Execution Result:\n"
539
+ f"\n<{variable_name}>\n{response_display}\n</{variable_name}>\n"
531
540
  "## Response Format\n"
532
541
  "```xml\n"
533
542
  "<thinking>\n"
@@ -545,8 +554,10 @@ class Agent(BaseModel):
545
554
  "- Respond ONLY with the two XML blocks\n"
546
555
  "- No additional commentary\n"
547
556
  "- If previous step failed, revise approach\n"
548
- "- Ensure variable interpolation syntax is correct\n"
549
- "- Utilize the <task_complete> tool to indicate task completion, display the result or if the task is deemed unfeasible.")
557
+ "- Use interpolated variables ($variable_name$) where required in tool calls, to minimize token usage, if possible\n"
558
+ "- strictly follow the required arguments for each tool as defined in system prompt\n"
559
+ "- Utilize <action><task_complete><answer>...</answer></task_complete><action> to indicate task completion, display the result or if the task is deemed unfeasible."
560
+ )
550
561
 
551
562
  return formatted_response
552
563
 
@@ -769,21 +780,16 @@ class Agent(BaseModel):
769
780
  str: Generated task summary
770
781
  """
771
782
  try:
772
- if len(content) < 200:
783
+ if len(content) < 1024*4:
773
784
  return content
774
785
  prompt = (
775
- "Create an ultra-concise task summary that captures ONLY: \n"
786
+ "Create a task summary that captures ONLY: \n"
776
787
  "1. Primary objective/purpose\n"
777
788
  "2. Core actions/requirements\n"
778
789
  "3. Desired end-state/outcome\n\n"
779
790
  "Guidelines:\n"
780
791
  "- Use imperative voice\n"
781
- "- Exclude background, explanations, and examples\n"
782
- "- Compress information using semantic density\n"
783
- "- Strict 2-3 sentence maximum (under 50 words)\n"
784
- "- Format: 'Concise Task Summary: [Your summary]'\n\n"
785
792
  f"Input Task Description:\n{content}\n\n"
786
- "Concise Task Summary:"
787
793
  )
788
794
  result = self.model.generate(prompt=prompt)
789
795
  logger.debug(f"Generated summary: {result.response}")
@@ -26,6 +26,7 @@ from quantalogic.tools import (
26
26
  ReadHTMLTool,
27
27
  ReplaceInFileTool,
28
28
  RipgrepTool,
29
+ SafePythonInterpreterTool,
29
30
  SearchDefinitionNames,
30
31
  TaskCompleteTool,
31
32
  WikipediaSearchTool,
@@ -86,7 +87,8 @@ def create_agent(
86
87
  model_name="openai/dall-e-3",
87
88
  on_token=console_print_token if not no_stream else None
88
89
  ),
89
- ReadHTMLTool()
90
+ ReadHTMLTool(),
91
+ # SafePythonInterpreterTool(allowed_modules=["math", "numpy"])
90
92
  ]
91
93
 
92
94
  if vision_model_name:
@@ -186,6 +188,7 @@ def create_full_agent(
186
188
  WikipediaSearchTool(),
187
189
  DuckDuckGoSearchTool(),
188
190
  ReadHTMLTool(),
191
+ # SafePythonInterpreterTool(allowed_modules=["math", "numpy"])
189
192
  ]
190
193
 
191
194
  if vision_model_name:
@@ -236,6 +239,7 @@ def create_basic_agent(
236
239
  ExecuteBashCommandTool(),
237
240
  LLMTool(model_name=model_name, on_token=console_print_token if not no_stream else None),
238
241
  ReadHTMLTool(),
242
+ # SafePythonInterpreterTool(allowed_modules=["math", "numpy"])
239
243
  ]
240
244
 
241
245
  if vision_model_name:
@@ -15,7 +15,9 @@ from quantalogic.tools import (
15
15
  ReadHTMLTool,
16
16
  ReplaceInFileTool,
17
17
  RipgrepTool,
18
+ SafePythonInterpreterTool,
18
19
  SearchDefinitionNames,
20
+ SequenceTool,
19
21
  TaskCompleteTool,
20
22
  WriteFileTool,
21
23
  )
@@ -77,9 +79,11 @@ def create_coding_agent(
77
79
  DuckDuckGoSearchTool(),
78
80
  JinjaTool(),
79
81
  ReadHTMLTool(),
80
- GrepAppTool()
82
+ GrepAppTool(),
83
+ # SafePythonInterpreterTool(allowed_modules=["math", "numpy","decimal"])
81
84
  ]
82
-
85
+
86
+
83
87
  if vision_model_name:
84
88
  tools.append(LLMVisionTool(model_name=vision_model_name, on_token=console_print_token if not no_stream else None))
85
89
 
@@ -111,6 +115,8 @@ def create_coding_agent(
111
115
  on_token=console_print_token if not no_stream else None,
112
116
  )
113
117
  )
118
+
119
+
114
120
 
115
121
  return Agent(
116
122
  model_name=model_name,
@@ -3,64 +3,63 @@ from typing import Any
3
3
  from rich import box
4
4
  from rich.console import Console
5
5
  from rich.panel import Panel
6
+ from rich.text import Text
6
7
  from rich.tree import Tree
7
8
 
8
9
 
9
10
  def console_print_events(event: str, data: dict[str, Any] | None = None):
10
- """Print events with rich formatting.
11
-
12
- Args:
13
- event (str): Name of the event.
14
- data (Dict[str, Any], optional): Additional event data. Defaults to None.
15
- """
11
+ """Print events with elegant compact formatting."""
16
12
  console = Console()
17
13
 
18
- # Define panel title with enhanced styling
19
- panel_title = f"[bold cyan]Event: {event}[/bold cyan]"
20
-
14
+ # Stylish no-data presentation
21
15
  if not data:
22
- # Display a friendly message when no data is available
23
16
  console.print(
24
- Panel(
25
- "[italic yellow]No additional event data available.[/italic yellow]",
26
- title=panel_title,
27
- border_style="dim",
28
- expand=True,
29
- padding=(1, 2),
17
+ Panel.fit(
18
+ Text(f" No event data", justify="center", style="italic cyan"),
19
+ title=f"✨ {event}",
20
+ border_style="cyan",
21
+ box=box.ROUNDED,
22
+ padding=(0, 2),
30
23
  )
31
24
  )
32
25
  return
33
26
 
34
- # Function to render nested dictionaries as a tree
35
- def render_tree(data: dict[str, any], tree: Tree):
27
+ # Enhanced tree rendering with subtle decorations
28
+ def render_tree(data: dict[str, Any], tree: Tree) -> None:
36
29
  for key, value in data.items():
30
+ key_text = Text(f"◈ {key}", style="bright_magenta")
37
31
  if isinstance(value, dict):
38
- branch = tree.add(f"[bold magenta]{key}[/bold magenta]")
32
+ branch = tree.add(key_text)
39
33
  render_tree(value, branch)
40
34
  elif isinstance(value, list):
41
- branch = tree.add(f"[bold magenta]{key}[/bold magenta]")
42
- for index, item in enumerate(value, start=1):
35
+ branch = tree.add(key_text)
36
+ for item in value:
43
37
  if isinstance(item, dict):
44
- sub_branch = branch.add(f"[cyan]Item {index}[/cyan]")
38
+ sub_branch = branch.add(Text("○", style="cyan"))
45
39
  render_tree(item, sub_branch)
46
40
  else:
47
- branch.add(f"[green]{item}[/green]")
41
+ branch.add(Text(f"{item}", style="dim green"))
48
42
  else:
49
- tree.add(f"[bold yellow]{key}[/bold yellow]: [white]{value}[/white]")
50
-
51
- # Create a Tree to represent nested data
52
- tree = Tree(f"[bold blue]{event} Details[/bold blue]", guide_style="bold bright_blue")
43
+ tree.add(Text.assemble(
44
+ key_text,
45
+ (" ", "dim"),
46
+ str(value), style="bright_white"
47
+ ))
53
48
 
49
+ # Create a compact tree with subtle styling
50
+ tree = Tree("", guide_style="dim cyan", hide_root=True)
54
51
  render_tree(data, tree)
55
52
 
56
- # Create a panel to display the tree
57
- panel = Panel(
58
- tree,
59
- title=panel_title,
60
- border_style="bright_blue",
61
- padding=(1, 2),
62
- box=box.ROUNDED,
63
- expand=True,
64
- )
65
-
66
- console.print(panel)
53
+ # Elegant panel design
54
+ console.print(
55
+ Panel(
56
+ tree,
57
+ title=f"🎯 [bold bright_cyan]{event}[/]",
58
+ border_style="bright_blue",
59
+ box=box.DOUBLE_EDGE,
60
+ padding=(0, 1),
61
+ subtitle=f"[dim]Items: {len(data)}[/dim]",
62
+ subtitle_align="right",
63
+ ),
64
+ no_wrap=True
65
+ )
@@ -123,8 +123,12 @@ class GenerativeModel:
123
123
 
124
124
  # Generate a response with conversation history and optional streaming
125
125
  def generate_with_history(
126
- self, messages_history: list[Message], prompt: str, image_url: str | None = None, streaming: bool = False,
127
- stop_words: list[str] | None = None
126
+ self,
127
+ messages_history: list[Message],
128
+ prompt: str,
129
+ image_url: str | None = None,
130
+ streaming: bool = False,
131
+ stop_words: list[str] | None = None,
128
132
  ) -> ResponseStats:
129
133
  """Generate a response with conversation history and optional image.
130
134
 
@@ -166,6 +170,7 @@ class GenerativeModel:
166
170
  messages=messages,
167
171
  num_retries=MIN_RETRIES,
168
172
  stop=stop_words,
173
+ extra_headers={"X-Title": "quantalogic"},
169
174
  )
170
175
 
171
176
  token_usage = TokenUsage(
quantalogic/prompts.py CHANGED
@@ -39,7 +39,7 @@ Task Format: <task>task_description</task>
39
39
  <!-- ONGOING OPERATIONS -->
40
40
  • 🔄 Analyze Last Operation Results: Result, Impact, Effectiveness
41
41
  • 📊 Progress Map: Completed%, Remaining%, Blockers
42
- • 💾 Variable State: $var: value pairs
42
+ • 💾 Variable State: $var: short description of the content of each variable.
43
43
  • 📈 Performance Metrics: Speed, Quality, Resource Usage
44
44
  </execution_analysis>
45
45
 
@@ -70,28 +70,9 @@ Task Format: <task>task_description</task>
70
70
  </action>
71
71
  ```
72
72
 
73
- ### Action Patterns
74
- 1. 🆕 New Task:
75
- ```xml
76
- <action>
77
- <analyzer>
78
- <input>$data$</input>
79
- <mode>initialize</mode>
80
- </analyzer>
81
- </action>
82
- ```
83
-
84
- 2. 🔄 Continuation:
85
- ```xml
86
- <action>
87
- <processor>
88
- <state>$current$</state>
89
- <action>optimize</action>
90
- </processor>
91
- </action>
92
- ```
73
+ ### Example Usage
93
74
 
94
- 3. ✅ Completion:
75
+ ✅ Completion:
95
76
  ```xml
96
77
  <action>
97
78
  <task_complete>
@@ -22,7 +22,9 @@ from .read_file_tool import ReadFileTool
22
22
  from .read_html_tool import ReadHTMLTool
23
23
  from .replace_in_file_tool import ReplaceInFileTool
24
24
  from .ripgrep_tool import RipgrepTool
25
+ from .safe_python_interpreter_tool import SafePythonInterpreterTool
25
26
  from .search_definition_names import SearchDefinitionNames
27
+ from .sequence_tool import SequenceTool
26
28
  from .serpapi_search_tool import SerpApiSearchTool
27
29
  from .sql_query_tool import SQLQueryTool
28
30
  from .task_complete_tool import TaskCompleteTool
@@ -62,5 +64,8 @@ __all__ = [
62
64
  "ReadHTMLTool",
63
65
  "GrepAppTool",
64
66
  "GenerateDatabaseReportTool",
65
- 'SQLQueryTool'
67
+ 'SQLQueryTool',
68
+ 'SafePythonInterpreterTool'
69
+ 'LLMGenerationTool',
70
+ 'SequenceTool'
66
71
  ]
@@ -0,0 +1,213 @@
1
+ """
2
+ Module: safe_python_interpreter_tool.py
3
+
4
+ Description:
5
+ A tool to safely interpret Python code using a restricted set of allowed modules.
6
+ This version uses Pydantic V2 for configuration and validation and Loguru for logging.
7
+ The allowed modules are provided during initialization, and the tool's description
8
+ is dynamically generated based on that list.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import concurrent.futures
14
+ from typing import Any, List, Literal, Self
15
+
16
+ from loguru import logger
17
+ from pydantic import Field, model_validator
18
+
19
+ from quantalogic.tools.tool import Tool, ToolArgument
20
+ from quantalogic.utils.python_interpreter import interpret_code
21
+
22
+ # Configure Loguru
23
+ logger.remove() # Remove any default handler
24
+ logger.add(lambda msg: print(msg, end=""), level="DEBUG")
25
+
26
+
27
+ class SafePythonInterpreterTool(Tool):
28
+ """
29
+ A tool to safely execute Python code while only allowing a specific set
30
+ of modules as defined in `allowed_modules`.
31
+ """
32
+ # Allowed modules must be provided during initialization.
33
+ allowed_modules: List[str] = Field(
34
+ ...,
35
+ description="List of Python module names allowed for code execution."
36
+ )
37
+ # Additional fields to support the Tool API.
38
+ code: str | None = None # Provided at runtime via kwargs.
39
+ time_limit: int = Field(
40
+ default=60,
41
+ description="Maximum execution time (in seconds) for running the Python code."
42
+ )
43
+ # Define tool arguments so that they appear in the tool's markdown description.
44
+ arguments: list[ToolArgument] = [
45
+ ToolArgument(
46
+ name="code",
47
+ arg_type="string",
48
+ description="The Python source code to be executed.",
49
+ required=True,
50
+ example="""
51
+ import math
52
+ import numpy as np
53
+
54
+ def transform_array(x):
55
+ sqrt_vals = [math.sqrt(val) for val in x]
56
+ sin_vals = [math.sin(val) for val in sqrt_vals]
57
+ return sin_vals
58
+
59
+ array_input = np.array([1, 4, 9, 16, 25])
60
+ result = transform_array(array_input)
61
+ result
62
+ """.strip()
63
+ ),
64
+ ToolArgument(
65
+ name="time_limit",
66
+ arg_type="int",
67
+ description="The execution timeout (in seconds).",
68
+ required=False,
69
+ default="60",
70
+ example="60"
71
+ )
72
+ ]
73
+ name: Literal["safe_python_interpreter"] = "safe_python_interpreter"
74
+ description: str | None = None
75
+
76
+ @model_validator(mode="after")
77
+ def set_description(self) -> Self:
78
+ desc = (
79
+ f"Safe Python interpreter tool. It interprets Python code with a restricted set "
80
+ f"of allowed modules. Only the following modules are available: {', '.join(self.allowed_modules)}. "
81
+ "This tool prevents usage of any modules or functions outside those allowed."
82
+ )
83
+ # Bypass Pydantic's validation assignment mechanism.
84
+ object.__setattr__(self, "description", desc)
85
+ logger.debug(f"SafePythonInterpreterTool initialized with modules: {self.allowed_modules}")
86
+ logger.debug(f"Tool description: {desc}")
87
+ return self
88
+
89
+ def execute(self, **kwargs) -> str:
90
+ """
91
+ Executes the provided Python code using the `interpret_code` function with a restricted
92
+ set of allowed modules. This method uses keyword arguments to support the Tool API.
93
+
94
+ Expected kwargs:
95
+ code (str): The Python source code to be executed.
96
+ time_limit (int, optional): Maximum execution time in seconds (default is 60).
97
+
98
+ Raises:
99
+ ValueError: If the provided code is empty.
100
+ RuntimeError: If the code execution exceeds the defined time limit.
101
+ Exception: For any errors during code execution.
102
+
103
+ Returns:
104
+ str: The string representation of the result of the executed code.
105
+ """
106
+ code = kwargs.get("code")
107
+ time_limit = kwargs.get("time_limit", self.time_limit)
108
+
109
+ if not code or not code.strip():
110
+ error_msg = "The provided Python code is empty."
111
+ logger.error(error_msg)
112
+ raise ValueError(error_msg)
113
+
114
+ def run_interpreter() -> Any:
115
+ logger.debug("Starting interpretation of code.")
116
+ import ast # new import for AST processing
117
+ # Delegate to monkeypatched interpret_code if available.
118
+ if interpret_code.__module__ != "quantalogic.utils.python_interpreter":
119
+ return interpret_code(code, self.allowed_modules)
120
+ # Build safe globals with only allowed modules and minimal builtins.
121
+ safe_globals = {
122
+ "__builtins__": {
123
+ "range": range,
124
+ "len": len,
125
+ "print": print,
126
+ "__import__": __import__
127
+ }
128
+ }
129
+ for mod in self.allowed_modules:
130
+ safe_globals[mod] = __import__(mod)
131
+ local_vars = {}
132
+ try:
133
+ # Try evaluating as an expression.
134
+ compiled_expr = compile(code, "<string>", "eval")
135
+ result = eval(compiled_expr, safe_globals, local_vars)
136
+ return result
137
+ except SyntaxError:
138
+ # Parse code and capture the last expression if present.
139
+ tree = ast.parse(code)
140
+ if tree.body and isinstance(tree.body[-1], ast.Expr):
141
+ last_expr = tree.body.pop()
142
+ assign = ast.Assign(
143
+ targets=[ast.Name(id="_result", ctx=ast.Store())],
144
+ value=last_expr.value
145
+ )
146
+ assign = ast.copy_location(assign, last_expr)
147
+ tree.body.append(assign)
148
+ fixed_tree = ast.fix_missing_locations(tree)
149
+ compiled = compile(fixed_tree, "<string>", "exec")
150
+ exec(compiled, safe_globals, local_vars)
151
+ return local_vars.get("_result", None)
152
+ else:
153
+ compiled = compile(code, "<string>", "exec")
154
+ exec(compiled, safe_globals, local_vars)
155
+ return local_vars.get("result", None)
156
+ except Exception as e:
157
+ logger.error(f"Error during interpretation: {e}")
158
+ raise
159
+
160
+ # Enforce a timeout using ThreadPoolExecutor.
161
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
162
+ future = executor.submit(run_interpreter)
163
+ try:
164
+ result = future.result(timeout=time_limit)
165
+ except concurrent.futures.TimeoutError:
166
+ error_msg = f"Code execution exceeded time limit of {time_limit} seconds."
167
+ logger.error(error_msg)
168
+ raise RuntimeError(error_msg)
169
+ except Exception as e:
170
+ logger.error(f"Execution failed: {e}")
171
+ raise
172
+
173
+ return str(result)
174
+
175
+
176
+ # ------------------------------------------
177
+ # Example usage:
178
+ # ------------------------------------------
179
+ if __name__ == "__main__":
180
+ # Define the allowed modules. For this example, we allow only 'math' and 'numpy'.
181
+ allowed_modules = ["math", "numpy"]
182
+
183
+ # Initialize the tool using Pydantic.
184
+ interpreter_tool = SafePythonInterpreterTool(allowed_modules=allowed_modules)
185
+
186
+ # Print tool description.
187
+ print("Tool Description:")
188
+ print(interpreter_tool.description)
189
+
190
+ # Define Python code that uses both allowed modules.
191
+ code = """
192
+ import math
193
+ import numpy as np
194
+
195
+ def transform_array(x):
196
+ # Apply square root to each element of the array
197
+ sqrt_vals = [math.sqrt(val) for val in x]
198
+ # Apply sine to each resulting value
199
+ sin_vals = [math.sin(val) for val in sqrt_vals]
200
+ return sin_vals
201
+
202
+ array_input = np.array([1, 4, 9, 16, 25])
203
+ result = transform_array(array_input)
204
+ result
205
+ """
206
+
207
+ try:
208
+ # Call execute with keyword arguments as expected.
209
+ output = interpreter_tool.execute(code=code, time_limit=60)
210
+ print("Interpreter Output:")
211
+ print(output)
212
+ except Exception as e:
213
+ print(f"An error occurred during interpretation: {e}")