feat: surface pilot intent from llm decisions

This commit is contained in:
Remi
2025-10-16 21:07:21 +02:00
parent d9de0c1c93
commit faebc91ebb
3 changed files with 32 additions and 6 deletions

View File

@@ -113,6 +113,7 @@ describe('routeDecision', () => {
assert.equal(result.decision.next_state, 'ACK')
assert.equal(result.trace?.calls.length ?? 0, 0)
assert.equal(result.trace?.autoSelection?.id, 'ACK')
assert.equal(result.pilot_intent ?? null, null)
})
it('falls back to heuristic selection when OpenAI call fails', async () => {
@@ -131,6 +132,7 @@ describe('routeDecision', () => {
assert.ok(result.trace?.calls[0]?.error)
assert.equal(result.trace?.fallback?.used, true)
assert.equal(result.decision.next_state, 'TAXI')
assert.equal(result.pilot_intent ?? null, null)
})
})

View File

@@ -914,6 +914,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
calls: [],
candidateTimeline: prepared.timeline,
}
let pilotIntent: string | null = null
if (prepared.autoSelected) {
trace.autoSelection = {
@@ -924,6 +925,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
return {
decision: { next_state: prepared.autoSelected.id },
trace,
pilot_intent: pilotIntent,
}
}
@@ -938,7 +940,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
reason: 'No viable candidates after heuristic evaluation; falling back to default transition.',
selected: fallbackState,
}
return { decision: { next_state: fallbackState }, trace }
return { decision: { next_state: fallbackState }, trace, pilot_intent: pilotIntent }
}
const optimizedInput = optimizeInputForLLM({
@@ -965,7 +967,10 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
const systemPrompt = [
'You are an assistant that selects the correct next state in an aviation decision tree.',
'Evaluate the pilot transmission and choose the most appropriate candidate state id from the provided list.',
'Respond strictly with a JSON object: {"next_state": "STATE_ID", "reason": "short rationale"}.',
[
'Respond strictly with a JSON object whose first property is "pilot_intent"',
'followed by "next_state" and "reason": {"pilot_intent": "intent", "next_state": "STATE_ID", "reason": "short rationale"}.',
].join(' '),
'Only use state ids that were provided. If you cannot decide, choose the best heuristic option.',
].join(' ')
@@ -991,23 +996,40 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
const rawResponse = await decide(systemPrompt, userPrompt)
callEntry.rawResponseText = rawResponse
const parsed = extractJsonObject(rawResponse)
if (parsed && typeof parsed.next_state === 'string' && parsed.next_state.trim().length > 0) {
if (parsed && typeof parsed === 'object') {
callEntry.response = parsed
if (typeof (parsed as any).pilot_intent === 'string') {
const intentValue = ((parsed as any).pilot_intent as string).trim()
if (intentValue) {
pilotIntent = intentValue
}
}
}
const nextState =
parsed && typeof parsed === 'object' && typeof (parsed as any).next_state === 'string'
? ((parsed as any).next_state as string).trim()
: ''
if (nextState.length > 0) {
const resolved =
prepared.finalCandidateIndex.get(parsed.next_state)
|| prepared.candidateIndex.get(parsed.next_state)
prepared.finalCandidateIndex.get(nextState)
|| prepared.candidateIndex.get(nextState)
if (resolved) {
return {
decision: { next_state: resolved.id },
trace,
pilot_intent: pilotIntent,
}
}
return {
decision: { next_state: parsed.next_state },
decision: { next_state: nextState },
trace,
pilot_intent: pilotIntent,
}
}
@@ -1027,6 +1049,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
return {
decision: { next_state: fallbackState },
trace,
pilot_intent: pilotIntent,
}
}
}

View File

@@ -120,4 +120,5 @@ export interface LLMDecisionResult {
decision: LLMDecision
trace?: LLMDecisionTrace
active_nodes?: ActiveNodeSummary[]
pilot_intent?: string | null
}