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
package/astra/tasks.py ADDED
@@ -0,0 +1,466 @@
1
+ """Task Execution System - Perform various system tasks from chat."""
2
+
3
+ import os
4
+ import subprocess
5
+ import shutil
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Any
9
+ from datetime import datetime
10
+
11
+
12
+ class TaskExecutor:
13
+ """Execute various system and file tasks."""
14
+
15
+ @staticmethod
16
+ def open_file(filepath: str) -> Dict[str, str]:
17
+ """Open a file with default application.
18
+
19
+ Args:
20
+ filepath: Path to file
21
+
22
+ Returns:
23
+ Status dict
24
+ """
25
+ try:
26
+ filepath = os.path.abspath(filepath)
27
+ if not os.path.exists(filepath):
28
+ return {'success': False, 'message': f'File not found: {filepath}'}
29
+
30
+ if os.name == 'nt': # Windows
31
+ os.startfile(filepath)
32
+ elif os.name == 'posix': # Linux/Mac
33
+ import subprocess
34
+ subprocess.Popen(['xdg-open' if os.uname()[0] == 'Linux' else 'open', filepath])
35
+
36
+ return {'success': True, 'message': f'Opening: {filepath}'}
37
+ except Exception as e:
38
+ return {'success': False, 'message': f'Error: {str(e)}'}
39
+
40
+ @staticmethod
41
+ def open_folder(folder_path: str) -> Dict[str, str]:
42
+ """Open folder in file explorer.
43
+
44
+ Args:
45
+ folder_path: Path to folder
46
+
47
+ Returns:
48
+ Status dict
49
+ """
50
+ try:
51
+ folder_path = os.path.abspath(folder_path)
52
+ if not os.path.isdir(folder_path):
53
+ return {'success': False, 'message': f'Folder not found: {folder_path}'}
54
+
55
+ if os.name == 'nt': # Windows
56
+ os.startfile(folder_path)
57
+ elif os.name == 'posix': # Linux/Mac
58
+ subprocess.Popen(['xdg-open' if os.uname()[0] == 'Linux' else 'open', folder_path])
59
+
60
+ return {'success': True, 'message': f'Opening folder: {folder_path}'}
61
+ except Exception as e:
62
+ return {'success': False, 'message': f'Error: {str(e)}'}
63
+
64
+ @staticmethod
65
+ def copy_file(source: str, dest: str) -> Dict[str, str]:
66
+ """Copy file from source to destination.
67
+
68
+ Args:
69
+ source: Source file path
70
+ dest: Destination file path
71
+
72
+ Returns:
73
+ Status dict
74
+ """
75
+ try:
76
+ source = os.path.abspath(source)
77
+ dest = os.path.abspath(dest)
78
+
79
+ if not os.path.exists(source):
80
+ return {'success': False, 'message': f'Source file not found: {source}'}
81
+
82
+ # Create destination directory if needed
83
+ dest_dir = os.path.dirname(dest)
84
+ if dest_dir and not os.path.exists(dest_dir):
85
+ os.makedirs(dest_dir, exist_ok=True)
86
+
87
+ shutil.copy2(source, dest)
88
+ return {'success': True, 'message': f'Copied: {source} → {dest}'}
89
+ except Exception as e:
90
+ return {'success': False, 'message': f'Error: {str(e)}'}
91
+
92
+ @staticmethod
93
+ def move_file(source: str, dest: str) -> Dict[str, str]:
94
+ """Move file from source to destination.
95
+
96
+ Args:
97
+ source: Source file path
98
+ dest: Destination file path
99
+
100
+ Returns:
101
+ Status dict
102
+ """
103
+ try:
104
+ source = os.path.abspath(source)
105
+ dest = os.path.abspath(dest)
106
+
107
+ if not os.path.exists(source):
108
+ return {'success': False, 'message': f'File not found: {source}'}
109
+
110
+ # Create destination directory if needed
111
+ dest_dir = os.path.dirname(dest)
112
+ if dest_dir and not os.path.exists(dest_dir):
113
+ os.makedirs(dest_dir, exist_ok=True)
114
+
115
+ shutil.move(source, dest)
116
+ return {'success': True, 'message': f'Moved: {source} → {dest}'}
117
+ except Exception as e:
118
+ return {'success': False, 'message': f'Error: {str(e)}'}
119
+
120
+ @staticmethod
121
+ def delete_file(filepath: str, confirm: bool = True) -> Dict[str, str]:
122
+ """Delete a file.
123
+
124
+ Args:
125
+ filepath: Path to file
126
+ confirm: Whether to require confirmation
127
+
128
+ Returns:
129
+ Status dict
130
+ """
131
+ try:
132
+ filepath = os.path.abspath(filepath)
133
+
134
+ if not os.path.exists(filepath):
135
+ return {'success': False, 'message': f'File not found: {filepath}'}
136
+
137
+ if not os.path.isfile(filepath):
138
+ return {'success': False, 'message': f'Not a file: {filepath}'}
139
+
140
+ os.remove(filepath)
141
+ return {'success': True, 'message': f'Deleted: {filepath}'}
142
+ except Exception as e:
143
+ return {'success': False, 'message': f'Error: {str(e)}'}
144
+
145
+ @staticmethod
146
+ def create_folder(folder_path: str) -> Dict[str, str]:
147
+ """Create a new folder.
148
+
149
+ Args:
150
+ folder_path: Path to create
151
+
152
+ Returns:
153
+ Status dict
154
+ """
155
+ try:
156
+ folder_path = os.path.abspath(folder_path)
157
+
158
+ if os.path.exists(folder_path):
159
+ return {'success': False, 'message': f'Folder already exists: {folder_path}'}
160
+
161
+ os.makedirs(folder_path, exist_ok=True)
162
+ return {'success': True, 'message': f'Created folder: {folder_path}'}
163
+ except Exception as e:
164
+ return {'success': False, 'message': f'Error: {str(e)}'}
165
+
166
+ @staticmethod
167
+ def delete_folder(folder_path: str, recursive: bool = False) -> Dict[str, str]:
168
+ """Delete a folder.
169
+
170
+ Args:
171
+ folder_path: Path to folder
172
+ recursive: Whether to delete recursively (all contents)
173
+
174
+ Returns:
175
+ Status dict
176
+ """
177
+ try:
178
+ folder_path = os.path.abspath(folder_path)
179
+
180
+ if not os.path.isdir(folder_path):
181
+ return {'success': False, 'message': f'Folder not found: {folder_path}'}
182
+
183
+ if recursive:
184
+ shutil.rmtree(folder_path)
185
+ else:
186
+ os.rmdir(folder_path)
187
+
188
+ return {'success': True, 'message': f'Deleted folder: {folder_path}'}
189
+ except Exception as e:
190
+ return {'success': False, 'message': f'Error: {str(e)}'}
191
+
192
+ @staticmethod
193
+ def get_file_info(filepath: str) -> Dict[str, Any]:
194
+ """Get detailed information about a file.
195
+
196
+ Args:
197
+ filepath: Path to file
198
+
199
+ Returns:
200
+ Details dict
201
+ """
202
+ try:
203
+ filepath = os.path.abspath(filepath)
204
+
205
+ if not os.path.exists(filepath):
206
+ return {'error': f'File not found: {filepath}'}
207
+
208
+ stat = os.stat(filepath)
209
+ path_obj = Path(filepath)
210
+
211
+ return {
212
+ 'path': filepath,
213
+ 'name': os.path.basename(filepath),
214
+ 'exists': True,
215
+ 'is_file': os.path.isfile(filepath),
216
+ 'is_dir': os.path.isdir(filepath),
217
+ 'size': stat.st_size,
218
+ 'size_mb': round(stat.st_size / 1024 / 1024, 2),
219
+ 'created': datetime.fromtimestamp(stat.st_ctime).isoformat(),
220
+ 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
221
+ 'extension': path_obj.suffix,
222
+ 'parent': str(path_obj.parent)
223
+ }
224
+ except Exception as e:
225
+ return {'error': str(e)}
226
+
227
+ @staticmethod
228
+ def rename_file(filepath: str, new_name: str) -> Dict[str, str]:
229
+ """Rename a file.
230
+
231
+ Args:
232
+ filepath: Path to file
233
+ new_name: New filename
234
+
235
+ Returns:
236
+ Status dict
237
+ """
238
+ try:
239
+ filepath = os.path.abspath(filepath)
240
+
241
+ if not os.path.exists(filepath):
242
+ return {'success': False, 'message': f'File not found: {filepath}'}
243
+
244
+ directory = os.path.dirname(filepath)
245
+ new_path = os.path.join(directory, new_name)
246
+
247
+ if os.path.exists(new_path):
248
+ return {'success': False, 'message': f'File already exists: {new_path}'}
249
+
250
+ os.rename(filepath, new_path)
251
+ return {'success': True, 'message': f'Renamed: {filepath} → {new_path}'}
252
+ except Exception as e:
253
+ return {'success': False, 'message': f'Error: {str(e)}'}
254
+
255
+ @staticmethod
256
+ def list_directory(dirpath: str, show_hidden: bool = False) -> Dict[str, Any]:
257
+ """List contents of a directory.
258
+
259
+ Args:
260
+ dirpath: Path to directory
261
+ show_hidden: Whether to show hidden files
262
+
263
+ Returns:
264
+ Contents dict
265
+ """
266
+ try:
267
+ dirpath = os.path.abspath(dirpath)
268
+
269
+ if not os.path.isdir(dirpath):
270
+ return {'error': f'Not a directory: {dirpath}'}
271
+
272
+ dirs = []
273
+ files = []
274
+
275
+ for item in sorted(os.listdir(dirpath)):
276
+ if not show_hidden and item.startswith('.'):
277
+ continue
278
+
279
+ item_path = os.path.join(dirpath, item)
280
+
281
+ if os.path.isdir(item_path):
282
+ dirs.append({
283
+ 'name': item,
284
+ 'path': item_path,
285
+ 'type': 'directory'
286
+ })
287
+ else:
288
+ try:
289
+ size = os.path.getsize(item_path)
290
+ files.append({
291
+ 'name': item,
292
+ 'path': item_path,
293
+ 'type': 'file',
294
+ 'size': size,
295
+ 'size_kb': round(size / 1024, 2)
296
+ })
297
+ except:
298
+ pass
299
+
300
+ return {
301
+ 'path': dirpath,
302
+ 'directories': dirs,
303
+ 'files': files,
304
+ 'total_items': len(dirs) + len(files)
305
+ }
306
+ except Exception as e:
307
+ return {'error': str(e)}
308
+
309
+ @staticmethod
310
+ def search_and_replace_in_file(filepath: str, search_text: str,
311
+ replace_text: str, backup: bool = True) -> Dict[str, str]:
312
+ """Search and replace text in a file.
313
+
314
+ Args:
315
+ filepath: Path to file
316
+ search_text: Text to find
317
+ replace_text: Replacement text
318
+ backup: Whether to create backup
319
+
320
+ Returns:
321
+ Status dict
322
+ """
323
+ try:
324
+ filepath = os.path.abspath(filepath)
325
+
326
+ if not os.path.exists(filepath):
327
+ return {'success': False, 'message': f'File not found: {filepath}'}
328
+
329
+ # Read file
330
+ with open(filepath, 'r', encoding='utf-8') as f:
331
+ content = f.read()
332
+
333
+ # Count replacements
334
+ count = content.count(search_text)
335
+
336
+ if count == 0:
337
+ return {'success': False, 'message': f'Text not found: {search_text}'}
338
+
339
+ # Backup if requested
340
+ if backup:
341
+ backup_path = filepath + '.backup'
342
+ shutil.copy2(filepath, backup_path)
343
+
344
+ # Replace
345
+ new_content = content.replace(search_text, replace_text)
346
+
347
+ # Write back
348
+ with open(filepath, 'w', encoding='utf-8') as f:
349
+ f.write(new_content)
350
+
351
+ msg = f'Replaced {count} occurrence{"s" if count != 1 else ""} in {filepath}'
352
+ if backup:
353
+ msg += f'\nBackup created: {backup_path}'
354
+
355
+ return {'success': True, 'message': msg}
356
+ except Exception as e:
357
+ return {'success': False, 'message': f'Error: {str(e)}'}
358
+
359
+ @staticmethod
360
+ def create_file(filepath: str, content: str = "") -> Dict[str, str]:
361
+ """Create a new file with optional content.
362
+
363
+ Args:
364
+ filepath: Path to create
365
+ content: Initial content
366
+
367
+ Returns:
368
+ Status dict
369
+ """
370
+ try:
371
+ filepath = os.path.abspath(filepath)
372
+
373
+ if os.path.exists(filepath):
374
+ return {'success': False, 'message': f'File already exists: {filepath}'}
375
+
376
+ # Create parent directories if needed
377
+ parent_dir = os.path.dirname(filepath)
378
+ if parent_dir and not os.path.exists(parent_dir):
379
+ os.makedirs(parent_dir, exist_ok=True)
380
+
381
+ with open(filepath, 'w', encoding='utf-8') as f:
382
+ f.write(content)
383
+
384
+ return {'success': True, 'message': f'Created file: {filepath}'}
385
+ except Exception as e:
386
+ return {'success': False, 'message': f'Error: {str(e)}'}
387
+
388
+ @staticmethod
389
+ def get_disk_usage(path: str = "/") -> Dict[str, Any]:
390
+ """Get disk usage information.
391
+
392
+ Args:
393
+ path: Path to check (root by default)
394
+
395
+ Returns:
396
+ Usage dict
397
+ """
398
+ try:
399
+ path = os.path.abspath(path)
400
+
401
+ if os.name == 'nt': # Windows
402
+ import ctypes
403
+ free_bytes = ctypes.c_ulonglong(0)
404
+ ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path),
405
+ free_bytes, None, None)
406
+ free = free_bytes.value
407
+
408
+ # Get total (approximate)
409
+ stat = os.statvfs(os.path.splitdrive(path)[0] + '\\') if hasattr(os, 'statvfs') else None
410
+ if stat:
411
+ total = stat.f_blocks * stat.f_frsize
412
+ else:
413
+ total = None
414
+ else: # Unix/Linux
415
+ stat = os.statvfs(path)
416
+ free = stat.f_bavail * stat.f_frsize
417
+ total = stat.f_blocks * stat.f_frsize
418
+
419
+ return {
420
+ 'path': path,
421
+ 'free_gb': round(free / 1024 / 1024 / 1024, 2) if free else None,
422
+ 'free_bytes': free,
423
+ 'total_gb': round(total / 1024 / 1024 / 1024, 2) if total else None,
424
+ 'total_bytes': total
425
+ }
426
+ except Exception as e:
427
+ return {'error': str(e)}
428
+
429
+
430
+ def format_task_result(result: Dict) -> str:
431
+ """Format task result for display."""
432
+ if isinstance(result, dict):
433
+ if 'error' in result:
434
+ return f"āŒ Error: {result['error']}"
435
+
436
+ if 'success' in result:
437
+ status = "āœ…" if result['success'] else "āŒ"
438
+ return f"{status} {result.get('message', 'Task completed')}"
439
+
440
+ if 'path' in result and 'is_file' in result:
441
+ # File info
442
+ output = f"\nšŸ“„ File Info: {result['name']}\n"
443
+ output += f" Path: {result['path']}\n"
444
+ output += f" Size: {result['size_mb']} MB ({result['size']} bytes)\n"
445
+ output += f" Modified: {result['modified']}\n"
446
+ return output
447
+
448
+ if 'directories' in result and 'files' in result:
449
+ # Directory listing
450
+ output = f"\nšŸ“ Directory: {result['path']}\n"
451
+ output += f" Folders: {len(result['directories'])}\n"
452
+ output += f" Files: {len(result['files'])}\n"
453
+
454
+ if result['directories']:
455
+ output += "\n Folders:\n"
456
+ for d in result['directories'][:10]:
457
+ output += f" šŸ“‚ {d['name']}\n"
458
+
459
+ if result['files']:
460
+ output += "\n Files:\n"
461
+ for f in result['files'][:10]:
462
+ output += f" šŸ“„ {f['name']} ({f['size_kb']} KB)\n"
463
+
464
+ return output
465
+
466
+ return str(result)
@@ -0,0 +1,17 @@
1
+ """Tools module for AstraAgent."""
2
+
3
+ from astra.tools.base import Tool, ToolResult, ToolRegistry
4
+ from astra.tools.shell import ShellTool
5
+ from astra.tools.file import FileReadTool, FileWriteTool, ListDirTool
6
+ from astra.tools.python import PythonTool
7
+ from astra.tools.web import WebSearchTool, WebFetchTool
8
+ from astra.tools.browser import BrowserTool
9
+ from astra.tools.git import GitTool
10
+ from astra.tools.memory_tool import MemoryTool
11
+
12
+ __all__ = [
13
+ "Tool", "ToolResult", "ToolRegistry",
14
+ "ShellTool", "FileReadTool", "FileWriteTool", "ListDirTool",
15
+ "PythonTool", "WebSearchTool", "WebFetchTool", "BrowserTool",
16
+ "GitTool", "MemoryTool"
17
+ ]