AI Assistant (optional)¶
HELIX ships an optional natural-language assistant that can run simulations and matches, and answer questions about the results you have generated — driving the same audited code you use by hand. It is entirely opt-in:
Completely optional — the app never depends on it
With no API key and no network HELIX behaves exactly as it
always has. The assistant subpackage (linac_gen.assist) is
imported by nothing at startup; it uses only the Python standard
library (a small urllib HTTP client — no SDK dependency);
and it opens no network connection until you explicitly send it a
message. Deleting the assist/ directory leaves the rest of
HELIX fully working (there is a regression test that proves it).
Backends — cloud, keyless subscription, or fully local¶
The assistant supports three backends, chosen from your configuration (or the panel's backend selector):
- Claude (subscription login — no API key) — reasons through the
Claude Agent SDK, which reuses your existing Claude Code login;
no API key is read or stored. This is the in-GUI equivalent of the MCP
route below: real Claude, on your subscription, right in the panel.
Requires the optional
claude-agent-sdkand a logged-inclaudeCLI (pip install linac_gen[assist-sdk]; install Claude Code and log in once). Needs network for reasoning; no local-model fallback. - Anthropic Messages API (cloud) — set
HELIX_ASSIST_API_KEY(or the standardANTHROPIC_API_KEY). Pay-per-token; no SDK needed. - Any OpenAI-compatible endpoint — set
HELIX_ASSIST_BASE_URL. This covers a local model server such as ollama (http://localhost:11434/v1),llama.cpp, or vLLM, so the assistant can run fully offline with no API key, matching HELIX's offline ethos.
Pick by what you have: a Claude subscription + Claude Code → the first, keyless option; an API key → the second; no key and privacy/offline needs → a local server. All three drive the identical audited tools with the identical tier-based confirmation.
# cloud
export ANTHROPIC_API_KEY=sk-...
python -m linac_gen assist examples/piplattice/fnalscl.lgproj
# fully local (ollama), no key
export HELIX_ASSIST_BASE_URL=http://localhost:11434/v1
export HELIX_ASSIST_MODEL=llama3.1
python -m linac_gen assist examples/pipii/btl/btl.lgproj
Nothing configured? python -m linac_gen assist prints setup guidance
and exits — it never breaks.
What it can do — and what it will refuse¶
The assistant's entire capability is a fixed registry of tools, each a
thin wrapper over an existing HELIX API. There is no shell, no
eval, no arbitrary file access. Tools are tiered:
| Tier | Examples | Confirmation |
|---|---|---|
| read | status, lattice info, query a result value, beam-parameter table, provenance, matched input Twiss | auto |
| compute | run envelope / multiparticle / match, parameter scan, compare to TraceWin | confirmed (session auto-approve optional) |
| mutate | load a lattice, set a beam field or element parameter, write results | always confirmed |
Every confirmation echoes the exact resolved call with units before anything runs — important because HELIX's conventions are exacting (mm, deg·MeV, and α_z = −TraceWin's sign). Long simulations run in the background and return a job you can poll or cancel.
When you ask for something unphysical or unrepresentable, the assistant relays HELIX's own loud refusal (the same k2 / kaz-kbz / halo-CSR guards the manual documents elsewhere) rather than guessing.
Audit trail and replay¶
Every session writes a JSONL ledger under
<calc-dir>/assist_sessions/ recording each tool call, its exact
parameters, tier, who approved it, status and duration — never API
keys. Assistant-initiated result files get assist_* attributes in
their HDF5 provenance group, so any number the assistant produced is
traceable back to the exact ledger entry.
A ledger can be replayed with no LLM and no network, re-executing its approved compute/mutate calls in order — deterministic audit:
Using your Claude subscription (MCP server)¶
Anthropic's policy does not permit a third-party app to offer claude.ai / Pro-Max login as an in-app option, so the built-in panel above authenticates with an API key or a local model. To use your own Claude subscription instead, run HELIX as an MCP server and drive it from Claude Code or Claude Desktop, which are already logged in with your subscription:
# HELIX exposes its tools over stdio — no API key, no network from
# HELIX's side (auth lives entirely in the Claude client)
python -m linac_gen assist --mcp examples/pipii/btl/btl.lgproj
Register it with Claude Code (.mcp.json in your project, or the
global config):
{
"mcpServers": {
"helix": {
"command": "python",
"args": ["-m", "linac_gen", "assist", "--mcp"]
}
}
}
Then, inside Claude Code, ask it to "load the BTL lattice and run the
envelope" — Claude calls the HELIX tools with your subscription, and
Claude Code's own permission prompts handle confirmation (HELIX
annotates read tools readOnlyHint and mutate tools destructiveHint
so reads auto-approve and writes/runs ask first). In this mode HELIX
is a tool provider, not an auth broker — the sanctioned way to pair
a Claude subscription with the simulator. Requires the optional
mcp package: pip install linac_gen[assist-mcp].
GUI¶
The desktop app exposes the assistant under Tools → Assistant… as a non-modal panel. On first open a short capabilities card lists what it can do, and a row of quick-action chips sits above the input — 🎓 Tour (the 15-station guided walkthrough of HELIX), 🧩 Drill (a hidden-fault training exercise), 🐍 Python (sandboxed analysis of the current results), and ❓ What can you do — each one sends the corresponding request through the normal chat path, so nothing is hidden behind knowing-what-to-ask. While the brain and voice models warm up in the background, anything you type is queued, not lost.
Gradient sensitivities. On linear decks (e.g. the BTL) the
grad_sensitivities tool answers "which knob most affects σ_y at the
exit?" exactly: one torch autograd reverse pass over the
differentiable transfer-matrix path returns d(exit σ)/d(knob) for
every quad gradient, solenoid field and dipole angle at once, ranked
and signed. It refuses decks the differentiable path cannot represent
faithfully (field maps, RF gaps, FREQ/energy cards) rather than
returning silently-wrong numbers — use parameter_scan there.
Instant commands. Unambiguous read-only requests skip the model
entirely and execute in milliseconds: "status", "summarize the
results", "show the RMS plot", "switch to the Results tab", "list
runs" — and, during a guided tour, bare "next" / "back" / "stop the
tour". The call is ledgered like any other (approved-by
fast_path), the model is quietly told about it at its next turn,
and anything nuanced still goes to the model — a missed shortcut
costs one model turn, a wrong one would hijack your question, so the
matcher insists on exact phrasings and read-tier tools only. The
Instant commands checkbox turns the feature off. A small ⏱
latency readout in the status row shows time-to-first-token (or the
instant-command round trip) and speech-to-text time, so
responsiveness is a number, not a feeling.
The panel is a designed chat, not a log window: rendered markdown replies in role-styled cards (your turns accent-barred, tool traces and events as muted metadata), syntax-highlighted code blocks, and inline thumbnails of every figure the assistant looks at (click to open the saved capture). Around it: an animated state orb (idle · thinking · responding
· listening · awaiting-confirm · error) that pulses and changes colour
with what the assistant is doing, a live-streaming transcript, an input
box, a ■ Stop button, an approve/deny strip that shows the echoed
call, a live progress readout for long runs, and a settings affordance
for the provider/model/key (stored in the app's settings, never in the
ledger). The provider row hides once
connected — the backend… button next to the status line re-opens it
at any time, so you can switch between the keyless subscription login,
an Anthropic API key, or a local server and press Connect; the
running session is closed and replaced cleanly. The model box is an
editable dropdown that picks the model for whichever backend you chose:
it suggests Anthropic model IDs for the API-key backend (blank =
claude-sonnet-5; typed gateway aliases like azure/claude-sonnet-4-6
also work), the fable / opus / sonnet / haiku aliases for the
subscription login, and — for a local backend with a known server URL —
it asks the
server for its installed models (ollama's /v1/models) and lists
them. Free text always wins over the suggestions, and for a local
server you can still type URL, model, or URL model in one line
(e.g. http://localhost:11434/v1 qwen2.5; a bare model name reuses the
saved URL). Actions that change
session state flow through the app's normal state signals, so the tabs
stay in sync.
Hands-free voice. Beyond hold-to-talk (the 🎤 button or holding
SPACE with the input line unfocused), the 👂 HELIX toggle keeps
the microphone open — fully locally — and wakes the assistant when you
say "HELIX" (a rising chime confirms; common mishearings are
accepted, and detection pauses while the assistant itself speaks so it
can never wake itself). After it finishes speaking a reply to a
voice turn — and after every station of an active guided tour, no
matter how the tour was started — the mic reopens for ~10 s
(HELIX_FOLLOWUP_S, 0 disables) — just keep talking ("next", "back",
or a question), no wake word and no clicking needed. You can also talk over
it: sustained speech while it talks cuts the audio and takes the
floor (HELIX_BARGE_IN=0 disables). While the wake mic is armed,
push-to-talk records through the same stream — one microphone owner,
always.
Better hearing with silero-VAD (optional)
Drop the ~2 MB silero_vad.onnx model into
~/.helix/assistant_models/ (next to the kokoro voice files) and
every listening gate — wake detection, capture endpointing, the
follow-up window, barge-in — upgrades from RMS loudness thresholds
to a real speech probability: fans and typing stop triggering
anything, and barge-in fires only on something that is loud AND
speech (a dropped mug no longer cuts the assistant off; its own
speaker bleed is still separated by the level calibration).
With the model present, wake detection also becomes
event-driven: Whisper runs the moment your utterance ends
(VAD offset) instead of on a fixed 1-second cadence — saying
"HELIX" is recognized noticeably faster, and continuous speech is
still checked every ~2.5 s so the word is never missed
mid-sentence. Without the model, or with HELIX_VAD=0, the
original RMS gates and polling cadence are used unchanged.
Sleep/wake resilience
If macOS sleeps while a microphone is open, CoreAudio can leave that stream permanently wedged. HELIX detects this on teardown and parks the wedged stream (it is never closed underneath an in-flight read — doing so crashes the process) and simply opens a fresh one on the next voice action. If the stream instead dies (device change, unplugged headset, wake from sleep), the assistant notices, prints "microphone stream lost — reopening", and re-arms hands-free listening automatically; push-to-talk also falls back to its own fresh recording stream whenever the shared wake stream is not running. If voice still seems unresponsive, toggle 👂 HELIX off and on again.
Confirming by voice. When a compute/mutate confirmation appears with voice active, the assistant speaks a short echo and listens: say "yes"/"confirm"/"go ahead" or "no"/"cancel". The words are matched locally (the model is never part of the decision), "no" outranks "yes" in a mixed utterance, and anything unclear gets "please say confirm, or cancel" — a misheard word can never approve an action. Units are spoken as words (millimeters, M-e-V, pi millimeter milliradian), and exact digits always stay on screen.
Stopping a reply. The ■ Stop button — or Esc — interrupts the current turn: streaming halts, speech is cut, and on the keyless Claude backend the server-side generation itself is interrupted. The session stays usable; just ask again. (Esc used to close the panel; it now stops instead — close with the window button or Ctrl+W.)
Run watching (proactive). With Watch runs ticked (the
default), every finished run — assistant-initiated or one you
started yourself — is inspected instantly by pure local numpy:
transmission drops vs the previous run (loss onset localized in s),
σ blow-ups, ≥2× emittance growth, and drift off the stored anomaly
baseline. Alerts appear as ‣ [event] lines (and are spoken when
narration is on): "run watch: transmission fell 97.30 % → 95.00 %
(loss starts near s = 218.8 m)". Each channel fires once and re-arms
on recovery or when the level stabilizes, with a rate limit so scans
can't spam; a baseline from a different lattice is announced once,
never silently ignored. No tokens are spent until something fires.
Machine events & narration. Job completions and run-watch alerts
appear in the transcript as ‣ [event] lines the moment they happen.
With Narrate events ticked, an event arriving while the assistant is
idle gives it an unprompted turn to react — with Speak replies on,
it speaks up on its own ("the multiparticle run just finished, the
transmission held at 97.3 %"). Events are injected into the model as
clearly-labelled system notes, never as your words.
Confirmation timeout. An unanswered confirmation auto-denies after
120 s (AssistConfig.confirm_timeout_s; 0 waits forever) — a forgotten
prompt can't leave a worker blocked all night. The denial is reported
to the model like any other, and the strip closes with a note.
Sight — the assistant can SEE figures. Ask "look at the phase-space
plot — is the beam matched?" and it captures the rendered figure
(look_at_plot / look_at_screen) and analyses the image itself —
matching, halo, filamentation, oscillation patterns. Captures are saved
under <calc-dir>/assist_captures/. (Images reach the model on the
keyless Claude backend; the other backends receive the saved-file path.)
Press the Run buttons. Ask "run the multiparticle simulation" (or
envelope) and run_in_gui starts it exactly as clicking Run does —
the same Numerics-tab grid/steps/integrator/backend and recording
options, the same progress bar, results into the same tabs. (The
run_envelope / run_mp tools still exist for headless/CLI use and for
one-off setting overrides, but in the GUI run_in_gui is preferred so
your on-screen settings are honoured.)
Physicist tools. parameter_scan sweeps one element parameter over
N points (envelope per point, background job, cancellable) and returns a
value → exit-metrics table; compare_runs diffs two result sets (either
.h5 files or current) with exit-KPI deltas and the max |Δσ| along s;
search_manual answers from this manual (offline, cited);
generate_report writes a markdown run report (KPIs, provenance and — in
the GUI — embedded figures) under <calc-dir>/reports/.
Guided tour & training drills. Say "give me a tour" and the assistant walks the interface station by station — 15 stops over the real tabs, narrated aloud with a hands-on exercise at most stops; "next", "back", or "jump to the Results one" all work, and the tour survives across turns. For training, "start a drill" injects a hidden fault into the loaded lattice (a detuned quad, an offset cavity phase) — hidden from the assistant too: the truth lives outside its context until the debrief, so it can relay the three graded hints without being able to spoil the answer. Diagnose from the observables, name the element (ambiguous names return candidates without submitting), and get a 0–100 score (diagnosis + speed + hint economy). The lattice is restored bit-exact at answer/give-up — or automatically if the session closes mid-drill.
Campaigns & verified tuning. run_campaign executes a multi-step
plan — parameter sets, envelope/multiparticle runs, anomaly checkpoints
— after one confirmation whose echo is the full numbered plan
("1. set QF.gradient = 5.5 … Any failure STOPS the chain; completed SET
steps are NOT reverted"). Every step is validated before you're
asked (an impossible plan never prompts), and the per-step gates are
deliberately bypassed once you approve the plan — the single ledger
record carries the complete step trail. tuning_plan is the cautious
sibling: 1–8 knobs, a declared success window on an exit KPI, a
verification run — and if the window is missed every knob is restored
bit-exact automatically, stated in the echo up front.
Analysis sandbox (run_python). The assistant can write and run
short Python analysis code — fits, FFTs, custom plots — in an
isolated subprocess: empty PYTHONPATH and linac_gen hard-
blocked (it cannot drive the simulator), no network, CPU and file-size
limits, 60 s cap. It sees only what is explicitly passed: results
columns via include= (full-precision data.npz) and small JSON via
data=. A saved PNG comes back as an image the assistant can see
and describe; captures land in <calc-dir>/assist_captures/. Compute
tier — each call confirms unless session auto-approve is on. It runs
real code on your machine: a machine-safety sandbox, not a security
jail.
Diagnosis & health. diagnose runs an ordered differential over
the loaded results — transmission-loss onset mapped to the containing
element ("cause is upstream"), σ blow-up onset, emittance growth,
energy deviation, and any parameter changes made in the last 30 minutes
(from the session ledger) — and suggests the next tool.
anomaly_baseline records a known-good result as the healthy
fingerprint (exit KPIs + σ/transmission/energy profiles);
anomaly_check scores any later result against it with per-feature
z-scores and top offenders, and refuses honestly when the baseline
belongs to a different lattice. shift_data gives a session briefing:
tools used, mutate/denied counts, result files written, current KPIs —
the "what did we do today" answer.
Memory. A persistent lab notebook (<calc-dir>/assist_notebook.md)
records a mechanical summary of every session (lattice, tools run,
results written, last conclusion) plus anything you ask it to "note
down" (notebook_note). Recent entries are loaded into the assistant's
context at start-up, so it remembers what was done and concluded in past
sessions (read_notebook reaches further back).
Pointing. highlight_element selects and scrolls to an element in
the Lattice tab and moves the s-cursor; set_cursor points at any s
position; get_gui_context tells the assistant what you currently see
("what am I looking at?").
Tab, subtab & plot control. The assistant can drive the app's own UI:
- "show me the Results tab" / "open Matching" →
show_tab. - "show the Breakdown view under Lattice" / "Numerics → Plot" →
show_tabwith a subtab (Lattice, Numerics and Error Study have nested subtabs;list_tabsreports them). - "open the phase-space plot" / "plot the RMS beam size" →
open_plotopens that window in the Results tab (switching there first);list_plotsreports the ~40 available plots.
All are read-tier, GUI-only actions wired through thread-safe queued
signals into the main window and the Results tab — navigation and plot
windows are never touched from the agent's worker thread, and nothing
here goes near the simulation core.
Voice (offline, push-to-talk)¶
The GUI panel has an optional offline voice mode — press-and-hold
the 🎤 button to speak, release to transcribe (local faster-whisper);
tick Speak replies to hear a concise spoken summary of each answer.
Install the audio stack with pip install linac_gen[assist-voice];
speech synthesis uses macOS say out of the box, or install
piper-tts for a higher-quality offline voice. With the stack absent
the mic/speak controls are simply disabled — the assistant stays
text-only.
Natural voice. With the optional kokoro-onnx package and its two
model files in ~/.helix/assistant_models/ (kokoro*.onnx +
voices*.bin, one-time download), replies are spoken in a natural
neural voice — sentence by sentence as the reply streams, with
barge-in (pressing the mic cuts the speech). Without kokoro the
speaker falls back to macOS say. Install:
pip install linac_gen[assist-voice-kokoro]. The mic level also
animates the orb's listening bars while you hold push-to-talk.
Two deliberate design choices:
- Push-to-talk, not a wake word — the mic is live only while you hold the button (reliable, no false triggers, no continuous capture).
- Precision stays on screen. The assistant speaks a summary with numbers coarsened ("σx at exit about 0.6 millimetres") while the exact digits and the echoed tool call remain in the text panel. Spoken numbers are also rewritten for the TTS engine — "0.62" is voiced as "0 point 62", scientific notation as "times ten to the …", so the decimal point is never silently skipped. You talk to it and hear the gist; you never rely on hearing a critical value — important because the beam-physics conventions (α_z sign, deg·MeV, mm-vs-m) are unforgiving.
Everything runs locally: no audio leaves the machine.