new version of pm using decision tree

This commit is contained in:
itsrubberduck
2025-09-16 12:23:32 +02:00
parent a9064e0ae3
commit 4b8da310b4
11 changed files with 651 additions and 1377 deletions

View File

@@ -0,0 +1,537 @@
const atcDecisionTree = {
"schema_version": "1.0",
"name": "icao_atc_decision_tree",
"description": "Machine-readable ICAO/FAA-style ATC flow for IFR, suited for LLM+Whisper+TTS loop.",
"start_state": "PREFLIGHT_START",
"end_states": ["FLOW_COMPLETE"],
"variables": {
"callsign": "DLH39A",
"acf_type": "A320",
"dep": "EDDF",
"dest": "EDDM",
"stand": "A12",
"runway": "25R",
"sid": "ANEKI 7S",
"transition": "ANEKI",
"squawk": "1234",
"initial_altitude_ft": 5000,
"climb_altitude_ft": 7000,
"cruise_flight_level": "FL360",
"star": "RNAV X",
"approach_type": "ILS Z",
"taxi_route": "V A",
"missed_approach": "as published",
"delivery_freq": "121.900",
"ground_freq": "121.700",
"tower_freq": "118.700",
"departure_freq": "125.350",
"approach_freq": "120.800",
"handoff_freq": "121.800",
"atis_code": "K",
"gate": "B24",
"trans_level": "FL070",
"qnh_hpa": 1015,
"remarks": "standard",
"time_now": "ISO8601"
},
"flags": {
"in_air": false,
"emergency_active": false,
"current_unit": "DEL",
"stack": []
},
"policies": {
"timeouts": {
"pilot_readback_timeout_s": 8,
"controller_ack_timeout_s": 6,
"no_reply_retry_after_s": 5,
"no_reply_max_retries": 2,
"lost_comms_detect_after_s": 90
},
"no_reply_sequence": [
{"after_s": 5, "controller_say_tpl": "{callsign}, confirm last transmission."},
{"after_s": 10, "controller_say_tpl": "{callsign}, do you read?"},
{"after_s": 20, "controller_say_tpl": "{callsign}, if you read, ident."}
],
"interrupts_allowed_when": {
"MAYDAY": "flags.in_air === true",
"PANPAN": "flags.in_air === true",
"TCAS_RA": "flags.in_air === true",
"GO_AROUND": "true",
"LOST_COMMS": "true",
"UNABLE": "true",
"STANDBY": "true"
}
},
"hooks": {
"on_state_enter": true,
"on_state_exit": true,
"on_timeout": true,
"on_interrupt": true,
"on_handoff": true,
"on_readback_check": true,
"on_pilot_speech": true,
"router_note": "Given pilot utterance → detect intent → if valid in active flow, branch; else jump to flow start with matching intent."
},
"roles": ["pilot", "atc", "system"],
"phases": ["Preflight","Clearance","PushStart","TaxiOut","Departure","Climb","Enroute","Descent","Approach","Landing","TaxiIn","Postflight","Interrupt","LostComms","Missed"],
"states": {
"PREFLIGHT_START": {
"role": "pilot",
"phase": "Preflight",
"prompt_out": "Initial contact with Clearance Delivery.",
"next": [{"to": "CD_CHECK_ATIS"}]
},
"CD_CHECK_ATIS": {
"role": "pilot",
"phase": "Clearance",
"utterance_tpl": "{callsign} information {atis_code}, IFR to {dest}, stand {stand}, request clearance.",
"next": [{"to": "CD_ISSUE_CLR"}],
"on_timeout": [{"after_s": 5, "action": "remind", "say_tpl": "{callsign}, say request."}]
},
"CD_ISSUE_CLR": {
"role": "atc",
"phase": "Clearance",
"say_tpl": "{callsign}, cleared to {dest} via {sid} departure, runway {runway}, climb {initial_altitude_ft} feet, squawk {squawk}.",
"readback_required": ["dest","sid","runway","initial_altitude_ft","squawk"],
"next": [
{"to": "CD_VERIFY_READBACK"},
{"to": "CD_AMEND_CLR", "when": "pilot_requests_amendment === true"}
]
},
"CD_VERIFY_READBACK": {
"role": "pilot",
"phase": "Clearance",
"utterance_tpl": "{callsign} cleared {dest} via {sid}, runway {runway}, climb {initial_altitude_ft}, squawk {squawk}.",
"next": [{"to": "CD_READBACK_CHECK"}]
},
"CD_READBACK_CHECK": {
"role": "atc",
"phase": "Clearance",
"auto": "check_readback",
"ok_next": [{"to": "CD_CLR_COMPLETE"}],
"bad_next": [{"to": "CD_READBACK_CORRECT"}],
"on_timeout": [{"after_s": 6, "action": "say", "say_tpl": "{callsign}, read back clearance."}]
},
"CD_READBACK_CORRECT": {
"role": "atc",
"phase": "Clearance",
"say_tpl": "{callsign}, negative; expect {sid}, runway {runway}, climb {initial_altitude_ft}, squawk {squawk}.",
"next": [{"to": "CD_VERIFY_READBACK"}]
},
"CD_AMEND_CLR": {
"role": "atc",
"phase": "Clearance",
"say_tpl": "{callsign}, amended clearance: {sid} {transition} departure, runway {runway}.",
"next": [{"to": "CD_VERIFY_READBACK"}]
},
"CD_CLR_COMPLETE": {
"role": "atc",
"phase": "Clearance",
"say_tpl": "{callsign}, readback correct. Start-up at own discretion. Contact Ground {ground_freq} when ready for push and start.",
"actions": [{"set": "flags.current_unit", "to": "DEL"}],
"handoff": {"to": "GROUND","freq": "{ground_freq}"},
"next": [{"to": "GRD_READY_FOR_PUSH"}]
},
"GRD_READY_FOR_PUSH": {
"role": "pilot",
"phase": "PushStart",
"utterance_tpl": "{callsign} stand {stand}, ready for push and start.",
"next": [
{"to": "GRD_PUSH_APPROVE", "when": "push_available === true"},
{"to": "GRD_PUSH_WAIT", "when": "push_available === false"}
]
},
"GRD_PUSH_WAIT": {
"role": "atc",
"phase": "PushStart",
"say_tpl": "{callsign}, push and start approved in {remarks} minutes. Expect taxi via {taxi_route}.",
"timer_next": [{"after_s": 60, "to": "GRD_PUSH_APPROVE"}],
"next": [{"to": "GRD_PUSH_APPROVE"}]
},
"GRD_PUSH_APPROVE": {
"role": "atc",
"phase": "PushStart",
"say_tpl": "{callsign}, push and start approved, facing {runway}. QNH {qnh_hpa}.",
"next": [{"to": "GRD_TAXI_REQUEST"}]
},
"GRD_TAXI_REQUEST": {
"role": "pilot",
"phase": "TaxiOut",
"utterance_tpl": "{callsign}, request taxi.",
"next": [{"to": "GRD_TAXI_INSTR"}]
},
"GRD_TAXI_INSTR": {
"role": "atc",
"phase": "TaxiOut",
"say_tpl": "{callsign}, taxi to runway {runway} via {taxi_route}, hold short runway {runway}.",
"readback_required": ["runway","taxi_route","hold_short"],
"next": [{"to": "GRD_TAXI_READBACK"}]
},
"GRD_TAXI_READBACK": {
"role": "pilot",
"phase": "TaxiOut",
"utterance_tpl": "{callsign} taxi to {runway} via {taxi_route}, holding short {runway}.",
"next": [{"to": "TWR_CONTACT"}]
},
"TWR_CONTACT": {
"role": "atc",
"phase": "TaxiOut",
"say_tpl": "{callsign}, contact Tower {tower_freq} when number one.",
"handoff": {"to": "TOWER","freq": "{tower_freq}"},
"next": [{"to": "TWR_LINEUP_REQ"}]
},
"TWR_LINEUP_REQ": {
"role": "pilot",
"phase": "Departure",
"utterance_tpl": "{callsign} holding short {runway}, ready for departure.",
"next": [
{"to": "TWR_LINEUP", "when": "runway_occupied === true"},
{"to": "TWR_TAKEOFF_CLR", "when": "runway_occupied === false"}
]
},
"TWR_LINEUP": {
"role": "atc",
"phase": "Departure",
"say_tpl": "{callsign}, line up and wait runway {runway}.",
"next": [{"to": "TWR_TAKEOFF_CLR"}]
},
"TWR_TAKEOFF_CLR": {
"role": "atc",
"phase": "Departure",
"say_tpl": "{callsign}, wind {remarks}, runway {runway} cleared for take-off.",
"readback_required": ["runway","cleared_takeoff"],
"actions": [{"set": "flags.in_air", "to": true}],
"next": [{"to": "TWR_TAKEOFF_READBACK"}]
},
"TWR_TAKEOFF_READBACK": {
"role": "pilot",
"phase": "Departure",
"utterance_tpl": "{callsign} cleared for take-off {runway}.",
"next": [{"to": "DEP_CONTACT"}]
},
"DEP_CONTACT": {
"role": "atc",
"phase": "Departure",
"say_tpl": "{callsign}, contact Departure {departure_freq}.",
"handoff": {"to": "DEPARTURE","freq": "{departure_freq}"},
"actions": [{"set": "flags.current_unit", "to": "DEP"}],
"next": [{"to": "DEP_IDENT"}]
},
"DEP_IDENT": {
"role": "pilot",
"phase": "Climb",
"utterance_tpl": "{callsign} passing {initial_altitude_ft}, on SID {sid}.",
"next": [{"to": "DEP_CLIMB_INSTR"}]
},
"DEP_CLIMB_INSTR": {
"role": "atc",
"phase": "Climb",
"say_tpl": "{callsign}, climb {climb_altitude_ft} feet, proceed direct {transition} if able.",
"next": [
{"to": "DEP_CLIMB_READBACK", "when": "pilot_able === true"},
{"to": "DEP_UNABLE_DIR", "when": "pilot_able === false"}
]
},
"DEP_UNABLE_DIR": {
"role": "pilot",
"phase": "Climb",
"utterance_tpl": "{callsign} unable direct {transition}.",
"next": [{"to": "DEP_ALT_RTE"}]
},
"DEP_ALT_RTE": {
"role": "atc",
"phase": "Climb",
"say_tpl": "{callsign}, continue SID, report passing {climb_altitude_ft}.",
"next": [{"to": "ENR_HANDOFF"}]
},
"DEP_CLIMB_READBACK": {
"role": "pilot",
"phase": "Climb",
"utterance_tpl": "{callsign} climb {climb_altitude_ft}, direct {transition}.",
"next": [{"to": "ENR_HANDOFF"}]
},
"ENR_HANDOFF": {
"role": "atc",
"phase": "Enroute",
"say_tpl": "{callsign}, contact Center {handoff_freq}.",
"handoff": {"to": "CENTER","freq": "{handoff_freq}"},
"actions": [{"set": "flags.current_unit", "to": "CTR"}],
"next": [{"to": "ENR_CRUISE"}]
},
"ENR_CRUISE": {
"role": "pilot",
"phase": "Enroute",
"auto": "monitor",
"next": [{"to": "DES_INITIATE"}]
},
"DES_INITIATE": {
"role": "atc",
"phase": "Descent",
"say_tpl": "{callsign}, descend via {star} {transition}, QNH {qnh_hpa}.",
"next": [{"to": "DES_READBACK"}]
},
"DES_READBACK": {
"role": "pilot",
"phase": "Descent",
"utterance_tpl": "{callsign} descend via {star} {transition}, QNH {qnh_hpa}.",
"next": [{"to": "APP_HANDOFF"}]
},
"APP_HANDOFF": {
"role": "atc",
"phase": "Descent",
"say_tpl": "{callsign}, contact Approach {approach_freq}.",
"handoff": {"to": "APPROACH","freq": "{approach_freq}"},
"actions": [{"set": "flags.current_unit", "to": "APP"}],
"next": [{"to": "APP_VECTORING"}]
},
"APP_VECTORING": {
"role": "atc",
"phase": "Approach",
"say_tpl": "{callsign}, turn left heading 220, descend to {initial_altitude_ft} feet, reduce speed {remarks}.",
"next": [{"to": "APP_CLEARED_APP"}]
},
"APP_CLEARED_APP": {
"role": "atc",
"phase": "Approach",
"say_tpl": "{callsign}, cleared {approach_type} approach runway {runway}, report established.",
"next": [{"to": "APP_ESTABLISHED"}]
},
"APP_ESTABLISHED": {
"role": "pilot",
"phase": "Approach",
"utterance_tpl": "{callsign} established localizer {runway}.",
"next": [{"to": "TWR_LAND_CONTACT"}]
},
"TWR_LAND_CONTACT": {
"role": "atc",
"phase": "Approach",
"say_tpl": "{callsign}, contact Tower {tower_freq}.",
"handoff": {"to": "TOWER","freq": "{tower_freq}"},
"actions": [{"set": "flags.current_unit", "to": "TWR"}],
"next": [{"to": "TWR_LAND_CLEARABLE"}]
},
"TWR_LAND_CLEARABLE": {
"role": "atc",
"phase": "Landing",
"condition": "runway_available === true",
"say_tpl": "{callsign}, wind {remarks}, runway {runway} cleared to land.",
"else_say_tpl": "{callsign}, continue approach, expect late landing clearance.",
"next": [
{"to": "TWR_LAND_READBACK", "when": "runway_available === true"},
{"to": "TWR_CONTINUE_APPROACH", "when": "runway_available === false"}
]
},
"TWR_CONTINUE_APPROACH": {
"role": "atc",
"phase": "Landing",
"say_tpl": "{callsign}, continue approach.",
"next": [{"to": "TWR_LAND_CLEARABLE"}]
},
"TWR_LAND_READBACK": {
"role": "pilot",
"phase": "Landing",
"utterance_tpl": "{callsign} cleared to land {runway}.",
"next": [{"to": "TWR_VACATE"}]
},
"TWR_VACATE": {
"role": "atc",
"phase": "Landing",
"say_tpl": "{callsign}, vacate via {taxi_route}, contact Ground {ground_freq}.",
"actions": [{"set": "flags.in_air", "to": false}],
"handoff": {"to": "GROUND","freq": "{ground_freq}"},
"next": [{"to": "GRD_TAXI_IN_REQ"}]
},
"GRD_TAXI_IN_REQ": {
"role": "pilot",
"phase": "TaxiIn",
"utterance_tpl": "{callsign} runway vacated, request taxi to stand.",
"next": [{"to": "GRD_TAXI_INSTR_IN"}]
},
"GRD_TAXI_INSTR_IN": {
"role": "atc",
"phase": "TaxiIn",
"say_tpl": "{callsign}, taxi to stand {gate} via {taxi_route}.",
"readback_required": ["gate","taxi_route"],
"next": [{"to": "GRD_TAXI_IN_READBACK"}]
},
"GRD_TAXI_IN_READBACK": {
"role": "pilot",
"phase": "TaxiIn",
"utterance_tpl": "{callsign} taxi to stand {gate} via {taxi_route}.",
"next": [{"to": "FLOW_COMPLETE"}]
},
"FLOW_COMPLETE": {
"role": "system",
"phase": "Postflight",
"auto": "end",
"next": []
},
/* ===== Interrupts (conditioned) ===== */
"INT_MAYDAY": {
"role": "pilot",
"phase": "Interrupt",
"guard": "flags.in_air === true",
"utterance_tpl": "MAYDAY MAYDAY MAYDAY, {callsign}, {problem}, intentions {intent}.",
"priority": "highest",
"actions": [{"set": "flags.emergency_active", "to": true}],
"next": [{"to": "ATC_MAYDAY_VECTOR"}]
},
"ATC_MAYDAY_VECTOR": {
"role": "atc",
"phase": "Interrupt",
"say_tpl": "{callsign}, roger MAYDAY, fly heading {remarks}, climb/descend {initial_altitude_ft}, cleared direct {dest} when able, QNH {qnh_hpa}.",
"next": [{"to": "ATC_MAYDAY_COORD"}]
},
"ATC_MAYDAY_COORD": {
"role": "system",
"phase": "Interrupt",
"actions": ["alert_emergency_services","notify_adjacent_units"],
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
"INT_PANPAN": {
"role": "pilot",
"phase": "Interrupt",
"guard": "flags.in_air === true",
"utterance_tpl": "PAN PAN PAN, {callsign}, {problem}, request priority.",
"priority": "high",
"actions": [{"set": "flags.emergency_active", "to": true}],
"next": [{"to": "ATC_PAN_ACK"}]
},
"ATC_PAN_ACK": {
"role": "atc",
"phase": "Interrupt",
"say_tpl": "{callsign}, PAN acknowledged, priority granted, expect vectors direct {dest} or nearest suitable.",
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
"INT_TCAS_RA": {
"role": "pilot",
"phase": "Interrupt",
"guard": "flags.in_air === true",
"utterance_tpl": "{callsign} TCAS RA, deviating.",
"actions": ["suspend_clearances"],
"next": [{"to": "ATC_TCAS_ACK"}]
},
"ATC_TCAS_ACK": {
"role": "atc",
"phase": "Interrupt",
"say_tpl": "{callsign}, roger TCAS RA, report clear of conflict.",
"next": [{"to": "ATC_TCAS_RESUME"}]
},
"ATC_TCAS_RESUME": {
"role": "pilot",
"phase": "Interrupt",
"utterance_tpl": "{callsign} clear of conflict, returning to clearance.",
"actions": [{"set": "flags.emergency_active", "to": false}],
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
"INT_GOA": {
"role": "pilot",
"phase": "Missed",
"utterance_tpl": "{callsign} going around, {missed_approach}.",
"actions": [{"set": "flags.in_air", "to": true}],
"next": [{"to": "ATC_GOA_INSTR"}]
},
"ATC_GOA_INSTR": {
"role": "atc",
"phase": "Missed",
"say_tpl": "{callsign}, roger go-around, fly published missed approach, climb {initial_altitude_ft}, contact Approach {approach_freq}.",
"handoff": {"to": "APPROACH","freq": "{approach_freq}"},
"next": [{"to": "APP_VECTORING"}]
},
"INT_NORDO": {
"role": "system",
"phase": "LostComms",
"trigger": "no_reply > policies.timeouts.lost_comms_detect_after_s",
"actions": ["lost_comms_procedure"],
"next": [{"to": "ATC_NORDO_ACTION"}]
},
"ATC_NORDO_ACTION": {
"role": "atc",
"phase": "LostComms",
"say_tpl": "(Transmitted blind) {callsign}, if you read, squawk IDENT and continue per last clearance. Expect vectors.",
"next": [{"to": "SYSTEM_NORDO_COORD"}]
},
"SYSTEM_NORDO_COORD": {
"role": "system",
"phase": "LostComms",
"actions": ["notify_adjacent_units","monitor_light_gun","publish_ATIS_note"],
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
"INT_UNABLE": {
"role": "pilot",
"phase": "Interrupt",
"utterance_tpl": "{callsign} unable {instruction}.",
"next": [{"to": "ATC_ALT_PROPOSAL"}]
},
"ATC_ALT_PROPOSAL": {
"role": "atc",
"phase": "Interrupt",
"say_tpl": "{callsign}, alternative: {alt_instruction}.",
"next": [
{"to": "PILOT_ACCEPT_ALT"},
{"to": "PILOT_REJECT_ALT"}
]
},
"PILOT_ACCEPT_ALT": {
"role": "pilot",
"phase": "Interrupt",
"utterance_tpl": "{callsign} wilco.",
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
"PILOT_REJECT_ALT": {
"role": "pilot",
"phase": "Interrupt",
"utterance_tpl": "{callsign} negative, request {intent}.",
"next": [{"to": "ATC_ALT_PROPOSAL"}]
},
"INT_STANDBY": {
"role": "pilot",
"phase": "Interrupt",
"utterance_tpl": "{callsign} standby.",
"actions": ["pause_exchange"],
"next": [{"to": "RESUME_PRIOR_FLOW"}]
},
/* ===== Generic glue & router ===== */
"RESUME_PRIOR_FLOW": {
"role": "system",
"phase": "Interrupt",
"auto": "pop_stack_or_route_by_intent",
"actions": [
{"if": "flags.emergency_active === true && flags.in_air === true", "set": "flags.emergency_active", "to": false}
],
"next": []
},
"GEN_NO_REPLY": {
"role": "system",
"phase": "Interrupt",
"trigger": "no_reply",
"policy_ref": "policies.no_reply_sequence",
"escalate_to": "INT_NORDO",
"next": []
}
}
}
export default atcDecisionTree;

View File

@@ -0,0 +1,468 @@
// composables/communicationsEngine.ts
import { ref, computed, readonly } from 'vue'
import atcDecisionTree from "./atcDecisionTree";
// --- DecisionTree-Types (aus ~/data/atcDecisionTree.json abgeleitet) ---
type Role = 'pilot' | 'atc' | 'system'
type Phase =
| 'Preflight' | 'Clearance' | 'PushStart' | 'TaxiOut' | 'Departure'
| 'Climb' | 'Enroute' | 'Descent' | 'Approach' | 'Landing'
| 'TaxiIn' | 'Postflight' | 'Interrupt' | 'LostComms' | 'Missed'
interface DTNext { to: string; when?: string }
interface DTHandoff { to: string; freq: string }
interface DTActionSet { set?: string; to?: any; if?: string }
interface DTState {
id?: string // im JSON sind Keys die IDs — hier zur Bequemlichkeit.
role: Role
phase: Phase
// Texte
say_tpl?: string
utterance_tpl?: string
else_say_tpl?: string
// Routing
next?: DTNext[]
ok_next?: DTNext[]
bad_next?: DTNext[]
timer_next?: { after_s: number; to: string }[]
handoff?: DTHandoff
condition?: string
guard?: string
trigger?: string
auto?: 'check_readback' | 'monitor' | 'end' | 'pop_stack_or_route_by_intent'
readback_required?: string[]
actions?: (DTActionSet | string)[]
}
interface DecisionTree {
schema_version: string
name: string
description: string
start_state: string
end_states: string[]
variables: Record<string, any>
flags: {
in_air: boolean
emergency_active: boolean
current_unit: string
stack: string[]
}
policies: {
timeouts: {
pilot_readback_timeout_s: number
controller_ack_timeout_s: number
no_reply_retry_after_s: number
no_reply_max_retries: number
lost_comms_detect_after_s: number
}
no_reply_sequence: { after_s: number; controller_say_tpl: string }[]
interrupts_allowed_when: Record<string, string>
}
hooks: Record<string, boolean | string>
roles: Role[]
phases: Phase[]
states: Record<string, DTState>
}
// --- ATC Decition Tree laden ---
// ----------------- Public API / Kontext -----------------
export interface FlightContext {
callsign: string
aircraft: string
dep: string
dest: string
stand: string
runway: string
squawk: string
atis_code: string
sid: string
transition: string
flight_level: string // z.B. "FL360"
ground_freq: string
tower_freq: string
departure_freq: string
approach_freq: string
handoff_freq: string
qnh_hpa: number | string
taxi_route: string
remarks?: string
time_now?: string
// intern
lastTransmission?: string
awaitingResponse?: boolean
}
export interface EngineLog {
timestamp: Date
frequency?: string
speaker: Role
message: string
normalized: string
state: string
}
// ----------------- NATO/ICAO Normalizer -----------------
const NATO_PHONETIC: Record<string, string> = {
A: 'Alpha', B: 'Bravo', C: 'Charlie', D: 'Delta', E: 'Echo', F: 'Foxtrot',
G: 'Golf', H: 'Hotel', I: 'India', J: 'Juliett', K: 'Kilo', L: 'Lima',
M: 'Mike', N: 'November', O: 'Oscar', P: 'Papa', Q: 'Quebec', R: 'Romeo',
S: 'Sierra', T: 'Tango', U: 'Uniform', V: 'Victor', W: 'Whiskey',
X: 'X-ray', Y: 'Yankee', Z: 'Zulu'
}
const ICAO_NUMBERS: Record<string, string> = {
'0': 'zero', '1': 'wun', '2': 'too', '3': 'tree', '4': 'fower',
'5': 'fife', '6': 'six', '7': 'seven', '8': 'eight', '9': 'niner'
}
function normalizeAltitude(altitude: number): string {
if (altitude >= 1000) {
const thousands = Math.floor(altitude / 1000).toString().split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
const remainder = altitude % 1000
return remainder > 0 ? `${thousands} thousand ${remainder}` : `${thousands} thousand`
}
return altitude.toString()
}
export function normalizeATCText(text: string, context: Record<string, any>): string {
let normalized = renderTpl(text, context)
// runway 25L → two five left
normalized = normalized.replace(/runway\s+(\d{1,2})([LRC]?)/gi, (_, num: string, suffix: string) => {
const n = num.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
const s = suffix === 'L' ? ' left' : suffix === 'R' ? ' right' : suffix === 'C' ? ' center' : ''
return `runway ${n}${s}`
})
// FL350 → flight level three five zero
normalized = normalized.replace(/FL(\d{3})/gi, (_, lvl: string) => {
const n = lvl.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
return `flight level ${n}`
})
// 5000 ft → five thousand
normalized = normalized.replace(/\b(\d{1,5})\s*(?:feet?|ft)\b/gi, (_, alt: string) => normalizeAltitude(parseInt(alt)))
// 121.9 → wun too wun decimal niner
normalized = normalized.replace(/(\d{3})\.(\d{1,3})/g, (_, a: string, b: string) => {
const A = a.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
const B = b.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
return `${A} decimal ${B}`
})
// squawk 4567 → fower fife six seven
normalized = normalized.replace(/squawk\s+(\d{4})/gi, (_, code: string) => {
const n = code.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
return `squawk ${n}`
})
// heading 270 → heading two seven zero
normalized = normalized.replace(/heading\s+(\d{3})/gi, (_, hdg: string) => {
const n = hdg.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
return `heading ${n}`
})
// via A, B5 → via Alpha, Bravo five
normalized = normalized.replace(/via\s+([A-Z](?:\d+)?(?:\s*,\s*[A-Z](?:\d+)?)*)/gi, (_, route: string) => {
const parts = route.split(/\s*,\s*/).map(seg =>
seg.replace(/([A-Z])(\d+)?/g, (_m, L: string, N?: string) => {
const letter = NATO_PHONETIC[L] || L
const number = N ? N.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') : ''
return number ? `${letter} ${number}` : letter
})
)
return `via ${parts.join(', ')}`
})
// A5 → Alpha five
normalized = normalized.replace(/\b([A-Z])(\d+)\b/g, (_m, L: string, N: string) => {
const letter = NATO_PHONETIC[L] || L
const number = N.split('').map(d => ICAO_NUMBERS[d] || d).join(' ')
return `${letter} ${number}`
})
// 280/15 → two eight zero at wun fife
normalized = normalized.replace(/(\d{3})\/(\d{1,2})/g, (_m, d: string, s: string) => {
const D = d.split('').map(x => ICAO_NUMBERS[x] || x).join(' ')
const S = s.split('').map(x => ICAO_NUMBERS[x] || x).join(' ')
return `${D} at ${S}`
})
return normalized
}
// ----------------- Template-Renderer -----------------
function renderTpl(tpl: string, ctx: Record<string, any>): string {
return tpl.replace(/\{([\w.]+)\}/g, (_m, key) => {
// Variablen können z.B. variables.callsign oder flags.in_air sein
const parts = key.split('.')
let cur: any = ctx
for (const p of parts) cur = cur?.[p]
return (cur ?? '').toString()
})
}
// ----------------- Engine -----------------
export default function useCommunicationsEngine() {
// Decision Tree in-Memory
const tree = ref<DecisionTree>(atcDecisionTree as DecisionTree)
const states = computed<Record<string, DTState>>(() => tree.value.states)
// Laufzeitkontext
const variables = ref<Record<string, any>>({ ...tree.value.variables })
const flags = ref({ ...tree.value.flags })
const currentStateId = ref<string>(tree.value.start_state)
const communicationLog = ref<EngineLog[]>([])
const currentState = computed<DTState>(() => {
const s = states.value[currentStateId.value]
return { ...s, id: currentStateId.value }
})
const nextCandidates = computed<string[]>(() => {
const s = currentState.value
const nxt = [
...(s.next ?? []),
...(s.ok_next ?? []),
...(s.bad_next ?? []),
...(s.timer_next?.map(t => ({ to: t.to })) ?? [])
].map(x => x.to)
// uniq
return Array.from(new Set(nxt))
})
// Frequenz aus Variablen (Controller-Einheit) ableiten
const activeFrequency = computed<string | undefined>(() => {
switch (flags.value.current_unit) {
case 'DEL': return renderTpl('{variables.delivery_freq}', { variables: variables.value })
case 'GROUND': return renderTpl('{variables.ground_freq}', { variables: variables.value })
case 'TOWER': return renderTpl('{variables.tower_freq}', { variables: variables.value })
case 'DEP': return renderTpl('{variables.departure_freq}', { variables: variables.value })
case 'APP': return renderTpl('{variables.approach_freq}', { variables: variables.value })
case 'CTR': return renderTpl('{variables.handoff_freq}', { variables: variables.value })
default: return undefined
}
})
// ---------- öffentliche API ----------
function initializeFlight(fpl: any) {
variables.value = {
...variables.value,
callsign: fpl.callsign,
acf_type: fpl.aircraft?.split('/')[0] || 'A320',
dep: fpl.dep || fpl.departure || 'EDDF',
dest: fpl.arr || fpl.arrival || 'EDDM',
stand: genStand(),
runway: genRunway(),
squawk: fpl.assignedsquawk || genSquawk(),
atis_code: genATIS(),
sid: genSID(fpl.route || ''),
transition: 'DCT',
cruise_flight_level: fpl.altitude ? `FL${String(Math.floor(parseInt(fpl.altitude) / 100)).padStart(3, '0')}` : 'FL360',
initial_altitude_ft: 5000,
climb_altitude_ft: 7000,
star: 'RNAV X',
approach_type: 'ILS Z',
taxi_route: 'A, V',
delivery_freq: '121.900',
ground_freq: '121.700',
tower_freq: '118.700',
departure_freq: '125.350',
approach_freq: '120.800',
handoff_freq: '121.800',
qnh_hpa: 1015,
remarks: 'standard',
time_now: new Date().toISOString()
}
flags.value = { ...flags.value, in_air: false, emergency_active: false, current_unit: 'DEL', stack: [] }
currentStateId.value = tree.value.start_state
communicationLog.value = []
}
// Pilot-Input verarbeiten → LLM bekommt State + Candidates
function buildLLMContext(pilotTranscript: string) {
const s = currentState.value
return {
state_id: s.id,
state: s,
candidates: nextCandidates.value.map(id => ({ id, state: states.value[id] })),
variables: variables.value,
flags: flags.value,
pilot_utterance: pilotTranscript
}
}
// LLM-Entscheidung anwenden
function applyLLMDecision(decision: { next_state: string; updates?: Record<string, any>; flags?: Record<string, any>; controller_say_tpl?: string }) {
// Variablen/Flags updaten
if (decision.updates) Object.assign(variables.value, decision.updates)
if (decision.flags) Object.assign(flags.value, decision.flags)
// ggf. ATC sagt etwas sofort (z.B. Reminders)
if (decision.controller_say_tpl) {
speak('atc', decision.controller_say_tpl, currentStateId.value)
}
moveTo(decision.next_state)
}
// Direkter Pilotenspruch → einfache Router-Heuristik (Interrupts/Readbacks). Für echte Logik nutzt ihr LLM:
function processPilotTransmission(transcript: string) {
// Interrupt-Shortcuts (nur in der Luft)
const t = transcript.toLowerCase()
if (flags.value.in_air && /^(mayday|pan\s*pan)/.test(t)) {
const intId = t.startsWith('mayday') ? 'INT_MAYDAY' : 'INT_PANPAN'
moveTo(intId)
speak('pilot', transcript, intId)
return
}
// Standard: Loggen & LLM-Context anbieten
speak('pilot', transcript, currentStateId.value)
}
// Zum State springen und Auto-Aktionen ausführen
function moveTo(stateId: string) {
if (!states.value[stateId]) return
// Stack-Mechanik (vereinfacht): Interrupts pushen aktuellen State
if (stateId.startsWith('INT_')) flags.value.stack.push(currentStateId.value)
currentStateId.value = stateId
const s = currentState.value
// Actions ausführen (nur SET/IF minimal)
for (const act of s.actions ?? []) {
if (typeof act === 'string') continue
if (act.if && !safeEvalBoolean(act.if)) continue
if (act.set) setByPath({ variables: variables.value, flags: flags.value }, act.set, act.to)
}
// Handoff → Frequenz/Unit setzen
if (s.handoff?.to) {
flags.value.current_unit = unitFromHandoff(s.handoff.to)
if (s.handoff.freq) {
// Frequenz ins variables-Objekt aufnehmen, falls dynamisch
variables.value.handoff_freq = renderTpl(s.handoff.freq, { ...exposeCtx() })
}
}
// Auto-Say (ATC/System)
if (s.say_tpl) speak(s.role, s.say_tpl, s.id!)
// Auto-Ende / Monitor / Check-Readback werden vom LLM/Client gesteuert.
}
// Letzten Flow wieder aufnehmen (z.B. nach Interrupt)
function resumePriorFlow() {
const prev = flags.value.stack.pop()
if (prev) moveTo(prev)
}
// Hilfsfunktionen
function speak(speaker: Role, tpl: string, stateId: string) {
const msg = renderTpl(tpl, exposeCtx())
communicationLog.value.push({
timestamp: new Date(),
frequency: activeFrequency.value,
speaker,
message: msg,
normalized: normalizeATCText(msg, exposeCtxFlat()),
state: stateId
})
}
function exposeCtx() {
return { variables: variables.value, flags: flags.value }
}
function exposeCtxFlat() {
// Für den Normalizer wollen wir einfache Platzhalter-Namen
return {
...variables.value,
...flags.value
}
}
function unitFromHandoff(to: string) {
if (/GROUND/i.test(to)) return 'GROUND'
if (/TOWER/i.test(to)) return 'TOWER'
if (/DEPART/i.test(to)) return 'DEP'
if (/APPROACH/i.test(to)) return 'APP'
if (/CENTER|CTR/i.test(to)) return 'CTR'
if (/DEL|DELIVERY/i.test(to)) return 'DEL'
return flags.value.current_unit
}
function genStand() {
const arr = ['A12','B15','C23','D8','E41','F18','G7','H33']
return arr[Math.floor(Math.random() * arr.length)]
}
function genRunway() {
const arr = ['25L','25R','07L','07R','18','36','09','27']
return arr[Math.floor(Math.random() * arr.length)]
}
function genSquawk() {
return String(Math.floor(Math.random() * 8000 + 1000)).padStart(4, '0')
}
function genATIS() {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
return letters[Math.floor(Math.random() * letters.length)]
}
function genSID(_route: string) {
const sids = ['SULUS5S', 'TOBAK2E', 'MARUN7F', 'CINDY1A', 'HELEN4B']
return sids[Math.floor(Math.random() * sids.length)]
}
// einfache bool-Guards (nur Variablen/Flags, keine eval unsicherer Ausdrücke)
function safeEvalBoolean(expr?: string): boolean {
if (!expr) return true
// sehr einfache Auswertung: flags.in_air === true
const m = expr.match(/^(variables|flags)\.([A-Za-z0-9_]+)\s*===\s*(true|false|'[^']*'|"[^"]*"|\d+)$/)
if (!m) return false
const bag = m[1] === 'variables' ? variables.value : flags.value as any
const key = m[2]
const rhsRaw = m[3]
let rhs: any = rhsRaw
if (rhsRaw === 'true') rhs = true
else if (rhsRaw === 'false') rhs = false
else if (/^\d+$/.test(rhsRaw)) rhs = Number(rhsRaw)
else rhs = rhsRaw.replace(/^['"]|['"]$/g, '')
return bag?.[key] === rhs
}
function setByPath(root: Record<string, any>, path: string, val: any) {
const parts = path.split('.')
let cur = root
for (let i = 0; i < parts.length - 1; i++) {
const p = parts[i]
if (!(p in cur)) cur[p] = {}
cur = cur[p]
}
cur[parts[parts.length - 1]] = val
}
return {
// state/vars/flags
currentState: computed(() => currentState.value),
currentStateId: readonly(currentStateId),
variables: readonly(variables),
flags: readonly(flags),
nextCandidates,
activeFrequency,
communicationLog: readonly(communicationLog),
// lifecycle
initializeFlight,
// Pilot in → baut Kontext für LLM
processPilotTransmission,
buildLLMContext,
applyLLMDecision,
// Flow-Steuerung
moveTo,
resumePriorFlow,
// utils
normalizeATCText
}
}

View File

@@ -0,0 +1,24 @@
// utils/openaiDecision.ts
export interface LLMDecisionInput {
state_id: string
state: any
candidates: Array<{ id: string; state: any }>
variables: Record<string, any>
flags: Record<string, any>
pilot_utterance: string
}
export interface LLMDecision {
next_state: string
updates?: Record<string, any>
flags?: Record<string, any>
controller_say_tpl?: string
}
/** Client-seitig: ruft den Backend-Endpunkt auf */
export async function decideNextStateLLM(input: LLMDecisionInput): Promise<LLMDecision> {
return await $fetch<LLMDecision>('/api/llm/decide', {
method: 'POST',
body: input
})
}