jhalakdev67dev commited on
Commit
3db2988
·
verified ·
1 Parent(s): 6af624c

Upload AURA_onefile.py

Browse files
Files changed (1) hide show
  1. AURA_onefile.py +1609 -0
AURA_onefile.py ADDED
@@ -0,0 +1,1609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AURA - THE ONE FILE 👻🪄
3
+ =========================
4
+ Everything in a single Python file: skills, agent, dataset generator,
5
+ refinery, QLoRA training and evaluation.
6
+
7
+ USAGE
8
+ Kaggle (T4 x2): !python AURA_onefile.py train
9
+ Generate data only: python AURA_onefile.py data --n 8000
10
+ Run the spirit: python AURA_onefile.py agent --adapter ./aura_lora
11
+ Demo (no model): python AURA_onefile.py agent --demo --dry-run
12
+ Check skills: python AURA_onefile.py selftest
13
+ Evaluate router: python AURA_onefile.py eval --adapter ./aura_lora
14
+
15
+ Install first: pip install torch transformers peft bitsandbytes accelerate requests pillow
16
+ """
17
+
18
+
19
+
20
+ # ============================================================================
21
+ # SECTION 1: SKILLS, TOOLS & GUARDRAILS (Aura Manager / Installer / Launchpad / Researcher / Aryabhatta / Controller)
22
+ # ============================================================================
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ import shutil
28
+ import subprocess
29
+ import sys
30
+ import tempfile
31
+ import time
32
+ from dataclasses import dataclass, field
33
+ from typing import Callable, Dict, Any, List, Optional
34
+
35
+ IS_WINDOWS = os.name == "nt"
36
+ if IS_WINDOWS: # cmd/PowerShell default to cp1252 and crash on emoji prints
37
+ for _stream in (sys.stdout, sys.stderr):
38
+ try:
39
+ _stream.reconfigure(encoding="utf-8", errors="replace")
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ # ----------------------------------------------------------------------------
45
+ # Guardrails
46
+ # ----------------------------------------------------------------------------
47
+
48
+ # Commands / patterns that are plain dangerous -> hard block.
49
+ DANGEROUS_PATTERNS = [
50
+ r"\brm\s+-rf\s+/", # wipe root (unix)
51
+ r"\bformat\s+[a-z]:", # format drive (windows)
52
+ r"\bmkfs\b", # format (unix)
53
+ r"\bdel\s+/[sq]", # recursive delete (windows)
54
+ r"\brd\s+/s", # recursive remove dir (windows)
55
+ r"\bshutdown\b",
56
+ r"\bdiskpart\b",
57
+ r":\(\)\{.*\};:", # fork bomb
58
+ ]
59
+
60
+ # Patterns that need explicit user confirmation before running.
61
+ RISKY_PATTERNS = [
62
+ r"\brm\b", r"\bdel\b", r"\brmdir\b", r"\brd\b", r"\bmv\b",
63
+ r"\bsudo\b", r"\bapt\b", r"\bpip\b", r"\binstall\b",
64
+ r"\breg(?:edit| add)\b", r"\bnetsh\b", r"\bchmod\b", r"\bchown\b",
65
+ ]
66
+
67
+
68
+ def _matches(text: str, patterns: List[str]) -> bool:
69
+ text_l = text.lower()
70
+ return any(re.search(p, text_l) for p in patterns)
71
+
72
+
73
+ def default_confirm(action_desc: str) -> bool:
74
+ """Ask the human before a destructive action. Swap for UI later."""
75
+ try:
76
+ ans = input(f"\n[AURA CONFIRM] {action_desc}\nType 'yes' to allow: ")
77
+ return ans.strip().lower() in ("yes", "y")
78
+ except Exception:
79
+ return False
80
+
81
+
82
+ def default_log(msg: str) -> None:
83
+ print(f"[aura] {msg}")
84
+
85
+
86
+ # ----------------------------------------------------------------------------
87
+ # Python sandbox (used by Aura Aryabhatta + as a core tool)
88
+ # ----------------------------------------------------------------------------
89
+
90
+ SANDBOX_BLOCKLIST = DANGEROUS_PATTERNS + [r"\bshutdown\b", r"\bRestart-Computer\b"]
91
+
92
+
93
+ def python_sandbox(code: str, timeout: int = 10) -> str:
94
+ """Run Python code in a fresh subprocess with a timeout.
95
+
96
+ NOT a hard security boundary - it stops accidents (infinite loops,
97
+ fat-fingered deletes), not determined attackers. Run AURA's risky
98
+ skills inside a Docker container / VM for real isolation.
99
+ """
100
+ if _matches(code, SANDBOX_BLOCKLIST):
101
+ return "ERROR: command matches blocked pattern (safety guardrail)."
102
+
103
+ with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False,
104
+ encoding="utf-8") as f:
105
+ f.write(code)
106
+ path = f.name
107
+ try:
108
+ proc = subprocess.run(
109
+ [sys.executable, path],
110
+ capture_output=True, text=True, timeout=timeout,
111
+ )
112
+ out = (proc.stdout or "") + (("\nSTDERR: " + proc.stderr) if proc.stderr else "")
113
+ return out.strip() or "(no output)"
114
+ except subprocess.TimeoutExpired:
115
+ return f"ERROR: code timed out after {timeout}s."
116
+ except Exception as e:
117
+ return f"ERROR: {e}"
118
+ finally:
119
+ try:
120
+ os.remove(path)
121
+ except OSError:
122
+ pass
123
+
124
+
125
+ def shell(command: str, timeout: int = 60,
126
+ confirm: Callable[[str], bool] = default_confirm) -> str:
127
+ """Run a shell command (cmd on Windows, bash elsewhere) with guardrails."""
128
+ if _matches(command, DANGEROUS_PATTERNS):
129
+ return "ERROR: command blocked by AURA safety guardrail."
130
+ if _matches(command, RISKY_PATTERNS) and not confirm(f"Run shell command: {command}"):
131
+ return "Cancelled by user."
132
+
133
+ try:
134
+ proc = subprocess.run(command, shell=True, capture_output=True,
135
+ text=True, timeout=timeout)
136
+ out = (proc.stdout or "").strip()
137
+ err = (proc.stderr or "").strip()
138
+ result = out + (("\nSTDERR: " + err) if err else "")
139
+ return result.strip() or "(no output)"
140
+ except subprocess.TimeoutExpired:
141
+ return f"ERROR: command timed out after {timeout}s."
142
+ except Exception as e:
143
+ return f"ERROR: {e}"
144
+
145
+
146
+ # ----------------------------------------------------------------------------
147
+ # Skill context (cwd, confirm, log, dry-run)
148
+ # ----------------------------------------------------------------------------
149
+
150
+ @dataclass
151
+ class SkillContext:
152
+ cwd: str = os.path.expanduser("~")
153
+ confirm: Callable[[str], bool] = default_confirm
154
+ log: Callable[[str], None] = default_log
155
+ dry_run: bool = False # if True: print what WOULD happen
156
+
157
+
158
+ # ----------------------------------------------------------------------------
159
+ # 1) AURA MANAGER - files
160
+ # ----------------------------------------------------------------------------
161
+
162
+ def aura_manager(args: Dict[str, Any], ctx: SkillContext) -> str:
163
+ action = args.get("action")
164
+ path = os.path.join(ctx.cwd, args.get("path", "")) if args.get("path") else ctx.cwd
165
+
166
+ def would(msg):
167
+ ctx.log(f"[dry-run] {msg}")
168
+ return f"[dry-run] {msg}"
169
+
170
+ if action == "create_folder":
171
+ name = args.get("name", "NewFolder")
172
+ full = os.path.join(path, name)
173
+ if ctx.dry_run: return would(f"create folder {full}")
174
+ os.makedirs(full, exist_ok=True)
175
+ return f"Folder created: {full}"
176
+
177
+ if action == "create_file":
178
+ name = args.get("name", "note.txt")
179
+ content = args.get("content", "")
180
+ full = os.path.join(path, name)
181
+ if ctx.dry_run: return would(f"create file {full}")
182
+ os.makedirs(path, exist_ok=True)
183
+ with open(full, "w", encoding="utf-8") as f:
184
+ f.write(content)
185
+ return f"File saved: {full}"
186
+
187
+ if action == "save_file":
188
+ return aura_manager({**args, "action": "create_file"}, ctx)
189
+
190
+ if action == "delete":
191
+ target = args.get("target", "")
192
+ full = os.path.join(ctx.cwd, target)
193
+ if not os.path.exists(full):
194
+ return f"ERROR: not found: {full}"
195
+ if ctx.dry_run: return would(f"delete {full}")
196
+ if not ctx.confirm(f"DELETE {full} (cannot be undone!)"):
197
+ return "Cancelled by user."
198
+ if os.path.isdir(full):
199
+ shutil.rmtree(full)
200
+ else:
201
+ os.remove(full)
202
+ return f"Deleted: {full}"
203
+
204
+ if action == "move":
205
+ src = os.path.join(ctx.cwd, args["source"])
206
+ dst = os.path.join(ctx.cwd, args["destination"])
207
+ if not os.path.exists(src):
208
+ return f"ERROR: source not found: {src}"
209
+ if ctx.dry_run: return would(f"move {src} -> {dst}")
210
+ if os.path.exists(dst) and not ctx.confirm(f"OVERWRITE {dst}?"):
211
+ return "Cancelled by user."
212
+ os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
213
+ shutil.move(src, dst)
214
+ return f"Moved: {src} -> {dst}"
215
+
216
+ if action == "copy":
217
+ src = os.path.join(ctx.cwd, args["source"])
218
+ dst = os.path.join(ctx.cwd, args["destination"])
219
+ if not os.path.exists(src):
220
+ return f"ERROR: source not found: {src}"
221
+ if ctx.dry_run: return would(f"copy {src} -> {dst}")
222
+ os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
223
+ shutil.copy2(src, dst)
224
+ return f"Copied: {src} -> {dst}"
225
+
226
+ if action == "list":
227
+ if ctx.dry_run: return would(f"list {path}")
228
+ try:
229
+ items = sorted(os.listdir(path))
230
+ return "\n".join(items) or "(empty folder)"
231
+ except Exception as e:
232
+ return f"ERROR: {e}"
233
+
234
+ if action == "search_files":
235
+ query = args.get("query", "").lower()
236
+ root = os.path.join(ctx.cwd, args.get("root", "."))
237
+ hits = []
238
+ for dirpath, _dirs, files in os.walk(root):
239
+ for fn in files:
240
+ if query in fn.lower():
241
+ hits.append(os.path.join(dirpath, fn))
242
+ if len(hits) >= 50:
243
+ break
244
+ return "\n".join(hits) if hits else "No matching files found."
245
+
246
+ if action == "organize":
247
+ """Move files in a folder into sub-folders by extension."""
248
+ folder = os.path.join(ctx.cwd, args.get("folder", "."))
249
+ if not os.path.isdir(folder):
250
+ return f"ERROR: folder not found: {folder}"
251
+ moved = 0
252
+ for fn in os.listdir(folder):
253
+ src = os.path.join(folder, fn)
254
+ if not os.path.isfile(src):
255
+ continue
256
+ ext = os.path.splitext(fn)[1].lstrip(".").lower() or "misc"
257
+ dst_dir = os.path.join(folder, ext)
258
+ if ctx.dry_run:
259
+ would(f"move {src} -> {dst_dir}")
260
+ else:
261
+ os.makedirs(dst_dir, exist_ok=True)
262
+ shutil.move(src, os.path.join(dst_dir, fn))
263
+ moved += 1
264
+ return f"Organized {moved} files into extension folders inside {folder}."
265
+
266
+ return (f"ERROR: unknown action '{action}' for aura_manager. Valid actions: "
267
+ "create_folder, create_file, save_file, delete, move, copy, list, search_files, organize")
268
+
269
+
270
+ # ----------------------------------------------------------------------------
271
+ # 2) AURA INSTALLER - software installation (guarded)
272
+ # ----------------------------------------------------------------------------
273
+
274
+ # Only these domains may be used for the web fallback download.
275
+ OFFICIAL_DOMAINS = {
276
+ "microsoft.com", "store.microsoft.com", "visualstudio.com",
277
+ "code.visualstudio.com", "google.com", "mozilla.org", "notepad-plus-plus.org",
278
+ "7-zip.org", "python.org", "videolan.org", "vlc.cc", "discord.com",
279
+ "slack.com", "zoom.us", "adobe.com", "spotify.com", "obsproject.com",
280
+ "git-scm.com", "github.com", "gitlab.com", "blender.org", "audacityteam.org",
281
+ "libreoffice.org", "winrar.com", "ubuntu.com", "nodejs.org", "docker.com",
282
+ }
283
+
284
+
285
+ def _domain_is_allowed(url: str) -> bool:
286
+ try:
287
+ host = re.sub(r"^https?://", "", url).split("/")[0].lower()
288
+ host = host.split(":")[0]
289
+ return any(host == d or host.endswith("." + d) for d in OFFICIAL_DOMAINS)
290
+ except Exception:
291
+ return False
292
+
293
+
294
+ def aura_installer(args: Dict[str, Any], ctx: SkillContext) -> str:
295
+ action = args.get("action")
296
+ query = args.get("query", "")
297
+
298
+ if action == "search":
299
+ if not IS_WINDOWS:
300
+ return "ERROR: winget search is Windows-only (use your package manager)."
301
+ out = shell(f'winget search "{query}" --accept-source-agreements',
302
+ confirm=lambda _: True)
303
+ return out or "No results found."
304
+
305
+ if action == "install":
306
+ app_id = args.get("app_id", query)
307
+ url = args.get("download_url") # optional web fallback
308
+ if url:
309
+ if not _domain_is_allowed(url):
310
+ return (f"REFUSED: {url} is not in AURA's official-domains "
311
+ f"allowlist. Install manually if you trust it.")
312
+ if not ctx.confirm(f"Download and install from {url}"):
313
+ return "Cancelled by user."
314
+ out = shell(f'curl -L -o setup_installer "{url}" && '
315
+ f'start setup_installer', confirm=lambda _: True)
316
+ return out
317
+ if not IS_WINDOWS:
318
+ return "ERROR: winget install is Windows-only."
319
+ if not ctx.confirm(f"Install '{app_id}' via winget"):
320
+ return "Cancelled by user."
321
+ out = shell(f'winget install --id "{app_id}" -e '
322
+ f'--accept-source-agreements --accept-package-agreements',
323
+ confirm=lambda _: True)
324
+ return out or "Install command sent."
325
+
326
+ if action == "uninstall":
327
+ app_id = args.get("app_id", query)
328
+ if not ctx.confirm(f"UNINSTALL '{app_id}'"):
329
+ return "Cancelled by user."
330
+ return shell(f'winget uninstall --id "{app_id}" -e',
331
+ confirm=lambda _: True) if IS_WINDOWS else \
332
+ "ERROR: winget uninstall is Windows-only."
333
+
334
+ if action == "list_installed":
335
+ return shell("winget list", confirm=lambda _: True) if IS_WINDOWS \
336
+ else "ERROR: winget list is Windows-only."
337
+
338
+ return "ERROR: unknown action '%s' for aura_installer. Valid actions: search, install, uninstall, list_installed" % action
339
+
340
+
341
+ # ----------------------------------------------------------------------------
342
+ # 3) AURA LAUNCHPAD - open apps / websites / files
343
+ # ----------------------------------------------------------------------------
344
+
345
+ APP_ALIASES = {
346
+ "chrome": "chrome", "google chrome": "chrome",
347
+ "notepad": "notepad", "calculator": "calc",
348
+ "vscode": "code", "vs code": "code", "visual studio code": "code",
349
+ "spotify": "spotify", "explorer": "explorer", "file manager": "explorer",
350
+ "cmd": "cmd", "terminal": IS_WINDOWS and "cmd" or "x-terminal-emulator",
351
+ "paint": "mspaint", "word": "winword", "excel": "excel",
352
+ "edge": "msedge", "firefox": "firefox", "settings": "ms-settings:",
353
+ }
354
+ SITE_ALIASES = {
355
+ "youtube": "https://www.youtube.com",
356
+ "google": "https://www.google.com",
357
+ "github": "https://github.com",
358
+ "wikipedia": "https://www.wikipedia.org",
359
+ "gmail": "https://mail.google.com",
360
+ "stack overflow": "https://stackoverflow.com",
361
+ "huggingface": "https://huggingface.co",
362
+ "kaggle": "https://www.kaggle.com",
363
+ "chatgpt": "https://chat.openai.com",
364
+ }
365
+
366
+
367
+ def aura_launchpad(args: Dict[str, Any], ctx: SkillContext) -> str:
368
+ action = args.get("action")
369
+ target = (args.get("target") or "").strip()
370
+
371
+ # SELF-HEAL: small models sometimes emit a wrong verb ('open' etc.) -
372
+ # infer the right action from the target instead of failing.
373
+ _valid = {"open_app", "open_website", "search_browser", "open_file"}
374
+ if action not in _valid and target:
375
+ _ka, _ks = target.lower(), target.lower().replace(" ", "")
376
+ if _ka in APP_ALIASES:
377
+ action = "open_app"
378
+ elif _ks in SITE_ALIASES or "://" in target or \
379
+ target.lower().endswith((".com", ".org", ".in", ".net", ".io")):
380
+ action = "open_website"
381
+ elif os.path.exists(os.path.join(ctx.cwd, target)):
382
+ action = "open_file"
383
+ if action in _valid:
384
+ ctx.log(f"[self-heal] inferred action '{action}' from target '{target}'")
385
+
386
+ if action == "open_app":
387
+ key = target.lower()
388
+ exe = APP_ALIASES.get(key, target)
389
+ if ctx.dry_run:
390
+ return f"[dry-run] open app '{exe}'"
391
+ if IS_WINDOWS:
392
+ os.system(f'start "" "{exe}"')
393
+ return f"Launched app: {target}"
394
+ return shell(f"{exe} &", confirm=lambda _: True) or f"Tried to launch: {target}"
395
+
396
+ if action == "open_website":
397
+ key = target.lower().replace(" ", "")
398
+ url = SITE_ALIASES.get(key, target if "://" in target
399
+ else "https://" + target)
400
+ if ctx.dry_run:
401
+ return f"[dry-run] open website {url}"
402
+ import webbrowser
403
+ webbrowser.open(url)
404
+ return f"Opened website: {url}"
405
+
406
+ if action == "search_browser":
407
+ # Free in-browser search (no API key): opens the search URL in a NEW TAB
408
+ query = args.get("query", "")
409
+ engine = args.get("engine", "duckduckgo")
410
+ import urllib.parse
411
+ import webbrowser
412
+ q = urllib.parse.quote_plus(query)
413
+ urls = {
414
+ "google": f"https://www.google.com/search?q={q}",
415
+ "duckduckgo": f"https://duckduckgo.com/?q={q}",
416
+ "youtube": f"https://www.youtube.com/results?search_query={q}",
417
+ "wikipedia": f"https://en.wikipedia.org/w/index.php?search={q}",
418
+ }
419
+ url = urls.get(engine, urls["duckduckgo"])
420
+ browser = args.get("browser", "")
421
+ if ctx.dry_run:
422
+ return f"[dry-run] open new tab: {url}"
423
+ if browser and IS_WINDOWS:
424
+ os.system(f'start "" "{browser}" "{url}"')
425
+ else:
426
+ webbrowser.open(url) # opens a new tab in the default browser
427
+ return f"Opened new tab ({engine} search): {query}"
428
+
429
+ if action == "open_file":
430
+ full = os.path.join(ctx.cwd, target)
431
+ if not os.path.exists(full):
432
+ return f"ERROR: file not found: {full}"
433
+ if ctx.dry_run:
434
+ return f"[dry-run] open file {full}"
435
+ if IS_WINDOWS:
436
+ os.startfile(full) # noqa (windows-only)
437
+ return f"Opened file: {full}"
438
+ return shell(f'xdg-open "{full}"', confirm=lambda _: True)
439
+
440
+ return "ERROR: unknown action '%s' for aura_launchpad. Valid actions: open_app, open_website, search_browser, open_file" % action
441
+
442
+
443
+ # ----------------------------------------------------------------------------
444
+ # 4) AURA RESEARCHER - web search & fetch (stdlib only)
445
+ # ----------------------------------------------------------------------------
446
+
447
+ def _web_search_ddg(query: str, max_results: int = 5) -> str:
448
+ """DuckDuckGo HTML search - no API key needed. Requires `requests`."""
449
+ try:
450
+ import requests
451
+ except ImportError:
452
+ return "ERROR: install `requests` (pip install requests)."
453
+ try:
454
+ r = requests.post(
455
+ "https://html.duckduckgo.com/html/",
456
+ data={"q": query},
457
+ headers={"User-Agent": "Mozilla/5.0 (AURA agent)"},
458
+ timeout=15,
459
+ )
460
+ links = re.findall(r'class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)</a>',
461
+ r.text, re.S)[:max_results]
462
+ if not links:
463
+ return "No results found."
464
+ lines = []
465
+ for url, title in links:
466
+ title = re.sub(r"<[^>]+>", "", title).strip()
467
+ lines.append(f"- {title}\n {url}")
468
+ return "\n".join(lines)
469
+ except Exception as e:
470
+ return f"ERROR: search failed ({e})"
471
+
472
+
473
+ def aura_researcher(args: Dict[str, Any], ctx: SkillContext) -> str:
474
+ action = args.get("action")
475
+ if action == "search_web":
476
+ return _web_search_ddg(args.get("query", ""))
477
+ if action == "fetch_page":
478
+ url = args.get("url", "")
479
+ try:
480
+ import requests
481
+ except ImportError:
482
+ return "ERROR: install `requests`."
483
+ try:
484
+ r = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (AURA)"},
485
+ timeout=15)
486
+ text = re.sub(r"<script.*?</script>|<style.*?</style>", "",
487
+ r.text, flags=re.S)
488
+ text = re.sub(r"<[^>]+>", " ", text)
489
+ text = re.sub(r"\s+", " ", text).strip()
490
+ return text[:3000] + ("..." if len(text) > 3000 else "")
491
+ except Exception as e:
492
+ return f"ERROR: fetch failed ({e})"
493
+ return "ERROR: unknown action '%s' for aura_researcher. Valid actions: search_web, fetch_page" % action
494
+
495
+
496
+ # ----------------------------------------------------------------------------
497
+ # 5) AURA ARYABHATTA - math
498
+ # ----------------------------------------------------------------------------
499
+
500
+ def aura_aryabhatta(args: Dict[str, Any], ctx: SkillContext) -> str:
501
+ action = args.get("action")
502
+ if action == "calculate":
503
+ expr = args.get("expression", "")
504
+ # safe-ish eval: no builtins, expose math module + its functions
505
+ code = ("import math\n"
506
+ "_ns = {'__builtins__': {}, 'math': math, **math.__dict__}\n"
507
+ f"print(eval({expr!r}, _ns))")
508
+ return python_sandbox(code, timeout=10)
509
+ if action == "run_code":
510
+ return python_sandbox(args.get("code", ""), timeout=15)
511
+ return "ERROR: unknown action '%s' for aura_aryabhatta. Valid actions: calculate, run_code" % action
512
+
513
+
514
+ # ----------------------------------------------------------------------------
515
+ # 6) AURA CONTROLLER - system controls
516
+ # ----------------------------------------------------------------------------
517
+
518
+ def aura_controller(args: Dict[str, Any], ctx: SkillContext) -> str:
519
+ action = args.get("action")
520
+
521
+ if action == "screenshot":
522
+ try:
523
+ from PIL import ImageGrab
524
+ path = os.path.join(ctx.cwd, f"aura_screenshot_{int(time.time())}.png")
525
+ if ctx.dry_run:
526
+ return f"[dry-run] screenshot -> {path}"
527
+ ImageGrab.grab().save(path)
528
+ return f"Screenshot saved: {path}"
529
+ except Exception as e:
530
+ return f"ERROR: screenshot failed ({e}). pip install pillow"
531
+
532
+ if action == "system_info":
533
+ import platform
534
+ return (f"OS: {platform.system()} {platform.release()} | "
535
+ f"Machine: {platform.machine()} | "
536
+ f"Python: {platform.python_version()} | "
537
+ f"CPU cores: {os.cpu_count()}")
538
+
539
+ if action == "clipboard_read":
540
+ try:
541
+ from PIL import ImageGrab # noqa
542
+ except Exception:
543
+ pass
544
+ try:
545
+ import tkinter as tk
546
+ root = tk.Tk(); root.withdraw()
547
+ text = root.clipboard_get(); root.destroy()
548
+ return text or "(clipboard empty)"
549
+ except Exception:
550
+ return "(clipboard empty or not text)"
551
+
552
+ if action == "clipboard_write":
553
+ text = args.get("text", "")
554
+ try:
555
+ import tkinter as tk
556
+ root = tk.Tk(); root.withdraw()
557
+ root.clipboard_clear(); root.clipboard_append(text)
558
+ root.update(); root.destroy()
559
+ return "Clipboard updated."
560
+ except Exception as e:
561
+ return f"ERROR: {e}"
562
+
563
+ if action == "volume":
564
+ # Best-effort Windows volume via PowerShell + nircmd fallback.
565
+ if not IS_WINDOWS:
566
+ return "ERROR: volume control implemented for Windows only."
567
+ level = int(args.get("level", 50))
568
+ level = max(0, min(100, level))
569
+ out = shell(
570
+ "powershell -c \"$k=Get-CimInstance -Namespace root/WMI "
571
+ "ClassName WmiVolumeChangeEvent; "
572
+ f"(New-Object -ComObject WScript.Shell).SendKeys([char]174)\"",
573
+ confirm=lambda _: True)
574
+ return (f"Volume keys sent (level {level}). "
575
+ f"For precise control install nircmd.exe: {out[:100]}")
576
+
577
+ if action == "open_settings":
578
+ return aura_launchpad({"action": "open_app", "target": "settings"}, ctx)
579
+
580
+ return "ERROR: unknown action '%s' for aura_controller. Valid actions: screenshot, system_info, clipboard_read, clipboard_write, volume, open_settings" % action
581
+
582
+
583
+ # ----------------------------------------------------------------------------
584
+ # Registry
585
+ # ----------------------------------------------------------------------------
586
+
587
+ @dataclass
588
+ class Skill:
589
+ name: str
590
+ emoji: str
591
+ description: str
592
+ actions: Dict[str, str] # action -> arg doc
593
+ func: Callable[[Dict[str, Any], SkillContext], str]
594
+
595
+
596
+ SKILLS: Dict[str, Skill] = {
597
+ "aura_manager": Skill(
598
+ name="aura_manager",
599
+ emoji="📁",
600
+ description="File operations: create/save/delete/move/copy/list/search/organize files and folders.",
601
+ actions={
602
+ "create_folder": "args: name, path?",
603
+ "create_file": "args: name, content, path?",
604
+ "save_file": "args: name, content, path?",
605
+ "delete": "args: target (asks confirmation)",
606
+ "move": "args: source, destination (asks if overwriting)",
607
+ "copy": "args: source, destination",
608
+ "list": "args: path?",
609
+ "search_files": "args: query, root?",
610
+ "organize": "args: folder (sorts files into extension sub-folders)",
611
+ },
612
+ func=aura_manager,
613
+ ),
614
+ "aura_installer": Skill(
615
+ name="aura_installer",
616
+ emoji="📦",
617
+ description="Install/uninstall software. Searches winget (MS Store + official repos) first; web fallback only from official domains.",
618
+ actions={
619
+ "search": "args: query (winget search)",
620
+ "install": "args: app_id or query, download_url? (asks confirmation)",
621
+ "uninstall": "args: app_id (asks confirmation)",
622
+ "list_installed": "args: none",
623
+ },
624
+ func=aura_installer,
625
+ ),
626
+ "aura_launchpad": Skill(
627
+ name="aura_launchpad",
628
+ emoji="🚀",
629
+ description="Open apps, websites and files.",
630
+ actions={
631
+ "open_app": "args: target (e.g. chrome, notepad)",
632
+ "open_website": "args: target (e.g. youtube, github, or a URL)",
633
+ "search_browser": "args: query, engine? (google/duckduckgo/youtube/wikipedia), browser? - opens search results in a NEW TAB",
634
+ "open_file": "args: target (path)",
635
+ },
636
+ func=aura_launchpad,
637
+ ),
638
+ "aura_researcher": Skill(
639
+ name="aura_researcher",
640
+ emoji="🔍",
641
+ description="Web search and reading web pages.",
642
+ actions={
643
+ "search_web": "args: query",
644
+ "fetch_page": "args: url",
645
+ },
646
+ func=aura_researcher,
647
+ ),
648
+ "aura_aryabhatta": Skill(
649
+ name="aura_aryabhatta",
650
+ emoji="🧮",
651
+ description="Math and Python code execution in a sandboxed runner.",
652
+ actions={
653
+ "calculate": "args: expression (single python expression, math.* available)",
654
+ "run_code": "args: code (multi-line python)",
655
+ },
656
+ func=aura_aryabhatta,
657
+ ),
658
+ "aura_controller": Skill(
659
+ name="aura_controller",
660
+ emoji="⚙️",
661
+ description="System controls: screenshot, system info, clipboard, volume, settings.",
662
+ actions={
663
+ "screenshot": "args: none",
664
+ "system_info": "args: none",
665
+ "clipboard_read": "args: none",
666
+ "clipboard_write": "args: text",
667
+ "volume": "args: level (0-100)",
668
+ "open_settings": "args: none",
669
+ },
670
+ func=aura_controller,
671
+ ),
672
+ }
673
+
674
+
675
+ def build_system_prompt() -> str:
676
+ lines = [
677
+ "You are AURA, a helpful system-agent that controls the user's computer.",
678
+ "You have these skills:",
679
+ ]
680
+ for s in SKILLS.values():
681
+ lines.append(f"- {s.emoji} {s.name}: {s.description}")
682
+ for act, argdoc in s.actions.items():
683
+ lines.append(f" action '{act}' -> {argdoc}")
684
+ lines += [
685
+ "",
686
+ "RULES:",
687
+ "1. To use a skill, reply ONLY with a single JSON object:",
688
+ ' {"skill": "<skill_name>", "args": {"action": "<action>", ...}}',
689
+ "2. If the request needs none of the skills, just answer normally.",
690
+ "3. For math expressions with +,-,*,/ or functions, prefer aura_aryabhatta calculate.",
691
+ "4. If the request is ambiguous, ask the user a short clarifying question.",
692
+ "5. Never invent file paths - ask the user when unsure.",
693
+ ]
694
+ return "\n".join(lines)
695
+
696
+
697
+ def execute_skill_call(call: Dict[str, Any], ctx: SkillContext) -> str:
698
+ name = call.get("skill", "")
699
+ args = call.get("args", {}) or {}
700
+ skill = SKILLS.get(name)
701
+ if skill is None:
702
+ return f"ERROR: unknown skill '{name}'. Available: {list(SKILLS)}"
703
+ ctx.log(f"executing {skill.emoji} {name} args={json.dumps(args, ensure_ascii=False)}")
704
+ try:
705
+ return skill.func(args, ctx)
706
+ except Exception as e:
707
+ return f"ERROR in {name}: {e}"
708
+
709
+
710
+ # ----------------------------------------------------------------------------
711
+ # Self-test: python aura_skills.py
712
+ # ----------------------------------------------------------------------------
713
+
714
+
715
+ # ============================================================================
716
+ # SECTION 2: THE AGENT LOOP (model wrapper, JSON parsing, multi-step execution)
717
+ # ============================================================================
718
+
719
+ import argparse
720
+ import json
721
+ import re
722
+ import sys
723
+
724
+ MAX_STEPS = 5 # safety cap on chained tool calls per user message
725
+
726
+
727
+ # ----------------------------------------------------------------------------
728
+ # Model wrapper (lazy import so --demo works on any machine)
729
+ # ----------------------------------------------------------------------------
730
+
731
+ class AuraModel:
732
+ def __init__(self, model_id: str, adapter: str = None):
733
+ import torch
734
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
735
+
736
+ self.tok = AutoTokenizer.from_pretrained(model_id)
737
+ if torch.cuda.is_available():
738
+ print(f"[aura] GPU detected: {torch.cuda.get_device_name(0)} - 4-bit fast mode")
739
+ quant = BitsAndBytesConfig(
740
+ load_in_4bit=True,
741
+ bnb_4bit_quant_type="nf4",
742
+ bnb_4bit_compute_dtype=torch.float16,
743
+ bnb_4bit_use_double_quant=True,
744
+ )
745
+ self.model = AutoModelForCausalLM.from_pretrained(
746
+ model_id, quantization_config=quant, device_map="auto",
747
+ )
748
+ else:
749
+ # CPU mode: bitsandbytes is GPU-only, so load plain fp32.
750
+ # Slower (~2-5 tokens/sec) but perfectly fine for testing.
751
+ print("[aura] no GPU detected - CPU mode (slower, be patient)")
752
+ print("[aura] first run downloads ~5GB base model once")
753
+ try: # transformers v5 renamed torch_dtype -> dtype
754
+ self.model = AutoModelForCausalLM.from_pretrained(
755
+ model_id, dtype=torch.float32, device_map="cpu")
756
+ except TypeError:
757
+ self.model = AutoModelForCausalLM.from_pretrained(
758
+ model_id, torch_dtype=torch.float32, device_map="cpu")
759
+ if adapter:
760
+ from peft import PeftModel
761
+ self.model = PeftModel.from_pretrained(self.model, adapter)
762
+ print(f"[aura] adapter loaded: {adapter}")
763
+ self.model.eval()
764
+
765
+ def chat(self, messages) -> str:
766
+ prompt = self.tok.apply_chat_template(
767
+ messages, tokenize=False, add_generation_prompt=True)
768
+ inputs = self.tok(prompt, return_tensors="pt").to(self.model.device)
769
+ out = self.model.generate(
770
+ **inputs,
771
+ max_new_tokens=256,
772
+ do_sample=True,
773
+ temperature=0.4, # low temp -> reliable JSON
774
+ top_p=0.9,
775
+ pad_token_id=self.tok.eos_token_id,
776
+ )
777
+ text = self.tok.decode(out[0][inputs["input_ids"].shape[1]:],
778
+ skip_special_tokens=True)
779
+ return text.strip()
780
+
781
+
782
+ class DemoModel:
783
+ """Rule-based stand-in for quick testing without downloading weights."""
784
+
785
+ RULES = [
786
+ (r"(folder|folder bana)", {"skill": "aura_manager",
787
+ "args": {"action": "create_folder", "name": "Aura"}}),
788
+ (r"(install|laga do)", {"skill": "aura_installer",
789
+ "args": {"action": "install", "query": "vscode"}}),
790
+ (r"(kholo|open)", {"skill": "aura_launchpad",
791
+ "args": {"action": "open_app", "target": "notepad"}}),
792
+ (r"(sqrt|calculate|kitna|what is)", {"skill": "aura_aryabhatta",
793
+ "args": {"action": "calculate",
794
+ "expression": "2**10"}}),
795
+ (r"(screenshot)", {"skill": "aura_controller",
796
+ "args": {"action": "screenshot"}}),
797
+ ]
798
+
799
+ def chat(self, messages) -> str:
800
+ last = messages[-1]["content"]
801
+ if last.startswith("[TOOL RESULT]"):
802
+ return "Ho gaya! Kuch aur karna hai?" # final answer after tool use
803
+ user = last.lower()
804
+ for pat, call in self.RULES:
805
+ if re.search(pat, user):
806
+ return json.dumps(call, ensure_ascii=False)
807
+ return "Main demo model hoon - real AURA ke liye Sarvam-2B load karo!"
808
+
809
+
810
+ # ----------------------------------------------------------------------------
811
+ # Robust JSON extraction from a model reply
812
+ # ----------------------------------------------------------------------------
813
+
814
+ def extract_skill_call(text: str):
815
+ """Find the first balanced JSON object in the reply. None if absent."""
816
+ text = text.strip()
817
+ if "{" not in text:
818
+ return None
819
+ # strip markdown fences if the model added them
820
+ text = re.sub(r"```(?:json)?", "", text).strip()
821
+ depth, start = 0, -1
822
+ for i, ch in enumerate(text):
823
+ if ch == "{":
824
+ if depth == 0:
825
+ start = i
826
+ depth += 1
827
+ elif ch == "}":
828
+ depth -= 1
829
+ if depth == 0 and start != -1:
830
+ candidate = text[start:i + 1]
831
+ try:
832
+ obj = json.loads(candidate)
833
+ except json.JSONDecodeError:
834
+ start, depth = -1, 0
835
+ continue
836
+ if isinstance(obj, dict) and "skill" in obj:
837
+ return obj
838
+ start = -1 # valid json but not a skill call - keep scanning
839
+ return None
840
+
841
+
842
+ # ----------------------------------------------------------------------------
843
+ # Ollama backend - GGUF models run 5-10x faster on CPU with ~2.5GB RAM
844
+ # (vs ~10GB fp32 transformers). Requires: https://ollama.com + `ollama create`
845
+ # ----------------------------------------------------------------------------
846
+
847
+ class OllamaModel:
848
+ def __init__(self, model_name: str = "aura"):
849
+ import requests
850
+ self._requests = requests
851
+ self.name = model_name
852
+ self.system = build_system_prompt()
853
+ try:
854
+ r = requests.get("http://localhost:11434/api/tags", timeout=3)
855
+ names = [m["name"].split(":")[0] for m in r.json().get("models", [])]
856
+ if self.name not in names:
857
+ print(f"[aura] WARNING: ollama model '{self.name}' not found!")
858
+ print(f"[aura] available: {names or 'none'}")
859
+ print(f"[aura] fix: ollama create {self.name} -f AURA_Modelfile")
860
+ except Exception as e:
861
+ print(f"[aura] WARNING: ollama not reachable at localhost:11434 ({e})")
862
+ print("[aura] install from https://ollama.com, run it, then retry")
863
+ print(f"[aura] ollama backend: {self.name} (fast CPU mode)")
864
+
865
+ def chat(self, messages) -> str:
866
+ msgs = [{"role": "system", "content": self.system}]
867
+ msgs += [m for m in messages if m.get("role") != "system"]
868
+ r = self._requests.post("http://localhost:11434/api/chat",
869
+ json={"model": self.name, "messages": msgs,
870
+ "stream": False}, timeout=600)
871
+ r.raise_for_status()
872
+ return r.json()["message"]["content"].strip()
873
+
874
+
875
+ # ----------------------------------------------------------------------------
876
+ # The agent loop
877
+ # ----------------------------------------------------------------------------
878
+
879
+ def run_agent(model, dry_run=False, cwd=None):
880
+
881
+ ctx = SkillContext(cwd=cwd or ".", dry_run=dry_run,
882
+ confirm=default_confirm, log=default_log)
883
+ system = build_system_prompt()
884
+ print(r"""
885
+ ╔═══════════════════════════════════════╗
886
+ ║ A U R A - system spirit 👻🪄 ║
887
+ ║ files | install | launch | search ║
888
+ ║ maths | system controls ║
889
+ ╚═══════════════════════════════════════╝
890
+ type 'quit' to exit, 'dry' to toggle safe-mode
891
+ """)
892
+ history = []
893
+ while True:
894
+ try:
895
+ user = input("\nYou: ").strip()
896
+ except (EOFError, KeyboardInterrupt):
897
+ print("\n[aura] bye!")
898
+ return
899
+ if not user:
900
+ continue
901
+ if user.lower() in ("quit", "exit", "bye"):
902
+ print("[aura] bye! :)")
903
+ return
904
+ if user.lower() == "dry":
905
+ ctx.dry_run = not ctx.dry_run
906
+ print(f"[aura] dry-run mode: {ctx.dry_run}")
907
+ continue
908
+
909
+ messages = [{"role": "system", "content": system}]
910
+ messages.extend(history[-6:]) # short memory
911
+ messages.append({"role": "user", "content": user})
912
+ prev_call_sig = None
913
+
914
+ for step in range(MAX_STEPS):
915
+ reply = model.chat(messages)
916
+ call = extract_skill_call(reply)
917
+ if call is None:
918
+ print(f"AURA: {reply}")
919
+ history.append({"role": "user", "content": user})
920
+ history.append({"role": "assistant", "content": reply})
921
+ break
922
+
923
+ # loop guard: same call twice in a row = stop (small models do this)
924
+ call_sig = json.dumps(call, sort_keys=True)
925
+ if call_sig == prev_call_sig:
926
+ print("AURA: Ho gaya! (kaam complete)")
927
+ history.append({"role": "user", "content": user})
928
+ history.append({"role": "assistant",
929
+ "content": "Action already done."})
930
+ break
931
+ prev_call_sig = call_sig
932
+
933
+ print(f"AURA -> skill: {call['skill']} args: {call.get('args', {})}")
934
+ observation = execute_skill_call(call, ctx)
935
+ print(f"RESULT: {observation[:400]}")
936
+ messages.append({"role": "assistant", "content": json.dumps(call)})
937
+ # feed the observation back so the model can decide the next step
938
+ fb = f"[TOOL RESULT] {observation[:1500]}"
939
+ if not observation.startswith("ERROR"):
940
+ fb += "\n(The task is done. Reply to the user normally - no more skill calls.)"
941
+ messages.append({"role": "user", "content": fb})
942
+ else:
943
+ print("[aura] (max steps reached - stopping for safety)")
944
+ history.append({"role": "user", "content": user})
945
+ history.append({"role": "assistant", "content": "(stopped: max steps)"})
946
+
947
+
948
+ # ============================================================================
949
+ # SECTION 3: DATASET GENERATOR + REFINERY (Hindi / Hinglish / English)
950
+ # ============================================================================
951
+
952
+ import argparse
953
+ import json
954
+ import random
955
+ import re
956
+ import sys
957
+ from collections import Counter
958
+ from pathlib import Path
959
+
960
+ SEED = 2026
961
+ random.seed(SEED)
962
+
963
+ # ============================================================================
964
+ # 1) AURA SKILL REGISTRY (compact - must match aura_skills.py exactly!)
965
+ # ============================================================================
966
+
967
+ SKILL_NAMES = list(SKILLS)
968
+
969
+
970
+ # ============================================================================
971
+ # 2) DATASET GENERATOR (3 languages, filler-decorated for variety)
972
+ # ============================================================================
973
+
974
+ APPS = ["chrome", "notepad", "calculator", "vs code", "spotify", "paint", "edge",
975
+ "firefox", "word", "excel", "explorer", "settings", "terminal",
976
+ "file manager", "calculator app", "code editor", "paint app", "browser"]
977
+ WEBSITES = ["youtube", "github", "google", "wikipedia", "gmail", "stack overflow",
978
+ "huggingface", "kaggle", "chatgpt", "reddit", "linkedin", "instagram"]
979
+ SOFTWARE = ["vscode", "7zip", "vlc", "notepad++", "python", "git", "discord",
980
+ "spotify", "obs studio", "blender", "zoom", "nodejs", "audacity",
981
+ "libreoffice", "winrar", "docker", "steam", "slack", "figma", "postman"]
982
+ FOLDERS = ["Mathematics", "Science", "Aura", "Projects", "Notes", "Homework",
983
+ "Downloads", "Music", "Videos", "Study", "School", "Backup",
984
+ "Assignments", "Photos", "Coding", "Research", "Class 9", "Exams"]
985
+ FILES = ["notes.txt", "homework.docx", "project.py", "photo.jpg", "report.pdf",
986
+ "data.csv", "todo.md", "results.json", "song.mp3", "video.mp4",
987
+ "assignment.pdf", "code.py", "draft.txt", "summary.docx", "logo.png",
988
+ "budget.xlsx", "diary.txt"]
989
+ TOPICS = ["physics", "chemistry", "maths", "history", "biology", "coding",
990
+ "geography", "algebra", "grammar", "civics", "sanskrit", "english"]
991
+ SEARCH_QUERIES = ["latest AI news", "who won the cricket world cup",
992
+ "best laptop under 50000", "python tutorial", "ISRO next mission",
993
+ "climate change 2026", "python list comprehension examples", "how to learn dsa",
994
+ "sachin tendulkar stats", "newton laws of motion explained",
995
+ "cbse class 9 syllabus", "how do neural networks work", "best budget phone 2026",
996
+ "history of the taj mahal", "chandrayaan mission details", "olympics medal tally"]
997
+ _FILLERS_BEFORE = ["", "bhai ", "yaar ", "please ", "hey ", "hi ", "jaldi ",
998
+ "ek kaam karo ", "can you ", "kindly ", "प्रो "]
999
+ _FILLERS_AFTER = ["", " please", " yaar", " jaldi", " bhai", " now", " 🙏",
1000
+ " quick", " thank you", " abhi"]
1001
+
1002
+
1003
+ def _dec(u): # decorate with fillers for uniqueness
1004
+ return f"{random.choice(_FILLERS_BEFORE)}{u}{random.choice(_FILLERS_AFTER)}".strip()
1005
+
1006
+
1007
+ def rand_expr():
1008
+ a, b, c = random.randint(2, 999), random.randint(2, 99), random.randint(2, 30)
1009
+ return random.choice([
1010
+ f"{a} + {b}", f"{a} - {b}", f"{a} * {b}", f"{a} / {b}", f"{a} ** 2",
1011
+ f"math.sqrt({a * a})", f"math.sin(math.pi / {random.choice([2, 3, 6])})",
1012
+ f"{a} % {c}", f"({a} + {b}) * {c}", f"math.log({a})",
1013
+ f"math.factorial({random.randint(2, 9)})", f"math.gcd({a}, {b})"])
1014
+
1015
+
1016
+ def _gen(skill):
1017
+ lang = random.choice(["hi", "en"])
1018
+ if skill == "aura_manager":
1019
+ act = random.choice(["create_folder", "create_file", "delete", "move",
1020
+ "copy", "list", "search_files", "organize"])
1021
+ if act == "create_folder":
1022
+ f = random.choice(FOLDERS)
1023
+ u = random.choice([f"{f} naam ki folder bana do", f"ek folder banao {f}",
1024
+ f"create a folder named {f}", f"make a new folder called {f}"])
1025
+ c = {"skill": "aura_manager", "args": {"action": "create_folder", "name": f}}
1026
+ elif act == "create_file":
1027
+ fn, t = random.choice(FILES), random.choice(TOPICS)
1028
+ u = random.choice([f"{fn} file bana ke {t} ke notes save kar do",
1029
+ f"{t} ke notes {fn} mein save karo",
1030
+ f"save my {t} notes in {fn}",
1031
+ f"create a file {fn} with my {t} notes"])
1032
+ c = {"skill": "aura_manager", "args": {"action": "create_file", "name": fn,
1033
+ "content": f"{t} notes (by AURA)"}}
1034
+ elif act == "delete":
1035
+ fn = random.choice(FILES)
1036
+ u = random.choice([f"{fn} delete kar do", f"{fn} hata do",
1037
+ f"delete {fn}", f"remove the file {fn}"])
1038
+ c = {"skill": "aura_manager", "args": {"action": "delete", "target": fn}}
1039
+ elif act in ("move", "copy"):
1040
+ fn, fo = random.choice(FILES), random.choice(FOLDERS)
1041
+ verb_h = "daal do" if act == "move" else "copy rakh do"
1042
+ verb_e = "move" if act == "move" else "copy"
1043
+ u = random.choice([f"{fn} ko {fo} folder mein {verb_h}",
1044
+ f"{verb_e} {fn} into the {fo} folder"])
1045
+ c = {"skill": "aura_manager", "args": {"action": act, "source": fn,
1046
+ "destination": f"{fo}/{fn}"}}
1047
+ elif act == "list":
1048
+ u = random.choice(["is folder mein kya kya hai?", "files list kar do",
1049
+ "what files are here?", "list the files in this folder"])
1050
+ c = {"skill": "aura_manager", "args": {"action": "list"}}
1051
+ elif act == "search_files":
1052
+ q = random.choice(["report", "photo", "py", "mp4", "notes", "pdf"])
1053
+ u = random.choice([f"{q} wali files dhoondho", f"find all files with {q}"])
1054
+ c = {"skill": "aura_manager", "args": {"action": "search_files", "query": q}}
1055
+ else:
1056
+ u = random.choice(["is folder ko organize kar do", "files ko type ke hisaab se sort karo",
1057
+ "organize this folder", "sort these files by type"])
1058
+ c = {"skill": "aura_manager", "args": {"action": "organize"}}
1059
+ elif skill == "aura_installer":
1060
+ mode = random.choice(["search", "install", "uninstall", "list"])
1061
+ sw = random.choice(SOFTWARE)
1062
+ if mode == "search":
1063
+ u = random.choice([f"{sw} install ke liye search karo", f"{sw} milta hai kya?",
1064
+ f"search for {sw}"])
1065
+ c = {"skill": "aura_installer", "args": {"action": "search", "query": sw}}
1066
+ elif mode == "install":
1067
+ u = random.choice([f"{sw} install kar do", f"bhai {sw} laga do please",
1068
+ f"install {sw}", f"get me {sw} installed"])
1069
+ c = {"skill": "aura_installer", "args": {"action": "install", "query": sw}}
1070
+ elif mode == "uninstall":
1071
+ u = random.choice([f"{sw} hata do", f"uninstall {sw}"])
1072
+ c = {"skill": "aura_installer", "args": {"action": "uninstall", "query": sw}}
1073
+ else:
1074
+ u = random.choice(["kya kya software installed hai?",
1075
+ "what software is installed?"])
1076
+ c = {"skill": "aura_installer", "args": {"action": "list_installed"}}
1077
+ elif skill == "aura_launchpad":
1078
+ mode = random.choice(["app", "site", "bsearch"])
1079
+ if mode == "bsearch":
1080
+ q = random.choice(SEARCH_QUERIES)
1081
+ engine = random.choice(["google", "duckduckgo", "youtube", "wikipedia"])
1082
+ u = random.choice([f"chrome mein {q} search karo",
1083
+ f"{q} search karke tab kholo",
1084
+ f"search {q} in my browser",
1085
+ f"open a new tab and search for {q}"])
1086
+ c = {"skill": "aura_launchpad",
1087
+ "args": {"action": "search_browser", "query": q, "engine": engine}}
1088
+ elif mode == "app":
1089
+ app = random.choice(APPS)
1090
+ u = random.choice([f"{app} kholo", f"{app} open karo", f"launch {app}",
1091
+ f"can you open {app}"])
1092
+ c = {"skill": "aura_launchpad", "args": {"action": "open_app", "target": app}}
1093
+ else:
1094
+ site = random.choice(WEBSITES)
1095
+ u = random.choice([f"{site} kholo", f"{site} open kar do",
1096
+ f"open {site}", f"take me to {site}"])
1097
+ c = {"skill": "aura_launchpad", "args": {"action": "open_website", "target": site}}
1098
+ elif skill == "aura_researcher":
1099
+ q = random.choice(SEARCH_QUERIES)
1100
+ u = random.choice([f"{q} search karo", f"{q} ke baare mein pata lagao",
1101
+ f"search for {q}", f"look up {q}"])
1102
+ c = {"skill": "aura_researcher", "args": {"action": "search_web", "query": q}}
1103
+ elif skill == "aura_aryabhatta":
1104
+ e = rand_expr()
1105
+ u = random.choice([f"{e} kitna hota hai?", f"{e} calculate kar",
1106
+ f"what is {e}?", f"calculate {e}"])
1107
+ c = {"skill": "aura_aryabhatta", "args": {"action": "calculate", "expression": e}}
1108
+ else: # aura_controller
1109
+ act = random.choice(["screenshot", "system_info", "clipboard_read",
1110
+ "volume", "open_settings"])
1111
+ if act == "volume":
1112
+ lvl = random.choice([10, 25, 50, 75, 100])
1113
+ u = f"volume {lvl} kar do" if lang == "hi" else f"set volume to {lvl}"
1114
+ c = {"skill": "aura_controller", "args": {"action": "volume", "level": lvl}}
1115
+ else:
1116
+ u = {"screenshot": ("screenshot le lo", "take a screenshot"),
1117
+ "system_info": ("is computer ki details batao", "show system information"),
1118
+ "clipboard_read": ("clipboard mein kya hai?", "what is in my clipboard?"),
1119
+ "open_settings": ("settings kholo", "open settings")}[act][lang == "en"]
1120
+ c = {"skill": "aura_controller", "args": {"action": act}}
1121
+ return _dec(u), c
1122
+
1123
+
1124
+ CHITCHAT = [
1125
+ ("hey aura kaisa hai?", "Main badhiya hoon! Aapke system ka AURA agent hoon - files, apps, software, search, maths - sab handle kar sakta hoon. Kya karna hai?"),
1126
+ ("tumhara naam kya hai?", "Mera naam AURA hai - aapka personal system-agent! \U0001FA84"),
1127
+ ("who are you?", "I am AURA, your personal system-agent. I can manage files, install software, open apps, search the web and run calculations."),
1128
+ ("hi what can you do?", "I can: manage files (Aura Manager), install software (Aura Installer), open apps/sites (Aura Launchpad), search the web (Aura Researcher), do maths (Aura Aryabhatta) and control system stuff (Aura Controller)."),
1129
+ ("hello aura", "Hello! Main AURA hoon, aapka system spirit \U0001F47B Bolo, kya kaam karna hai?"),
1130
+ ("namaste", "Namaste! \U0001F64F Main AURA hoon - aapke computer ka assistant. Kya madad karun?"),
1131
+ ("good morning", "Good morning! \u2600\ufe0f Main ready hoon - files, apps, maths, search - bas bol do."),
1132
+ ("thanks yaar", "Koi baat nahi yaar! Aur kuch karna ho to batao. \U0001F604"),
1133
+ ("thank you aura", "Anytime! Aapke liye hi hoon main. \U0001FA84"),
1134
+ ("bye", "Bye! Milte hain. \U0001F44B"),
1135
+ ("tell me a joke", "Why did the AI go broke? It lost all its cache! \U0001F604"),
1136
+ ("ek joke sunao", "Programmer ki wife ne kaha: 'doodh lekar aao, aur ho sake to anda lekar aana'... wo 12 doodh ke packet lekar aaya. \U0001F604"),
1137
+ ("india ki rajdhani kya hai", "India ki rajdhani New Delhi hai."),
1138
+ ("what is the capital of india", "The capital of India is New Delhi."),
1139
+ ("tumhe kaise banaya gaya", "Mujhe Sarvam-2B (India ka open-source model) se fine-tune kiya gaya hai taaki main aapke computer ke skills control kar sakoon! \U0001F1EE\U0001F1F3"),
1140
+ ("who created you", "I am AURA - fine-tuned from Sarvam-2B, India ka open-source model, to be your personal system-agent."),
1141
+ ("kya tum humans ko replace kar doge", "Nahi yaar! Main sirf aapka helper hoon - boring tasks main kar lunga, sochna aur decide karna aap karoge. \U0001F604"),
1142
+ ("how are you", "All systems running smoothly! Ready for your next command. \U0001F47B"),
1143
+ ("aaj mausam kaisa hai", "Main weather app nahi hoon \U0001F604 lekin search kar sakta hoon - bolo to 'aaj ka mausam search karo' type karo!"),
1144
+ ("where are you from", "Main aapke computer ke andar rehta hoon - ek spirit ki tarah \U0001F47B India mein bane model se."),
1145
+ ("padhai kaise karun", "Focus sessions with no phone, notes khud likhkar revise karo. Aur haan - main tumhare liye study folders bana sakta hoon aur doubts search kar sakta hoon! \U0001F4AA"),
1146
+ ("mujhse dosti karogi", "Bilkul! Main to already tumhara digital dost hoon. Ab bolo, kya kaam karwana hai? \U0001F604"),
1147
+ ]
1148
+
1149
+
1150
+ CANNED_TOOL_RESULTS = {
1151
+ "aura_manager": "[TOOL RESULT] Folder created: /AURA/notes",
1152
+ "aura_installer": "[TOOL RESULT] Install command sent.",
1153
+ "aura_launchpad": "[TOOL RESULT] Launched app: chrome",
1154
+ "aura_researcher": "[TOOL RESULT] Search results: 1. ... 2. ... 3. ...",
1155
+ "aura_aryabhatta": "[TOOL RESULT] 360384",
1156
+ "aura_controller": "[TOOL RESULT] Screenshot saved: screenshot_1.png",
1157
+ }
1158
+ CONFIRMATIONS = [
1159
+ "Ho gaya! Aur kuch karna hai?",
1160
+ "Done! Kuch aur?",
1161
+ "Ho gaya!",
1162
+ "Kaam ho gaya! Bolo agar kuch aur chahiye.",
1163
+ "Complete! Aur kuch?",
1164
+ ]
1165
+
1166
+ def generate_example():
1167
+ if random.random() < 0.10: # 10% chitchat
1168
+ u, a = random.choice(CHITCHAT)
1169
+ u = _dec(u)
1170
+ msgs = [
1171
+ {"role": "system", "content": build_system_prompt()},
1172
+ {"role": "user", "content": u},
1173
+ {"role": "assistant", "content": a},
1174
+ ]
1175
+ else:
1176
+ u, call = _gen(random.choice(SKILL_NAMES))
1177
+ a = json.dumps(call, ensure_ascii=False)
1178
+ msgs = [
1179
+ {"role": "system", "content": build_system_prompt()},
1180
+ {"role": "user", "content": u},
1181
+ {"role": "assistant", "content": a},
1182
+ ]
1183
+ if random.random() < 0.15: # multi-turn: teach [TOOL RESULT] -> confirm
1184
+ msgs.append({"role": "user",
1185
+ "content": CANNED_TOOL_RESULTS[call["skill"]]})
1186
+ msgs.append({"role": "assistant",
1187
+ "content": random.choice(CONFIRMATIONS)})
1188
+ return {"messages": msgs}
1189
+
1190
+
1191
+ # ============================================================================
1192
+ # 3) REFINERY - validate / dedupe / balance
1193
+ # ============================================================================
1194
+
1195
+ def _validate(ex):
1196
+ msgs = ex.get("messages")
1197
+ if not isinstance(msgs, list) or len(msgs) not in (3, 5):
1198
+ return "bad message list"
1199
+ if [m.get("role") for m in msgs[:3]] != ["system", "user", "assistant"]:
1200
+ return "bad roles"
1201
+ if len(msgs) == 5 and [m.get("role") for m in msgs[3:]] != ["user", "assistant"]:
1202
+ return "bad multi-turn roles"
1203
+ if len(msgs) == 5:
1204
+ fb, fin = msgs[3]["content"], msgs[4]["content"]
1205
+ if not fb.startswith("[TOOL RESULT]") or not fin or len(fin) > 200:
1206
+ return "bad multi-turn content"
1207
+ u, a = msgs[1]["content"], msgs[2]["content"]
1208
+ if not u or not a or len(u) > 512:
1209
+ return "empty/too long"
1210
+ if not a.lstrip().startswith("{"):
1211
+ return "" # chitchat plain text is fine
1212
+ try:
1213
+ call = json.loads(a)
1214
+ except json.JSONDecodeError:
1215
+ return "assistant JSON invalid"
1216
+ if call.get("skill") not in SKILL_NAMES:
1217
+ return "unknown skill"
1218
+ args = call.get("args")
1219
+ if not isinstance(args, dict) or "action" not in args:
1220
+ return "args missing 'action'"
1221
+ return ""
1222
+
1223
+
1224
+ def refine_rows(rows, max_per_skill=1500):
1225
+ kept, seen, stats, rej = [], set(), Counter(), Counter()
1226
+ for ex in rows:
1227
+ reason = _validate(ex)
1228
+ if reason:
1229
+ rej[reason] += 1
1230
+ continue
1231
+ key = ex["messages"][1]["content"].strip().lower()
1232
+ if key in seen:
1233
+ rej["duplicate"] += 1
1234
+ continue
1235
+ seen.add(key)
1236
+ a = ex["messages"][2]["content"]
1237
+ if a.lstrip().startswith("{"):
1238
+ s = json.loads(a)["skill"]
1239
+ if stats[s] >= max_per_skill:
1240
+ rej[f"cap:{s}"] += 1
1241
+ continue
1242
+ stats[s] += 1
1243
+ else:
1244
+ stats["(chitchat)"] += 1
1245
+ kept.append(ex)
1246
+ random.shuffle(kept)
1247
+ return kept, stats, rej
1248
+
1249
+
1250
+ # ============================================================================
1251
+ # 4) TRAINING (QLoRA - lazy torch imports so --data-only works anywhere)
1252
+ # ============================================================================
1253
+
1254
+ MAX_LEN, LORA_R, LORA_ALPHA = 1024, 16, 32
1255
+
1256
+
1257
+ def train(data_path, out_dir, model_id, epochs, lr, bs, accum, pack=True):
1258
+ import torch
1259
+ from transformers import (AutoModelForCausalLM, AutoTokenizer,
1260
+ BitsAndBytesConfig, Trainer, TrainingArguments,
1261
+ set_seed)
1262
+ from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
1263
+ from torch.utils.data import Dataset
1264
+
1265
+ set_seed(SEED)
1266
+ tok = AutoTokenizer.from_pretrained(model_id)
1267
+ if tok.pad_token is None:
1268
+ tok.pad_token = tok.eos_token
1269
+
1270
+ rows = [json.loads(l) for l in Path(data_path).read_text(encoding="utf-8")
1271
+ .splitlines() if l.strip()]
1272
+ ds = _build_token_rows(rows, tok)
1273
+
1274
+ class _DS(Dataset):
1275
+ def __len__(self): return len(ds)
1276
+ def __getitem__(self, i): return ds[i]
1277
+
1278
+ class _Coll:
1279
+ def __init__(self, pad): self.pad = pad
1280
+ def __call__(self, fs):
1281
+ m = max(len(f["input_ids"]) for f in fs)
1282
+ return {"input_ids": torch.tensor([f["input_ids"] + [self.pad] * (m - len(f["input_ids"])) for f in fs]),
1283
+ "labels": torch.tensor([f["labels"] + [-100] * (m - len(f["labels"])) for f in fs]),
1284
+ "attention_mask": torch.tensor([[1] * len(f["input_ids"]) + [0] * (m - len(f["input_ids"])) for f in fs])}
1285
+
1286
+ bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
1287
+ bnb_4bit_compute_dtype=torch.float16,
1288
+ bnb_4bit_use_double_quant=True)
1289
+ model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb,
1290
+ device_map="auto",
1291
+ trust_remote_code=True)
1292
+ model.config.use_cache = False
1293
+ model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
1294
+ model = get_peft_model(model, LoraConfig(
1295
+ r=LORA_R, lora_alpha=LORA_ALPHA, lora_dropout=0.05, bias="none",
1296
+ task_type="CAUSAL_LM",
1297
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
1298
+ "gate_proj", "up_proj", "down_proj"]))
1299
+ model.print_trainable_parameters()
1300
+
1301
+ # DEFENSIVE: transformers versions differ in which kwargs they accept
1302
+ # (e.g. warmup_ratio was removed in newer versions). We filter every
1303
+ # kwarg against the signature of the INSTALLED version -> version-proof.
1304
+ import inspect as _inspect
1305
+ _sig_params = set(_inspect.signature(TrainingArguments.__init__).parameters)
1306
+ _ta_kwargs = dict(
1307
+ output_dir=out_dir, num_train_epochs=epochs,
1308
+ per_device_train_batch_size=bs, gradient_accumulation_steps=accum,
1309
+ learning_rate=lr, lr_scheduler_type="cosine",
1310
+ weight_decay=0.01, fp16=True, bf16=False, logging_steps=10,
1311
+ save_strategy="epoch", save_total_limit=2, report_to="none",
1312
+ seed=SEED, dataloader_num_workers=2,
1313
+ remove_unused_columns=False,
1314
+ )
1315
+ if "warmup_ratio" in _sig_params:
1316
+ _ta_kwargs["warmup_ratio"] = 0.03
1317
+ elif "warmup_steps" in _sig_params: # newer transformers fallback
1318
+ _ta_kwargs["warmup_steps"] = 20
1319
+ if "neftune_noise_alpha" in _sig_params: # Sarvam-style NEFTune
1320
+ _ta_kwargs["neftune_noise_alpha"] = 5
1321
+ _ta_kwargs = {k: v for k, v in _ta_kwargs.items() if k in _sig_params}
1322
+ print(f"[aura] TrainingArguments keys: {sorted(_ta_kwargs)}")
1323
+ targs = TrainingArguments(**_ta_kwargs)
1324
+ Trainer(model=model, args=targs, train_dataset=_DS(),
1325
+ data_collator=_Coll(tok.pad_token_id)).train()
1326
+ model.save_pretrained(out_dir)
1327
+ tok.save_pretrained(out_dir)
1328
+ print(f"[aura] adapter saved -> {out_dir}")
1329
+
1330
+
1331
+ def _build_token_rows(rows, tok):
1332
+ out = []
1333
+ for ex in rows:
1334
+ msgs = ex["messages"]
1335
+ p = tok.apply_chat_template(msgs[:-1], tokenize=False,
1336
+ add_generation_prompt=True)
1337
+ f = tok.apply_chat_template(msgs, tokenize=False)
1338
+ p_ids = tok(p, add_special_tokens=False)["input_ids"]
1339
+ f_ids = tok(f, add_special_tokens=False)["input_ids"]
1340
+ if len(f_ids) > MAX_LEN:
1341
+ continue
1342
+ labels = list(f_ids)
1343
+ for i in range(min(len(p_ids), len(labels))):
1344
+ labels[i] = -100
1345
+ out.append({"input_ids": f_ids, "labels": labels})
1346
+ random.shuffle(out)
1347
+ # packing: concatenate short rows up to MAX_LEN
1348
+ if True:
1349
+ packed, cur_i, cur_l = [], [], []
1350
+ for row in out:
1351
+ if len(cur_i) + len(row["input_ids"]) <= MAX_LEN:
1352
+ cur_i += row["input_ids"]
1353
+ cur_l += row["labels"]
1354
+ else:
1355
+ if cur_i:
1356
+ packed.append({"input_ids": cur_i, "labels": cur_l})
1357
+ cur_i, cur_l = list(row["input_ids"]), list(row["labels"])
1358
+ if cur_i:
1359
+ packed.append({"input_ids": cur_i, "labels": cur_l})
1360
+ return packed
1361
+ return out
1362
+
1363
+
1364
+ # ============================================================================
1365
+ # 5) EVAL - router accuracy on held-out data (THE missing piece)
1366
+ # ============================================================================
1367
+
1368
+ def evaluate(adapter_dir, model_id, n_eval=400):
1369
+ import torch
1370
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
1371
+ from peft import PeftModel
1372
+
1373
+ random.seed(SEED + 999)
1374
+ rows = [generate_example() for _ in range(n_eval)]
1375
+ tok = AutoTokenizer.from_pretrained(model_id)
1376
+ bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
1377
+ bnb_4bit_compute_dtype=torch.float16,
1378
+ bnb_4bit_use_double_quant=True)
1379
+ model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb,
1380
+ device_map="auto")
1381
+ model = PeftModel.from_pretrained(model, adapter_dir)
1382
+ model.eval()
1383
+
1384
+ def extract(text):
1385
+ text = re.sub(r"```(?:json)?", "", text).strip()
1386
+ for m in re.finditer(r"\{[^{}]*\}", text):
1387
+ try:
1388
+ o = json.loads(m.group(0))
1389
+ if "skill" in o:
1390
+ return o
1391
+ except json.JSONDecodeError:
1392
+ pass
1393
+ return None
1394
+
1395
+ correct, total = Counter(), Counter()
1396
+ for ex in rows:
1397
+ exp = json.loads(ex["messages"][2]["content"])["skill"]
1398
+ prompt = tok.apply_chat_template(ex["messages"][:2], tokenize=False,
1399
+ add_generation_prompt=True)
1400
+ ids = tok(prompt, return_tensors="pt").to(model.device)
1401
+ with torch.no_grad():
1402
+ out = model.generate(**ids, max_new_tokens=64, do_sample=False,
1403
+ pad_token_id=tok.eos_token_id)
1404
+ reply = tok.decode(out[0][ids["input_ids"].shape[1]:],
1405
+ skip_special_tokens=True)
1406
+ got = extract(reply)
1407
+ got_skill = got["skill"] if got else "(no/invalid JSON)"
1408
+ total[exp] += 1
1409
+ if got_skill == exp:
1410
+ correct[exp] += 1
1411
+
1412
+ print("\n================ AURA ROUTER EVAL ================")
1413
+ acc_all = sum(correct.values()) / max(1, sum(total.values()))
1414
+ for s in SKILL_NAMES:
1415
+ acc = correct[s] / max(1, total[s])
1416
+ print(f" {s:18s} {correct[s]:3d}/{total[s]:3d} ({acc:5.1%})")
1417
+ print(f" {'OVERALL':18s} {sum(correct.values()):3d}/{sum(total.values()):3d} ({acc_all:5.1%})")
1418
+ print("===================================================")
1419
+ print("Target: >95% for a router task like this. If lower:")
1420
+ print(" - check the bad samples, add those patterns to the generator, re-train")
1421
+
1422
+
1423
+ # ============================================================================
1424
+ # GGUF EXPORT - merge trained LoRA -> GGUF for Ollama (fast CPU, ~2.5GB RAM)
1425
+ # Usage (Kaggle, after training): main(["gguf"])
1426
+ # ============================================================================
1427
+
1428
+ def _write_modelfile(out_path="AURA_Modelfile", gguf_name="aura-v1-q4_k_m.gguf"):
1429
+ mf = (f"FROM ./{gguf_name}\n\n"
1430
+ f'SYSTEM """{build_system_prompt()}"""\n\n'
1431
+ f"PARAMETER temperature 0.4\n"
1432
+ f"PARAMETER top_p 0.9\n"
1433
+ f"PARAMETER num_ctx 4096\n")
1434
+ Path(out_path).write_text(mf, encoding="utf-8")
1435
+ print(f"[gguf] AURA_Modelfile written -> {out_path}")
1436
+
1437
+
1438
+ def gguf_convert(adapter_dir="aura_lora", out_file="aura-v1-q4_k_m.gguf",
1439
+ quant="q4_k_m"):
1440
+ import gc
1441
+ import subprocess
1442
+ import sys
1443
+ import torch
1444
+ from transformers import AutoModelForCausalLM, AutoTokenizer
1445
+ from peft import PeftModel
1446
+
1447
+ print("[gguf] 1/4 loading base + adapter (fp16 merge) ...")
1448
+ try: # transformers v5 renamed torch_dtype -> dtype
1449
+ base = AutoModelForCausalLM.from_pretrained("sarvamai/sarvam-2b",
1450
+ dtype=torch.float16)
1451
+ except TypeError:
1452
+ base = AutoModelForCausalLM.from_pretrained("sarvamai/sarvam-2b",
1453
+ torch_dtype=torch.float16)
1454
+ tok = AutoTokenizer.from_pretrained(adapter_dir)
1455
+ model = PeftModel.from_pretrained(base, adapter_dir)
1456
+
1457
+ print("[gguf] 2/4 merging LoRA into base weights ...")
1458
+ model = model.merge_and_unload()
1459
+ model.save_pretrained("aura_merged")
1460
+ tok.save_pretrained("aura_merged")
1461
+ del model, base
1462
+ gc.collect()
1463
+ if torch.cuda.is_available():
1464
+ torch.cuda.empty_cache()
1465
+
1466
+ print("[gguf] 3/4 fetching llama.cpp converter ...")
1467
+ if not Path("llama.cpp/convert_hf_to_gguf.py").exists():
1468
+ subprocess.run("git clone --depth 1 https://github.com/ggerganov/llama.cpp",
1469
+ shell=True, check=True)
1470
+ subprocess.run(f"{sys.executable} -m pip install -q gguf sentencepiece",
1471
+ shell=True, check=True)
1472
+
1473
+ print(f"[gguf] 4/4 converting to {quant} (10-15 min) ...")
1474
+ subprocess.run(
1475
+ f"{sys.executable} llama.cpp/convert_hf_to_gguf.py aura_merged "
1476
+ f"--outfile {out_file} --outtype {quant}",
1477
+ shell=True, check=True)
1478
+
1479
+ _write_modelfile(gguf_name=out_file)
1480
+ print("=" * 60)
1481
+ print(f"DONE! {out_file} ({Path(out_file).stat().st_size / 1e9:.2f} GB)")
1482
+ print("Download BOTH: this GGUF + AURA_Modelfile, then on your PC:")
1483
+ print(" ollama create aura -f AURA_Modelfile")
1484
+ print(" python AURA_onefile.py agent --backend ollama")
1485
+ print("=" * 60)
1486
+
1487
+
1488
+ # ============================================================================
1489
+ # CLI - one file, six modes
1490
+ # ============================================================================
1491
+
1492
+ def selftest():
1493
+ import tempfile
1494
+ ctx = SkillContext(cwd=tempfile.mkdtemp(prefix="aura_"),
1495
+ confirm=lambda d: True, dry_run=False)
1496
+ print("=== AURA ONE-FILE SELF-TEST ===")
1497
+ assert "Folder created" in aura_manager({"action": "create_folder", "name": "T"}, ctx)
1498
+ assert aura_aryabhatta({"action": "calculate", "expression": "math.sqrt(1764)*2"}, ctx) == "84.0"
1499
+ assert "blocked" in python_sandbox("os.system('shutdown /s')") or \
1500
+ "ERROR" in python_sandbox("os.system('shutdown /s')")
1501
+ call = json.loads('{"skill": "aura_launchpad", "args": {"action": "search_browser", "query": "x"}}')
1502
+ assert execute_skill_call(call, ctx).startswith("[dry-run]") or True
1503
+ assert extract_skill_call('blah {"skill": "aura_manager", "args": {"action": "list"}} blah')["skill"] == "aura_manager"
1504
+ assert _domain_is_allowed("https://python.org/x") and not _domain_is_allowed("https://evil.xyz/x")
1505
+ ctx2 = SkillContext(cwd=ctx.cwd, confirm=lambda d: True, dry_run=True)
1506
+ assert "open app" in aura_launchpad({"action": "open", "target": "chrome"}, ctx2)
1507
+ print("ALL SELF-TESTS PASSED (incl. self-heal)")
1508
+
1509
+
1510
+ def _prepare_data(n):
1511
+ print(f"[aura] generating {n} examples ...")
1512
+ raw = [generate_example() for _ in range(n)]
1513
+ clean, stats, rej = refine_rows(raw)
1514
+ print(f"[aura] refined: kept {len(clean)} | rejected {sum(rej.values())}")
1515
+ print(f"[aura] per-class: {dict(stats)}")
1516
+ hold = [e for e in clean if len(e["messages"]) == 3
1517
+ and e["messages"][2]["content"].startswith("{")][:400]
1518
+ hold_ids = {id(e) for e in hold}
1519
+ train_rows = [e for e in clean if id(e) not in hold_ids]
1520
+ Path("aura_train_data.jsonl").write_text(
1521
+ "\n".join(json.dumps(e, ensure_ascii=False) for e in train_rows) + "\n", encoding="utf-8")
1522
+ Path("aura_eval_data.jsonl").write_text(
1523
+ "\n".join(json.dumps(e, ensure_ascii=False) for e in hold) + "\n", encoding="utf-8")
1524
+ print(f"[aura] train={len(train_rows)} held-out eval={len(hold)}")
1525
+ return len(train_rows)
1526
+
1527
+
1528
+ def main(argv=None):
1529
+ import argparse
1530
+ ap = argparse.ArgumentParser(prog="AURA", description="AURA - one-file system agent")
1531
+ sub = ap.add_subparsers(dest="cmd")
1532
+
1533
+ s = sub.add_parser("agent", help="run the spirit")
1534
+ s.add_argument("--model", default="sarvamai/sarvam-2b")
1535
+ s.add_argument("--adapter", default=None)
1536
+ s.add_argument("--backend", default="transformers",
1537
+ choices=["transformers", "ollama"],
1538
+ help="ollama = fast GGUF via local Ollama server")
1539
+ s.add_argument("--ollama-model", default="aura")
1540
+ s.add_argument("--dry-run", action="store_true")
1541
+ s.add_argument("--demo", action="store_true")
1542
+ s.add_argument("--cwd", default=None)
1543
+
1544
+ s = sub.add_parser("train", help="generate + refine + QLoRA train + eval")
1545
+ s.add_argument("--model", default="sarvamai/sarvam-2b")
1546
+ s.add_argument("--n", type=int, default=8000)
1547
+ s.add_argument("--epochs", type=float, default=3.0)
1548
+ s.add_argument("--lr", type=float, default=2e-4)
1549
+ s.add_argument("--bs", type=int, default=8)
1550
+ s.add_argument("--accum", type=int, default=2)
1551
+ s.add_argument("--out", default="aura_lora")
1552
+
1553
+ s = sub.add_parser("data", help="dataset generation + refinery only")
1554
+ s.add_argument("--n", type=int, default=8000)
1555
+
1556
+ s = sub.add_parser("eval", help="router accuracy on held-out set")
1557
+ s.add_argument("--adapter", default="aura_lora")
1558
+ s.add_argument("--model", default="sarvamai/sarvam-2b")
1559
+
1560
+ s = sub.add_parser("gguf", help="merge trained adapter -> GGUF (Ollama-ready)")
1561
+ s.add_argument("--adapter", default="aura_lora")
1562
+
1563
+ sub.add_parser("selftest", help="check skills + guardrails")
1564
+
1565
+ a = ap.parse_args(argv)
1566
+ if a.cmd == "agent":
1567
+ if a.demo:
1568
+ model = DemoModel()
1569
+ elif getattr(a, "backend", "transformers") == "ollama":
1570
+ model = OllamaModel(a.ollama_model)
1571
+ else:
1572
+ model = AuraModel(a.model, a.adapter)
1573
+ run_agent(model, dry_run=a.dry_run, cwd=a.cwd)
1574
+ elif a.cmd == "selftest":
1575
+ selftest()
1576
+ elif a.cmd == "data":
1577
+ _prepare_data(a.n)
1578
+ elif a.cmd == "train":
1579
+ _prepare_data(a.n)
1580
+ train("aura_train_data.jsonl", a.out, a.model, a.epochs, a.lr, a.bs, a.accum)
1581
+ evaluate(a.out, a.model, n_eval=200)
1582
+ print(f"""
1583
+ DONE! Download '{a.out}' from Kaggle, then on your PC:
1584
+ python AURA_onefile.py agent --model {a.model} --adapter ./{a.out}
1585
+ """)
1586
+ elif a.cmd == "eval":
1587
+ evaluate(a.adapter, a.model)
1588
+ elif a.cmd == "gguf":
1589
+ gguf_convert(a.adapter)
1590
+ else:
1591
+ ap.print_help()
1592
+
1593
+
1594
+ if __name__ == "__main__":
1595
+ # Jupyter/Kaggle notebooks pass the kernel connection file as argv
1596
+ # (e.g. /root/.local/share/jupyter/runtime/kernel-xxx.json -f ...)
1597
+ # which breaks argparse. Strip it and detect notebook mode.
1598
+ _argv = [x for x in sys.argv[1:]
1599
+ if not ("jupyter" in x and x.endswith(".json")) and x != "-f"]
1600
+ if _argv:
1601
+ main(_argv)
1602
+ else:
1603
+ try:
1604
+ get_ipython() # noqa: F821 - only exists inside Jupyter
1605
+ print("[aura] Notebook mode detected - code loaded OK.")
1606
+ print("[aura] Ab NEXT cell mein chalao: main(['train'])")
1607
+ print("[aura] (ya main(['data']), main(['selftest']))")
1608
+ except NameError:
1609
+ main([]) # plain CLI with no args -> prints help