mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
feat(openai): Drastically reduce LLM Tokens
chore: Optimize the input data sent to the OpenAI API to reduce token usage and improve decision quality. Add quick responses for common pilot utterances like radio checks and emergencies to avoid unnecessary LLM calls. The key changes are: - Use a smaller LLM model (`gpt-5-nano`) to reduce token usage - Optimize the input data by only sending relevant candidate and context information - Implement quick responses for common pilot utterances like radio checks and emergencies - Ensure ATC responses are only included when necessary to reduce token usage
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
// server/utils/openai.ts
|
||||
import OpenAI from 'openai'
|
||||
|
||||
const MODEL = process.env.LLM_MODEL || 'gpt-4o-mini'
|
||||
const MODEL = process.env.LLM_MODEL || 'gpt-5-nano'
|
||||
export const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY!})
|
||||
|
||||
export async function decide(system: string, user: string): Promise<string> {
|
||||
const body = {
|
||||
const r = await openai.chat.completions.create({
|
||||
model: MODEL,
|
||||
response_format: {type: 'text'},
|
||||
messages: [
|
||||
{role: 'system', content: system},
|
||||
{role: 'user', content: user}
|
||||
],
|
||||
}
|
||||
console.log('OpenAI request:', body)
|
||||
const r = await openai.chat.completions.create(body)
|
||||
console.log('OpenAI response:', r)
|
||||
]
|
||||
})
|
||||
return r.choices?.[0]?.message?.content?.trim() || ''
|
||||
}
|
||||
|
||||
/** Typen wie im Frontend */
|
||||
export interface LLMDecisionInput {
|
||||
state_id: string
|
||||
state: any
|
||||
@@ -34,116 +29,163 @@ export interface LLMDecision {
|
||||
updates?: Record<string, any>
|
||||
flags?: Record<string, any>
|
||||
controller_say_tpl?: string
|
||||
off_schema?: boolean // Flag wenn LLM frei geantwortet hat
|
||||
radio_check?: boolean // Flag für Radio Check
|
||||
off_schema?: boolean
|
||||
radio_check?: boolean
|
||||
}
|
||||
|
||||
// Token-optimierte Eingabe für OpenAI
|
||||
// Optimierte aber ausreichende Eingabe für gute Entscheidungen
|
||||
function optimizeInputForLLM(input: LLMDecisionInput) {
|
||||
// Nur relevante Candidate-Daten senden
|
||||
// Nur relevante Candidate-Daten (reduziert aber nicht zu stark)
|
||||
const candidates = input.candidates.map(c => ({
|
||||
id: c.id,
|
||||
role: c.state.role,
|
||||
phase: c.state.phase,
|
||||
say_tpl: c.state.say_tpl,
|
||||
utterance_tpl: c.state.utterance_tpl
|
||||
say_tpl: c.state.say_tpl ? c.state.say_tpl.substring(0, 100) : undefined // Gekürzt auf 100 Zeichen
|
||||
}))
|
||||
|
||||
// Nur wichtige Variablen senden
|
||||
const relevantVars = {
|
||||
// Wichtige Variablen für Context
|
||||
const essentials = {
|
||||
callsign: input.variables.callsign,
|
||||
dep: input.variables.dep,
|
||||
dest: input.variables.dest,
|
||||
runway: input.variables.runway,
|
||||
squawk: input.variables.squawk,
|
||||
current_frequency: input.variables[`${input.flags.current_unit?.toLowerCase()}_freq`]
|
||||
current_unit: input.flags.current_unit,
|
||||
in_air: input.flags.in_air
|
||||
}
|
||||
|
||||
return {
|
||||
state_id: input.state_id,
|
||||
current_role: input.state.role,
|
||||
current_phase: input.state.phase,
|
||||
current_role: input.state.role,
|
||||
candidates: candidates,
|
||||
variables: relevantVars,
|
||||
flags: input.flags,
|
||||
context: essentials,
|
||||
pilot_utterance: input.pilot_utterance
|
||||
}
|
||||
}
|
||||
|
||||
export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecision> {
|
||||
const pilotText = input.pilot_utterance.toLowerCase().trim()
|
||||
|
||||
// Sofortige Erkennung ohne LLM für häufige Cases
|
||||
if (pilotText.includes('radio check') || pilotText.includes('signal test') ||
|
||||
(pilotText.includes('read') && (pilotText.includes('check') || pilotText.includes('you')))) {
|
||||
return {
|
||||
next_state: input.state_id,
|
||||
radio_check: true,
|
||||
controller_say_tpl: `${input.variables.callsign}, read you five by five.`
|
||||
}
|
||||
}
|
||||
|
||||
// Emergency ohne LLM
|
||||
if (pilotText.startsWith('mayday') && input.flags.in_air) {
|
||||
return { next_state: 'INT_MAYDAY' }
|
||||
}
|
||||
if (pilotText.startsWith('pan pan') && input.flags.in_air) {
|
||||
return { next_state: 'INT_PANPAN' }
|
||||
}
|
||||
|
||||
const optimizedInput = optimizeInputForLLM(input)
|
||||
|
||||
// Prüfe ob nächste States ATC-Responses brauchen
|
||||
const atcCandidates = input.candidates.filter(c =>
|
||||
c.state.role === 'atc' || c.state.say_tpl || c.id.startsWith('INT_')
|
||||
)
|
||||
|
||||
// Wenn keine ATC-States verfügbar, einfache Transition ohne Response
|
||||
if (atcCandidates.length === 0 && input.candidates.length > 0) {
|
||||
return { next_state: input.candidates[0].id }
|
||||
}
|
||||
|
||||
// Kompakter aber informativer Prompt - nur ATC responses wenn nötig
|
||||
const system = [
|
||||
'You are an ATC state router with flexible response capability.',
|
||||
'Return STRICT JSON with keys: next_state (string), optional updates (object), flags (object), controller_say_tpl (string), off_schema (boolean), radio_check (boolean).',
|
||||
'You are an ATC state router. Return strict JSON.',
|
||||
'Keys: next_state, controller_say_tpl (optional), off_schema (optional).',
|
||||
'',
|
||||
'PRIMARY ROUTING:',
|
||||
'- Pick next_state from candidates[].id when pilot utterance matches expected flow',
|
||||
'- Use "RESUME_PRIOR_FLOW" to return from interrupts',
|
||||
'- Use "GEN_NO_REPLY" if pilot did not respond clearly',
|
||||
'ROUTING: Pick next_state from candidates[].id when pilot matches expected flow.',
|
||||
'ATC RESPONSES: Only include controller_say_tpl if:',
|
||||
'- Next state role is "atc" OR has say_tpl',
|
||||
'- OR off_schema=true (pilot needs response but no candidate fits)',
|
||||
'- OR pilot needs acknowledgment/correction',
|
||||
'',
|
||||
'SPECIAL CASES:',
|
||||
'- RADIO CHECK: If pilot says "radio check" or requests signal test, set radio_check=true and respond with appropriate signal strength',
|
||||
'- OFF-SCHEMA: If pilot says something that doesn\'t match any candidate but requires ATC response, set off_schema=true and provide appropriate controller_say_tpl',
|
||||
'PILOT STATES: If next state role is "pilot", NO controller_say_tpl needed.',
|
||||
'DEFAULT: Use "GEN_NO_REPLY" if unclear.',
|
||||
'',
|
||||
'INTERRUPTS (only if flags.in_air === true):',
|
||||
'- "INT_MAYDAY" for MAYDAY emergencies',
|
||||
'- "INT_PANPAN" for PAN-PAN situations',
|
||||
'',
|
||||
'RESPONSE GENERATION:',
|
||||
'- Always use proper ATC phraseology in controller_say_tpl',
|
||||
'- Include callsign and relevant flight data',
|
||||
'- Keep responses professional and concise',
|
||||
'',
|
||||
'If uncertain, prefer appropriate ATC response over silence.'
|
||||
'Use proper ATC phraseology when controller_say_tpl is included.'
|
||||
].join(' ')
|
||||
|
||||
// Update optimized input to indicate which candidates need ATC responses
|
||||
optimizedInput.atc_candidates = atcCandidates.map(c => c.id)
|
||||
|
||||
const user = JSON.stringify(optimizedInput)
|
||||
|
||||
const body = {
|
||||
model: MODEL,
|
||||
response_format: {type: 'json_object'},
|
||||
messages: [
|
||||
{role: 'system', content: system},
|
||||
{role: 'user', content: user}
|
||||
],
|
||||
}
|
||||
|
||||
const r = await openai.chat.completions.create(body)
|
||||
console.log('OpenAI router response:', r)
|
||||
|
||||
const raw = r.choices?.[0]?.message?.content || '{}'
|
||||
try {
|
||||
const r = await openai.chat.completions.create({
|
||||
model: MODEL,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user }
|
||||
]
|
||||
})
|
||||
|
||||
const raw = r.choices?.[0]?.message?.content || '{}'
|
||||
const parsed = JSON.parse(raw)
|
||||
|
||||
// Minimal-Check
|
||||
// Minimal validation
|
||||
if (!parsed.next_state || typeof parsed.next_state !== 'string') {
|
||||
throw new Error('Missing next_state')
|
||||
}
|
||||
|
||||
// Radio Check Handling
|
||||
if (parsed.radio_check) {
|
||||
const callsign = input.variables.callsign || ''
|
||||
const freq = optimizedInput.variables.current_frequency || ''
|
||||
parsed.controller_say_tpl = `${callsign}, ${freq}, read you five by five.`
|
||||
throw new Error('Invalid next_state')
|
||||
}
|
||||
|
||||
return parsed as LLMDecision
|
||||
} catch (e) {
|
||||
console.error('LLM JSON parse error:', e)
|
||||
|
||||
// Fallback: Check for radio check in pilot utterance
|
||||
const pilotText = input.pilot_utterance.toLowerCase()
|
||||
if (pilotText.includes('radio check') || pilotText.includes('signal') || pilotText.includes('read')) {
|
||||
const callsign = input.variables.callsign || ''
|
||||
} catch (e) {
|
||||
console.error('LLM JSON parse error, using smart fallback:', e)
|
||||
|
||||
// Smart keyword-based fallback - nur ATC response wenn nötig
|
||||
const callsign = input.variables.callsign || ''
|
||||
|
||||
// Pilot braucht Clearance → ATC muss antworten
|
||||
if (pilotText.includes('clearance') || pilotText.includes('request clearance')) {
|
||||
return {
|
||||
next_state: input.state_id, // Stay in current state
|
||||
radio_check: true,
|
||||
controller_say_tpl: `${callsign}, read you five by five.`
|
||||
next_state: 'CD_ISSUE_CLR',
|
||||
off_schema: true,
|
||||
controller_say_tpl: `${callsign}, standby for clearance.`
|
||||
}
|
||||
}
|
||||
|
||||
// Standard fallback
|
||||
return {next_state: 'GEN_NO_REPLY'}
|
||||
// Pilot fragt nach Taxi → ATC muss antworten
|
||||
if (pilotText.includes('taxi') || pilotText.includes('pushback')) {
|
||||
return {
|
||||
next_state: 'GRD_TAXI_INSTR',
|
||||
off_schema: true,
|
||||
controller_say_tpl: `${callsign}, standby for taxi instructions.`
|
||||
}
|
||||
}
|
||||
|
||||
// Pilot ready for takeoff → ATC muss antworten
|
||||
if (pilotText.includes('takeoff') || pilotText.includes('ready')) {
|
||||
return {
|
||||
next_state: 'TWR_TAKEOFF_CLR',
|
||||
off_schema: true,
|
||||
controller_say_tpl: `${callsign}, standby for takeoff clearance.`
|
||||
}
|
||||
}
|
||||
|
||||
// Pilot readback oder acknowledgment → keine ATC response nötig
|
||||
if (pilotText.includes('wilco') || pilotText.includes('roger') ||
|
||||
pilotText.includes('cleared') || pilotText.includes('copied')) {
|
||||
return {
|
||||
next_state: input.candidates[0]?.id || 'GEN_NO_REPLY'
|
||||
// Keine controller_say_tpl - Pilot hat nur acknowledged
|
||||
}
|
||||
}
|
||||
|
||||
// Generic fallback - nur response wenn unklar was pilot will
|
||||
return {
|
||||
next_state: 'GEN_NO_REPLY',
|
||||
off_schema: true,
|
||||
controller_say_tpl: `${callsign}, say again your last transmission.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user