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.
Files changed (63) hide show
  1. package/.env.template +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +333 -0
  4. package/astra/__init__.py +15 -0
  5. package/astra/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/astra/__pycache__/chat.cpython-314.pyc +0 -0
  7. package/astra/__pycache__/cli.cpython-314.pyc +0 -0
  8. package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
  9. package/astra/__pycache__/updater.cpython-314.pyc +0 -0
  10. package/astra/chat.py +763 -0
  11. package/astra/cli.py +913 -0
  12. package/astra/core/__init__.py +8 -0
  13. package/astra/core/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
  15. package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
  16. package/astra/core/__pycache__/memory.cpython-314.pyc +0 -0
  17. package/astra/core/__pycache__/reasoning.cpython-314.pyc +0 -0
  18. package/astra/core/__pycache__/state.cpython-314.pyc +0 -0
  19. package/astra/core/agent.py +515 -0
  20. package/astra/core/config.py +247 -0
  21. package/astra/core/memory.py +782 -0
  22. package/astra/core/reasoning.py +423 -0
  23. package/astra/core/state.py +366 -0
  24. package/astra/core/voice.py +144 -0
  25. package/astra/llm/__init__.py +32 -0
  26. package/astra/llm/__pycache__/__init__.cpython-314.pyc +0 -0
  27. package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
  28. package/astra/llm/providers.py +530 -0
  29. package/astra/planning/__init__.py +117 -0
  30. package/astra/prompts.py +289 -0
  31. package/astra/reflection/__init__.py +181 -0
  32. package/astra/search.py +469 -0
  33. package/astra/tasks.py +466 -0
  34. package/astra/tools/__init__.py +17 -0
  35. package/astra/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/astra/tools/__pycache__/advanced.cpython-314.pyc +0 -0
  37. package/astra/tools/__pycache__/base.cpython-314.pyc +0 -0
  38. package/astra/tools/__pycache__/browser.cpython-314.pyc +0 -0
  39. package/astra/tools/__pycache__/file.cpython-314.pyc +0 -0
  40. package/astra/tools/__pycache__/git.cpython-314.pyc +0 -0
  41. package/astra/tools/__pycache__/memory_tool.cpython-314.pyc +0 -0
  42. package/astra/tools/__pycache__/python.cpython-314.pyc +0 -0
  43. package/astra/tools/__pycache__/shell.cpython-314.pyc +0 -0
  44. package/astra/tools/__pycache__/web.cpython-314.pyc +0 -0
  45. package/astra/tools/__pycache__/windows.cpython-314.pyc +0 -0
  46. package/astra/tools/advanced.py +251 -0
  47. package/astra/tools/base.py +344 -0
  48. package/astra/tools/browser.py +93 -0
  49. package/astra/tools/file.py +476 -0
  50. package/astra/tools/git.py +74 -0
  51. package/astra/tools/memory_tool.py +89 -0
  52. package/astra/tools/python.py +238 -0
  53. package/astra/tools/shell.py +183 -0
  54. package/astra/tools/web.py +804 -0
  55. package/astra/tools/windows.py +542 -0
  56. package/astra/updater.py +450 -0
  57. package/astra/utils/__init__.py +230 -0
  58. package/bin/astraagent.js +73 -0
  59. package/bin/postinstall.js +25 -0
  60. package/config.json.template +52 -0
  61. package/main.py +16 -0
  62. package/package.json +51 -0
  63. 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)}")