mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-06 09:16:26 +08:00
Add readback correction states and improve validation
This commit is contained in:
@@ -67,7 +67,10 @@ export interface LLMDecision {
|
||||
type ReadbackStatus = 'ok' | 'missing' | 'incorrect' | 'uncertain'
|
||||
|
||||
const READBACK_REQUIREMENTS: Record<string, string[]> = {
|
||||
CD_READBACK_CHECK: ['dest', 'sid', 'runway', 'initial_altitude_ft', 'squawk']
|
||||
CD_READBACK_CHECK: ['dest', 'sid', 'runway', 'initial_altitude_ft', 'squawk'],
|
||||
GRD_TAXI_READBACK_CHECK: ['runway', 'taxi_route', 'hold_short'],
|
||||
TWR_TAKEOFF_READBACK_CHECK: ['runway', 'cleared_takeoff'],
|
||||
GRD_TAXI_IN_READBACK_CHECK: ['gate', 'taxi_route']
|
||||
}
|
||||
|
||||
const READBACK_JSON_SCHEMA = {
|
||||
@@ -134,6 +137,19 @@ function buildSpokenVariants(key: string, value: string): string[] {
|
||||
variants.add(normalized)
|
||||
variants.add(normalized.toUpperCase())
|
||||
|
||||
if (key === 'hold_short') {
|
||||
const base = normalized.replace(/^holding\s+short/i, 'hold short')
|
||||
variants.add(base)
|
||||
if (!/\brunway\b/i.test(base)) {
|
||||
variants.add(base.replace(/^(hold short)/i, '$1 runway'))
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'cleared_takeoff') {
|
||||
variants.add(normalized.replace(/take-off/gi, 'takeoff'))
|
||||
variants.add(normalized.replace(/take-off/gi, 'take off'))
|
||||
}
|
||||
|
||||
if (/^[A-Z]{3,4}$/.test(normalized.toUpperCase())) {
|
||||
variants.add(toPhonetic(normalized))
|
||||
}
|
||||
@@ -188,6 +204,42 @@ function fallbackNextState(input: LLMDecisionInput): string {
|
||||
return input.candidates[0]?.id || input.state_id || 'GEN_NO_REPLY'
|
||||
}
|
||||
|
||||
function resolveReadbackValue(key: string, input: LLMDecisionInput): string | null {
|
||||
const rawValue = input.variables?.[key]
|
||||
if (rawValue !== undefined && rawValue !== null) {
|
||||
const trimmed = `${rawValue}`.trim()
|
||||
if (trimmed.length > 0) {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'hold_short': {
|
||||
const runway = input.variables?.runway
|
||||
if (typeof runway === 'string' && runway.trim().length > 0) {
|
||||
return `holding short ${runway}`.trim()
|
||||
}
|
||||
return 'holding short'
|
||||
}
|
||||
case 'cleared_takeoff': {
|
||||
const runway = input.variables?.runway
|
||||
if (typeof runway === 'string' && runway.trim().length > 0) {
|
||||
return `cleared for take-off ${runway}`.trim()
|
||||
}
|
||||
return 'cleared for take-off'
|
||||
}
|
||||
case 'cleared_to_land': {
|
||||
const runway = input.variables?.runway
|
||||
if (typeof runway === 'string' && runway.trim().length > 0) {
|
||||
return `cleared to land runway ${runway}`.trim()
|
||||
}
|
||||
return 'cleared to land'
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Extrahiere verwendete Variablen aus Templates
|
||||
function extractTemplateVariables(text?: string): string[] {
|
||||
if (!text) return []
|
||||
@@ -283,14 +335,24 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
|
||||
|
||||
async function handleReadbackCheck(): Promise<LLMDecision> {
|
||||
const requiredKeys = READBACK_REQUIREMENTS[input.state_id] || input.state.readback_required || []
|
||||
const expectedItems = requiredKeys
|
||||
.map(key => ({ key, value: input.variables?.[key] }))
|
||||
.filter(item => item.value !== undefined && item.value !== null && `${item.value}`.trim().length > 0)
|
||||
.map(item => ({
|
||||
key: item.key,
|
||||
value: String(item.value),
|
||||
spoken_variants: buildSpokenVariants(item.key, String(item.value))
|
||||
}))
|
||||
const expectedItems = requiredKeys.reduce<Array<{ key: string; value: string; spoken_variants: string[] }>>((acc, key) => {
|
||||
const value = resolveReadbackValue(key, input)
|
||||
if (!value) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const normalizedValue = String(value)
|
||||
if (!normalizedValue.trim().length) {
|
||||
return acc
|
||||
}
|
||||
|
||||
acc.push({
|
||||
key,
|
||||
value: normalizedValue,
|
||||
spoken_variants: buildSpokenVariants(key, normalizedValue)
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const okNext = pickTransition(input.state.ok_next, input.candidates)
|
||||
const badNext = pickTransition(input.state.bad_next, input.candidates)
|
||||
|
||||
@@ -114,6 +114,7 @@ const atcDecisionTree = {
|
||||
"role": "atc",
|
||||
"phase": "Clearance",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["dest","sid","runway","initial_altitude_ft","squawk"],
|
||||
"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."}]
|
||||
@@ -179,7 +180,21 @@ const atcDecisionTree = {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiOut",
|
||||
"utterance_tpl": "{callsign} taxi to {runway} via {taxi_route}, holding short {runway}.",
|
||||
"next": [{"to": "TWR_CONTACT"}]
|
||||
"next": [{"to": "GRD_TAXI_READBACK_CHECK"}]
|
||||
},
|
||||
"GRD_TAXI_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["runway","taxi_route","hold_short"],
|
||||
"ok_next": [{"to": "TWR_CONTACT"}],
|
||||
"bad_next": [{"to": "GRD_TAXI_READBACK_CORRECT"}]
|
||||
},
|
||||
"GRD_TAXI_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiOut",
|
||||
"say_tpl": "{callsign}, negative; taxi to runway {runway} via {taxi_route}, hold short runway {runway}.",
|
||||
"next": [{"to": "GRD_TAXI_READBACK"}]
|
||||
},
|
||||
|
||||
"TWR_CONTACT": {
|
||||
@@ -216,7 +231,21 @@ const atcDecisionTree = {
|
||||
"role": "pilot",
|
||||
"phase": "Departure",
|
||||
"utterance_tpl": "{callsign} cleared for take-off {runway}.",
|
||||
"next": [{"to": "DEP_CONTACT"}]
|
||||
"next": [{"to": "TWR_TAKEOFF_READBACK_CHECK"}]
|
||||
},
|
||||
"TWR_TAKEOFF_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["runway","cleared_takeoff"],
|
||||
"ok_next": [{"to": "DEP_CONTACT"}],
|
||||
"bad_next": [{"to": "TWR_TAKEOFF_READBACK_CORRECT"}]
|
||||
},
|
||||
"TWR_TAKEOFF_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "Departure",
|
||||
"say_tpl": "{callsign}, negative; runway {runway}, cleared for take-off.",
|
||||
"next": [{"to": "TWR_TAKEOFF_READBACK"}]
|
||||
},
|
||||
|
||||
"DEP_CONTACT": {
|
||||
@@ -373,7 +402,21 @@ const atcDecisionTree = {
|
||||
"role": "pilot",
|
||||
"phase": "TaxiIn",
|
||||
"utterance_tpl": "{callsign} taxi to stand {gate} via {taxi_route}.",
|
||||
"next": [{"to": "FLOW_COMPLETE"}]
|
||||
"next": [{"to": "GRD_TAXI_IN_READBACK_CHECK"}]
|
||||
},
|
||||
"GRD_TAXI_IN_READBACK_CHECK": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiIn",
|
||||
"auto": "check_readback",
|
||||
"readback_required": ["gate","taxi_route"],
|
||||
"ok_next": [{"to": "FLOW_COMPLETE"}],
|
||||
"bad_next": [{"to": "GRD_TAXI_IN_READBACK_CORRECT"}]
|
||||
},
|
||||
"GRD_TAXI_IN_READBACK_CORRECT": {
|
||||
"role": "atc",
|
||||
"phase": "TaxiIn",
|
||||
"say_tpl": "{callsign}, negative; taxi to stand {gate} via {taxi_route}.",
|
||||
"next": [{"to": "GRD_TAXI_IN_READBACK"}]
|
||||
},
|
||||
"FLOW_COMPLETE": {
|
||||
"role": "system",
|
||||
|
||||
Reference in New Issue
Block a user