314 lines
8.0 KiB
TypeScript
314 lines
8.0 KiB
TypeScript
/** 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<string, unknown>;
|
|
resultAssignmentMode?: ActionResultAssignmentMode;
|
|
resultAssignments?: Record<string, string>;
|
|
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<string, string> = {
|
|
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<string, LucideIcon>;
|
|
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<string, RuntimeNodeSpec>;
|
|
|
|
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);
|
|
});
|
|
}
|