astraagent 2.25.6
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.
- package/.env.template +22 -0
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/astra/__init__.py +15 -0
- package/astra/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/__pycache__/chat.cpython-314.pyc +0 -0
- package/astra/__pycache__/cli.cpython-314.pyc +0 -0
- package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
- package/astra/__pycache__/updater.cpython-314.pyc +0 -0
- package/astra/chat.py +763 -0
- package/astra/cli.py +913 -0
- package/astra/core/__init__.py +8 -0
- package/astra/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/memory.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/reasoning.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/state.cpython-314.pyc +0 -0
- package/astra/core/agent.py +515 -0
- package/astra/core/config.py +247 -0
- package/astra/core/memory.py +782 -0
- package/astra/core/reasoning.py +423 -0
- package/astra/core/state.py +366 -0
- package/astra/core/voice.py +144 -0
- package/astra/llm/__init__.py +32 -0
- package/astra/llm/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
- package/astra/llm/providers.py +530 -0
- package/astra/planning/__init__.py +117 -0
- package/astra/prompts.py +289 -0
- package/astra/reflection/__init__.py +181 -0
- package/astra/search.py +469 -0
- package/astra/tasks.py +466 -0
- package/astra/tools/__init__.py +17 -0
- package/astra/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/advanced.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/base.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/browser.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/file.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/git.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/memory_tool.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/python.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/shell.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/web.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/windows.cpython-314.pyc +0 -0
- package/astra/tools/advanced.py +251 -0
- package/astra/tools/base.py +344 -0
- package/astra/tools/browser.py +93 -0
- package/astra/tools/file.py +476 -0
- package/astra/tools/git.py +74 -0
- package/astra/tools/memory_tool.py +89 -0
- package/astra/tools/python.py +238 -0
- package/astra/tools/shell.py +183 -0
- package/astra/tools/web.py +804 -0
- package/astra/tools/windows.py +542 -0
- package/astra/updater.py +450 -0
- package/astra/utils/__init__.py +230 -0
- package/bin/astraagent.js +73 -0
- package/bin/postinstall.js +25 -0
- package/config.json.template +52 -0
- package/main.py +16 -0
- package/package.json +51 -0
- package/pyproject.toml +72 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python Code Execution Tool for AstraAgent.
|
|
3
|
+
Safely execute Python code in an isolated environment.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import sys
|
|
8
|
+
import io
|
|
9
|
+
import traceback
|
|
10
|
+
import asyncio
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
13
|
+
|
|
14
|
+
from astra.tools.base import Tool, ToolParameter, ToolResult, ToolCategory, RiskLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PythonTool(Tool):
|
|
18
|
+
"""Tool for executing Python code."""
|
|
19
|
+
|
|
20
|
+
name = "python"
|
|
21
|
+
description = "Execute Python code and return the result. Supports data analysis, calculations, and scripting."
|
|
22
|
+
category = ToolCategory.CODE_EXECUTION
|
|
23
|
+
risk_level = RiskLevel.MEDIUM
|
|
24
|
+
|
|
25
|
+
# Dangerous imports/operations to block
|
|
26
|
+
BLOCKED_IMPORTS = [
|
|
27
|
+
'subprocess', 'os.system', 'os.popen', 'os.spawn',
|
|
28
|
+
'pty', 'pexpect', 'paramiko', 'fabric'
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
BLOCKED_BUILTINS = ['eval', 'exec', 'compile', '__import__']
|
|
32
|
+
|
|
33
|
+
def __init__(self,
|
|
34
|
+
timeout: int = 30,
|
|
35
|
+
max_memory_mb: int = 512,
|
|
36
|
+
allow_imports: List[str] = None,
|
|
37
|
+
sandbox_mode: bool = True):
|
|
38
|
+
super().__init__()
|
|
39
|
+
self.timeout = timeout
|
|
40
|
+
self.max_memory_mb = max_memory_mb
|
|
41
|
+
self.allow_imports = allow_imports
|
|
42
|
+
self.sandbox_mode = sandbox_mode
|
|
43
|
+
|
|
44
|
+
# Persistent namespace for REPL-like behavior
|
|
45
|
+
self._namespace: Dict[str, Any] = {}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def parameters(self) -> List[ToolParameter]:
|
|
49
|
+
return [
|
|
50
|
+
ToolParameter(
|
|
51
|
+
name="code",
|
|
52
|
+
description="Python code to execute",
|
|
53
|
+
type="string",
|
|
54
|
+
required=True
|
|
55
|
+
),
|
|
56
|
+
ToolParameter(
|
|
57
|
+
name="timeout",
|
|
58
|
+
description="Execution timeout in seconds",
|
|
59
|
+
type="integer",
|
|
60
|
+
required=False,
|
|
61
|
+
default=30
|
|
62
|
+
),
|
|
63
|
+
ToolParameter(
|
|
64
|
+
name="reset_namespace",
|
|
65
|
+
description="Clear the execution namespace before running",
|
|
66
|
+
type="boolean",
|
|
67
|
+
required=False,
|
|
68
|
+
default=False
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
def _validate_code(self, code: str) -> Optional[str]:
|
|
73
|
+
"""Validate code for safety issues."""
|
|
74
|
+
if not self.sandbox_mode:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
tree = ast.parse(code)
|
|
79
|
+
except SyntaxError as e:
|
|
80
|
+
return f"Syntax error: {e}"
|
|
81
|
+
|
|
82
|
+
for node in ast.walk(tree):
|
|
83
|
+
# Check for dangerous imports
|
|
84
|
+
if isinstance(node, ast.Import):
|
|
85
|
+
for alias in node.names:
|
|
86
|
+
if alias.name in self.BLOCKED_IMPORTS:
|
|
87
|
+
return f"Import blocked for safety: {alias.name}"
|
|
88
|
+
|
|
89
|
+
elif isinstance(node, ast.ImportFrom):
|
|
90
|
+
if node.module in self.BLOCKED_IMPORTS:
|
|
91
|
+
return f"Import blocked for safety: {node.module}"
|
|
92
|
+
|
|
93
|
+
# Check for dangerous function calls
|
|
94
|
+
elif isinstance(node, ast.Call):
|
|
95
|
+
if isinstance(node.func, ast.Name):
|
|
96
|
+
if node.func.id in self.BLOCKED_BUILTINS:
|
|
97
|
+
return f"Function blocked for safety: {node.func.id}"
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def _create_safe_namespace(self) -> Dict[str, Any]:
|
|
102
|
+
"""Create a restricted namespace for code execution."""
|
|
103
|
+
import math
|
|
104
|
+
import json
|
|
105
|
+
import re
|
|
106
|
+
from datetime import datetime, timedelta
|
|
107
|
+
|
|
108
|
+
safe_builtins = {
|
|
109
|
+
'abs': abs, 'all': all, 'any': any, 'bin': bin, 'bool': bool,
|
|
110
|
+
'chr': chr, 'dict': dict, 'dir': dir, 'divmod': divmod,
|
|
111
|
+
'enumerate': enumerate, 'filter': filter, 'float': float,
|
|
112
|
+
'format': format, 'frozenset': frozenset, 'getattr': getattr,
|
|
113
|
+
'hasattr': hasattr, 'hash': hash, 'hex': hex, 'id': id,
|
|
114
|
+
'int': int, 'isinstance': isinstance, 'issubclass': issubclass,
|
|
115
|
+
'iter': iter, 'len': len, 'list': list, 'map': map, 'max': max,
|
|
116
|
+
'min': min, 'next': next, 'oct': oct, 'ord': ord, 'pow': pow,
|
|
117
|
+
'print': print, 'range': range, 'repr': repr, 'reversed': reversed,
|
|
118
|
+
'round': round, 'set': set, 'slice': slice, 'sorted': sorted,
|
|
119
|
+
'str': str, 'sum': sum, 'tuple': tuple, 'type': type, 'zip': zip,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
namespace = {
|
|
123
|
+
'__builtins__': safe_builtins,
|
|
124
|
+
'math': math,
|
|
125
|
+
'json': json,
|
|
126
|
+
're': re,
|
|
127
|
+
'datetime': datetime,
|
|
128
|
+
'timedelta': timedelta,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Try to import common data science libraries if available
|
|
132
|
+
try:
|
|
133
|
+
import numpy as np
|
|
134
|
+
namespace['np'] = np
|
|
135
|
+
namespace['numpy'] = np
|
|
136
|
+
except ImportError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
import pandas as pd
|
|
141
|
+
namespace['pd'] = pd
|
|
142
|
+
namespace['pandas'] = pd
|
|
143
|
+
except ImportError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
return namespace
|
|
147
|
+
|
|
148
|
+
async def execute(self, code: str, timeout: int = None,
|
|
149
|
+
reset_namespace: bool = False) -> ToolResult:
|
|
150
|
+
"""Execute Python code."""
|
|
151
|
+
|
|
152
|
+
# Validate code
|
|
153
|
+
validation_error = self._validate_code(code)
|
|
154
|
+
if validation_error:
|
|
155
|
+
return ToolResult.error_result(validation_error)
|
|
156
|
+
|
|
157
|
+
# Reset namespace if requested
|
|
158
|
+
if reset_namespace:
|
|
159
|
+
self._namespace = {}
|
|
160
|
+
|
|
161
|
+
# Prepare namespace
|
|
162
|
+
exec_namespace = self._create_safe_namespace()
|
|
163
|
+
exec_namespace.update(self._namespace)
|
|
164
|
+
|
|
165
|
+
# Capture output
|
|
166
|
+
stdout_capture = io.StringIO()
|
|
167
|
+
stderr_capture = io.StringIO()
|
|
168
|
+
|
|
169
|
+
execution_timeout = timeout or self.timeout
|
|
170
|
+
result_value = None
|
|
171
|
+
|
|
172
|
+
def run_code():
|
|
173
|
+
nonlocal result_value
|
|
174
|
+
try:
|
|
175
|
+
# Try to evaluate as expression first
|
|
176
|
+
tree = ast.parse(code, mode='eval')
|
|
177
|
+
result_value = eval(compile(tree, '<string>', 'eval'), exec_namespace)
|
|
178
|
+
except SyntaxError:
|
|
179
|
+
# Not an expression, execute as statements
|
|
180
|
+
exec(code, exec_namespace)
|
|
181
|
+
# Check if there's a 'result' variable
|
|
182
|
+
if 'result' in exec_namespace:
|
|
183
|
+
result_value = exec_namespace['result']
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
|
|
187
|
+
# Run with timeout
|
|
188
|
+
await asyncio.wait_for(
|
|
189
|
+
asyncio.get_event_loop().run_in_executor(None, run_code),
|
|
190
|
+
timeout=execution_timeout
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Update persistent namespace (excluding builtins)
|
|
194
|
+
for key, value in exec_namespace.items():
|
|
195
|
+
if key != '__builtins__':
|
|
196
|
+
self._namespace[key] = value
|
|
197
|
+
|
|
198
|
+
# Build output
|
|
199
|
+
stdout_str = stdout_capture.getvalue()
|
|
200
|
+
stderr_str = stderr_capture.getvalue()
|
|
201
|
+
|
|
202
|
+
output_parts = []
|
|
203
|
+
if result_value is not None:
|
|
204
|
+
output_parts.append(f"Result: {repr(result_value)}")
|
|
205
|
+
if stdout_str:
|
|
206
|
+
output_parts.append(f"Output:\n{stdout_str}")
|
|
207
|
+
if stderr_str:
|
|
208
|
+
output_parts.append(f"Stderr:\n{stderr_str}")
|
|
209
|
+
|
|
210
|
+
output = "\n".join(output_parts) if output_parts else "Code executed successfully (no output)"
|
|
211
|
+
|
|
212
|
+
return ToolResult.success_result(
|
|
213
|
+
output=output,
|
|
214
|
+
metadata={
|
|
215
|
+
'result': result_value,
|
|
216
|
+
'stdout': stdout_str,
|
|
217
|
+
'stderr': stderr_str,
|
|
218
|
+
'namespace_vars': list(self._namespace.keys())
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except asyncio.TimeoutError:
|
|
223
|
+
return ToolResult.error_result(
|
|
224
|
+
f"Code execution timed out after {execution_timeout} seconds"
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
error_msg = traceback.format_exc()
|
|
228
|
+
return ToolResult.error_result(
|
|
229
|
+
f"Execution error: {str(e)}\n{error_msg}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def reset(self):
|
|
233
|
+
"""Reset the execution namespace."""
|
|
234
|
+
self._namespace = {}
|
|
235
|
+
|
|
236
|
+
def get_namespace_vars(self) -> List[str]:
|
|
237
|
+
"""Get list of variables in the namespace."""
|
|
238
|
+
return list(self._namespace.keys())
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell Command Tool for AstraAgent.
|
|
3
|
+
Executes shell commands with safety guardrails.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import shlex
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from astra.tools.base import Tool, ToolParameter, ToolResult, ToolCategory, RiskLevel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShellTool(Tool):
|
|
17
|
+
"""Tool for executing shell commands."""
|
|
18
|
+
|
|
19
|
+
name = "shell"
|
|
20
|
+
description = "Execute shell commands on the system. Use with caution."
|
|
21
|
+
category = ToolCategory.SHELL
|
|
22
|
+
risk_level = RiskLevel.HIGH
|
|
23
|
+
|
|
24
|
+
# Dangerous patterns to block
|
|
25
|
+
DANGEROUS_PATTERNS = [
|
|
26
|
+
"rm -rf /",
|
|
27
|
+
"rm -rf /*",
|
|
28
|
+
"mkfs",
|
|
29
|
+
"dd if=",
|
|
30
|
+
":(){ :|:& };:",
|
|
31
|
+
"> /dev/sda",
|
|
32
|
+
"chmod -R 777 /",
|
|
33
|
+
"wget | sh",
|
|
34
|
+
"curl | sh",
|
|
35
|
+
"format c:",
|
|
36
|
+
"del /f /s /q c:\\",
|
|
37
|
+
"rd /s /q c:\\",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def __init__(self, allowed_commands: List[str] = None, blocked_commands: List[str] = None):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.allowed_commands = allowed_commands # None = all allowed
|
|
43
|
+
self.blocked_commands = blocked_commands or []
|
|
44
|
+
self.is_windows = platform.system() == "Windows"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def parameters(self) -> List[ToolParameter]:
|
|
48
|
+
return [
|
|
49
|
+
ToolParameter(
|
|
50
|
+
name="command",
|
|
51
|
+
description="The shell command to execute",
|
|
52
|
+
type="string",
|
|
53
|
+
required=True
|
|
54
|
+
),
|
|
55
|
+
ToolParameter(
|
|
56
|
+
name="working_dir",
|
|
57
|
+
description="Working directory for the command",
|
|
58
|
+
type="string",
|
|
59
|
+
required=False,
|
|
60
|
+
default=None
|
|
61
|
+
),
|
|
62
|
+
ToolParameter(
|
|
63
|
+
name="timeout",
|
|
64
|
+
description="Timeout in seconds",
|
|
65
|
+
type="integer",
|
|
66
|
+
required=False,
|
|
67
|
+
default=60
|
|
68
|
+
),
|
|
69
|
+
ToolParameter(
|
|
70
|
+
name="capture_output",
|
|
71
|
+
description="Whether to capture stdout/stderr",
|
|
72
|
+
type="boolean",
|
|
73
|
+
required=False,
|
|
74
|
+
default=True
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
def _is_dangerous(self, command: str) -> bool:
|
|
79
|
+
"""Check if command is dangerous."""
|
|
80
|
+
cmd_lower = command.lower()
|
|
81
|
+
for pattern in self.DANGEROUS_PATTERNS + self.blocked_commands:
|
|
82
|
+
if pattern.lower() in cmd_lower:
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def _is_allowed(self, command: str) -> bool:
|
|
87
|
+
"""Check if command is in allowed list."""
|
|
88
|
+
if self.allowed_commands is None:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
cmd_parts = command.split()
|
|
92
|
+
if not cmd_parts:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
base_cmd = cmd_parts[0]
|
|
96
|
+
return base_cmd in self.allowed_commands
|
|
97
|
+
|
|
98
|
+
async def execute(self, command: str, working_dir: str = None,
|
|
99
|
+
timeout: int = 60, capture_output: bool = True) -> ToolResult:
|
|
100
|
+
"""Execute a shell command."""
|
|
101
|
+
|
|
102
|
+
# Safety checks
|
|
103
|
+
if self._is_dangerous(command):
|
|
104
|
+
return ToolResult.error_result(
|
|
105
|
+
f"Command blocked for safety: {command[:100]}..."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if not self._is_allowed(command):
|
|
109
|
+
return ToolResult.error_result(
|
|
110
|
+
f"Command not in allowed list: {command.split()[0]}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Resolve working directory
|
|
114
|
+
if working_dir:
|
|
115
|
+
if not os.path.isdir(working_dir):
|
|
116
|
+
return ToolResult.error_result(f"Working directory not found: {working_dir}")
|
|
117
|
+
else:
|
|
118
|
+
working_dir = os.getcwd()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Execute command
|
|
122
|
+
if self.is_windows:
|
|
123
|
+
# Windows: use shell=True
|
|
124
|
+
process = await asyncio.create_subprocess_shell(
|
|
125
|
+
command,
|
|
126
|
+
stdout=asyncio.subprocess.PIPE if capture_output else None,
|
|
127
|
+
stderr=asyncio.subprocess.PIPE if capture_output else None,
|
|
128
|
+
cwd=working_dir
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
# Unix: parse and execute
|
|
132
|
+
args = shlex.split(command)
|
|
133
|
+
process = await asyncio.create_subprocess_exec(
|
|
134
|
+
*args,
|
|
135
|
+
stdout=asyncio.subprocess.PIPE if capture_output else None,
|
|
136
|
+
stderr=asyncio.subprocess.PIPE if capture_output else None,
|
|
137
|
+
cwd=working_dir
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Wait for completion with timeout
|
|
141
|
+
try:
|
|
142
|
+
stdout, stderr = await asyncio.wait_for(
|
|
143
|
+
process.communicate(),
|
|
144
|
+
timeout=timeout
|
|
145
|
+
)
|
|
146
|
+
except asyncio.TimeoutError:
|
|
147
|
+
process.kill()
|
|
148
|
+
return ToolResult.error_result(
|
|
149
|
+
f"Command timed out after {timeout} seconds"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Decode output
|
|
153
|
+
stdout_str = stdout.decode('utf-8', errors='replace') if stdout else ""
|
|
154
|
+
stderr_str = stderr.decode('utf-8', errors='replace') if stderr else ""
|
|
155
|
+
|
|
156
|
+
# Build result
|
|
157
|
+
if process.returncode == 0:
|
|
158
|
+
return ToolResult.success_result(
|
|
159
|
+
output=stdout_str,
|
|
160
|
+
metadata={
|
|
161
|
+
'stderr': stderr_str,
|
|
162
|
+
'return_code': process.returncode,
|
|
163
|
+
'command': command,
|
|
164
|
+
'working_dir': working_dir
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
return ToolResult.error_result(
|
|
169
|
+
error=f"Command failed with code {process.returncode}:\n{stderr_str or stdout_str}",
|
|
170
|
+
metadata={
|
|
171
|
+
'stdout': stdout_str,
|
|
172
|
+
'stderr': stderr_str,
|
|
173
|
+
'return_code': process.returncode,
|
|
174
|
+
'command': command
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
except FileNotFoundError:
|
|
179
|
+
return ToolResult.error_result(f"Command not found: {command.split()[0]}")
|
|
180
|
+
except PermissionError:
|
|
181
|
+
return ToolResult.error_result(f"Permission denied for command: {command}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return ToolResult.error_result(f"Error executing command: {str(e)}")
|