/** 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" | "handoff" | "end"; export type ContextPolicy = "inherit" | "fresh"; export type KnowledgeMode = "automatic" | "on_demand" | "disabled"; export type AgentEntryMode = "wait_user" | "generate" | "fixed_speech"; export type ActionResultAssignmentMode = "inherit" | "override" | "none"; export type ActionUserInputPolicy = "queue" | "block"; export type EdgeMode = "llm" | "expression" | "always"; 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; entrySpeech?: 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; userInputPolicy?: ActionUserInputPolicy; speech?: string; showMessage?: boolean; title?: string; confirmLabel?: string; requireConfirmation?: boolean; targetType?: "ai" | "human" | "queue" | "phone"; target?: string; message?: string; scope?: "flow" | "session"; [key: string]: unknown; }; /** * 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; priority: number; condition?: string; expression?: { combinator: "and" | "or"; rules: ExpressionRule[] }; label?: string; transitionSpeech?: 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: { globalPrompt: 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: { globalPrompt: "你是一个友好、专业的语音助手。请使用简短、自然、适合口语表达的句子。", 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 视频助手,有什么可以帮你?", showMessage: false, title: "重要提示", message: "", confirmLabel: "确认", requireConfirmation: false, }, }, { id: "agent-1", type: "agent", position: { x: 360, y: 480 }, data: { name: "Agent", prompt: "了解用户需求并提供清晰、准确的帮助。", contextPolicy: "inherit", inheritGlobalConfig: true, entryMode: "wait_user", entrySpeech: "", }, }, { 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); }); }