Intent parsing & LLM modes¶
Available now -- the rules-based parser, an optional LLM fallback, and the Settings page's ATC tab are all real and wired together end to end.
The rules-based parser¶
Every pilot transmission has to become a structured Intent before the ATC
engine can act on it. xatc.phraseology.intent.parse_intent
does this with plain keyword matching and regex-style extraction -- no model, no network
call, fully deterministic and unit tested.
parse_intent(text, phase=Phase.TAXI_OUT, expected_callsign="November five four seven Golf Alpha")
Given "Seattle Ground, request taxi at Alpha eleven, ready to copy," this returns
IntentType.REQUEST_TAXI at confidence 0.85, with the slot {"location": "A11"}
extracted from the phrase after "at". Given "Seattle Departure, November five four
seven Golf Alpha, with you at two thousand five hundred climbing one zero thousand,"
it returns IntentType.CHECK_IN with {"altitude_ft": 2500, "climbing_to_ft": 10000}.
Mismatches (an intent that's implausible for the current flight phase, or a spoken callsign that doesn't match the one expected) only ever reduce confidence -- never change the intent type or force it to zero.
Number normalization¶
xatc.phraseology.number_normalizer turns spoken numbers into values, one fragment at
a time: "two five zero zero" → 2500 ft, "flight level three five zero" → 35,000 ft,
"four five two one" (squawk) → "4521" (rejects anything outside 0-7, since squawk
codes are octal), "one one niner point niner" (frequency) → 119,900 kHz.
The LLM fallback (Nova Lite)¶
xatc.intent_llm.NovaLiteIntentClassifier calls Amazon Bedrock's Nova Lite model
(amazon.nova-lite-v1:0) through the Converse API, forced to a single tool call so its
output is always a validated Intent, never free text -- the same "never generates ATC
wording" boundary the voice layer has (see the core design
principle). This only ever helps classify what the
pilot said; it never produces what ATC says back.
xatc.intent_llm.IntentRouter is what's actually injected into the engine, with three
runtime-switchable modes:
| Mode | Behavior |
|---|---|
| Disabled | Rules only. The classifier is never called. |
Backup (fallback) |
Rules run first; the classifier is only called when rules land on UNKNOWN or below a confidence threshold. |
| Primary | The classifier runs first; rules run instead whenever it fails, times out, or a per-session call cap is hit. |
A live classifier call blocks for up to about 1.5 seconds in Backup/Primary mode -- acceptable at this project's current traffic scale (one aircraft), not something that's been made asynchronous.
The Settings page¶
The Settings page's ATC tab has three radio buttons --
Disabled, Backup, Primary -- matching the modes above, applying immediately, plus an LLM
status line (call count, average response time, last error). This is real,
live-switchable state now: selecting a mode calls IntentRouter.set_mode, persists it
to settings.json, and every connected panel sees the update via the same broadcast
sim_status and other session state already uses.
xatc run --intent-mode {disabled,fallback,primary} sets it for one run without
changing what's persisted; with no flag, the last-used mode is remembered, defaulting to
Backup when --voice is on and Disabled otherwise.
Both of the LLM helpers below are only wired in on a live voice flight (--voice) with
intent parsing not set to Disabled -- with voice off, or the intent mode Disabled, neither
one is ever instantiated, and the rules-only path (and a plain "say again") is all there
is.
The readback judge (Nova Lite)¶
xatc.readback_llm.NovaLiteReadbackJudge gets a second opinion whenever the rules-based
readback checker has just rejected what you read back. It's a
one-way safety net: the judge can only turn a rules reject into an accept, and only on
a confident "yes" -- it's asked whether a real controller would accept this readback, and
a "no," a low-confidence answer, a timeout, or a missing AWS connection all keep the rules
checker's original verdict instead. It can never invent a readback or override a rules
accept; nothing about the flight depends on the judge succeeding.
Conversational ATC (F17)¶
When a pilot transmission doesn't match anything the rules parser
or a scripted handler recognizes, xatc.atc_conversation.NovaLiteController gets a shot
at it. It doesn't get to write ATC's reply -- it picks exactly one action from a small,
closed set (answer, unable, heading, altitude, direct), which the engine then
validates and renders in its own phraseology, the same as every other clearance. An
answer (free-text, for things like acknowledging a request that needs no clearance) is
still passed through an answer_is_safe guardrail before it's allowed to be spoken. If
the model fails, times out, or proposes an action the engine can't validate, the pilot
just hears "say again" -- the same as the rules parser's own fallback.
Configuration¶
xatc run --voice --intent-mode fallback # or disabled / primary
Nova Lite calls need bedrock:InvokeModel/InvokeModelWithResponseStream on top of the
IAM permissions Voice already needs -- see the repository's infra/ for the
managed policy.
Limitations¶
- A live classifier call can add up to ~1.5s of latency in Backup/Primary mode; the engine call is synchronous, not asynchronous.
IntentType.REQUEST_APPROACHexists in the shared contract but nothing produces it yet, rules or LLM.REQUEST_PUSHBACK(pushback),REQUEST_ALTITUDE,REQUEST_DIRECT,REQUEST_DEVIATION, andUNABLE(en-route requests) are all wired now.- The engine doesn't yet pass which controller position a transmission arrived on into the classifier, even though both the router and the classifier already accept it -- a small follow-up on the engine side, not required for this to work today.