quantalogic 0.60.0__py3-none-any.whl → 0.61.0__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_config.py +5 -5
- quantalogic/agent_factory.py +2 -2
- quantalogic/codeact/__init__.py +0 -0
- quantalogic/codeact/agent.py +499 -0
- quantalogic/codeact/cli.py +232 -0
- quantalogic/codeact/constants.py +9 -0
- quantalogic/codeact/events.py +78 -0
- quantalogic/codeact/llm_util.py +76 -0
- quantalogic/codeact/prompts/error_format.j2 +11 -0
- quantalogic/codeact/prompts/generate_action.j2 +26 -0
- quantalogic/codeact/prompts/generate_program.j2 +39 -0
- quantalogic/codeact/prompts/response_format.j2 +11 -0
- quantalogic/codeact/tools_manager.py +135 -0
- quantalogic/codeact/utils.py +135 -0
- quantalogic/coding_agent.py +2 -2
- quantalogic/python_interpreter/__init__.py +23 -0
- quantalogic/python_interpreter/assignment_visitors.py +63 -0
- quantalogic/python_interpreter/base_visitors.py +20 -0
- quantalogic/python_interpreter/class_visitors.py +22 -0
- quantalogic/python_interpreter/comprehension_visitors.py +172 -0
- quantalogic/python_interpreter/context_visitors.py +59 -0
- quantalogic/python_interpreter/control_flow_visitors.py +88 -0
- quantalogic/python_interpreter/exception_visitors.py +109 -0
- quantalogic/python_interpreter/exceptions.py +39 -0
- quantalogic/python_interpreter/execution.py +202 -0
- quantalogic/python_interpreter/function_utils.py +386 -0
- quantalogic/python_interpreter/function_visitors.py +209 -0
- quantalogic/python_interpreter/import_visitors.py +28 -0
- quantalogic/python_interpreter/interpreter_core.py +358 -0
- quantalogic/python_interpreter/literal_visitors.py +74 -0
- quantalogic/python_interpreter/misc_visitors.py +148 -0
- quantalogic/python_interpreter/operator_visitors.py +108 -0
- quantalogic/python_interpreter/scope.py +10 -0
- quantalogic/python_interpreter/visit_handlers.py +110 -0
- quantalogic/tools/__init__.py +5 -4
- quantalogic/tools/action_gen.py +366 -0
- quantalogic/tools/python_tool.py +13 -0
- quantalogic/tools/{search_definition_names.py → search_definition_names_tool.py} +2 -2
- quantalogic/tools/tool.py +116 -22
- quantalogic/utils/__init__.py +0 -1
- quantalogic/utils/test_python_interpreter.py +119 -0
- {quantalogic-0.60.0.dist-info → quantalogic-0.61.0.dist-info}/METADATA +7 -2
- {quantalogic-0.60.0.dist-info → quantalogic-0.61.0.dist-info}/RECORD +46 -14
- quantalogic/utils/python_interpreter.py +0 -905
- {quantalogic-0.60.0.dist-info → quantalogic-0.61.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.60.0.dist-info → quantalogic-0.61.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.60.0.dist-info → quantalogic-0.61.0.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)
|