Refactor workflow agent and routing components for improved functionality

- Introduce WorkflowAgentStage to manage agent stage configurations and enhance interaction with the workflow engine.
- Implement WorkflowEdgeEvaluator for priority-aware edge evaluation, improving routing decisions based on conditions and user turns.
- Update WorkflowBrain to handle user turns and routing more effectively, ensuring agents cannot have only one default path.
- Enhance CallEndCoordinator to track speech events and manage call termination based on queued speech.
- Add new models and output handling for workflow interactions, improving clarity and maintainability.
- Update tests to validate the new routing logic and agent behavior under various scenarios.
This commit is contained in:
Xin Wang
2026-07-17 22:37:15 +08:00
parent 162a3d8bec
commit bdf3d3dd9c
23 changed files with 1374 additions and 475 deletions

View File

@@ -16,7 +16,7 @@ import {
useNodesState,
useReactFlow,
} from "@xyflow/react";
import { Braces, Plus, Settings2, X } from "lucide-react";
import { Braces, Plus, Settings2 } from "lucide-react";
import {
useCallback,
useEffect,
@@ -26,13 +26,6 @@ import {
} from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
@@ -40,6 +33,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { AddNodeDialog } from "./AddNodeDialog";
import { edgeTypes } from "./ConditionEdge";
import { hasDefaultPath, newEdgeData } from "./edge-rules";
import {
@@ -52,8 +46,8 @@ import { nodeTypes } from "./GenericNode";
import { EdgeSettingsPanel } from "./panels/EdgeSettingsPanel";
import { GlobalSettingsPanel } from "./panels/GlobalSettingsPanel";
import { NodeSettingsPanel } from "./panels/NodeSettingsPanel";
import { WorkflowSidePanel } from "./WorkflowSidePanel";
import {
accentVar,
defaultGraph,
type NodeSpecMap,
type RuntimeNodeSpec,
@@ -195,7 +189,9 @@ export function WorkflowCanvas({
settings.tts,
settings.toolIds,
settings.knowledgeBaseId,
settings.knowledgeRetrievalConfig,
settings.knowledgeRetrievalConfig.mode,
settings.knowledgeRetrievalConfig.topN,
settings.knowledgeRetrievalConfig.scoreThreshold,
settings.allowInterrupt,
settings.turnConfig,
]);
@@ -203,6 +199,9 @@ export function WorkflowCanvas({
const onConnect = useCallback(
(connection: Connection) => {
if (!connection.source || !connection.target) return;
const sourceType = nodes.find(
(node) => node.id === connection.source,
)?.type;
setEdges((eds) =>
addEdge(
{
@@ -210,13 +209,13 @@ export function WorkflowCanvas({
id: `e-${connection.source}-${connection.target}-${Date.now()}`,
type: "condition",
animated: true,
data: newEdgeData(eds, connection.source),
data: newEdgeData(eds, connection.source, sourceType),
},
eds,
),
);
},
[setEdges],
[nodes, setEdges],
);
// 连线约束:不能连入开始节点(无入边句柄),不能自连。
@@ -277,7 +276,7 @@ export function WorkflowCanvas({
target: id,
type: "condition",
animated: true,
data: newEdgeData(currentEdges, source.id),
data: newEdgeData(currentEdges, source.id, source.type),
},
currentEdges,
);
@@ -577,9 +576,11 @@ export function WorkflowCanvas({
</ReactFlow>
</section>
{/* 添加节点弹窗 */}
<Dialog
<AddNodeDialog
open={addOpen}
connectingFromSource={Boolean(addSourceId)}
specs={addableSpecs}
canAdd={canAddSpec}
onOpenChange={(open) => {
setAddOpen(open);
if (!open) {
@@ -587,131 +588,43 @@ export function WorkflowCanvas({
setAddPosition(null);
}
}}
>
<DialogContent className="gap-0 overflow-hidden border border-hairline bg-card p-0 shadow-2xl sm:max-w-[500px]">
<DialogHeader className="relative overflow-hidden border-b border-hairline px-6 py-6 pr-16">
<div
aria-hidden
className="pointer-events-none absolute -right-14 -top-16 h-40 w-40 rounded-full opacity-40 blur-3xl"
style={{
background:
"radial-gradient(circle, var(--gradient-sky), transparent 68%)",
}}
/>
<div className="relative flex items-start gap-4">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Plus size={18} />
</div>
<div>
<div className="caption-label text-muted-soft">
</div>
<DialogTitle className="font-display display-sm mt-1 text-ink">
</DialogTitle>
<DialogDescription className="mt-2 leading-6">
{addSourceId
? "选择节点类型,将在当前节点下方创建并自动连接。"
: "选择节点类型并添加到画布中央,随后可编辑内容并建立连线。"}
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex max-h-[440px] flex-col gap-3 overflow-y-auto bg-canvas-soft/70 p-4">
{addableSpecs.length === 0 ? (
<p className="rounded-2xl border border-dashed border-hairline-strong bg-card px-4 py-8 text-center text-sm text-muted-soft">
</p>
) : null}
{addableSpecs.map((spec) => {
const Icon = spec.icon;
const canAdd = canAddSpec(spec);
return (
<button
key={spec.type}
type="button"
disabled={!canAdd}
className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md disabled:cursor-not-allowed disabled:opacity-55 disabled:hover:translate-y-0 disabled:hover:border-hairline disabled:hover:shadow-sm"
onClick={() => addNode(spec)}
>
<span
aria-hidden
className="absolute left-5 right-5 top-0 h-px"
style={{
background: `linear-gradient(90deg, transparent, var(${accentVar(spec.accent)}), transparent)`,
}}
/>
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-foreground transition-transform group-hover:scale-105"
style={{
background: `color-mix(in srgb, var(${accentVar(spec.accent)}) 28%, var(--surface-strong))`,
}}
>
<Icon size={17} />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<span>{spec.displayName}</span>
{!canAdd && (
<span className="rounded-full bg-surface-strong px-2 py-0.5 text-[10px] font-normal text-muted-foreground">
</span>
)}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
{spec.description}
</div>
</div>
<Plus
size={15}
className="mt-1 shrink-0 text-muted-soft transition-colors group-hover:text-foreground"
/>
</button>
);
})}
</div>
</DialogContent>
</Dialog>
onSelect={addNode}
/>
{(debugOpen ||
settingsOpen ||
(editingNode && editingSpec) ||
editingEdge) && (
<aside className="absolute inset-y-0 right-0 z-40 flex w-1/2 flex-col overflow-hidden rounded-r-2xl border-l border-hairline bg-card shadow-2xl">
<WorkflowSidePanel
title={
debugOpen
? undefined
: settingsOpen
? "工作流设置"
: editingEdge
? "编辑连接条件"
: `编辑${editingSpec?.displayName ?? "节点"}`
}
closeLabel={
settingsOpen
? "关闭工作流设置"
: editingEdge
? "关闭边编辑"
: "关闭节点编辑"
}
onClose={
debugOpen
? undefined
: () => {
if (settingsOpen) setSettingsOpen(false);
else if (editingEdge) onEditingEdgeIdChange(null);
else onEditingNodeIdChange(null);
}
}
>
{debugOpen ? (
debugPanel
) : settingsOpen ||
(editingNode && editingSpec) ||
editingEdge ? (
<>
<div className="flex min-h-14 shrink-0 items-center gap-3 border-b border-hairline px-4 py-3">
<button
type="button"
aria-label={
settingsOpen
? "关闭工作流设置"
: editingEdge
? "关闭边编辑"
: "关闭节点编辑"
}
title="关闭"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground"
onClick={() => {
if (settingsOpen) setSettingsOpen(false);
else if (editingEdge) onEditingEdgeIdChange(null);
else onEditingNodeIdChange(null);
}}
>
<X size={16} />
</button>
<h2 className="min-w-0 truncate text-sm font-medium text-foreground">
{settingsOpen
? "工作流设置"
: editingEdge
? "编辑连接条件"
: `编辑${editingSpec?.displayName ?? "节点"}`}
</h2>
</div>
) : (
<div className="scrollbar-subtle min-h-0 flex-1 overflow-y-auto bg-canvas-soft px-4 pb-5 pt-4">
{settingsOpen ? (
<GlobalSettingsPanel
@@ -741,6 +654,16 @@ export function WorkflowCanvas({
<EdgeSettingsPanel
key={editingEdge.id}
edge={editingEdge}
sourceType={
nodes.find(
(node) => node.id === editingEdge.source,
)?.type as string | undefined
}
isOnlyOutgoing={
edges.filter(
(edge) => edge.source === editingEdge.source,
).length === 1
}
hasOtherDefaultPath={hasDefaultPath(
edges,
editingEdge.source,
@@ -752,9 +675,8 @@ export function WorkflowCanvas({
/>
) : null}
</div>
</>
) : null}
</aside>
)}
</WorkflowSidePanel>
)}
</div>
</EdgeActionContext.Provider>