AQIT 0.1.0
Loading...
Searching...
No Matches
bridge.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6"""
7Tool dispatch bridge: call(tool_name, args, ctx) -> dict.
8Routes tool names to their compute implementations with no HTTP, no tab_queue,
9and no gpu_executor. All GPU imports happen lazily inside handler functions so
10this module imports cleanly even without a GPU or heavy ML deps installed.
11
12ctx must contain: session_id, api_key, base_url, state (dict with activeModelId, memory).
13"""
14from __future__ import annotations
15
16from typing import Any, Callable
17
18# ---------------------------------------------------------------------------
19# In-process sandbox state store (dataset/config tools)
20# ---------------------------------------------------------------------------
21_SANDBOX_STATE: dict[str, Any] = {}
22
23# ---------------------------------------------------------------------------
24# Helpers
25# ---------------------------------------------------------------------------
26
27def _get_model_id(ctx: dict, args: dict) -> str:
28 from aquin.compute.model_loader import get_active_model_id
29
30 state = ctx.get("state", {})
31 model_id = args.get("model_id") or state.get("activeModelId") or get_active_model_id()
32 if not model_id:
33 raise ValueError("No model loaded for this session. Run: aquin load --model <id>")
34 return model_id
35
36
37def _as_str_list(value: Any, *, fallback: list[str] | None = None) -> list[str]:
38 """Normalize CLI/API list flags — never iterate a JSON string character-by-character."""
39 import json
40
41 if value is None:
42 return fallback or []
43 if isinstance(value, list):
44 return [str(v) for v in value]
45 if isinstance(value, str):
46 stripped = value.strip()
47 if stripped.startswith("["):
48 try:
49 parsed = json.loads(stripped)
50 if isinstance(parsed, list):
51 return [str(v) for v in parsed]
52 except json.JSONDecodeError:
53 pass
54 return [value]
55 return [str(value)]
56
57
58def _as_json_dict(value: Any) -> dict | None:
59 """Parse topics and other object-shaped CLI flags."""
60 import json
61
62 if value is None:
63 return None
64 if isinstance(value, dict):
65 return value
66 if isinstance(value, str):
67 stripped = value.strip()
68 if stripped.startswith("{"):
69 try:
70 parsed = json.loads(stripped)
71 if isinstance(parsed, dict):
72 return parsed
73 except json.JSONDecodeError:
74 pass
75 return None
76
77
78def _as_json_list(value: Any) -> list:
79 """Parse JSON array CLI flags (capabilities, evals, etc.)."""
80 import json
81
82 if value is None:
83 return []
84 if isinstance(value, list):
85 return value
86 if isinstance(value, str):
87 stripped = value.strip()
88 if stripped.startswith("["):
89 try:
90 parsed = json.loads(stripped)
91 if isinstance(parsed, list):
92 return parsed
93 except json.JSONDecodeError:
94 pass
95 return []
96
97
98def _get_model(ctx: dict):
99 """Return the loaded model, loading it if necessary."""
100 from aquin.compute.model_loader import get_active_model_id, get_loaded_model, load_model
101
102 model = get_loaded_model()
103 if model is None:
104 model_id = ctx.get("state", {}).get("activeModelId") or get_active_model_id()
105 if not model_id:
106 raise ValueError("No model loaded for this session. Run: aquin load --model <id>")
107 model = load_model(model_id)
108 return model
109
110
111def _not_impl(tool_name: str) -> dict:
112 return {"status": "not_implemented", "tool": tool_name}
113
114
115# ---------------------------------------------------------------------------
116# Individual handlers
117# ---------------------------------------------------------------------------
118
119def _run_steer_and_show(args: dict, ctx: dict) -> dict:
120 from aquin.compute.model_loader import resolve_model_id
121 from aquin.compute.steer_vector import run_steer_with_vector
122
123 model_id = _get_model_id(ctx, args)
124 try:
125 model_id = resolve_model_id(model_id)
126 except Exception as e:
127 return {"error": str(e)}
128
129 if args.get("eval") and (args.get("save") or args.get("out") or args.get("output_path")):
130 return {"error": "Cannot combine --eval with --save. Export the vector first, then steer --eval --vector <path>."}
131
132 vector_path = args.get("vector") or args.get("vector_path")
133 feature_idx_raw = args.get("feature_idx")
134 if vector_path:
135 feature_idx = int(feature_idx_raw) if feature_idx_raw is not None else None
136 elif feature_idx_raw is not None:
137 feature_idx = int(feature_idx_raw)
138 else:
139 return {"error": "Provide --feature_idx <n> or --vector <path> to a saved LAT file."}
140
141 steer_strength = float(args.get("steer_strength", args.get("strength", 20.0)))
142 explicit_prompt = args.get("prompt")
143 if explicit_prompt is not None:
144 prompt = str(explicit_prompt)
145 elif args.get("eval"):
146 prompt = None
147 else:
148 prompt = ctx.get("state", {}).get("memory", {}).get("lastPrompt") or "Hello"
149 layer = args.get("layer")
150 if layer is not None:
151 layer = int(layer)
152 max_new_tokens = int(args.get("max_new_tokens", 80))
153
154 return run_steer_with_vector(
155 model_id=model_id,
156 prompt=prompt,
157 steer_strength=steer_strength,
158 layer=layer,
159 feature_idx=feature_idx,
160 vector_path=vector_path,
161 feature_label=args.get("feature_label"),
162 max_new_tokens=max_new_tokens,
163 ctx=ctx,
164 args=args,
165 )
166
167
168def _extract_steer_vector(args: dict, ctx: dict) -> dict:
169 from aquin.compute.model_loader import resolve_model_id
170 from aquin.compute.steer_vector import extract_steer_vector
171
172 model_id = _get_model_id(ctx, args)
173 try:
174 model_id = resolve_model_id(model_id)
175 except Exception as e:
176 return {"error": str(e)}
177
178 output = args.get("save") or args.get("out") or args.get("output_path")
179 if not output:
180 return {"error": "--save <path> is required."}
181
182 feature_idx_raw = args.get("feature_idx")
183 if feature_idx_raw is None:
184 return {"error": "--feature_idx <n> is required."}
185
186 layer = args.get("layer")
187 try:
188 return extract_steer_vector(
189 model_id,
190 int(feature_idx_raw),
191 output,
192 layer=int(layer) if layer is not None else None,
193 feature_label=args.get("feature_label"),
194 probe_id=args.get("probe_id"),
195 )
196 except (ValueError, FileNotFoundError) as e:
197 return {"error": str(e)}
198 except Exception as e:
199 return {"error": f"steer --save failed: {e}"}
200
201
202def _run_multi_steer(args: dict, ctx: dict) -> dict:
203 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id, get_config
204 from aquin.compute.feature_analysis import load_sae, load_norm
205 import torch
207 model_id = _get_model_id(ctx, args)
208 try:
209 model_id = resolve_model_id(model_id)
210 except Exception as e:
211 return {"error": str(e)}
212
213 # features: list of {feature_idx, strength}
214 features = args.get("features", [])
215 if not features:
216 feature_idx = int(args.get("feature_idx", 0))
217 strength = float(args.get("steer_strength", args.get("strength", 20.0)))
218 features = [{"feature_idx": feature_idx, "strength": strength}]
219
220 prompt = args.get("prompt") or ctx.get("state", {}).get("memory", {}).get("lastPrompt") or "Hello"
221 layer = args.get("layer")
222
223 model = get_loaded_model()
224 if model is None:
225 try:
226 model = load_model(model_id)
227 except Exception as e:
228 return {"error": str(e)}
229
230 try:
231 sae = load_sae(model_id, layer)
232 except Exception as e:
233 return {"error": f"SAE load failed: {e}"}
234
235 cfg = get_config(model_id)
236 resolved_layer = layer if layer is not None else cfg["sae_layer"]
237 hook_name = f"blocks.{resolved_layer}.hook_resid_post"
238
239 from aquin.compute.causal_trace import run_chat, _format_prompt
240
241 try:
242 baseline = run_chat(prompt, model_id=model_id, max_new_tokens=80, temperature=0.0)
243 except Exception as e:
244 return {"error": f"Baseline generation failed: {e}"}
245
246 steer_vecs = []
247 for f in features:
248 fidx = int(f.get("feature_idx", 0))
249 s = float(f.get("strength", 20.0))
250 steer_vecs.append((
251 sae.W_dec[fidx].to(device=model.W_E.device, dtype=model.W_E.dtype),
252 s,
253 ))
254
255 def _multi_steer_hook(value, hook):
256 for vec, s in steer_vecs:
257 value = value + s * vec.unsqueeze(0).unsqueeze(0)
258 return value
259
260 try:
261 fmt_prompt = _format_prompt(model, prompt)
262 tokens = model.to_tokens(fmt_prompt)
263 cur = tokens
264 steered_token_ids = []
265 for _ in range(80):
266 with torch.no_grad():
267 out = model.run_with_hooks(cur, fwd_hooks=[(hook_name, _multi_steer_hook)])
268 next_id = int(out[0, -1].argmax().item())
269 if next_id == model.tokenizer.eos_token_id:
270 break
271 steered_token_ids.append(next_id)
272 cur = torch.cat([cur, torch.tensor([[next_id]], device=cur.device)], dim=1)
273 steered_response = model.tokenizer.decode(steered_token_ids, skip_special_tokens=True)
274 except Exception as e:
275 steered_response = f"[steer error: {e}]"
276
277 from aquin.compute.feature_analysis import format_feature_ref, resolve_feature_label
278 enriched_features = []
279 for f in features:
280 fidx = int(f.get("feature_idx", 0))
281 label = f.get("label") or resolve_feature_label(fidx, ctx=ctx, args=args, layer=layer)
282 enriched_features.append({
283 **f,
284 "feature_idx": fidx,
285 "label": label,
286 "feature_ref": format_feature_ref(fidx, label),
287 })
288
289 return {
290 "features": enriched_features,
291 "layer": resolved_layer,
292 "prompt": prompt,
293 "baseline_response": baseline,
294 "steered_response": steered_response,
295 }
296
297
298def _run_consistency_eval(args: dict, ctx: dict) -> dict:
299 from aquin.compute.evals import consistency_eval
300 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
301 model_id = _get_model_id(ctx, args)
302 try:
303 model_id = resolve_model_id(model_id)
304 except Exception as e:
305 return {"error": str(e)}
306 model = get_loaded_model()
307 if model is None:
308 model = load_model(model_id)
309 query = args.get("query") or args.get("prompt") or "Hello"
310 templates_raw = args.get("templates")
311 templates = _as_str_list(templates_raw) or None
312 return consistency_eval(query, model, templates=templates or None)
313
314
315def _run_suppression_eval(args: dict, ctx: dict) -> dict:
316 from aquin.compute.evals import suppression_eval
317 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
318 model_id = _get_model_id(ctx, args)
319 try:
320 model_id = resolve_model_id(model_id)
321 except Exception as e:
322 return {"error": str(e)}
323 model = get_loaded_model()
324 if model is None:
325 model = load_model(model_id)
326 topics = _as_json_dict(args.get("topics"))
327 return suppression_eval(model, topics=topics)
328
329
330def _run_boundary_eval(args: dict, ctx: dict) -> dict:
331 from aquin.compute.evals import boundary_eval
332 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
333 model_id = _get_model_id(ctx, args)
334 try:
335 model_id = resolve_model_id(model_id)
336 except Exception as e:
337 return {"error": str(e)}
338 model = get_loaded_model()
339 if model is None:
340 model = load_model(model_id)
341 prompts = _as_str_list(
342 args.get("prompts"),
343 fallback=[args.get("prompt", "Hello")],
344 )
345 if not prompts:
346 prompts = ["Hello"]
347 return boundary_eval(prompts, model)
348
349
350def _run_audit(args: dict, ctx: dict) -> dict:
351 from aquin.compute.evals import consistency_eval, suppression_eval, boundary_eval
352 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
353 model_id = _get_model_id(ctx, args)
354 try:
355 model_id = resolve_model_id(model_id)
356 except Exception as e:
357 return {"error": str(e)}
358 model = get_loaded_model()
359 if model is None:
360 model = load_model(model_id)
361 query = args.get("query") or args.get("prompt") or "Hello"
362 prompts = _as_str_list(args.get("prompts"), fallback=[query])
363 consistency = consistency_eval(query, model)
364 suppression = suppression_eval(model)
365 boundary = boundary_eval(prompts, model)
366 return {
367 "consistency": consistency,
368 "suppression": suppression,
369 "boundary": boundary,
370 "model_id": model_id,
371 }
372
373
374def _run_benchmarks_on_top_feature(args: dict, ctx: dict) -> dict:
375 from aquin.compute.interp_score import run_interp_score
376 from aquin.compute.feature_analysis import load_sae
377 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
378 model_id = _get_model_id(ctx, args)
379 try:
380 model_id = resolve_model_id(model_id)
381 except Exception as e:
382 return {"error": str(e)}
383 feature_idx = int(args.get("feature_idx", 0))
384 memory = ctx.get("state", {}).get("memory") or {}
385 layer_raw = args.get("layer")
386 if layer_raw is None and memory.get("lastSaeLayer") is not None:
387 layer = int(memory["lastSaeLayer"])
388 elif layer_raw is not None:
389 layer = int(layer_raw)
390 else:
391 layer = None
392 prompt = args.get("prompt") or memory.get("lastPrompt") or "Hello"
393 model = get_loaded_model()
394 if model is None:
395 try:
396 model = load_model(model_id)
397 except Exception as e:
398 return {"error": str(e)}
399 try:
400 sae = load_sae(model_id, layer)
401 except Exception as e:
402 return {"error": f"SAE load failed: {e}"}
403 from aquin.compute.openai_client import get_openai_client
404 client = get_openai_client(ctx)
405 interp = run_interp_score(
406 feature_idx, prompt, model, sae, client,
407 model_id=model_id, layer=layer,
408 )
409 from aquin.compute.feature_analysis import format_feature_ref
410 label = interp.get("label")
411 return {
412 "feature_idx": feature_idx,
413 "feature_ref": format_feature_ref(feature_idx, label),
414 "model_id": model_id,
415 "label": label,
416 "score": interp.get("score"),
417 "purity_score": interp.get("purity_score"),
418 "mui_score": interp.get("mui_score"),
419 "mui_per_position": interp.get("mui_per_position"),
420 "mui_mean_kl": interp.get("mui_mean_kl"),
421 "baseline_entropy": interp.get("baseline_entropy"),
422 "positive_mean": interp.get("positive_mean"),
423 "negative_mean": interp.get("negative_mean"),
424 "positive_examples": interp.get("positive_examples", []),
425 "negative_examples": interp.get("negative_examples", []),
426 "error": interp.get("error"),
427 }
428
429
430def _check_weights(args: dict, ctx: dict) -> dict:
431 from datetime import datetime, timezone
432
433 import torch
435 from aquin.compute.weight_rank import run_weight_rank_analysis
436 from aquin.compute.weight_trojans import run_weight_trojan_analysis
437 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
438
439 model_id = _get_model_id(ctx, args)
440 try:
441 model_id = resolve_model_id(model_id)
442 except Exception as e:
443 return {"error": str(e)}
444 try:
445 model = get_loaded_model()
446 if model is None:
447 model = load_model(model_id)
448 except Exception as exc:
449 from aquin.user_errors import friendly_message
450 return {"error": friendly_message(exc), "model_id": model_id}
451
452 layer_range = args.get("layer_range")
453 try:
454 trojan = run_weight_trojan_analysis(model, layer_range=layer_range)
455 except NotImplementedError as exc:
456 return {
457 "error": (
458 "Weight trojan scan is not supported for this model architecture. "
459 f"{exc}".strip()
460 ),
461 "model_id": model_id,
462 }
463 except Exception as _oom_exc:
464 from aquin.compute.device import empty_device_cache, is_oom_error
465 if not is_oom_error(_oom_exc):
466 raise
467 empty_device_cache()
468 return {
469 "error": (
470 "GPU ran out of memory during weight trojan scan. "
471 "The model already fills most VRAM — try unloading other processes "
472 "or run on a host with more headroom."
473 ),
474 "model_id": model_id,
475 }
476
477 if trojan.get("error"):
478 return {"error": trojan["error"], "model_id": model_id}
479
480 collapse_threshold = float(args.get("collapse_threshold") or 0.1)
481 try:
482 from aquin.compute.device import empty_device_cache
483 empty_device_cache()
484 rank = run_weight_rank_analysis(model, collapse_threshold=collapse_threshold)
485 except NotImplementedError as exc:
486 return {
487 "error": (
488 "Weight rank analysis is not supported for this model architecture. "
489 f"{exc}".strip()
490 ),
491 "model_id": model_id,
492 **trojan,
493 }
494 except Exception as _oom_exc:
495 from aquin.compute.device import empty_device_cache, is_oom_error
496 if not is_oom_error(_oom_exc):
497 raise
498 empty_device_cache()
499 return {
500 "error": (
501 "GPU ran out of memory during weight rank analysis. "
502 "Trojan scan completed; rank pass needs more free VRAM."
503 ),
504 "model_id": model_id,
505 **trojan,
506 }
507 rank["model_id"] = model_id
508
509 generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
510 return {
511 "model_id": model_id,
512 "generated_at": generated_at,
513 **trojan,
514 "rank": rank,
515 }
516
517
518def _normalize_umap_points(raw: list[dict]) -> list[dict]:
519 """Map bridge output to web UMAPExplorer shape (feature_idx, x/y/z in 0–1)."""
520 rows: list[dict] = []
521 for p in raw:
522 idx = p.get("feature_idx", p.get("i"))
523 if idx is None:
524 continue
525 try:
526 x, y = float(p["x"]), float(p["y"])
527 except (KeyError, TypeError, ValueError):
528 continue
529 z = float(p.get("z", 0))
530 rows.append({
531 "feature_idx": int(idx),
532 "x": x,
533 "y": y,
534 "z": z,
535 "label": p.get("label"),
536 })
537 if not rows:
538 return []
539 xs = [r["x"] for r in rows]
540 ys = [r["y"] for r in rows]
541 zs = [r["z"] for r in rows]
542 xmin, xmax = min(xs), max(xs)
543 ymin, ymax = min(ys), max(ys)
544 zmin, zmax = min(zs), max(zs)
545 xspan = max(xmax - xmin, 1e-9)
546 yspan = max(ymax - ymin, 1e-9)
547 zspan = max(zmax - zmin, 1e-9)
548 flat_z = zspan < 1e-9
549 out: list[dict] = []
550 for r in rows:
551 out.append({
552 "feature_idx": r["feature_idx"],
553 "x": (r["x"] - xmin) / xspan,
554 "y": (r["y"] - ymin) / yspan,
555 "z": 0.0 if flat_z else (r["z"] - zmin) / zspan,
556 **({"label": r["label"]} if r.get("label") else {}),
557 })
558 return out
559
560
561def _try_fetch_precomputed_umap(model_id: str, layer: int) -> list[dict] | None:
562 """Load precomputed 3D UMAP from the public SAE DB when available."""
563 try:
564 import os
565 import requests
566 from aquin.saes import list_saes
567
568 rows = list_saes(model=model_id)
569 match = next(
570 (r for r in rows if r.get("layer") == layer and r.get("umap_key")),
571 None,
572 )
573 if not match:
574 return None
575 base = os.environ.get("AQUIN_BASE_URL", "https://api.aquin.app").rstrip("/")
576 resp = requests.get(f"{base}/api/public/saes/{match['id']}/umap", timeout=120)
577 resp.raise_for_status()
578 data = resp.json()
579 raw = data.get("points") if isinstance(data, dict) else data
580 if not isinstance(raw, list) or not raw:
581 return None
582 return raw
583 except Exception:
584 return None
585
586
587def _compute_umap_points(model_id: str, layer: int | None) -> tuple[list[dict], int]:
588 """Project SAE decoder weights into 3D (matches /db precomputed UMAPs)."""
589 from aquin.compute.feature_analysis import load_sae
590 import torch
592 sae = load_sae(model_id, layer)
593 W = sae.W_dec.float().cpu().detach()
594 try:
595 import umap
596 reducer = umap.UMAP(n_components=3, random_state=42, n_neighbors=15)
597 embedding = reducer.fit_transform(W.numpy())
598 points = [
599 {"feature_idx": i, "x": float(r[0]), "y": float(r[1]), "z": float(r[2])}
600 for i, r in enumerate(embedding)
601 ]
602 except ImportError:
603 W_centered = W - W.mean(0)
604 _, _, Vt = torch.linalg.svd(W_centered, full_matrices=False)
605 proj = (W_centered @ Vt[:3].T).numpy()
606 points = [
607 {"feature_idx": i, "x": float(r[0]), "y": float(r[1]), "z": float(r[2])}
608 for i, r in enumerate(proj)
609 ]
610 except Exception as e:
611 raise RuntimeError(f"UMAP failed: {e}") from e
612 return points, len(points)
613
614
615def _ensure_umap_loaded(args: dict, ctx: dict) -> dict:
616 from aquin.compute.sync_slim import downsample_umap_points
617 from aquin.compute.model_loader import resolve_model_id, get_config
618
619 model_id = _get_model_id(ctx, args)
620 try:
621 model_id = resolve_model_id(model_id)
622 except Exception as e:
623 return {"error": str(e)}
624
625 cfg = get_config(model_id)
626 layer = args.get("layer")
627 resolved_layer = layer if layer is not None else cfg["sae_layer"]
628
629 source = "precomputed"
630 try:
631 raw_points = _try_fetch_precomputed_umap(model_id, resolved_layer)
632 if raw_points is None:
633 source = "computed"
634 raw_points, _ = _compute_umap_points(model_id, layer)
635 except Exception as e:
636 return {"error": str(e)}
637
638 normalized = _normalize_umap_points(raw_points)
639 sync_points = downsample_umap_points(normalized)
640 return {
641 "points": sync_points,
642 "n_features": len(normalized),
643 "n_points": len(sync_points),
644 "layer": resolved_layer,
645 "model_id": model_id,
646 "source": source,
647 }
648
649
650def _run_layer_analysis(args: dict, ctx: dict) -> dict:
651 from aquin.compute.layer_analysis import run_layer_analysis
652 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
653
654 model_id = _get_model_id(ctx, args)
655 try:
656 model_id = resolve_model_id(model_id)
657 except Exception as e:
658 return {"error": str(e)}
659 model = get_loaded_model()
660 if model is None:
661 model = load_model(model_id)
662 try:
663 return run_layer_analysis(model, {**args, "model_id": model_id})
664 except Exception as e:
665 return {"error": str(e)}
666
667
668def _run_sae_stats(args: dict, ctx: dict) -> dict:
669 import json
670 import os
671
672 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
673 from aquin.compute.sae_stats import run_sae_stats
674
675 model_id = (
676 args.get("model_id")
677 or ctx.get("state", {}).get("activeModelId")
678 or get_active_model_id()
679 )
680 if model_id:
681 try:
682 model_id = resolve_model_id(str(model_id))
683 args = {**args, "model_id": model_id}
684 except ValueError:
685 pass
686
687 result = run_sae_stats(args)
688 if result.get("error"):
689 return result
690
691 save_path = args.get("save")
692 if save_path:
693 from pathlib import Path
694 p = Path(os.path.expanduser(str(save_path)))
695 p.parent.mkdir(parents=True, exist_ok=True)
696 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
697 result["saved_to"] = str(p)
698 return result
699
700
701def _run_weight_diff(args: dict, ctx: dict) -> dict:
702 import json
703 import os
704
705 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
706 from aquin.compute.weight_diff import run_weight_diff_from_args
707
708 model_id = (
709 args.get("model_id")
710 or ctx.get("state", {}).get("activeModelId")
711 or get_active_model_id()
712 )
713 if model_id:
714 try:
715 model_id = resolve_model_id(str(model_id))
716 args = {**args, "model_id": model_id}
717 except ValueError:
718 pass
719
720 result = run_weight_diff_from_args(args)
721 if result.get("error"):
722 return result
723
724 save_path = args.get("save")
725 if save_path:
726 from pathlib import Path
727 p = Path(os.path.expanduser(str(save_path)))
728 p.parent.mkdir(parents=True, exist_ok=True)
729 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
730 result["saved_to"] = str(p)
731 return result
732
733
734def _run_merge_analysis(args: dict, ctx: dict) -> dict:
735 import json
736 import os
737
738 from aquin.compute.merge_analysis import run_merge_analysis_from_args
739 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
740
741 model_id = (
742 args.get("model_id")
743 or ctx.get("state", {}).get("activeModelId")
744 or get_active_model_id()
745 )
746 if model_id:
747 try:
748 model_id = resolve_model_id(str(model_id))
749 args = {**args, "model_id": model_id}
750 except ValueError:
751 pass
752
753 result = run_merge_analysis_from_args(args)
754 if result.get("error"):
755 return result
756
757 save_path = args.get("save")
758 if save_path:
759 from pathlib import Path
760 p = Path(os.path.expanduser(str(save_path)))
761 p.parent.mkdir(parents=True, exist_ok=True)
762 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
763 result["saved_to"] = str(p)
764 return result
765
766
767def _run_trajectory_analysis(args: dict, ctx: dict) -> dict:
768 import json
769 import os
770
771 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
772 from aquin.compute.trajectory_analysis import run_trajectory_analysis_from_args
773
774 model_id = (
775 args.get("model_id")
776 or ctx.get("state", {}).get("activeModelId")
777 or get_active_model_id()
778 )
779 if model_id:
780 try:
781 model_id = resolve_model_id(str(model_id))
782 args = {**args, "model_id": model_id}
783 except ValueError:
784 pass
785
786 result = run_trajectory_analysis_from_args(args)
787 if result.get("error"):
788 return result
789
790 save_path = args.get("save")
791 if save_path:
792 from pathlib import Path
793 p = Path(os.path.expanduser(str(save_path)))
794 p.parent.mkdir(parents=True, exist_ok=True)
795 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
796 result["saved_to"] = str(p)
797 return result
798
799
800def _run_residual_drift(args: dict, ctx: dict) -> dict:
801 import json
802 import os
803
804 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
805 from aquin.compute.residual_drift import run_residual_drift_from_args
806
807 model_id = (
808 args.get("model_id")
809 or ctx.get("state", {}).get("activeModelId")
810 or get_active_model_id()
811 )
812 if model_id:
813 try:
814 model_id = resolve_model_id(str(model_id))
815 args = {**args, "model_id": model_id}
816 except ValueError:
817 pass
818
819 result = run_residual_drift_from_args(args)
820 if result.get("error"):
821 return result
822
823 save_path = args.get("save")
824 if save_path:
825 from pathlib import Path
826 p = Path(os.path.expanduser(str(save_path)))
827 p.parent.mkdir(parents=True, exist_ok=True)
828 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
829 result["saved_to"] = str(p)
830 return result
831
832
833def _run_confidence_analysis(args: dict, ctx: dict) -> dict:
834 import json
835 import os
836
837 from aquin.compute.confidence_analysis import run_confidence_analysis_from_args
838 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
839
840 model_id = (
841 args.get("model_id")
842 or ctx.get("state", {}).get("activeModelId")
843 or get_active_model_id()
844 )
845 if model_id:
846 try:
847 model_id = resolve_model_id(str(model_id))
848 args = {**args, "model_id": model_id}
849 except ValueError:
850 pass
851
852 result = run_confidence_analysis_from_args(args)
853 if result.get("error"):
854 return result
855
856 save_path = args.get("save")
857 if save_path:
858 from pathlib import Path
859 p = Path(os.path.expanduser(str(save_path)))
860 p.parent.mkdir(parents=True, exist_ok=True)
861 p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
862 result["saved_to"] = str(p)
863 return result
864
865
866def _token_kl_divergence(clean_logits, perturbed_logits) -> float:
867 """KL(clean || perturbed) on a single token distribution — stable at zero mass."""
868 import torch.nn.functional as F
869
870 clean_log = F.log_softmax(clean_logits.float(), dim=-1)
871 pert_log = F.log_softmax(perturbed_logits.float(), dim=-1)
872 clean_prob = clean_log.exp()
873 kl = (clean_prob * (clean_log - pert_log)).sum()
874 val = float(kl.item())
875 return 0.0 if val != val else max(val, 0.0)
876
877
878def _run_perturbation_sensitivity(args: dict, ctx: dict) -> dict:
879 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id, get_config
880 from aquin.compute.feature_analysis import load_sae
881 import torch
882 model_id = _get_model_id(ctx, args)
883 try:
884 model_id = resolve_model_id(model_id)
885 except Exception as e:
886 return {"error": str(e)}
887 model = get_loaded_model()
888 if model is None:
889 model = load_model(model_id)
890 prompt = args.get("prompt") or "Hello world"
891 n_channels = int(args.get("n_channels", 10))
892 layer = args.get("layer")
893 cfg = get_config(model_id)
894 resolved_layer = layer if layer is not None else cfg["sae_layer"]
895 hook_name = f"blocks.{resolved_layer}.hook_resid_post"
896 tokens = model.to_tokens(prompt)
897 with torch.no_grad():
898 clean_logits = model(tokens)
899 clean_last = clean_logits[0, -1]
900 try:
901 sae = load_sae(model_id, resolved_layer)
902 n_features = sae.W_dec.shape[0]
903 except Exception:
904 n_features = 64
905 sae = None
906 results = []
907 for ch in range(min(n_channels, n_features)):
908 def _zero_hook(value, hook, ch_=ch):
909 if sae is not None:
910 direction = sae.W_dec[ch_].to(device=value.device, dtype=value.dtype)
911 coeff = (value * direction).sum(dim=-1, keepdim=True)
912 return value - coeff * direction
913 value = value.clone()
914 value[..., ch_] = 0.0
915 return value
916
917 with torch.no_grad():
918 perturbed = model.run_with_hooks(tokens, fwd_hooks=[(hook_name, _zero_hook)])
919 kl = _token_kl_divergence(clean_last, perturbed[0, -1])
920 results.append({"channel": ch, "kl_divergence": round(kl, 6)})
921 results.sort(key=lambda x: x["kl_divergence"], reverse=True)
922
923 channels = [
924 {
925 "channel": r["channel"],
926 "kl_mean": r["kl_divergence"],
927 "kl_max": r["kl_divergence"],
928 "kl_std": 0.0,
929 "layer_contributions": [{"layer": resolved_layer, "kl": r["kl_divergence"]}],
930 }
931 for r in results
932 ]
933 kl_vals = [c["kl_mean"] for c in channels]
934 global_mean = sum(kl_vals) / len(kl_vals) if kl_vals else 0.0
935 global_max = max(kl_vals) if kl_vals else 0.0
936
937 return {
938 "prompt": prompt,
939 "method": args.get("method") or "dropout",
940 "n_channels_sampled": len(channels),
941 "d_model": model.cfg.d_model,
942 "channels": channels,
943 "global_mean_kl": round(global_mean, 6),
944 "global_max_kl": round(global_max, 6),
945 "threshold_critical": round(global_max * 0.85, 6) if global_max else 0.01,
946 "threshold_high": round(global_mean + (global_max - global_mean) * 0.5, 6) if kl_vals else 0.005,
947 "perturbation_results": results,
948 "layer": resolved_layer,
949 "model_id": model_id,
950 }
951
952
953def _run_attention_routing(args: dict, ctx: dict) -> dict:
954 from aquin.compute.attention_routing import run_attention_routing
955 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
956
957 model_id = _get_model_id(ctx, args)
958 try:
959 model_id = resolve_model_id(model_id)
960 except Exception as e:
961 return {"error": str(e)}
962 model = get_loaded_model()
963 if model is None:
964 model = load_model(model_id)
965 prompt = args.get("prompt") or args.get("text")
966 if not prompt or not str(prompt).strip():
967 return {"error": "Missing --prompt. Usage: aquin check attention --prompt <text>"}
968 prompt = str(prompt).strip()
969 top_k = int(args.get("top_k") or args.get("topk") or 5)
970 try:
971 return run_attention_routing(model, prompt, top_k=top_k, model_id=model_id)
972 except Exception as e:
973 return {"error": str(e)}
974
975
976def _run_find_feature(args: dict, ctx: dict) -> dict:
977 from aquin.compute.find_feature import run_find_feature
978 from aquin.compute.model_loader import resolve_model_id
979
980 model_id = _get_model_id(ctx, args)
981 try:
982 model_id = resolve_model_id(model_id)
983 except Exception as e:
984 return {"error": str(e)}
985
986 benchmark_top = int(args.get("benchmark_top") or args.get("benchmark_top_k") or 0)
987 openai_client = None
988 if benchmark_top > 0:
989 try:
990 from aquin.compute.openai_client import get_openai_client
991 openai_client = get_openai_client(ctx)
992 except Exception:
993 benchmark_top = 0
994
995 session_id = ctx.get("session_id") or (ctx.get("state") or {}).get("session_id")
996 checkpoint = args.get("checkpoint") or None
997 if checkpoint == "":
998 checkpoint = None
999
1000 try:
1001 return run_find_feature(
1002 model_id,
1003 scorer=str(args.get("scorer") or "deception"),
1004 prompts_path=args.get("prompts") or args.get("prompts_path"),
1005 layer=int(args["layer"]) if args.get("layer") is not None else None,
1006 checkpoint_path=checkpoint,
1007 top_k=int(args.get("top_k") or args.get("top") or 20),
1008 direction=str(args.get("direction") or "both"),
1009 conditioning=str(args.get("conditioning") or "behavior"),
1010 benchmark_top=benchmark_top,
1011 persist_key=args.get("persist") or None,
1012 session_id=str(session_id) if session_id else None,
1013 openai_client=openai_client,
1014 )
1015 except Exception as e:
1016 return {"error": str(e)}
1017
1018# ---------------------------------------------------------------------------
1019# Dataset / config state tools
1020# ---------------------------------------------------------------------------
1021
1022def _dataset_generate(args: dict, ctx: dict) -> dict:
1023 from pathlib import Path
1024
1025 from aquin.compute.dataset_generate import run_dataset_generate
1027 topic = str(args.get("topic") or "").strip()
1028 if not topic:
1029 return {"error": "Pass --topic <text> (e.g. aquin simulate --topic flowers)"}
1030
1031 try:
1032 count = int(args.get("count") or 5)
1033 except (TypeError, ValueError):
1034 return {"error": "count must be an integer"}
1035
1036 cwd = args.get("cwd") or ctx.get("cwd") or Path.cwd()
1037 output = args.get("save")
1038
1039 try:
1040 result = run_dataset_generate(
1041 topic=topic,
1042 count=count,
1043 cwd=cwd,
1044 output=str(output) if output else None,
1045 )
1046 except ValueError as e:
1047 return {"error": str(e)}
1048 except OSError as e:
1049 return {"error": f"Could not write dataset file: {e}"}
1050
1051 ds = _SANDBOX_STATE.setdefault("dataset", {})
1052 ds["rows"] = result["rows"]
1053 ds["topic"] = result["topic"]
1054 ds["path"] = result["path"]
1055
1056 return result
1057
1058
1059def _session_sim_kind_filter(ctx: dict) -> str | None:
1060 """LLM sessions list LLM sims."""
1061 return "simulate"
1062
1063def _assemble_simulation_result(events: list[dict]) -> dict:
1064 """Fold simulation SSE events into one result dict for compare/load."""
1065 skip = {"log", "state"}
1066 result: dict = {}
1067 for ev in events:
1068 key = ev.get("type")
1069 if not key or key in skip:
1070 continue
1071 payload = {k: v for k, v in ev.items() if k != "type"}
1072 if key == "meta":
1073 result.setdefault("meta", {}).update(payload)
1074 elif key == "step":
1075 loss = ev.get("loss")
1076 if loss is not None:
1077 result.setdefault("lossHistory", []).append(loss)
1078 elif key == "gradHeatmap":
1079 result.setdefault("gradHeatmap", []).append(payload)
1080 elif key == "signal":
1081 result.setdefault("signals", []).append({
1082 "signalType": ev.get("signalType", ""),
1083 "severity": ev.get("severity", ""),
1084 "message": ev.get("message", ""),
1085 "step": ev.get("step", 0),
1086 })
1087 else:
1088 result[key] = payload
1089 return result
1090
1091
1092def _persist_simulation_run(events: list[dict], meta: dict) -> str:
1093 """Save simulation output under ~/.aquin/runs/<id> for list/compare."""
1094 import json
1095 import uuid
1096 from datetime import datetime, timezone
1097 from pathlib import Path
1098
1099 run_id = uuid.uuid4().hex[:12]
1100 run_dir = Path.home() / ".aquin" / "runs" / run_id
1101 run_dir.mkdir(parents=True, exist_ok=True)
1102 result = _assemble_simulation_result(events)
1103 result.update({k: v for k, v in meta.items() if v is not None})
1104 (run_dir / "result.json").write_text(json.dumps(result, default=str))
1105 run_meta = {
1106 "run_id": run_id,
1107 "saved_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1108 **meta,
1109 }
1110 (run_dir / "meta.json").write_text(json.dumps(run_meta, default=str))
1111 return run_id
1112
1113
1114def _load_simulation_result(run_id: str) -> dict:
1115 import json
1116 from pathlib import Path
1117
1118 run_dir = Path.home() / ".aquin" / "runs" / run_id
1119 result_file = run_dir / "result.json"
1120 if result_file.exists():
1121 return json.loads(result_file.read_text())
1122 meta_file = run_dir / "meta.json"
1123 if meta_file.exists():
1124 return json.loads(meta_file.read_text())
1125 return {"status": "not_found", "run_id": run_id}
1126
1127
1128def _list_simulation_runs(args: dict, ctx: dict) -> dict:
1129 import json
1130 from datetime import datetime, timezone
1131 from pathlib import Path
1133 from aquin.compute.card_mapper import _simulation_method_from_meta
1134
1135 runs_dir = Path.home() / ".aquin" / "runs"
1136 if not runs_dir.exists():
1137 return {"runs": [], "count": 0}
1138
1139 entries: list[dict] = []
1140 kind_filter = _session_sim_kind_filter(ctx)
1141 for run_dir in runs_dir.iterdir():
1142 if not run_dir.is_dir():
1143 continue
1144 run_id = run_dir.name
1145 meta: dict = {}
1146 meta_file = run_dir / "meta.json"
1147 if meta_file.exists():
1148 try:
1149 meta = json.loads(meta_file.read_text())
1150 except json.JSONDecodeError:
1151 meta = {}
1152
1153 entry_kind = str(meta.get("kind") or "simulate")
1154 if kind_filter and entry_kind != kind_filter:
1155 continue
1156
1157 n_samples = None
1158 harmful_count = None
1159 method = None
1160 result_file = run_dir / "result.json"
1161 if result_file.exists():
1162 try:
1163 result = json.loads(result_file.read_text())
1164 dq = result.get("datasetQuality")
1165 if isinstance(dq, dict):
1166 n_samples = dq.get("nSamples")
1167 harmful_count = dq.get("harmfulCount")
1168 meta_block = result.get("meta")
1169 if isinstance(meta_block, dict) and meta_block.get("algoConfig"):
1170 method = _simulation_method_from_meta(meta_block)
1171 except json.JSONDecodeError:
1172 pass
1173
1174 saved_at = str(meta.get("saved_at") or "")
1175 if not saved_at:
1176 try:
1177 saved_at = datetime.fromtimestamp(
1178 run_dir.stat().st_mtime, tz=timezone.utc
1179 ).strftime("%Y-%m-%dT%H:%M:%SZ")
1180 except OSError:
1181 saved_at = ""
1182
1183 entries.append({
1184 "run_id": run_id,
1185 "saved_at": saved_at,
1186 "model_id": str(meta.get("model_id") or meta.get("modelId") or ""),
1187 "topic": meta.get("topic"),
1188 "dataset_path": meta.get("dataset_path") or meta.get("pairs_path"),
1189 "kind": entry_kind,
1190 "n_samples": n_samples,
1191 "harmful_count": harmful_count,
1192 "method": method or "LoRA",
1193 })
1194
1195 entries.sort(key=lambda e: e.get("saved_at") or "", reverse=True)
1196 return {"runs": entries, "count": len(entries)}
1197
1198
1199def _load_simulation_run(args: dict, ctx: dict) -> dict:
1200 run_id = args.get("run_id", "")
1201 data = _load_simulation_result(run_id)
1202 if data.get("status") == "not_found":
1203 return data
1204 return {"run_id": run_id, **data}
1205
1206
1207def _compare_simulations(args: dict, ctx: dict) -> dict:
1208 """Compare two simulation runs."""
1209 run_id_a = args.get("run_id_a") or args.get("run_id_before") or ""
1210 run_id_b = args.get("run_id_b") or args.get("run_id_after") or ""
1211 label_a = args.get("label_a", "Before")
1212 label_b = args.get("label_b", "After")
1213
1214 result_a = args.get("result_a") or (_load_simulation_result(run_id_a) if run_id_a else {})
1215 result_b = args.get("result_b") or (_load_simulation_result(run_id_b) if run_id_b else {})
1216
1217 if result_a.get("status") == "not_found" or result_b.get("status") == "not_found":
1218 missing = [
1219 rid for rid, data in ((run_id_a, result_a), (run_id_b, result_b))
1220 if data.get("status") == "not_found" and rid
1221 ]
1222 return {"error": f"Simulation run not found: {', '.join(missing)}"}
1223
1224 if not result_a or not result_b:
1225 return {
1226 "error": "Two simulation results required. Run simulate twice, then: "
1227 "aquin compare simulation --run_id_a <id> --run_id_b <id>"
1228 }
1229
1230 from aquin.compute.train_simulate import compare_simulation_results
1231 comparison = compare_simulation_results(
1232 result_a, result_b, label_a=label_a, label_b=label_b,
1233 run_id_a=run_id_a, run_id_b=run_id_b,
1234 )
1235 return {
1236 "run_id_a": run_id_a,
1237 "run_id_b": run_id_b,
1238 "label_a": label_a,
1239 "label_b": label_b,
1240 "comparison": comparison,
1241 "attack_surface": comparison.get("modelScores", {}),
1242 }
1243
1244
1245def _simulation_error(events: list) -> str | None:
1246 for ev in events:
1247 if ev.get("type") == "error" and ev.get("message"):
1248 return str(ev["message"])
1249 if ev.get("type") == "log" and str(ev.get("line", "")).startswith("ERROR:"):
1250 return str(ev["line"])[6:].strip()
1251 return None
1252
1253
1254
1255def _wrap_simulation_result(events: list, final: dict, meta: dict) -> dict:
1256 run_id = _persist_simulation_run(events, meta)
1257 assembled = _assemble_simulation_result(events)
1258 err = _simulation_error(events)
1259 return {
1260 "events": events,
1261 "result": assembled or final,
1262 "run_id": run_id,
1263 "status": "error" if err else "complete",
1264 **({"error": err} if err else {}),
1265 **{k: v for k, v in meta.items() if k not in ("events", "result", "run_id", "status", "error")},
1266 }
1267
1268
1269# ---------------------------------------------------------------------------
1270# Custom eval
1271# ---------------------------------------------------------------------------
1272
1273def _run_custom_eval(args: dict, ctx: dict) -> dict:
1274 from aquin.compute.evals import custom_eval
1275 from aquin.compute.model_loader import resolve_model_id
1276 model_id = _get_model_id(ctx, args)
1277 try:
1278 model_id = resolve_model_id(model_id)
1279 except Exception as e:
1280 return {"error": str(e)}
1281 scorer_type = args.get("scorer_type") or "semantic_similarity"
1282 if scorer_type != "semantic_similarity":
1283 return {
1284 "error": (
1285 "run_custom_eval only supports scorer_type=semantic_similarity. "
1286 "Use built-in evals or run custom logic outside Aquin."
1287 ),
1288 }
1289 prompts = _as_str_list(args.get("prompts"))
1290 reference_answers = _as_str_list(args.get("reference_answers"))
1291 return custom_eval(
1292 args.get("name") or "Custom eval",
1293 prompts,
1294 model_id,
1295 reference_answers=reference_answers,
1296 threshold=float(args.get("threshold", 0.5)),
1297 max_tokens=int(args.get("max_tokens", 40)),
1298 temperature=float(args.get("temperature", 0.0)),
1299 description=args.get("description"),
1300 )
1301
1302
1303# ---------------------------------------------------------------------------
1304# Card builders
1305# ---------------------------------------------------------------------------
1306
1307def _run_red_team(args: dict, ctx: dict) -> dict:
1308 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
1309 from aquin.compute.red_team import run_red_team
1310
1311 model_id = _get_model_id(ctx, args)
1312 try:
1313 model_id = resolve_model_id(model_id)
1314 except Exception as e:
1315 return {"error": str(e)}
1316
1317 # Product: always the full six-vector suite. Ignore partial vectors args
1318 # so agents/users never get "pick a vector" / unknown-vector loops.
1319 model = get_loaded_model()
1320 if model is None:
1321 try:
1322 model = load_model(model_id)
1323 except Exception as e:
1324 return {"error": str(e)}
1325
1326 return run_red_team(model, model_id, vectors=None)
1327
1328
1329def _run_logit_lens(args: dict, ctx: dict) -> dict:
1330 from aquin.compute.causal_trace import run_logit_lens
1331 from aquin.compute.model_loader import resolve_model_id
1332 model_id = _get_model_id(ctx, args)
1333 try:
1334 model_id = resolve_model_id(model_id)
1335 except Exception as e:
1336 return {"error": str(e)}
1337 prompt = args.get("prompt") or ctx.get("state", {}).get("memory", {}).get("lastPrompt") or "Hello"
1338 top_k = int(args.get("top_k", 5))
1339 try:
1340 results = run_logit_lens(prompt, model_id=model_id, top_k=top_k)
1341 return {"results": results}
1342 except Exception as e:
1343 return {"error": str(e)}
1344
1345
1346def _run_trace(args: dict, ctx: dict) -> dict:
1347 from aquin.compute.causal_trace import run_trace
1348 from aquin.compute.model_loader import resolve_model_id
1349 model_id = _get_model_id(ctx, args)
1350 try:
1351 model_id = resolve_model_id(model_id)
1352 except Exception as e:
1353 return {"error": str(e)}
1354 prompt = args.get("prompt") or ctx.get("state", {}).get("memory", {}).get("lastPrompt") or "Hello"
1355 target = args.get("target") or ""
1356 try:
1357 results = run_trace(prompt, target, model_id=model_id)
1358 return {"results": results}
1359 except Exception as e:
1360 return {"error": str(e)}
1361
1362
1363def _run_prompt_attribution(args: dict, ctx: dict) -> dict:
1364 from aquin.compute.causal_trace import run_prompt_attribution
1365 from aquin.compute.model_loader import resolve_model_id
1366 model_id = _get_model_id(ctx, args)
1367 try:
1368 model_id = resolve_model_id(model_id)
1369 except Exception as e:
1370 return {"error": str(e)}
1371 try:
1372 result = run_prompt_attribution(
1373 args.get("prompt", ""),
1374 args.get("response", ""),
1375 args.get("prompt_tokens", []),
1376 args.get("response_tokens", []),
1377 args.get("sig_prompt_tis", []),
1378 args.get("sig_response_tis", []),
1379 model_id=model_id,
1380 noise_scale=float(args.get("noise_scale", 3.0)),
1381 n_noise_runs=int(args.get("n_noise_runs", 5)),
1382 )
1383 return result
1384 except Exception as e:
1385 return {"error": str(e)}
1386
1387
1388def _run_full_inspection_bridge(args: dict, ctx: dict) -> dict:
1389 """Delegates to pipelines.run_full_inspection (same logic as inspect.py handler)."""
1390 from aquin.compute.pipelines import run_full_inspection as _pipeline
1391 from aquin.compute.model_loader import resolve_model_id
1392 state = ctx.get("state", {})
1393 prompt = args.get("prompt") or state.get("lastPrompt") or "Hello"
1394 model_id = _get_model_id(ctx, args)
1395 try:
1396 model_id = resolve_model_id(model_id)
1397 except Exception as e:
1398 return {"error": str(e)}
1399 layer_raw = args.get("layer")
1400 layer = int(layer_raw) if layer_raw is not None else None
1401 result = _pipeline(prompt, model_id, ctx, layer=layer)
1402 if "error" in result:
1403 return result
1404 return {
1405 "content": {
1406 "prompt": result["prompt"],
1407 "response": result["response"],
1408 "model_id": result["model_id"],
1409 "top_features": result["top_features"],
1410 "sae_layer": result["sae_layer"],
1411 },
1412 "card": {
1413 "type": "inspectionFull",
1414 "data": result,
1415 },
1416 }
1417
1418
1419def _get_feature_logits_bridge(args: dict, ctx: dict) -> dict:
1420 from aquin.compute.feature_analysis import get_feature_logits as _get_logits
1421 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
1422 model_id = _get_model_id(ctx, args)
1423 try:
1424 model_id = resolve_model_id(model_id)
1425 except Exception as e:
1426 return {"error": str(e)}
1427 feature_idx = int(args.get("feature_idx", 0))
1428 layer = args.get("layer")
1429 model = get_loaded_model()
1430 if model is None:
1431 try:
1432 model = load_model(model_id)
1433 except Exception as e:
1434 return {"error": str(e)}
1435 try:
1436 from aquin.compute.feature_analysis import enrich_feature_tool_result, prompt_for_labeling
1437 from aquin.compute.openai_client import get_openai_client
1438 result = _get_logits(feature_idx, model, model_id=model_id, layer=layer, top_k=int(args.get("top_k", 10)))
1439 enrich_feature_tool_result(
1440 result,
1441 prompt=prompt_for_labeling(ctx, args),
1442 model=model,
1443 client=get_openai_client(ctx),
1444 model_id=model_id,
1445 layer=layer,
1446 )
1447 return {"content": result}
1448 except Exception as e:
1449 return {"error": str(e)}
1450
1451
1452def _get_feature_neighbors_bridge(args: dict, ctx: dict) -> dict:
1453 from aquin.compute.feature_analysis import get_feature_neighbors as _get_neighbors
1454 from aquin.compute.model_loader import resolve_model_id
1455 model_id = _get_model_id(ctx, args)
1456 try:
1457 model_id = resolve_model_id(model_id)
1458 except Exception as e:
1459 return {"error": str(e)}
1460 feature_idx = int(args.get("feature_idx", 0))
1461 layer = args.get("layer")
1462 try:
1463 from aquin.compute.feature_analysis import enrich_feature_tool_result, prompt_for_labeling
1464 from aquin.compute.model_loader import get_loaded_model, load_model
1465 from aquin.compute.openai_client import get_openai_client
1466 result = _get_neighbors(feature_idx, model_id=model_id, layer=layer, top_k=int(args.get("top_k", 8)))
1467 model = get_loaded_model()
1468 if model is None:
1469 model = load_model(model_id)
1470 enrich_feature_tool_result(
1471 result,
1472 prompt=prompt_for_labeling(ctx, args),
1473 model=model,
1474 client=get_openai_client(ctx),
1475 model_id=model_id,
1476 layer=layer,
1477 label_neighbors=True,
1478 )
1479 return {"content": result}
1480 except Exception as e:
1481 return {"error": str(e)}
1482
1483
1484# ---------------------------------------------------------------------------
1485# SSE training launchers — synchronous wrappers for thread-based workers
1486# ---------------------------------------------------------------------------
1487# Each worker was designed for FastAPI SSE streaming. Here we run it in a
1488# thread with a real asyncio loop+queue, drain all emitted payloads, then
1489# return the final result dict. The caller (executor.py) pushes events to
1490# cloud via push_events after the call returns.
1491
1492def _drain_queue(worker_fn, req, extra_kwargs=None, on_event=None):
1493 """
1494 Run worker_fn(req, queue, loop) in a thread. Drain the queue until a
1495 sentinel {"__done__": True} payload arrives. Return (events, final_result).
1496 """
1497 import asyncio
1498 import threading
1499 import math
1500
1501 def _sanitize(obj):
1502 if isinstance(obj, float):
1503 return None if (math.isnan(obj) or math.isinf(obj)) else obj
1504 if isinstance(obj, dict):
1505 return {k: _sanitize(v) for k, v in obj.items()}
1506 if isinstance(obj, list):
1507 return [_sanitize(v) for v in obj]
1508 return obj
1509
1510 loop = asyncio.new_event_loop()
1511 queue: asyncio.Queue = asyncio.Queue()
1512
1513 def _run():
1514 if extra_kwargs:
1515 worker_fn(req, queue, loop, **extra_kwargs)
1516 else:
1517 worker_fn(req, queue, loop)
1518
1519 t = threading.Thread(target=_run, daemon=True)
1520 t.start()
1521
1522 events = []
1523 final = {}
1524
1525 async def _collect():
1526 nonlocal final
1527 while True:
1528 item = await queue.get()
1529 if item.get("__done__"):
1530 final = {k: v for k, v in item.items() if k != "__done__"}
1531 break
1532 events.append(_sanitize(item))
1533 if on_event:
1534 on_event(item)
1535
1536 loop.run_until_complete(_collect())
1537 t.join(timeout=5)
1538 loop.close()
1539 return events, final
1540
1541
1542def _build_simulate_req(args: dict, ctx: dict):
1543 from aquin.compute.train_simulate import SimulateRequest
1544 from aquin.compute.simulate_inputs import prepare_simulation_args
1545
1546 merged = prepare_simulation_args(args)
1547 sandbox = _SANDBOX_STATE
1548 rows = (
1549 merged.get("rows")
1550 or sandbox.get("dataset", {}).get("rows", [])
1551 )
1552
1553 model_id = _get_model_id(ctx, merged)
1554 try:
1555 from aquin.compute.model_loader import resolve_model_id
1556 model_id = resolve_model_id(model_id)
1557 except ValueError:
1558 pass
1559 return SimulateRequest(
1560 model_id=model_id,
1561 rank=int(merged.get("rank", 8)),
1562 alpha=int(merged.get("alpha", 16)),
1563 lr=float(merged.get("lr", 2e-4)),
1564 epochs=int(merged.get("epochs", 3)),
1565 dropout=float(merged.get("dropout", 0.05)),
1566 target_modules=merged.get("targetModules", []),
1567 warmup_steps=int(merged.get("warmupSteps", 0)),
1568 grad_clip=float(merged.get("gradClip", 1.0)),
1569 weight_decay=float(merged.get("weightDecay", 0.01)),
1570 grad_accum_steps=int(merged.get("gradAccumSteps", 1)),
1571 optimizer=merged.get("optimizer", "adamw"),
1572 scheduler=merged.get("scheduler", "cosine"),
1573 max_seq_len=int(merged.get("maxSeqLen", 128)),
1574 dataset=rows or [],
1575 use_qlora=bool(merged.get("useQlora", False)),
1576 use_rlhf=bool(merged.get("use_rlhf", False)),
1577 rlhf_beta=float(merged.get("rlhf_beta", 0.1)),
1578 )
1579
1580
1581def _print_simulate_progress(ev: dict) -> None:
1582 """Print key simulation metrics as they arrive (CLI live output)."""
1583 t = ev.get("type")
1584 if t == "datasetQuality":
1585 print(
1586 f"[simulate] dataset: {ev.get('nSamples', '?')} samples · "
1587 f"diversity={float(ev.get('diversityScore', 0)):.2f} · "
1588 f"harmful={ev.get('harmfulCount', 0)}",
1589 flush=True,
1590 )
1591 elif t == "step" and ev.get("loss") is not None:
1592 print(f"[simulate] step {ev.get('step', '?')} loss={float(ev['loss']):.4f}", flush=True)
1593 elif t == "saePrediction":
1594 feats = ev.get("topFeatures") or []
1595 strengthen = sum(1 for f in feats if f.get("direction") == "strengthen")
1596 suppress = sum(1 for f in feats if f.get("direction") == "suppress")
1597 print(
1598 f"[simulate] SAE prediction: {strengthen} strengthen · {suppress} suppress "
1599 f"({len(feats)} top features)",
1600 flush=True,
1601 )
1602 elif t == "saeDiff":
1603 print(
1604 f"[simulate] SAE diff: {ev.get('nChanged', '?')}/{ev.get('nFeatures', '?')} changed "
1605 f"mean|Δ|={ev.get('meanAbsDelta', '?')}",
1606 flush=True,
1607 )
1608 elif t == "influenceScores":
1609 method = ev.get("method", "lissa")
1610 print(
1611 f"[simulate] influence ({method}): {ev.get('nHelpful', 0)} helpful · "
1612 f"{ev.get('nHarmful', 0)} harmful samples",
1613 flush=True,
1614 )
1615 elif t == "lossSharpness":
1616 lam = ev.get("maxEigenvalue")
1617 lam_s = f"{float(lam):.2e}" if isinstance(lam, (int, float)) else "?"
1618 print(
1619 f"[simulate] loss landscape: {ev.get('sharpnessLabel', '?')} · λ={lam_s}",
1620 flush=True,
1621 )
1622 elif t == "modelDiff":
1623 parts: list[str] = []
1624 for label, key in (
1625 ("consistency", "consistencyScore"),
1626 ("suppression", "suppressionScore"),
1627 ("robustness", "robustnessScore"),
1628 ):
1629 val = ev.get(key)
1630 if isinstance(val, (int, float)):
1631 parts.append(f"{label}={val:.2f}")
1632 if parts:
1633 print(f"[simulate] attack surface: {' · '.join(parts)}", flush=True)
1634 elif t == "calibration":
1635 print(
1636 f"[simulate] calibration: base_ece={ev.get('base_ece')} "
1637 f"ft_ece={ev.get('ft_ece')} "
1638 f"low_conf={len(ev.get('low_confidence_rows') or [])}",
1639 flush=True,
1640 )
1641 elif t == "signal":
1642 print(f"[simulate] ⚠ {ev.get('message', ev.get('signalType', 'signal'))}", flush=True)
1643
1644
1645
1646
1647
1648
1649def _run_simulation(args: dict, ctx: dict) -> dict:
1650 """Full training simulation. Pass --dataset /path, --algo /path, or --topic."""
1651 session_model_id: str | None = None
1652 try:
1653 from aquin.compute.train_simulate import _run_simulation as _worker
1654 from aquin.compute.simulate_inputs import prepare_simulation_args
1655
1656 merged = prepare_simulation_args(args)
1657 rows = (
1658 merged.get("rows")
1659 or _SANDBOX_STATE.get("dataset", {}).get("rows", [])
1660 )
1661 topic = merged.get("topic")
1662 if not rows:
1663 if not topic:
1664 return {
1665 "error": "Pass --dataset /path/to/file, or aquin simulate --topic for a quick probe."
1666 }
1667 rows = [
1668 {"instruction": f"What is {topic}?", "response": f"A question about {topic}."},
1669 {"instruction": f"Explain {topic} simply.", "response": f"{topic} is a topic."},
1670 ]
1671 session_model_id = _get_model_id(ctx, merged)
1672 req = _build_simulate_req({**merged, "rows": rows}, ctx)
1673 session_model_id = req.model_id
1674 meta = {"model_id": req.model_id, "topic": topic, "kind": "simulate"}
1675 if merged.get("dataset_path"):
1676 meta["dataset_path"] = merged["dataset_path"]
1677 if merged.get("algo_path"):
1678 meta["algo_path"] = merged["algo_path"]
1679 events, final = _drain_queue(_worker, req, on_event=_print_simulate_progress)
1680 return _wrap_simulation_result(events, final, meta)
1681 except (FileNotFoundError, ValueError) as e:
1682 return {"error": str(e)}
1683 except Exception as e:
1684 return {"error": f"run_simulation failed: {e}"}
1685 finally:
1686 if session_model_id:
1687 from aquin.compute.vram_guard import restore_session_model
1688
1689 restore_session_model(session_model_id)
1690
1691
1692# ---------------------------------------------------------------------------
1693# TOOL_ROUTES
1694# ---------------------------------------------------------------------------
1695# run_full_inspection, get_feature_logits, get_feature_neighbors
1696# are also registered directly in inspect.py — bridge entries here are the
1697# canonical compute-only path used by bridge.call() and raw CLI (Step 57).
1698
1699TOOL_ROUTES: dict[str, Callable[[dict, dict], dict]] = {
1700 # Full inspection pipeline
1701 "run_full_inspection": _run_full_inspection_bridge,
1702 # Direct compute routes (server.py route index)
1703 "run_logit_lens": _run_logit_lens,
1704 "run_trace": _run_trace,
1705 "run_prompt_attribution": _run_prompt_attribution,
1706 # Feature drill-down
1707 "get_feature_logits": _get_feature_logits_bridge,
1708 "get_feature_neighbors": _get_feature_neighbors_bridge,
1709 # Feature analysis
1710 "run_steer_and_show": _run_steer_and_show,
1711 "extract_steer_vector": _extract_steer_vector,
1712 "run_multi_steer": _run_multi_steer,
1713 # Evals
1714 "run_consistency_eval": _run_consistency_eval,
1715 "run_suppression_eval": _run_suppression_eval,
1716 "run_boundary_eval": _run_boundary_eval,
1717 "run_audit": _run_audit,
1718 # Benchmarks / interp
1719 "run_benchmarks_on_top_feature": _run_benchmarks_on_top_feature,
1720 "run_find_feature": _run_find_feature,
1721 # Weights
1722 "check_weights": _check_weights,
1723 # UMAP
1724 "ensure_umap_loaded": _ensure_umap_loaded,
1725 # Model internals
1726 "run_layer_analysis": _run_layer_analysis,
1727 "run_sae_stats": _run_sae_stats,
1728 "run_confidence_analysis": _run_confidence_analysis,
1729 "run_weight_diff": _run_weight_diff,
1730 "run_merge_analysis": _run_merge_analysis,
1731 "run_trajectory_analysis": _run_trajectory_analysis,
1732 "run_residual_drift": _run_residual_drift,
1733 "run_perturbation_sensitivity": _run_perturbation_sensitivity,
1734 "run_attention_routing": _run_attention_routing,
1735 # Dataset / config state
1736 "dataset_generate": _dataset_generate,
1737 # SSE streaming simulation
1738 "run_simulation": _run_simulation,
1739 # Simulation run list
1740 "list_simulation_runs": _list_simulation_runs,
1741 "load_simulation_run": _load_simulation_run,
1742 "compare_simulations": _compare_simulations,
1743 # UI-only tools
1744 "run_red_team": _run_red_team,
1745 # Card builders
1746 "run_custom_eval": _run_custom_eval,
1747}
1748
1749
1750# ---------------------------------------------------------------------------
1751# Public API
1752# ---------------------------------------------------------------------------
1753
1754def call(tool_name: str, args: dict, ctx: dict) -> dict:
1755 """
1756 Dispatch a tool call to its compute implementation.
1757 Returns a result dict. Never raises — errors are returned as {"error": ...}.
1758 """
1759 handler = TOOL_ROUTES.get(tool_name)
1760 if handler is None:
1761 return {"error": f"Unknown tool: {tool_name}"}
1762 try:
1763 return handler(args, ctx)
1764 except Exception as exc:
1765 from aquin.user_errors import friendly_message, log_debug
1766 log_debug(tool_name, exc)
1767 return {"error": friendly_message(exc)}
dict _run_merge_analysis(dict args, dict ctx)
Definition bridge.py:738
dict call(str tool_name, dict args, dict ctx)
Definition bridge.py:1758
dict _run_trajectory_analysis(dict args, dict ctx)
Definition bridge.py:771
dict _run_steer_and_show(dict args, dict ctx)
Definition bridge.py:123
_drain_queue(worker_fn, req, extra_kwargs=None, on_event=None)
Definition bridge.py:1496
str|None _simulation_error(list events)
Definition bridge.py:1249
dict _check_weights(dict args, dict ctx)
Definition bridge.py:434
dict _run_prompt_attribution(dict args, dict ctx)
Definition bridge.py:1367
list _as_json_list(Any value)
Definition bridge.py:82
dict _run_perturbation_sensitivity(dict args, dict ctx)
Definition bridge.py:882
dict _run_simulation(dict args, dict ctx)
Definition bridge.py:1653
dict _compare_simulations(dict args, dict ctx)
Definition bridge.py:1211
dict _list_simulation_runs(dict args, dict ctx)
Definition bridge.py:1132
dict _run_attention_routing(dict args, dict ctx)
Definition bridge.py:957
dict _ensure_umap_loaded(dict args, dict ctx)
Definition bridge.py:619
dict _assemble_simulation_result(list[dict] events)
Definition bridge.py:1067
str _persist_simulation_run(list[dict] events, dict meta)
Definition bridge.py:1096
dict _run_residual_drift(dict args, dict ctx)
Definition bridge.py:804
tuple[list[dict], int] _compute_umap_points(str model_id, int|None layer)
Definition bridge.py:591
dict _run_logit_lens(dict args, dict ctx)
Definition bridge.py:1333
dict _run_trace(dict args, dict ctx)
Definition bridge.py:1350
dict _run_custom_eval(dict args, dict ctx)
Definition bridge.py:1277
dict|None _as_json_dict(Any value)
Definition bridge.py:62
dict _run_red_team(dict args, dict ctx)
Definition bridge.py:1311
dict _wrap_simulation_result(list events, dict final, dict meta)
Definition bridge.py:1259
None _print_simulate_progress(dict ev)
Definition bridge.py:1585
dict _not_impl(str tool_name)
Definition bridge.py:115
_build_simulate_req(dict args, dict ctx)
Definition bridge.py:1546
dict _load_simulation_run(dict args, dict ctx)
Definition bridge.py:1203
float _token_kl_divergence(clean_logits, perturbed_logits)
Definition bridge.py:870
dict _run_sae_stats(dict args, dict ctx)
Definition bridge.py:672
list[dict] _normalize_umap_points(list[dict] raw)
Definition bridge.py:522
dict _run_suppression_eval(dict args, dict ctx)
Definition bridge.py:319
dict _get_feature_logits_bridge(dict args, dict ctx)
Definition bridge.py:1423
dict _load_simulation_result(str run_id)
Definition bridge.py:1118
str _get_model_id(dict ctx, dict args)
Definition bridge.py:31
dict _dataset_generate(dict args, dict ctx)
Definition bridge.py:1026
list[dict]|None _try_fetch_precomputed_umap(str model_id, int layer)
Definition bridge.py:565
dict _run_layer_analysis(dict args, dict ctx)
Definition bridge.py:654
dict _run_multi_steer(dict args, dict ctx)
Definition bridge.py:206
dict _run_confidence_analysis(dict args, dict ctx)
Definition bridge.py:837
dict _run_boundary_eval(dict args, dict ctx)
Definition bridge.py:334
dict _get_feature_neighbors_bridge(dict args, dict ctx)
Definition bridge.py:1456
dict _run_find_feature(dict args, dict ctx)
Definition bridge.py:980
_get_model(dict ctx)
Definition bridge.py:102
str|None _session_sim_kind_filter(dict ctx)
Definition bridge.py:1063
dict _run_benchmarks_on_top_feature(dict args, dict ctx)
Definition bridge.py:378
dict _run_audit(dict args, dict ctx)
Definition bridge.py:354
dict _extract_steer_vector(dict args, dict ctx)
Definition bridge.py:172
dict _run_full_inspection_bridge(dict args, dict ctx)
Definition bridge.py:1392
dict _run_consistency_eval(dict args, dict ctx)
Definition bridge.py:302
list[str] _as_str_list(Any value, *, list[str]|None fallback=None)
Definition bridge.py:41
dict _run_weight_diff(dict args, dict ctx)
Definition bridge.py:705