mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-15 03:25:40 +08:00
Improve flow activation handling and expose active nodes
This commit is contained in:
@@ -5,7 +5,8 @@ import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { execFile } from "node:child_process";
|
||||
import { getOpenAIClient, routeDecision, type LLMDecisionResult } from "../../utils/openai";
|
||||
import { getOpenAIClient, routeDecision } from "../../utils/openai";
|
||||
import type { LLMDecisionResult } from "~~/shared/types/llm";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { TransmissionLog } from "../../models/TransmissionLog";
|
||||
import { getUserFromEvent } from "../../utils/auth";
|
||||
@@ -33,6 +34,7 @@ interface PTTResponse {
|
||||
transcription: string;
|
||||
decision?: LLMDecisionResult['decision'];
|
||||
trace?: LLMDecisionResult['trace'];
|
||||
active_nodes?: LLMDecisionResult['active_nodes'];
|
||||
}
|
||||
|
||||
async function sh(cmd: string, args: string[]) {
|
||||
@@ -273,6 +275,9 @@ export default defineEventHandler(async (event) => {
|
||||
if (decisionResult?.trace) {
|
||||
result.trace = decisionResult.trace;
|
||||
}
|
||||
if (decisionResult?.active_nodes?.length) {
|
||||
result.active_nodes = decisionResult.active_nodes;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import OpenAI from 'openai'
|
||||
import {spellIcaoDigits, toIcaoPhonetic} from '../../shared/utils/radioSpeech'
|
||||
import type {
|
||||
ActiveNodeSummary,
|
||||
CandidateTraceEntry,
|
||||
CandidateTraceStep,
|
||||
DecisionCandidateTimeline,
|
||||
@@ -9,6 +10,7 @@ import type {
|
||||
FlowActivationMode,
|
||||
LLMDecision,
|
||||
LLMDecisionInput,
|
||||
LLMDecisionResult,
|
||||
LLMDecisionTrace,
|
||||
LLMDecisionTraceCall,
|
||||
} from '../../shared/types/llm'
|
||||
@@ -74,10 +76,6 @@ export async function decide(system: string, user: string): Promise<string> {
|
||||
return r.choices?.[0]?.message?.content?.trim() || ''
|
||||
}
|
||||
|
||||
export interface LLMDecisionResult {
|
||||
decision: LLMDecision
|
||||
trace?: LLMDecisionTrace
|
||||
}
|
||||
|
||||
type ReadbackStatus = 'ok' | 'missing' | 'incorrect' | 'uncertain'
|
||||
|
||||
@@ -212,6 +210,8 @@ interface DecisionCandidate {
|
||||
interface PreparedCandidateResult {
|
||||
finalCandidates: DecisionCandidate[]
|
||||
candidateFlowMap: Map<string, string>
|
||||
candidateIndex: Map<string, DecisionCandidate>
|
||||
finalCandidateIndex: Map<string, DecisionCandidate>
|
||||
activeFlowSlug: string
|
||||
flowEntryModes: Map<string, FlowActivationMode>
|
||||
timeline: DecisionCandidateTimeline
|
||||
@@ -726,6 +726,11 @@ async function prepareDecisionCandidates(
|
||||
}
|
||||
}
|
||||
|
||||
const finalCandidateIndex = new Map<string, DecisionCandidate>()
|
||||
for (const candidate of finalCandidates) {
|
||||
finalCandidateIndex.set(candidate.id, candidate)
|
||||
}
|
||||
|
||||
const timeline: DecisionCandidateTimeline = {
|
||||
steps: timelineSteps,
|
||||
fallbackUsed,
|
||||
@@ -735,6 +740,8 @@ async function prepareDecisionCandidates(
|
||||
return {
|
||||
finalCandidates,
|
||||
candidateFlowMap,
|
||||
candidateIndex: candidateMap,
|
||||
finalCandidateIndex,
|
||||
activeFlowSlug,
|
||||
flowEntryModes,
|
||||
timeline,
|
||||
@@ -918,21 +925,98 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
|
||||
}
|
||||
|
||||
const finalize = (decision: LLMDecision): LLMDecisionResult => {
|
||||
const targetState = decision.next_state
|
||||
const targetState = typeof decision.next_state === 'string' ? decision.next_state : ''
|
||||
const normalizedControllerSay = typeof decision.controller_say_tpl === 'string'
|
||||
? decision.controller_say_tpl.trim()
|
||||
: ''
|
||||
|
||||
const targetCandidate = targetState
|
||||
? prepared.finalCandidateIndex.get(targetState) || prepared.candidateIndex.get(targetState)
|
||||
: undefined
|
||||
const targetFlow = targetCandidate?.flow || (targetState ? candidateFlowMap.get(targetState) : undefined)
|
||||
const candidateSayTemplate = targetCandidate?.state?.say_tpl
|
||||
const candidateRole = targetCandidate?.state?.role
|
||||
|
||||
if ((!normalizedControllerSay.length) && candidateRole === 'atc' && typeof candidateSayTemplate === 'string') {
|
||||
decision.controller_say_tpl = candidateSayTemplate
|
||||
}
|
||||
|
||||
let activation = resolveActivationInstruction(decision.activate_flow as any)
|
||||
if (!activation && targetState) {
|
||||
const targetFlow = candidateFlowMap.get(targetState)
|
||||
if (targetFlow && targetFlow !== activeFlowSlug) {
|
||||
activation = resolveActivationInstruction(targetFlow)
|
||||
}
|
||||
if (!activation && targetFlow && targetFlow !== activeFlowSlug) {
|
||||
activation = resolveActivationInstruction(targetFlow)
|
||||
}
|
||||
|
||||
const finalControllerSay = typeof decision.controller_say_tpl === 'string'
|
||||
? decision.controller_say_tpl.trim()
|
||||
: ''
|
||||
const atcWillSpeak = Boolean(
|
||||
(finalControllerSay && finalControllerSay.length)
|
||||
|| (candidateRole === 'atc' && typeof candidateSayTemplate === 'string' && candidateSayTemplate.trim().length)
|
||||
)
|
||||
|
||||
if (targetCandidate?.state?.auto === 'pop_stack_or_route_by_intent') {
|
||||
decision.resume_previous = true
|
||||
}
|
||||
|
||||
if (activation) {
|
||||
if (activation.slug !== activeFlowSlug && atcWillSpeak && activation.mode !== 'main') {
|
||||
activation.mode = 'linear'
|
||||
}
|
||||
decision.activate_flow = activation
|
||||
} else if (decision.activate_flow) {
|
||||
delete (decision as any).activate_flow
|
||||
}
|
||||
|
||||
let activeNodes: ActiveNodeSummary[] | undefined
|
||||
if (activation?.mode === 'parallel') {
|
||||
const nodes: ActiveNodeSummary[] = []
|
||||
|
||||
if (input.state_id && activeFlowSlug) {
|
||||
const previousSay = typeof input.state?.say_tpl === 'string' ? input.state.say_tpl : undefined
|
||||
nodes.push({
|
||||
flow: activeFlowSlug,
|
||||
state: input.state_id,
|
||||
role: input.state?.role,
|
||||
say_tpl: previousSay,
|
||||
controller_say_tpl: input.state?.role === 'atc' ? previousSay : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (targetState) {
|
||||
const flowForTarget = targetFlow || activeFlowSlug
|
||||
if (flowForTarget) {
|
||||
nodes.push({
|
||||
flow: flowForTarget,
|
||||
state: targetState,
|
||||
role: candidateRole,
|
||||
say_tpl: typeof candidateSayTemplate === 'string' ? candidateSayTemplate : undefined,
|
||||
controller_say_tpl: candidateRole === 'atc'
|
||||
? (decision.controller_say_tpl || candidateSayTemplate || undefined)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length) {
|
||||
const seen = new Set<string>()
|
||||
activeNodes = nodes.filter(node => {
|
||||
if (!node.flow || !node.state) {
|
||||
return false
|
||||
}
|
||||
const key = `${node.flow}::${node.state}`
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if (!activeNodes?.length) {
|
||||
activeNodes = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const shouldAttachTrace = Boolean(
|
||||
trace.calls.length
|
||||
|| trace.fallback
|
||||
@@ -940,10 +1024,14 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
|
||||
|| trace.autoSelection
|
||||
)
|
||||
|
||||
if (!shouldAttachTrace) {
|
||||
return { decision }
|
||||
const result: LLMDecisionResult = { decision }
|
||||
if (shouldAttachTrace) {
|
||||
result.trace = trace
|
||||
}
|
||||
return { decision, trace }
|
||||
if (activeNodes && activeNodes.length) {
|
||||
result.active_nodes = activeNodes
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function handleReadbackCheck(): Promise<LLMDecisionResult> {
|
||||
|
||||
@@ -17,6 +17,14 @@ export interface FlowActivationInstruction {
|
||||
mode?: FlowActivationMode
|
||||
}
|
||||
|
||||
export interface ActiveNodeSummary {
|
||||
flow: string
|
||||
state: string
|
||||
role?: string
|
||||
say_tpl?: string
|
||||
controller_say_tpl?: string
|
||||
}
|
||||
|
||||
export interface CandidateTraceEntry {
|
||||
id: string
|
||||
flow: string
|
||||
@@ -107,3 +115,9 @@ export interface LLMDecisionTrace {
|
||||
reason?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface LLMDecisionResult {
|
||||
decision: LLMDecision
|
||||
trace?: LLMDecisionTrace
|
||||
active_nodes?: ActiveNodeSummary[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user