Refactor workflow routing and greeting management in Brain classes
- Update WorkflowBrain to handle greeting playback more effectively, ensuring that the initial greeting completes before transitioning to the first node. - Introduce new methods for managing greeting states and conditions, enhancing the interaction flow for user turns. - Refactor WorkflowLLMRouter to improve routing logic and ensure proper handling of conditional paths. - Enhance tests to verify the correct behavior of greeting management and routing under various scenarios, including waiting for audio playback to finish. - Update frontend components to reflect changes in edge handling and improve user experience in workflow configurations.
This commit is contained in:
@@ -49,7 +49,11 @@ export function ConditionEdge({
|
||||
const label = (
|
||||
(data?.label as string) ||
|
||||
(mode === "llm" ? (data?.condition as string) : "") ||
|
||||
(mode === "expression" ? "变量表达式" : "默认路径")
|
||||
(mode === "llm"
|
||||
? "大模型判断"
|
||||
: mode === "expression"
|
||||
? "表达式"
|
||||
: "默认路径")
|
||||
).toString().trim();
|
||||
const expanded = hovered || selected;
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { edgeTypes } from "./ConditionEdge";
|
||||
import { hasDefaultPath, newEdgeData } from "./edge-rules";
|
||||
import {
|
||||
ActiveNodeContext,
|
||||
EdgeActionContext,
|
||||
@@ -201,9 +202,7 @@ export function WorkflowCanvas({
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
const sourceType = nodes.find((node) => node.id === connection.source)?.type;
|
||||
const priority =
|
||||
edges.filter((edge) => edge.source === connection.source).length * 10 + 10;
|
||||
if (!connection.source || !connection.target) return;
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
@@ -211,20 +210,13 @@ export function WorkflowCanvas({
|
||||
id: `e-${connection.source}-${connection.target}-${Date.now()}`,
|
||||
type: "condition",
|
||||
animated: true,
|
||||
data:
|
||||
sourceType === "agent"
|
||||
? {
|
||||
mode: "llm",
|
||||
priority,
|
||||
condition: "当前阶段任务已经完成",
|
||||
}
|
||||
: { mode: "always", priority },
|
||||
data: newEdgeData(eds, connection.source),
|
||||
},
|
||||
eds,
|
||||
),
|
||||
);
|
||||
},
|
||||
[nodes, edges, setEdges],
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
// 连线约束:不能连入开始节点(无入边句柄),不能自连。
|
||||
@@ -278,8 +270,6 @@ export function WorkflowCanvas({
|
||||
setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
|
||||
if (source) {
|
||||
setEdges((currentEdges) => {
|
||||
const priority =
|
||||
currentEdges.filter((edge) => edge.source === source.id).length * 10 + 10;
|
||||
return addEdge(
|
||||
{
|
||||
id: `e-${source.id}-${id}-${Date.now()}`,
|
||||
@@ -287,14 +277,7 @@ export function WorkflowCanvas({
|
||||
target: id,
|
||||
type: "condition",
|
||||
animated: true,
|
||||
data:
|
||||
source.type === "agent"
|
||||
? {
|
||||
mode: "llm",
|
||||
priority,
|
||||
condition: "当前阶段任务已经完成",
|
||||
}
|
||||
: { mode: "always", priority },
|
||||
data: newEdgeData(currentEdges, source.id),
|
||||
},
|
||||
currentEdges,
|
||||
);
|
||||
@@ -363,9 +346,16 @@ export function WorkflowCanvas({
|
||||
patch: WorkflowGraph["edges"][number]["data"],
|
||||
) => {
|
||||
setEdges((es) =>
|
||||
es.map((e) =>
|
||||
e.id === id ? { ...e, data: { ...(e.data ?? {}), ...patch } } : e,
|
||||
),
|
||||
es.map((e) => {
|
||||
if (e.id !== id) return e;
|
||||
if (
|
||||
patch.mode === "always" &&
|
||||
hasDefaultPath(es, e.source, e.id)
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
return { ...e, data: { ...(e.data ?? {}), ...patch } };
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setEdges],
|
||||
@@ -392,7 +382,7 @@ export function WorkflowCanvas({
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return source.type === "agent" || outgoingCount === 0;
|
||||
return true;
|
||||
},
|
||||
[edges, nodes, specsByType],
|
||||
);
|
||||
@@ -751,7 +741,11 @@ export function WorkflowCanvas({
|
||||
<EdgeSettingsPanel
|
||||
key={editingEdge.id}
|
||||
edge={editingEdge}
|
||||
sourceType={nodes.find((node) => node.id === editingEdge.source)?.type}
|
||||
hasOtherDefaultPath={hasDefaultPath(
|
||||
edges,
|
||||
editingEdge.source,
|
||||
editingEdge.id,
|
||||
)}
|
||||
onChange={(patch) =>
|
||||
updateEdgeData(editingEdge.id, patch)
|
||||
}
|
||||
@@ -769,4 +763,3 @@ export function WorkflowCanvas({
|
||||
</NodeSpecsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
42
frontend/src/components/workflow/edge-rules.ts
Normal file
42
frontend/src/components/workflow/edge-rules.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/** Shared rules for creating and editing Workflow outgoing paths. */
|
||||
|
||||
import type { Edge } from "@xyflow/react";
|
||||
|
||||
import type { WorkflowEdgeData } from "./specs";
|
||||
|
||||
export function isDefaultPath(edge: Pick<Edge, "data">): boolean {
|
||||
const mode = (edge.data as Partial<WorkflowEdgeData> | undefined)?.mode;
|
||||
return !mode || mode === "always";
|
||||
}
|
||||
|
||||
export function hasDefaultPath(
|
||||
edges: Edge[],
|
||||
sourceId: string,
|
||||
excludingEdgeId?: string,
|
||||
): boolean {
|
||||
return edges.some(
|
||||
(edge) =>
|
||||
edge.source === sourceId &&
|
||||
edge.id !== excludingEdgeId &&
|
||||
isDefaultPath(edge),
|
||||
);
|
||||
}
|
||||
|
||||
export function nextEdgePriority(edges: Edge[], sourceId: string): number {
|
||||
const priorities = edges
|
||||
.filter((edge) => edge.source === sourceId)
|
||||
.map((edge) => Number((edge.data as Partial<WorkflowEdgeData>)?.priority))
|
||||
.filter(Number.isFinite);
|
||||
return priorities.length === 0 ? 10 : Math.max(...priorities) + 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first path from a node is deterministic. Further paths start as an
|
||||
* incomplete LLM condition so the graph never silently gains two defaults.
|
||||
*/
|
||||
export function newEdgeData(edges: Edge[], sourceId: string): WorkflowEdgeData {
|
||||
const priority = nextEdgePriority(edges, sourceId);
|
||||
return hasDefaultPath(edges, sourceId)
|
||||
? { mode: "llm", priority, condition: "" }
|
||||
: { mode: "always", priority };
|
||||
}
|
||||
@@ -27,11 +27,11 @@ import type { ExpressionRule, WorkflowEdgeData } from "../specs";
|
||||
|
||||
export function EdgeSettingsPanel({
|
||||
edge,
|
||||
sourceType,
|
||||
hasOtherDefaultPath,
|
||||
onChange,
|
||||
}: {
|
||||
edge: Edge;
|
||||
sourceType?: string;
|
||||
hasOtherDefaultPath: boolean;
|
||||
onChange: (patch: WorkflowEdgeData) => void;
|
||||
}) {
|
||||
const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
|
||||
@@ -100,15 +100,19 @@ export function EdgeSettingsPanel({
|
||||
<SectionCard
|
||||
icon={<GitBranch size={15} />}
|
||||
title="路由方式"
|
||||
description="选择由 Agent 判断、动态变量表达式判断,或作为确定性默认路径"
|
||||
description="每个节点最多一条默认路径,也可以配置多条条件路径"
|
||||
>
|
||||
<NodeSelect
|
||||
label="判断方式"
|
||||
label="条件类型"
|
||||
value={mode}
|
||||
options={[
|
||||
...(sourceType === "agent" ? [{ value: "llm", label: "LLM 判断" }] : []),
|
||||
{ value: "expression", label: "动态变量表达式" },
|
||||
{ value: "always", label: "默认路径" },
|
||||
{
|
||||
value: "always",
|
||||
label: "默认路径",
|
||||
disabled: mode !== "always" && hasOtherDefaultPath,
|
||||
},
|
||||
{ value: "llm", label: "大模型判断" },
|
||||
{ value: "expression", label: "表达式" },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
const nextMode =
|
||||
@@ -118,6 +122,11 @@ export function EdgeSettingsPanel({
|
||||
}}
|
||||
allowNone={false}
|
||||
/>
|
||||
{mode !== "always" && hasOtherDefaultPath && (
|
||||
<span className="-mt-1 block text-xs text-muted-foreground">
|
||||
当前节点已经有一条默认路径;其它出边需要使用大模型判断或表达式。
|
||||
</span>
|
||||
)}
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
优先级
|
||||
@@ -133,7 +142,7 @@ export function EdgeSettingsPanel({
|
||||
className="border-hairline-strong bg-background text-foreground"
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
同一节点的边按数字从小到大依次判断。
|
||||
条件路径按数字从小到大判断,默认路径始终作为兜底。
|
||||
</span>
|
||||
</label>
|
||||
</SectionCard>
|
||||
@@ -333,5 +342,3 @@ export function EdgeSettingsPanel({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -201,7 +201,11 @@ export function NodeSelect({
|
||||
<SelectContent className="border-hairline bg-popover text-popover-foreground">
|
||||
{allowNone && <SelectItem value={NONE}>{noneLabel}</SelectItem>}
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -211,4 +215,3 @@ export function NodeSelect({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export type WorkflowSettings = {
|
||||
turnConfig: TurnConfig;
|
||||
};
|
||||
|
||||
export type ModelOption = { value: string; label: string };
|
||||
export type ModelOption = { value: string; label: string; disabled?: boolean };
|
||||
|
||||
export type WorkflowEditorProps = {
|
||||
value?: WorkflowGraph;
|
||||
|
||||
Reference in New Issue
Block a user