From 43f2f42238674a6017fdc3f4e1e2c2bb198ec59d Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:28:47 +0200 Subject: [PATCH] Add waitlist admin view and log OpenAI decision traces --- server/api/atc/ptt.post.ts | 7 +- server/utils/openai.ts | 184 +++++++++++++++++++++++++------------ 2 files changed, 129 insertions(+), 62 deletions(-) diff --git a/server/api/atc/ptt.post.ts b/server/api/atc/ptt.post.ts index c8f4958..9936952 100644 --- a/server/api/atc/ptt.post.ts +++ b/server/api/atc/ptt.post.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; import { execFile } from "node:child_process"; -import { getOpenAIClient, routeDecision } from "../../utils/openai"; +import { getOpenAIClient, routeDecision, type LLMDecisionResult } from "../../utils/openai"; import { createReadStream } from "node:fs"; import { TransmissionLog } from "../../models/TransmissionLog"; import { getUserFromEvent } from "../../utils/auth"; @@ -139,6 +139,7 @@ export default defineEventHandler(async (event) => { const shouldAutoDecide = body.autoDecide !== false; + let decisionResult: LLMDecisionResult | null = null; let decision: PTTResponse['decision']; if (shouldAutoDecide) { @@ -148,7 +149,8 @@ export default defineEventHandler(async (event) => { pilot_utterance: transcribedText }; - decision = await routeDecision(decisionInput); + decisionResult = await routeDecision(decisionInput); + decision = decisionResult.decision; } // 5. Cleanup @@ -169,6 +171,7 @@ export default defineEventHandler(async (event) => { moduleId: body.moduleId, lessonId: body.lessonId, decision, + decisionTrace: decisionResult?.trace, autoDecide: shouldAutoDecide, }, }) diff --git a/server/utils/openai.ts b/server/utils/openai.ts index 6c61ded..d69e237 100644 --- a/server/utils/openai.ts +++ b/server/utils/openai.ts @@ -64,6 +64,28 @@ export interface LLMDecision { radio_check?: boolean } +export interface LLMDecisionTraceCall { + stage: 'readback-check' | 'decision' + request: Record + response?: any + rawResponseText?: string + error?: string +} + +export interface LLMDecisionTrace { + calls: LLMDecisionTraceCall[] + fallback?: { + used: boolean + reason?: string + selected?: string + } +} + +export interface LLMDecisionResult { + decision: LLMDecision + trace?: LLMDecisionTrace +} + type ReadbackStatus = 'ok' | 'missing' | 'incorrect' | 'uncertain' const READBACK_REQUIREMENTS: Record = { @@ -329,11 +351,19 @@ function optimizeInputForLLM(input: LLMDecisionInput) { } } -export async function routeDecision(input: LLMDecisionInput): Promise { +export async function routeDecision(input: LLMDecisionInput): Promise { const pilotUtterance = (input.pilot_utterance || '').trim() const pilotText = pilotUtterance.toLowerCase() + const trace: LLMDecisionTrace = { calls: [] } - async function handleReadbackCheck(): Promise { + const finalize = (decision: LLMDecision): LLMDecisionResult => { + if (!trace.calls.length && !trace.fallback) { + return { decision } + } + return { decision, trace } + } + + async function handleReadbackCheck(): Promise { const requiredKeys = READBACK_REQUIREMENTS[input.state_id] || input.state.readback_required || [] const expectedItems = requiredKeys.reduce>((acc, key) => { const value = resolveReadbackValue(key, input) @@ -359,7 +389,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise c.state?.role === 'system') if (interruptCandidate) { - return { next_state: interruptCandidate.id } + return finalize({ next_state: interruptCandidate.id }) } } // 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 { + return finalize({ 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' } + return finalize({ next_state: 'INT_MAYDAY' }) } if (pilotText.startsWith('pan pan') && input.flags.in_air) { - return { next_state: 'INT_PANPAN' } + return finalize({ next_state: 'INT_PANPAN' }) } const optimizedInput = optimizeInputForLLM(input) @@ -464,7 +509,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise 0) { - return { next_state: input.candidates[0].id } + return finalize({ next_state: input.candidates[0].id }) } // Kompakter aber informativer Prompt - mit Variable-Info für intelligente Responses @@ -493,23 +538,32 @@ export async function routeDecision(input: LLMDecisionInput): Promise + trace.fallback = fallbackInfo console.error('LLM JSON parse error, using smart fallback:', e) // Smart keyword-based fallback - mit Template-Variablen @@ -529,45 +588,50 @@ export async function routeDecision(input: LLMDecisionInput): Promise