quantalogic 0.60.0__py3-none-any.whl → 0.61.1__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 (48) hide show
  1. quantalogic/agent_config.py +5 -5
  2. quantalogic/agent_factory.py +2 -2
  3. quantalogic/codeact/__init__.py +0 -0
  4. quantalogic/codeact/agent.py +499 -0
  5. quantalogic/codeact/cli.py +232 -0
  6. quantalogic/codeact/constants.py +9 -0
  7. quantalogic/codeact/events.py +78 -0
  8. quantalogic/codeact/llm_util.py +76 -0
  9. quantalogic/codeact/prompts/error_format.j2 +11 -0
  10. quantalogic/codeact/prompts/generate_action.j2 +26 -0
  11. quantalogic/codeact/prompts/generate_program.j2 +39 -0
  12. quantalogic/codeact/prompts/response_format.j2 +11 -0
  13. quantalogic/codeact/tools_manager.py +135 -0
  14. quantalogic/codeact/utils.py +135 -0
  15. quantalogic/coding_agent.py +2 -2
  16. quantalogic/python_interpreter/__init__.py +23 -0
  17. quantalogic/python_interpreter/assignment_visitors.py +63 -0
  18. quantalogic/python_interpreter/base_visitors.py +20 -0
  19. quantalogic/python_interpreter/class_visitors.py +22 -0
  20. quantalogic/python_interpreter/comprehension_visitors.py +172 -0
  21. quantalogic/python_interpreter/context_visitors.py +59 -0
  22. quantalogic/python_interpreter/control_flow_visitors.py +88 -0
  23. quantalogic/python_interpreter/exception_visitors.py +109 -0
  24. quantalogic/python_interpreter/exceptions.py +39 -0
  25. quantalogic/python_interpreter/execution.py +202 -0
  26. quantalogic/python_interpreter/function_utils.py +386 -0
  27. quantalogic/python_interpreter/function_visitors.py +209 -0
  28. quantalogic/python_interpreter/import_visitors.py +28 -0
  29. quantalogic/python_interpreter/interpreter_core.py +358 -0
  30. quantalogic/python_interpreter/literal_visitors.py +74 -0
  31. quantalogic/python_interpreter/misc_visitors.py +148 -0
  32. quantalogic/python_interpreter/operator_visitors.py +108 -0
  33. quantalogic/python_interpreter/scope.py +10 -0
  34. quantalogic/python_interpreter/visit_handlers.py +110 -0
  35. quantalogic/tools/__init__.py +5 -4
  36. quantalogic/tools/action_gen.py +366 -0
  37. quantalogic/tools/python_tool.py +13 -0
  38. quantalogic/tools/{search_definition_names.py → search_definition_names_tool.py} +2 -2
  39. quantalogic/tools/tool.py +116 -22
  40. quantalogic/tools/utils/generate_database_report.py +2 -2
  41. quantalogic/utils/__init__.py +0 -1
  42. quantalogic/utils/test_python_interpreter.py +119 -0
  43. {quantalogic-0.60.0.dist-info → quantalogic-0.61.1.dist-info}/METADATA +8 -2
  44. {quantalogic-0.60.0.dist-info → quantalogic-0.61.1.dist-info}/RECORD +47 -15
  45. quantalogic/utils/python_interpreter.py +0 -905
  46. {quantalogic-0.60.0.dist-info → quantalogic-0.61.1.dist-info}/LICENSE +0 -0
  47. {quantalogic-0.60.0.dist-info → quantalogic-0.61.1.dist-info}/WHEEL +0 -0
  48. {quantalogic-0.60.0.dist-info → quantalogic-0.61.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,109 @@
1
+ import ast
2
+ from typing import Any, Optional, Tuple
3
+
4
+ from .exceptions import BaseExceptionGroup, ReturnException, WrappedException
5
+ from .interpreter_core import ASTInterpreter
6
+
7
+ async def visit_Try(self: ASTInterpreter, node: ast.Try, wrap_exceptions: bool = True) -> Any:
8
+ result: Any = None
9
+ try:
10
+ for stmt in node.body:
11
+ result = await self.visit(stmt, wrap_exceptions=False)
12
+ except ReturnException as ret:
13
+ raise ret
14
+ except Exception as e:
15
+ original_e = e.original_exception if isinstance(e, WrappedException) else e
16
+ for handler in node.handlers:
17
+ exc_type = await self._resolve_exception_type(handler.type)
18
+ if exc_type and isinstance(original_e, exc_type):
19
+ if handler.name:
20
+ self.set_variable(handler.name, original_e)
21
+ handler_result = None
22
+ try:
23
+ for stmt in handler.body:
24
+ handler_result = await self.visit(stmt, wrap_exceptions=True)
25
+ except ReturnException as ret:
26
+ raise ret
27
+ if handler_result is not None:
28
+ result = handler_result
29
+ break
30
+ else:
31
+ raise
32
+ else:
33
+ for stmt in node.orelse:
34
+ result = await self.visit(stmt, wrap_exceptions=True)
35
+ finally:
36
+ for stmt in node.finalbody:
37
+ await self.visit(stmt, wrap_exceptions=True)
38
+ return result
39
+
40
+ async def visit_TryStar(self: ASTInterpreter, node: ast.TryStar, wrap_exceptions: bool = True) -> Any:
41
+ result: Any = None
42
+ exc_info: Optional[Tuple] = None
43
+
44
+ try:
45
+ for stmt in node.body:
46
+ result = await self.visit(stmt, wrap_exceptions=False)
47
+ except BaseException as e:
48
+ exc_info = (type(e), e, e.__traceback__)
49
+ handled = False
50
+ if isinstance(e, BaseExceptionGroup):
51
+ remaining_exceptions = []
52
+ for handler in node.handlers:
53
+ if handler.type is None:
54
+ exc_type = BaseException
55
+ elif isinstance(handler.type, ast.Name):
56
+ exc_type = self.get_variable(handler.type.id)
57
+ else:
58
+ exc_type = await self.visit(handler.type, wrap_exceptions=True)
59
+ matching_exceptions = [ex for ex in e.exceptions if isinstance(ex, exc_type)]
60
+ if matching_exceptions:
61
+ if handler.name:
62
+ self.set_variable(handler.name, BaseExceptionGroup("", matching_exceptions))
63
+ for stmt in handler.body:
64
+ result = await self.visit(stmt, wrap_exceptions=True)
65
+ handled = True
66
+ remaining_exceptions.extend([ex for ex in e.exceptions if not isinstance(ex, exc_type)])
67
+ if remaining_exceptions and not handled:
68
+ raise BaseExceptionGroup("Uncaught exceptions", remaining_exceptions)
69
+ if handled:
70
+ exc_info = None
71
+ else:
72
+ for handler in node.handlers:
73
+ if handler.type is None:
74
+ exc_type = BaseException
75
+ elif isinstance(handler.type, ast.Name):
76
+ exc_type = self.get_variable(handler.type.id)
77
+ else:
78
+ exc_type = await self.visit(handler.type, wrap_exceptions=True)
79
+ if exc_info and issubclass(exc_info[0], exc_type):
80
+ if handler.name:
81
+ self.set_variable(handler.name, exc_info[1])
82
+ for stmt in handler.body:
83
+ result = await self.visit(stmt, wrap_exceptions=True)
84
+ exc_info = None
85
+ handled = True
86
+ break
87
+ if exc_info and not handled:
88
+ raise exc_info[1]
89
+ else:
90
+ for stmt in node.orelse:
91
+ result = await self.visit(stmt, wrap_exceptions=True)
92
+ finally:
93
+ for stmt in node.finalbody:
94
+ try:
95
+ await self.visit(stmt, wrap_exceptions=True)
96
+ except ReturnException:
97
+ raise
98
+ except Exception:
99
+ if exc_info:
100
+ raise exc_info[1]
101
+ raise
102
+
103
+ return result
104
+
105
+ async def visit_Raise(self: ASTInterpreter, node: ast.Raise, wrap_exceptions: bool = True) -> None:
106
+ exc = await self.visit(node.exc, wrap_exceptions=wrap_exceptions) if node.exc else None
107
+ if exc:
108
+ raise exc
109
+ raise Exception("Raise with no exception specified")
@@ -0,0 +1,39 @@
1
+ import ast
2
+ from typing import Any, List
3
+
4
+ class ReturnException(Exception):
5
+ def __init__(self, value: Any) -> None:
6
+ self.value: Any = value
7
+
8
+ class BreakException(Exception):
9
+ pass
10
+
11
+ class ContinueException(Exception):
12
+ pass
13
+
14
+ class BaseExceptionGroup(Exception):
15
+ def __init__(self, message: str, exceptions: List[Exception]):
16
+ super().__init__(message)
17
+ self.exceptions = exceptions
18
+ self.message = message
19
+
20
+ def __str__(self):
21
+ return f"{self.message}: {', '.join(str(e) for e in self.exceptions)}"
22
+
23
+ class WrappedException(Exception):
24
+ def __init__(self, message: str, original_exception: Exception, lineno: int, col: int, context_line: str):
25
+ super().__init__(message)
26
+ self.original_exception: Exception = original_exception
27
+ self.lineno: int = lineno
28
+ self.col: int = col
29
+ self.context_line: str = context_line
30
+ self.message = original_exception.args[0] if original_exception.args else str(original_exception)
31
+
32
+ def __str__(self):
33
+ return f"Error line {self.lineno}, col {self.col}:\n{self.context_line}\nDescription: {self.message}"
34
+
35
+ def has_await(node: ast.AST) -> bool:
36
+ for child in ast.walk(node):
37
+ if isinstance(child, ast.Await):
38
+ return True
39
+ return False
@@ -0,0 +1,202 @@
1
+ import ast
2
+ import asyncio
3
+ import textwrap
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+ from .interpreter_core import ASTInterpreter
9
+ from .function_utils import Function, AsyncFunction
10
+ from .exceptions import WrappedException
11
+
12
+ @dataclass
13
+ class AsyncExecutionResult:
14
+ result: Any
15
+ error: Optional[str]
16
+ execution_time: float
17
+ local_variables: Optional[Dict[str, Any]] = None # Added to store local variables
18
+
19
+ def optimize_ast(tree: ast.AST) -> ast.AST:
20
+ """Perform constant folding and basic optimizations on the AST."""
21
+ class ConstantFolder(ast.NodeTransformer):
22
+ def visit_BinOp(self, node):
23
+ self.generic_visit(node)
24
+ if isinstance(node.left, ast.Constant) and isinstance(node.right, ast.Constant):
25
+ left, right = node.left.value, node.right.value
26
+ if isinstance(left, (int, float)) and isinstance(right, (int, float)):
27
+ if isinstance(node.op, ast.Add):
28
+ return ast.Constant(value=left + right)
29
+ elif isinstance(node.op, ast.Sub):
30
+ return ast.Constant(value=left - right)
31
+ elif isinstance(node.op, ast.Mult):
32
+ return ast.Constant(value=left * right)
33
+ elif isinstance(node.op, ast.Div) and right != 0:
34
+ return ast.Constant(value=left / right)
35
+ return node
36
+
37
+ def visit_If(self, node):
38
+ self.generic_visit(node)
39
+ if isinstance(node.test, ast.Constant):
40
+ if node.test.value:
41
+ return ast.Module(body=node.body, type_ignores=[])
42
+ else:
43
+ return ast.Module(body=node.orelse, type_ignores=[])
44
+ return node
45
+
46
+ return ConstantFolder().visit(tree)
47
+
48
+ class ControlledEventLoop:
49
+ """Encapsulated event loop management to prevent unauthorized access"""
50
+ def __init__(self):
51
+ self._loop = None
52
+ self._created = False
53
+ self._lock = asyncio.Lock()
54
+
55
+ async def get_loop(self) -> asyncio.AbstractEventLoop:
56
+ async with self._lock:
57
+ if self._loop is None:
58
+ self._loop = asyncio.new_event_loop()
59
+ self._created = True
60
+ return self._loop
61
+
62
+ async def cleanup(self):
63
+ async with self._lock:
64
+ if self._created and self._loop and not self._loop.is_closed():
65
+ for task in asyncio.all_tasks(self._loop):
66
+ task.cancel()
67
+ await asyncio.gather(*asyncio.all_tasks(self._loop), return_exceptions=True)
68
+ self._loop.close()
69
+ self._loop = None
70
+ self._created = False
71
+
72
+ async def run_task(self, coro, timeout: float) -> Any:
73
+ return await asyncio.wait_for(coro, timeout=timeout)
74
+
75
+ async def execute_async(
76
+ code: str,
77
+ entry_point: Optional[str] = None,
78
+ args: Optional[Tuple] = None,
79
+ kwargs: Optional[Dict[str, Any]] = None,
80
+ timeout: float = 30,
81
+ allowed_modules: List[str] = ['asyncio'],
82
+ namespace: Optional[Dict[str, Any]] = None,
83
+ max_memory_mb: int = 1024
84
+ ) -> AsyncExecutionResult:
85
+ start_time = time.time()
86
+ event_loop_manager = ControlledEventLoop()
87
+
88
+ try:
89
+ ast_tree = optimize_ast(ast.parse(textwrap.dedent(code)))
90
+ loop = await event_loop_manager.get_loop()
91
+
92
+ # Remove direct asyncio access from builtins
93
+ safe_namespace = namespace.copy() if namespace else {}
94
+ safe_namespace.pop('asyncio', None) # Prevent direct asyncio access
95
+
96
+ interpreter = ASTInterpreter(
97
+ allowed_modules=allowed_modules,
98
+ restrict_os=True,
99
+ namespace=safe_namespace,
100
+ max_memory_mb=max_memory_mb,
101
+ source=code # Pass source code for better error context
102
+ )
103
+ interpreter.loop = loop
104
+
105
+ async def run_execution():
106
+ return await interpreter.execute_async(ast_tree)
107
+
108
+ await event_loop_manager.run_task(run_execution(), timeout=timeout)
109
+
110
+ if entry_point:
111
+ func = interpreter.env_stack[0].get(entry_point)
112
+ if not func:
113
+ raise NameError(f"Function '{entry_point}' not found in the code")
114
+ args = args or ()
115
+ kwargs = kwargs or {}
116
+ if isinstance(func, AsyncFunction) or asyncio.iscoroutinefunction(func):
117
+ # Expect a tuple (result, local_vars) from AsyncFunction
118
+ execution_result = await event_loop_manager.run_task(func(*args, **kwargs), timeout=timeout)
119
+ if isinstance(execution_result, tuple) and len(execution_result) == 2:
120
+ result, local_vars = execution_result
121
+ else:
122
+ result, local_vars = execution_result, {}
123
+ elif isinstance(func, Function):
124
+ result = await func(*args, **kwargs)
125
+ local_vars = {} # Non-async functions don't yet support local var return
126
+ else:
127
+ result = func(*args, **kwargs)
128
+ if asyncio.iscoroutine(result):
129
+ result = await event_loop_manager.run_task(result, timeout=timeout)
130
+ local_vars = {}
131
+ if asyncio.iscoroutine(result):
132
+ result = await event_loop_manager.run_task(result, timeout=timeout)
133
+ else:
134
+ result = await interpreter.execute_async(ast_tree)
135
+ local_vars = {k: v for k, v in interpreter.env_stack[-1].items() if not k.startswith('__')}
136
+
137
+ # Filter out internal variables if not already filtered
138
+ filtered_local_vars = local_vars if local_vars else {}
139
+ if not entry_point: # Apply filtering only for module-level execution
140
+ filtered_local_vars = {k: v for k, v in local_vars.items() if not k.startswith('__')}
141
+
142
+ return AsyncExecutionResult(
143
+ result=result,
144
+ error=None,
145
+ execution_time=time.time() - start_time,
146
+ local_variables=filtered_local_vars
147
+ )
148
+ except asyncio.TimeoutError as e:
149
+ return AsyncExecutionResult(
150
+ result=None,
151
+ error=f'TimeoutError: Execution exceeded {timeout} seconds',
152
+ execution_time=time.time() - start_time
153
+ )
154
+ except WrappedException as e:
155
+ return AsyncExecutionResult(
156
+ result=None,
157
+ error=str(e),
158
+ execution_time=time.time() - start_time
159
+ )
160
+ except Exception as e:
161
+ error_type = type(getattr(e, 'original_exception', e)).__name__
162
+ error_msg = f'{error_type}: {str(e)}'
163
+ if hasattr(e, 'lineno') and hasattr(e, 'col_offset'):
164
+ error_msg += f' at line {e.lineno}, col {e.col_offset}'
165
+ return AsyncExecutionResult(
166
+ result=None,
167
+ error=error_msg,
168
+ execution_time=time.time() - start_time
169
+ )
170
+ finally:
171
+ await event_loop_manager.cleanup()
172
+
173
+ def interpret_ast(ast_tree: ast.AST, allowed_modules: List[str], source: str = "", restrict_os: bool = False, namespace: Optional[Dict[str, Any]] = None) -> Any:
174
+ ast_tree = optimize_ast(ast_tree)
175
+ event_loop_manager = ControlledEventLoop()
176
+
177
+ # Remove asyncio from namespace
178
+ safe_namespace = namespace.copy() if namespace else {}
179
+ safe_namespace.pop('asyncio', None)
180
+
181
+ interpreter = ASTInterpreter(allowed_modules=allowed_modules, source=source, restrict_os=restrict_os, namespace=safe_namespace)
182
+
183
+ async def run_interpreter():
184
+ loop = await event_loop_manager.get_loop()
185
+ interpreter.loop = loop
186
+ result = await interpreter.visit(ast_tree, wrap_exceptions=True)
187
+ return result
188
+
189
+ try:
190
+ loop = asyncio.new_event_loop()
191
+ asyncio.set_event_loop(loop)
192
+ interpreter.loop = loop
193
+ result = loop.run_until_complete(run_interpreter())
194
+ return result
195
+ finally:
196
+ if not loop.is_closed():
197
+ loop.close()
198
+
199
+ def interpret_code(source_code: str, allowed_modules: List[str], restrict_os: bool = False, namespace: Optional[Dict[str, Any]] = None) -> Any:
200
+ dedented_source = textwrap.dedent(source_code).strip()
201
+ tree: ast.AST = ast.parse(dedented_source)
202
+ return interpret_ast(tree, allowed_modules, source=dedented_source, restrict_os=restrict_os, namespace=namespace)