Enhance conversation history and runtime variable management

- Update ConversationRecorder to include source and nodeId metadata in transcripts for better context tracking.
- Introduce optional variable handling in DynamicVariableStore, allowing for unset variables to be rendered as empty without raising errors.
- Refactor WorkflowBrain to apply turn configurations and manage interaction policies dynamically, improving agent responsiveness.
- Implement tests to ensure proper handling of updated session variables and workflow metadata in various scenarios.
This commit is contained in:
Xin Wang
2026-07-14 11:08:11 +08:00
parent 665f727796
commit f74040adf3
18 changed files with 848 additions and 194 deletions

View File

@@ -122,6 +122,10 @@ import {
defaultGraph,
type WorkflowGraph,
} from "@/components/workflow/specs";
import {
defaultTurnConfig,
normalizeTurnConfig,
} from "@/lib/turn-config";
type RuntimeMode = "pipeline" | "realtime";
@@ -185,24 +189,6 @@ const assistantTypes: AssistantType[] = [
"OpenCode",
];
function defaultTurnConfig(): TurnConfig {
return {
bargeIn: {
strategy: "vad",
},
vad: {
confidence: 0.7,
startSecs: 0.2,
stopSecs: 0.2,
minVolume: 0.6,
},
turnDetection: {
strategy: "silence",
silenceTimeoutSecs: 0.6,
},
};
}
function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig {
return {
mode: "automatic",
@@ -305,6 +291,45 @@ function activeDynamicVariableDefinitions(
);
}
function activeWorkflowDynamicVariableDefinitions(
graph: WorkflowGraph,
saved: Record<string, DynamicVariableDefinition>,
): Record<string, DynamicVariableDefinition> {
const names = new Set(extractDynamicVariableNames(JSON.stringify(graph)));
// Expression operands and Action assignment destinations are variable
// references even though they do not use the {{placeholder}} syntax.
for (const edge of graph.edges) {
for (const rule of edge.data.expression?.rules ?? []) {
if (isPublicDynamicVariableName(rule.variable)) names.add(rule.variable);
}
}
for (const node of graph.nodes) {
for (const name of Object.keys(node.data.resultAssignments ?? {})) {
if (isPublicDynamicVariableName(name)) names.add(name);
}
}
return Object.fromEntries(
[...names].sort().map((name) => [
name,
saved[name] ?? {
type: "string",
required: false,
default: null,
},
]),
);
}
function isPublicDynamicVariableName(name: string): boolean {
return (
/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) &&
!name.startsWith("system__") &&
!name.startsWith("secret__")
);
}
function blankFastGptForm(name: string): FastGptForm {
return {
name,
@@ -492,6 +517,11 @@ export function AssistantPage(props: AssistantPageProps) {
[form.prompt, form.greeting],
form.dynamicVariableDefinitions,
);
const effectiveWorkflowDynamicVariableDefinitions =
activeWorkflowDynamicVariableDefinitions(
workflowGraph,
workflowDynamicVariableDefinitions,
);
const loadAssistants = useCallback(async () => {
setListLoading(true);
@@ -603,7 +633,7 @@ export function AssistantPage(props: AssistantPageProps) {
knowledgeRetrievalConfig:
a.knowledgeRetrievalConfig ?? defaultKnowledgeRetrievalConfig(),
enableInterrupt: a.enableInterrupt,
turnConfig: a.turnConfig,
turnConfig: normalizeTurnConfig(a.turnConfig),
visionEnabled: a.visionEnabled,
visionModelResourceId: a.visionModelResourceId ?? "",
toolIds: a.toolIds ?? [],
@@ -711,7 +741,7 @@ export function AssistantPage(props: AssistantPageProps) {
name: workflowName,
graph: workflowGraph,
settings: workflowSettings,
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions,
}),
);
}
@@ -756,7 +786,7 @@ export function AssistantPage(props: AssistantPageProps) {
asr: a.modelResourceIds.ASR ?? "",
voice: a.modelResourceIds.TTS ?? "",
enableInterrupt: a.enableInterrupt,
turnConfig: a.turnConfig,
turnConfig: normalizeTurnConfig(a.turnConfig),
};
setDifyForm(next);
return next;
@@ -786,7 +816,7 @@ export function AssistantPage(props: AssistantPageProps) {
asr: a.modelResourceIds.ASR ?? "",
voice: a.modelResourceIds.TTS ?? "",
enableInterrupt: a.enableInterrupt,
turnConfig: a.turnConfig,
turnConfig: normalizeTurnConfig(a.turnConfig),
};
setFastGptForm(next);
return next;
@@ -818,7 +848,7 @@ export function AssistantPage(props: AssistantPageProps) {
asr: a.modelResourceIds.ASR ?? "",
voice: a.modelResourceIds.TTS ?? "",
enableInterrupt: a.enableInterrupt,
turnConfig: a.turnConfig,
turnConfig: normalizeTurnConfig(a.turnConfig),
visionEnabled: a.visionEnabled,
visionModelResourceId: a.visionModelResourceId ?? "",
};
@@ -872,21 +902,29 @@ export function AssistantPage(props: AssistantPageProps) {
assistant.knowledgeRetrievalConfig.scoreThreshold,
},
globalPrompt: graph.settings?.globalPrompt ?? "",
allowInterrupt: assistant.enableInterrupt,
turnConfig: assistant.turnConfig,
allowInterrupt:
graph.settings?.enableInterrupt ?? assistant.enableInterrupt,
turnConfig: normalizeTurnConfig(
graph.settings?.turnConfig ?? assistant.turnConfig,
),
};
setWorkflowName(assistant.name);
setWorkflowGraph(graph);
setWorkflowSettings(wfSettings);
const dynamicVariableDefinitions =
activeWorkflowDynamicVariableDefinitions(
graph,
assistant.dynamicVariableDefinitions ?? {},
);
setWorkflowDynamicVariableDefinitions(
assistant.dynamicVariableDefinitions ?? {},
dynamicVariableDefinitions,
);
setSavedSnapshot(
JSON.stringify({
name: assistant.name,
graph,
settings: wfSettings,
dynamicVariableDefinitions: assistant.dynamicVariableDefinitions ?? {},
dynamicVariableDefinitions,
}),
);
}
@@ -941,7 +979,7 @@ export function AssistantPage(props: AssistantPageProps) {
knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig,
toolIds: workflowSettings.toolIds,
graph: workflowGraph as unknown as Record<string, unknown>,
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions,
}),
);
}
@@ -961,7 +999,8 @@ export function AssistantPage(props: AssistantPageProps) {
name: workflowName,
graph: workflowGraph,
settings: workflowSettings,
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
dynamicVariableDefinitions:
effectiveWorkflowDynamicVariableDefinitions,
})
: null;
const dirty =
@@ -1473,7 +1512,9 @@ export function AssistantPage(props: AssistantPageProps) {
hasUnsavedChanges={dirty}
onNodeActive={setActiveNodeId}
dynamicVariablesEnabled
dynamicVariableDefinitions={workflowDynamicVariableDefinitions}
dynamicVariableDefinitions={
effectiveWorkflowDynamicVariableDefinitions
}
/>
}
activeNodeId={activeNodeId}
@@ -1492,7 +1533,7 @@ export function AssistantPage(props: AssistantPageProps) {
<DynamicVariablesDialog
open={dynamicVariablesOpen}
onOpenChange={setDynamicVariablesOpen}
definitions={workflowDynamicVariableDefinitions}
definitions={effectiveWorkflowDynamicVariableDefinitions}
onChange={setWorkflowDynamicVariableDefinitions}
/>
</div>
@@ -1872,80 +1913,6 @@ export function AssistantPage(props: AssistantPageProps) {
<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-3 md:grid-cols-2">
<div
role="button"
tabIndex={0}
onClick={() => updateForm("runtimeMode", "pipeline")}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
updateForm("runtimeMode", "pipeline");
}
}}
className={[
"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-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="text-sm font-medium text-foreground">Pipeline </span>
<HelpHint text="通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。" />
</div>
</div>
{form.runtimeMode === "pipeline" && (
<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>
</div>
<div
role="button"
tabIndex={0}
onClick={() => updateForm("runtimeMode", "realtime")}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
updateForm("runtimeMode", "realtime");
}
}}
className={[
"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-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="text-sm font-medium text-foreground">Realtime </span>
<HelpHint text="使用原生实时语音模型,模型直接处理音频输入并生成语音回复。" />
</div>
</div>
{form.runtimeMode === "realtime" && (
<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>
</div>
</div>
</SectionCard>
<SectionCard
icon={<MessageSquareText size={15} />}
title="提示词"
@@ -1963,12 +1930,38 @@ export function AssistantPage(props: AssistantPageProps) {
/>
</SectionCard>
{form.runtimeMode === "pipeline" ? (
<SectionCard
icon={<Brain size={15} />}
title="模型配置"
description="从「模型资源」中选择大语言模型、语音识别与语音合成"
>
<SectionCard
icon={<Bot size={15} />}
title="开场白"
description="助手与用户首次对话时的开场语"
>
<TextAreaField
value={form.greeting}
onChange={(value) => updateForm("greeting", value)}
placeholder="请输入助手开场白"
/>
<DynamicVariableEditorHint
count={Object.keys(effectiveDynamicVariableDefinitions).length}
onOpen={() => setDynamicVariablesOpen(true)}
/>
</SectionCard>
<SectionCard
icon={<Brain size={15} />}
title="模型配置"
description={
form.runtimeMode === "pipeline"
? "选择运行方式,以及大语言模型、语音识别与语音合成资源"
: "选择运行方式Realtime 模型内置语音识别与语音合成"
}
>
<RuntimeModeSelector
value={form.runtimeMode}
onChange={(runtimeMode) => updateForm("runtimeMode", runtimeMode)}
/>
{form.runtimeMode === "pipeline" ? (
<>
<ToggleRow
title="视觉理解"
hint="开启后,开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时,大语言模型本身必须支持图片输入。"
@@ -2007,13 +2000,8 @@ export function AssistantPage(props: AssistantPageProps) {
options={credOptions("TTS")}
noneLabel="无"
/>
</SectionCard>
) : (
<SectionCard
icon={<Brain size={15} />}
title="模型配置"
description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成"
>
</>
) : (
<ResourceSelectField
label="Realtime 模型"
value={form.realtimeModel}
@@ -2021,23 +2009,7 @@ export function AssistantPage(props: AssistantPageProps) {
options={credOptions("Realtime")}
noneLabel="无"
/>
</SectionCard>
)}
<SectionCard
icon={<Bot size={15} />}
title="开场白"
description="助手与用户首次对话时的开场语"
>
<TextAreaField
value={form.greeting}
onChange={(value) => updateForm("greeting", value)}
placeholder="请输入助手开场白"
/>
<DynamicVariableEditorHint
count={Object.keys(effectiveDynamicVariableDefinitions).length}
onOpen={() => setDynamicVariablesOpen(true)}
/>
)}
</SectionCard>
{form.runtimeMode === "pipeline" && (
@@ -2206,10 +2178,23 @@ function DebugDrawer({
>({});
const recording =
preview.status === "connecting" || preview.status === "connected";
const dynamicVariableEntries = Object.entries(dynamicVariableDefinitions);
const displayedDefinitions = { ...dynamicVariableDefinitions };
for (const [name, value] of Object.entries(preview.sessionVariables)) {
displayedDefinitions[name] ??= {
type:
typeof value === "number"
? "number"
: typeof value === "boolean"
? "boolean"
: "string",
required: false,
default: null,
};
}
const dynamicVariableEntries = Object.entries(displayedDefinitions);
const resolvedDynamicVariables: Record<string, string | number | boolean> = {};
let dynamicVariablesError = "";
for (const [name, definition] of dynamicVariableEntries) {
for (const [name, definition] of Object.entries(dynamicVariableDefinitions)) {
const value = dynamicVariableValues[name] ?? definition.default;
if (value === null || value === undefined || value === "") {
if (definition.required && !dynamicVariablesError) {
@@ -2265,7 +2250,8 @@ function DebugDrawer({
<DynamicVariableValuesPopover
entries={dynamicVariableEntries}
values={dynamicVariableValues}
disabled={recording}
sessionValues={preview.sessionVariables}
readOnly={recording}
onChange={setDynamicVariableValues}
/>
)}
@@ -2330,12 +2316,14 @@ function DebugDrawer({
function DynamicVariableValuesPopover({
entries,
values,
disabled,
sessionValues,
readOnly,
onChange,
}: {
entries: [string, DynamicVariableDefinition][];
values: Record<string, string | number | boolean>;
disabled: boolean;
sessionValues: Record<string, string | number | boolean>;
readOnly: boolean;
onChange: React.Dispatch<
React.SetStateAction<Record<string, string | number | boolean>>
>;
@@ -2354,10 +2342,9 @@ function DynamicVariableValuesPopover({
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
aria-label="设置本次会话变量"
title="本次会话变量"
className="relative flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
aria-label={readOnly ? "查看本次会话变量" : "设置本次会话变量"}
title={readOnly ? "查看实时会话变量" : "本次会话变量"}
className="relative flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground"
>
<Braces size={15} />
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full border border-card bg-surface-strong px-1 text-[9px] tabular-nums text-foreground">
@@ -2376,7 +2363,9 @@ function DynamicVariableValuesPopover({
</div>
<p className="text-xs leading-5 text-muted-foreground">
{readOnly
? "当前值会在 Action 或工具更新变量后实时刷新。"
: "这些值只用于下一次调试会话,不会修改助手配置。"}
</p>
</div>
<div className="max-h-72 space-y-3 overflow-y-auto pr-1">
@@ -2386,11 +2375,14 @@ function DynamicVariableValuesPopover({
</div>
<p className="mt-1 text-[11px] leading-5 text-muted-foreground">
{"{{variable_name}}"}
Action
</p>
</div>
) : entries.map(([name, definition]) => {
const value = values[name] ?? definition.default ?? "";
const value = readOnly
? sessionValues[name] ?? definition.default ?? ""
: values[name] ?? definition.default ?? "";
return (
<label key={name} className="block space-y-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-foreground">
@@ -2408,6 +2400,7 @@ function DynamicVariableValuesPopover({
</span>
{definition.type === "boolean" ? (
<Select
disabled={readOnly}
value={value === "" ? "unset" : String(value)}
onValueChange={(next) =>
setValue(
@@ -2430,6 +2423,7 @@ function DynamicVariableValuesPopover({
</Select>
) : (
<Input
disabled={readOnly}
type={definition.type === "number" ? "number" : "text"}
value={typeof value === "boolean" ? String(value) : value}
onChange={(event) => {
@@ -3465,7 +3459,7 @@ function DynamicVariablesDialog({
</DialogTitle>
<DialogDescription className="text-xs leading-5">
使
</DialogDescription>
</DialogHeader>
@@ -3493,7 +3487,7 @@ function DynamicVariablesDialog({
<div className="rounded-xl border border-dashed border-hairline-strong bg-canvas-soft px-4 py-5 text-center">
<div className="text-sm font-medium text-foreground"></div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
{"{{customer_name}}"}
{"{{customer_name}}"}
</p>
</div>
) : (
@@ -3661,6 +3655,78 @@ function DynamicVariablesDialog({
);
}
function RuntimeModeSelector({
value,
onChange,
}: {
value: RuntimeMode;
onChange: (mode: RuntimeMode) => void;
}) {
const options = [
{
value: "pipeline" as const,
label: "Pipeline 模式",
hint: "通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。",
icon: Waypoints,
},
{
value: "realtime" as const,
label: "Realtime 模式",
hint: "使用原生实时语音模型,模型直接处理音频输入并生成语音回复。",
icon: AudioLines,
},
];
return (
<div className="grid grid-cols-1 gap-3 border-b border-hairline-soft pb-4 md:grid-cols-2">
{options.map((option) => {
const Icon = option.icon;
const selected = value === option.value;
const select = () => onChange(option.value);
return (
<div
key={option.value}
role="button"
tabIndex={0}
onClick={select}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
select();
}
}}
className={[
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
selected
? "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-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Icon size={15} />
</div>
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium text-foreground">
{option.label}
</span>
<HelpHint text={option.hint} />
</div>
</div>
{selected && (
<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>
</div>
);
})}
</div>
);
}
function TextAreaField({
label,
value,

View File

@@ -56,14 +56,27 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
<div
data-node-id={id}
className={cn(
"group relative w-[250px] rounded-2xl border bg-card p-4 text-card-foreground shadow-sm transition-[border-color,box-shadow,transform]",
"group relative isolate w-[250px] rounded-2xl border bg-card p-4 text-card-foreground shadow-sm transition-[border-color,box-shadow,transform]",
isActive
? "border-success shadow-[0_12px_34px_color-mix(in_srgb,var(--success)_20%,transparent)] ring-2 ring-success/50"
? "border-success/90 shadow-[0_0_0_1px_color-mix(in_srgb,var(--success)_38%,transparent),0_0_18px_color-mix(in_srgb,var(--success)_38%,transparent),0_0_46px_color-mix(in_srgb,var(--success)_22%,transparent),0_16px_38px_color-mix(in_srgb,var(--success)_18%,transparent)] ring-1 ring-success/60"
: selected
? "border-primary shadow-[0_12px_34px_color-mix(in_srgb,var(--primary)_16%,transparent)]"
: "border-hairline hover:border-hairline-strong hover:shadow-md",
)}
>
{isActive && (
<>
<div
aria-hidden
className="pointer-events-none absolute -inset-1 rounded-[1.2rem] border border-success/35 opacity-80 motion-safe:animate-pulse"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-5 -bottom-px h-px bg-gradient-to-r from-transparent via-success to-transparent shadow-[0_0_12px_var(--success)]"
/>
</>
)}
{spec.hasTarget && (
<Handle
type="target"
@@ -81,9 +94,12 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
/>
{isActive && (
<div className="absolute -top-3 left-3 flex items-center gap-1.5 rounded-full bg-success px-2 py-0.5 text-[10px] font-medium text-on-primary shadow-sm">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-on-primary" />
<div className="absolute -top-3 left-3 flex items-center gap-2 rounded-full border border-success/60 bg-card/95 px-2.5 py-1 text-[10px] font-semibold tracking-[0.14em] text-success shadow-[0_0_16px_color-mix(in_srgb,var(--success)_42%,transparent)] backdrop-blur-sm">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-success opacity-55 motion-safe:animate-ping" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-success shadow-[0_0_8px_var(--success)]" />
</span>
LIVE ·
</div>
)}

View File

@@ -78,6 +78,7 @@ import {
import { Switch } from "@/components/ui/switch";
import type { KnowledgeRetrievalConfig, TurnConfig } from "@/lib/api";
import { TurnConfigEditor } from "@/components/turn-config-editor";
import { normalizeTurnConfig } from "@/lib/turn-config";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
@@ -195,6 +196,8 @@ function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
knowledgeMode: "automatic",
knowledgeTopN: 5,
knowledgeScoreThreshold: 0,
enableInterrupt: true,
turnConfig: defaultGraph().settings.turnConfig,
},
nodes: nodes.map((n) => ({
id: n.id,
@@ -261,6 +264,8 @@ function Canvas({
knowledgeTopN: settings.knowledgeRetrievalConfig.topN,
knowledgeScoreThreshold:
settings.knowledgeRetrievalConfig.scoreThreshold,
enableInterrupt: settings.allowInterrupt,
turnConfig: settings.turnConfig,
};
onChangeRef.current?.(graph);
}, [
@@ -273,6 +278,8 @@ function Canvas({
settings.toolIds,
settings.knowledgeBaseId,
settings.knowledgeRetrievalConfig,
settings.allowInterrupt,
settings.turnConfig,
]);
const onConnect = useCallback(
@@ -1519,6 +1526,9 @@ function AgentPanelForm({
topN: Number(draft.knowledgeTopN ?? 5),
scoreThreshold: Number(draft.knowledgeScoreThreshold ?? 0),
};
const agentTurnConfig = normalizeTurnConfig(
draft.turnConfig ?? workflowSettings.turnConfig,
);
const setInheritance = (inheritGlobalConfig: boolean) => {
if (inheritGlobalConfig) {
@@ -1550,6 +1560,9 @@ function AgentPanelForm({
knowledgeScoreThreshold:
draft.knowledgeScoreThreshold ??
workflowSettings.knowledgeRetrievalConfig.scoreThreshold,
enableInterrupt:
draft.enableInterrupt ?? workflowSettings.allowInterrupt,
turnConfig: agentTurnConfig,
});
};
@@ -1592,10 +1605,10 @@ function AgentPanelForm({
<SectionCard
icon={<MessageSquareText size={15} />}
title="提示词"
title={inheritsGlobal ? "任务" : "提示词"}
description={
inheritsGlobal
? "描述当前阶段任务,并与工作流全局提示词合并"
? "描述当前阶段要完成的目标;角色、能力和通用规则继承工作流全局配置"
: "描述当前独立助手的角色、能力和回答要求"
}
>
@@ -1603,7 +1616,11 @@ function AgentPanelForm({
rows={8}
value={draft.prompt ?? ""}
onChange={(event) => set("prompt", event.target.value)}
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
placeholder={
inheritsGlobal
? "例如:确认用户身份,并收集需要查询的订单编号"
: "请输入提示词,描述助手的角色、能力和回答要求"
}
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</SectionCard>
@@ -1714,6 +1731,23 @@ function AgentPanelForm({
onChange={(toolIds) => set("toolIds", toolIds)}
/>
</SectionCard>
<SectionCard
icon={<Sparkles size={15} />}
title="交互策略"
description="配置当前 Agent 独立使用的打断和轮次检测策略"
>
<TurnConfigEditor
enabled={
draft.enableInterrupt ?? workflowSettings.allowInterrupt
}
config={agentTurnConfig}
onEnabledChange={(enableInterrupt) =>
set("enableInterrupt", enableInterrupt)
}
onConfigChange={(turnConfig) => set("turnConfig", turnConfig)}
/>
</SectionCard>
</>
)}
</div>

View File

@@ -3,7 +3,8 @@
import * as LucideIcons from "lucide-react";
import { Circle, type LucideIcon } from "lucide-react";
import type { NodeSpecDto } from "@/lib/api";
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
import { defaultTurnConfig } from "@/lib/turn-config";
export type WorkflowNodeType = "start" | "agent" | "action" | "handoff" | "end";
export type ContextPolicy = "inherit" | "fresh";
@@ -37,6 +38,8 @@ export type WorkflowNodeData = {
llmResourceId?: string;
asrResourceId?: string;
ttsResourceId?: string;
enableInterrupt?: boolean;
turnConfig?: TurnConfig;
toolId?: string;
arguments?: Record<string, unknown>;
resultAssignments?: Record<string, string>;
@@ -142,6 +145,8 @@ export type WorkflowGraph = {
knowledgeMode: "automatic" | "on_demand";
knowledgeTopN: number;
knowledgeScoreThreshold: number;
enableInterrupt: boolean;
turnConfig: TurnConfig;
};
nodes: Array<{
id: string;
@@ -172,6 +177,8 @@ export function defaultGraph(): WorkflowGraph {
knowledgeMode: "automatic",
knowledgeTopN: 5,
knowledgeScoreThreshold: 0,
enableInterrupt: true,
turnConfig: defaultTurnConfig(),
},
nodes: [
{