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:
itsrubberduck
2025-09-16 13:18:57 +02:00
parent 198386ff94
commit e84feefbe2

View File

@@ -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.`
}
}
}