Enhance workflow routing and agent configuration management

- Introduce WorkflowLLMRouter for pre-response LLM routing, allowing agents to determine the appropriate function to call based on user input.
- Implement UserTurnRoutingProcessor to manage user turns before reaching the LLM, ensuring proper routing and handling of user messages.
- Refactor WorkflowBrain to integrate new routing logic and enhance agent stage configuration, including entry modes and resource management.
- Update service factory to support dynamic LLM resource configuration based on workflow settings.
- Add tests for new routing functionality and ensure proper handling of user messages in various scenarios.
This commit is contained in:
Xin Wang
2026-07-14 09:36:28 +08:00
parent 32aef14ddb
commit 72856bf3a7
19 changed files with 2611 additions and 552 deletions

View File

@@ -21,7 +21,6 @@ import {
Save,
Mic,
Send,
HelpCircle,
Waypoints,
AudioLines,
Terminal,
@@ -87,12 +86,6 @@ import { PageHeader } from "@/components/ui/page-header";
import { FilterPills } from "@/components/ui/filter-pills";
import { SearchInput } from "@/components/ui/search-input";
import { ListToolbar } from "@/components/ui/list-toolbar";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
@@ -124,6 +117,7 @@ import {
WorkflowEditor,
type WorkflowSettings,
} from "@/components/workflow/WorkflowEditor";
import { HelpHint, SectionCard } from "@/components/editor/section-card";
import {
defaultGraph,
type WorkflowGraph,
@@ -362,7 +356,7 @@ type AssistantTypeOption = {
label: string;
description: string;
icon: React.ReactNode;
/** 提示词、Dify、FastGPT 类型已落地,工作流暂时显示占位页 */
/** 提示词、工作流、Dify、FastGPT 已落地OpenCode 暂时显示即将上线 */
available: boolean;
};
@@ -379,7 +373,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
label: "使用工作流构建",
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
icon: <Workflow size={20} />,
available: false,
available: true,
},
{
type: "Dify",
@@ -400,7 +394,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
label: "使用 OpenCode 构建",
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
icon: <Terminal size={20} />,
available: true,
available: false,
},
];
@@ -472,12 +466,23 @@ export function AssistantPage(props: AssistantPageProps) {
);
const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
globalPrompt: defaultGraph().settings.globalPrompt,
llm: defaultGraph().settings.defaultLlmResourceId,
asr: defaultGraph().settings.defaultAsrResourceId,
tts: defaultGraph().settings.defaultTtsResourceId,
toolIds: defaultGraph().settings.toolIds,
knowledgeBaseId: defaultGraph().settings.knowledgeBaseId,
knowledgeRetrievalConfig: {
mode: defaultGraph().settings.knowledgeMode,
topN: defaultGraph().settings.knowledgeTopN,
scoreThreshold: defaultGraph().settings.knowledgeScoreThreshold,
},
allowInterrupt: true,
turnConfig: defaultTurnConfig(),
});
const [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] =
useState<Record<string, DynamicVariableDefinition>>({});
const [workflowDebugOpen, setWorkflowDebugOpen] = useState(false);
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
const [workflowEditingNodeId, setWorkflowEditingNodeId] = useState<string | null>(null);
const [workflowEditingEdgeId, setWorkflowEditingEdgeId] = useState<string | null>(null);
const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
@@ -847,9 +852,25 @@ export function AssistantPage(props: AssistantPageProps) {
? (assistant.graph as WorkflowGraph)
: defaultGraph();
const wfSettings: WorkflowSettings = {
llm: assistant.modelResourceIds.LLM,
llm:
graph.settings?.defaultLlmResourceId ||
assistant.modelResourceIds.LLM,
asr: graph.settings?.defaultAsrResourceId || assistant.modelResourceIds.ASR,
tts: graph.settings?.defaultTtsResourceId || assistant.modelResourceIds.TTS,
toolIds: graph.settings?.toolIds ?? [],
knowledgeBaseId:
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
knowledgeRetrievalConfig: {
mode:
graph.settings?.knowledgeMode ||
assistant.knowledgeRetrievalConfig.mode,
topN:
graph.settings?.knowledgeTopN ??
assistant.knowledgeRetrievalConfig.topN,
scoreThreshold:
graph.settings?.knowledgeScoreThreshold ??
assistant.knowledgeRetrievalConfig.scoreThreshold,
},
globalPrompt: graph.settings?.globalPrompt ?? "",
allowInterrupt: assistant.enableInterrupt,
turnConfig: assistant.turnConfig,
@@ -916,6 +937,9 @@ export function AssistantPage(props: AssistantPageProps) {
...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
},
knowledgeBaseId: workflowSettings.knowledgeBaseId || null,
knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig,
toolIds: workflowSettings.toolIds,
graph: workflowGraph as unknown as Record<string, unknown>,
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
}),
@@ -1386,7 +1410,7 @@ export function AssistantPage(props: AssistantPageProps) {
<span
role="alert"
title={saveError}
className="line-clamp-2 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
className="line-clamp-1 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
>
{saveError}
</span>
@@ -1396,6 +1420,7 @@ export function AssistantPage(props: AssistantPageProps) {
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
disabled={!editingId}
onClick={() => {
setWorkflowSettingsOpen(false);
setWorkflowEditingNodeId(null);
setWorkflowEditingEdgeId(null);
setWorkflowDebugOpen(true);
@@ -1430,6 +1455,8 @@ export function AssistantPage(props: AssistantPageProps) {
onEditingNodeIdChange={setWorkflowEditingNodeId}
editingEdgeId={workflowEditingEdgeId}
onEditingEdgeIdChange={setWorkflowEditingEdgeId}
settingsOpen={workflowSettingsOpen}
onSettingsOpenChange={setWorkflowSettingsOpen}
debugOpen={workflowDebugOpen}
onDebugOpenChange={(open) => {
setWorkflowDebugOpen(open);
@@ -1508,10 +1535,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Boxes size={18} />}
icon={<Boxes size={15} />}
title="Dify 应用配置"
description="从「模型资源」中选择 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。"
>
@@ -1525,7 +1552,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="语音配置"
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 Dify 应用提供,请前往 Dify 平台配置。"
>
@@ -1546,7 +1573,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1604,10 +1631,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Database size={18} />}
icon={<Database size={15} />}
title="FastGPT 应用配置"
description="从「模型资源」中选择 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。"
>
@@ -1621,7 +1648,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="语音配置"
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 FastGPT 应用提供,请前往 FastGPT 平台配置。"
>
@@ -1642,7 +1669,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1704,10 +1731,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Terminal size={18} />}
icon={<Terminal size={15} />}
title="OpenCode 服务配置"
description="从「模型资源」中选择 OpenCode 服务资源。"
>
@@ -1721,7 +1748,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<MessageSquareText size={18} />}
icon={<MessageSquareText size={15} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
@@ -1734,7 +1761,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型与语音配置"
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
>
@@ -1779,7 +1806,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1843,10 +1870,10 @@ export function AssistantPage(props: AssistantPageProps) {
}
/>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div
role="button"
tabIndex={0}
@@ -1858,25 +1885,25 @@ export function AssistantPage(props: AssistantPageProps) {
}
}}
className={[
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
form.runtimeMode === "pipeline"
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
].join(" ")}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Waypoints size={18} />
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Waypoints size={15} />
</div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">Pipeline </span>
<span className="text-sm font-medium text-foreground">Pipeline </span>
<HelpHint text="通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。" />
</div>
</div>
{form.runtimeMode === "pipeline" && (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={14} />
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={12} />
</span>
)}
</div>
@@ -1893,25 +1920,25 @@ export function AssistantPage(props: AssistantPageProps) {
}
}}
className={[
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
form.runtimeMode === "realtime"
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
].join(" ")}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<AudioLines size={18} />
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<AudioLines size={15} />
</div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">Realtime </span>
<span className="text-sm font-medium text-foreground">Realtime </span>
<HelpHint text="使用原生实时语音模型,模型直接处理音频输入并生成语音回复。" />
</div>
</div>
{form.runtimeMode === "realtime" && (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={14} />
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={12} />
</span>
)}
</div>
@@ -1920,7 +1947,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<MessageSquareText size={18} />}
icon={<MessageSquareText size={15} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
@@ -1938,7 +1965,7 @@ export function AssistantPage(props: AssistantPageProps) {
{form.runtimeMode === "pipeline" ? (
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型配置"
description="从「模型资源」中选择大语言模型、语音识别与语音合成"
>
@@ -1983,7 +2010,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
) : (
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型配置"
description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成"
>
@@ -1998,7 +2025,7 @@ export function AssistantPage(props: AssistantPageProps) {
)}
<SectionCard
icon={<Bot size={18} />}
icon={<Bot size={15} />}
title="开场白"
description="助手与用户首次对话时的开场语"
>
@@ -2015,7 +2042,7 @@ export function AssistantPage(props: AssistantPageProps) {
{form.runtimeMode === "pipeline" && (
<SectionCard
icon={<Database size={18} />}
icon={<Database size={15} />}
title="知识库配置"
description="选择助手回答时可检索的业务知识来源"
>
@@ -2041,7 +2068,7 @@ export function AssistantPage(props: AssistantPageProps) {
)}
<SectionCard
icon={<Wrench size={18} />}
icon={<Wrench size={15} />}
title="工具"
description="配置该提示词助手可以调用的工具"
>
@@ -2053,7 +2080,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -3377,29 +3404,6 @@ function EditableTitle({
);
}
function HelpHint({ text }: { text: string }) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="查看说明"
onClick={(event) => event.stopPropagation()}
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle size={14} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-72 text-sm leading-6 text-muted-foreground"
>
{text}
</PopoverContent>
</Popover>
);
}
function DynamicVariableEditorHint({
count,
onOpen,
@@ -3657,45 +3661,6 @@ function DynamicVariablesDialog({
);
}
function SectionCard({
icon,
title,
description,
children,
}: {
icon?: React.ReactNode;
title?: string;
description?: string;
children: React.ReactNode;
}) {
const hasHeader = Boolean(title);
return (
<Card className="rounded-2xl border-hairline bg-card text-card-foreground shadow-sm">
{hasHeader && (
<CardHeader>
<div className="flex items-center gap-3">
{icon && (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
)}
<div className="flex items-center gap-1.5">
<CardTitle className="text-base font-medium">{title}</CardTitle>
{description && <HelpHint text={description} />}
</div>
</div>
</CardHeader>
)}
<CardContent className={hasHeader ? "space-y-4" : undefined}>
{children}
</CardContent>
</Card>
);
}
function TextAreaField({
label,
value,
@@ -3712,7 +3677,7 @@ function TextAreaField({
return (
<label className="block">
{label && (
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
)}
<Textarea
value={value}
@@ -3721,7 +3686,7 @@ function TextAreaField({
rows={rows}
// Override ui/textarea's field-sizing-content so `rows` sets a real height
// instead of collapsing to min-h-16 when the value is short.
className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
);
@@ -3747,7 +3712,7 @@ function ResourceSelectField({
return (
<div className="block">
{label && (
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
)}
<Select
@@ -4056,18 +4021,18 @@ function ToggleRow({
<div
className={[
"flex items-center justify-between border border-hairline bg-canvas-soft",
hasIcon ? "rounded-2xl p-5" : "rounded-xl p-4",
hasIcon ? "rounded-xl p-3.5" : "rounded-xl px-3.5 py-3",
].join(" ")}
>
<div>
<div
className={[
"flex items-center font-medium text-foreground",
hasIcon ? "gap-3" : "gap-1.5",
"flex items-center text-sm font-medium text-foreground",
hasIcon ? "gap-2.5" : "gap-1.5",
].join(" ")}
>
{icon && (
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</span>
)}
@@ -4077,7 +4042,7 @@ function ToggleRow({
</span>
</div>
{description && (
<div className="mt-1 text-sm text-muted-foreground">
<div className="mt-1 text-xs text-muted-foreground">
{description}
</div>
)}