/** Workflow v3 public graph types and defaults. */ import * as LucideIcons from "lucide-react"; import { Circle, type LucideIcon } from "lucide-react"; import type { NodeSpecDto, TurnConfig } from "@/lib/api"; import { defaultTurnConfig } from "@/lib/turn-config"; export type WorkflowNodeType = | "start" | "agent" | "message" | "action" | "update_state" | "handoff" | "end"; export type ContextPolicy = "inherit" | "fresh"; export type KnowledgeMode = "automatic" | "on_demand" | "disabled"; export type AgentEntryMode = "wait_user" | "generate"; export type MessageCompletionPolicy = | "interruptible" | "playback" | "confirmation"; export type ActionResultAssignmentMode = "inherit" | "override" | "none"; export type ActionUserInputPolicy = "queue" | "block"; export type EdgeMode = "llm" | "expression" | "always"; export type WorkflowRuntimeMode = "pipeline" | "realtime"; export type WorkflowLlmRoutingMode = "llm_router" | "edge_tool"; export type ExpressionOperator = | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "contains" | "in" | "exists"; export type WorkflowNodeData = { name: string; prompt?: string; contextPolicy?: ContextPolicy; inheritGlobalConfig?: boolean; entryMode?: AgentEntryMode; stateVariableNames?: string[]; toolIds?: string[]; knowledgeBaseId?: string; knowledgeMode?: KnowledgeMode; knowledgeTopN?: number; knowledgeScoreThreshold?: number; llmResourceId?: string; asrResourceId?: string; ttsResourceId?: string; visionEnabled?: boolean; visionModelResourceId?: string; enableInterrupt?: boolean; turnConfig?: TurnConfig; toolId?: string; arguments?: Record; resultAssignmentMode?: ActionResultAssignmentMode; resultAssignments?: Record; assignments?: Record; userInputPolicy?: ActionUserInputPolicy; speech?: string; /** Legacy field read only while an older graph is open in the editor. */ showMessage?: boolean; title?: string; confirmLabel?: string; completionPolicy?: MessageCompletionPolicy; /** Legacy field read only while an older graph is open in the editor. */ requireConfirmation?: boolean; targetType?: "ai" | "human" | "queue" | "phone"; target?: string; message?: string; scope?: "flow" | "session"; [key: string]: unknown; }; /** Infer the new Message policy for graphs saved before it was explicit. */ export function messageCompletionPolicy( data: WorkflowNodeData, ): MessageCompletionPolicy { if ( data.completionPolicy === "interruptible" || data.completionPolicy === "playback" || data.completionPolicy === "confirmation" ) { return data.completionPolicy; } return data.requireConfirmation === true ? "confirmation" : "playback"; } /** * Resolve how an Action writes tool results while preserving workflows saved * before resultAssignmentMode existed. Those nodes explicitly passed an empty * mapping at runtime, so an absent/empty mapping means "none", not "inherit". */ export function actionResultAssignmentMode( data: WorkflowNodeData, ): ActionResultAssignmentMode { if ( data.resultAssignmentMode === "inherit" || data.resultAssignmentMode === "override" || data.resultAssignmentMode === "none" ) { return data.resultAssignmentMode; } return Object.keys(data.resultAssignments ?? {}).length > 0 ? "override" : "none"; } /** Older Action nodes allowed input to continue, which matches queue. */ export function actionUserInputPolicy( data: WorkflowNodeData, ): ActionUserInputPolicy { return data.userInputPolicy === "block" ? "block" : "queue"; } export type ExpressionRule = { variable: string; operator: ExpressionOperator; value?: unknown; }; export type WorkflowEdgeData = { mode: EdgeMode; /** Internal route order, assigned automatically when an edge is created. */ priority: number; condition?: string; expression?: { combinator: "and" | "or"; rules: ExpressionRule[] }; label?: string; }; export type FieldSpec = { key: string; label: string; type: "text" | "textarea" | "switch"; required?: boolean; default?: unknown; }; export type RuntimeNodeSpec = { type: string; displayName: string; description: string; icon: LucideIcon; accent: string; addable: boolean; hasTarget: boolean; hasSource: boolean; constraints: { minIncoming?: number; maxIncoming?: number; minOutgoing?: number; maxOutgoing?: number; minInstances?: number; maxInstances?: number; }; fields: FieldSpec[]; }; export const ACCENT_VAR: Record = { mint: "--gradient-mint", sky: "--gradient-sky", rose: "--gradient-rose", peach: "--gradient-peach", lavender: "--gradient-lavender", }; export function accentVar(accent: string): string { return ACCENT_VAR[accent] ?? ACCENT_VAR.sky; } export function resolveIcon(name: string): LucideIcon { const icons = LucideIcons as unknown as Record; return icons[name] ?? Circle; } export function toRuntimeSpec(dto: NodeSpecDto): RuntimeNodeSpec { return { type: dto.name, displayName: dto.displayName, description: dto.description, icon: resolveIcon(dto.icon), accent: dto.accent, addable: dto.addable, hasTarget: dto.constraints.maxIncoming !== 0, hasSource: dto.constraints.maxOutgoing !== 0, constraints: dto.constraints, fields: dto.fields.map((field) => ({ key: field.key, label: field.label, type: field.type, required: field.required, default: field.default, })), }; } export type NodeSpecMap = Record; export type WorkflowGraph = { specVersion: 3; settings: { runtimeMode: WorkflowRuntimeMode; llmRoutingMode: WorkflowLlmRoutingMode; globalPrompt: string; defaultRealtimeResourceId: string; defaultLlmResourceId: string; defaultAsrResourceId: string; defaultTtsResourceId: string; visionEnabled: boolean; visionModelResourceId: string; toolIds: string[]; knowledgeBaseId: string; knowledgeMode: "automatic" | "on_demand"; knowledgeTopN: number; knowledgeScoreThreshold: number; enableInterrupt: boolean; turnConfig: TurnConfig; }; nodes: Array<{ id: string; type: WorkflowNodeType; position: { x: number; y: number }; data: WorkflowNodeData; }>; edges: Array<{ id: string; source: string; target: string; data: WorkflowEdgeData; }>; viewport?: { x: number; y: number; zoom: number }; }; export function defaultGraph(): WorkflowGraph { return { specVersion: 3, settings: { runtimeMode: "pipeline", llmRoutingMode: "edge_tool", globalPrompt: "你是一个友好、专业的语音助手。请使用简短、自然、适合口语表达的句子。", defaultRealtimeResourceId: "", defaultLlmResourceId: "", defaultAsrResourceId: "", defaultTtsResourceId: "", visionEnabled: false, visionModelResourceId: "", toolIds: [], knowledgeBaseId: "", knowledgeMode: "automatic", knowledgeTopN: 5, knowledgeScoreThreshold: 0, enableInterrupt: true, turnConfig: defaultTurnConfig(), }, nodes: [ { id: "start", type: "start", position: { x: 360, y: 60 }, data: { name: "Start", }, }, { id: "message-1", type: "message", position: { x: 360, y: 260 }, data: { name: "开场消息", speech: "你好,我是 AI 视频助手,有什么可以帮你?", title: "重要提示", message: "", confirmLabel: "确认", completionPolicy: "playback", }, }, { id: "agent-1", type: "agent", position: { x: 360, y: 480 }, data: { name: "Agent", prompt: "了解用户需求并提供清晰、准确的帮助。", contextPolicy: "inherit", inheritGlobalConfig: true, entryMode: "wait_user", stateVariableNames: [], }, }, { id: "end", type: "end", position: { x: 360, y: 700 }, data: { name: "End", message: "感谢你的来电,再见。", scope: "session", }, }, ], edges: [ { id: "e-start-message", source: "start", target: "message-1", data: { mode: "always", priority: 0 }, }, { id: "e-message-agent", source: "message-1", target: "agent-1", data: { mode: "always", priority: 0 }, }, { id: "e-agent-end", source: "agent-1", target: "end", data: { mode: "llm", priority: 10, condition: "当前阶段任务已经完成,适合结束会话", }, }, ], }; } /** The camera stays connected when at least one effective Agent can use it. */ export function workflowUsesVision(graph: WorkflowGraph): boolean { return graph.nodes.some((node) => { if (node.type !== "agent") return false; const inheritsGlobal = node.data.inheritGlobalConfig !== false; return inheritsGlobal ? graph.settings.visionEnabled : Boolean(node.data.visionEnabled); }); }