feat: add system tools and state updates
This commit is contained in:
@@ -72,6 +72,8 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
||||
messagePolicy === "playback" ? "播放完继续" : null,
|
||||
messagePolicy === "confirmation" ? "确认后继续" : null,
|
||||
].filter(Boolean)
|
||||
: type === "update_state"
|
||||
? [`${Object.keys(nodeData.assignments ?? {}).length} 个变量`]
|
||||
: [];
|
||||
|
||||
return (
|
||||
|
||||
@@ -72,6 +72,8 @@ function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
systemTools: [],
|
||||
stateVariableNames: [],
|
||||
});
|
||||
} else if (spec.type === "action") {
|
||||
Object.assign(data, {
|
||||
@@ -79,6 +81,8 @@ function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
resultAssignmentMode: "inherit",
|
||||
userInputPolicy: "queue",
|
||||
});
|
||||
} else if (spec.type === "update_state") {
|
||||
Object.assign(data, { assignments: {} });
|
||||
} else if (spec.type === "message") {
|
||||
Object.assign(data, {
|
||||
speech: "",
|
||||
@@ -170,6 +174,7 @@ export function WorkflowCanvas({
|
||||
specsByType,
|
||||
toolOptions = [],
|
||||
knowledgeOptions = [],
|
||||
dynamicVariableOptions,
|
||||
}: WorkflowEditorProps & { specsByType: NodeSpecMap }) {
|
||||
const initial = useMemo(() => toFlow(value ?? defaultGraph()), [value]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
|
||||
@@ -780,6 +785,7 @@ export function WorkflowCanvas({
|
||||
asrOptions={modelOptions.asr}
|
||||
ttsOptions={modelOptions.tts}
|
||||
visionOptions={modelOptions.vision}
|
||||
dynamicVariableOptions={dynamicVariableOptions}
|
||||
workflowSettings={settings}
|
||||
onChange={(patch) =>
|
||||
updateNodeData(editingNode.id, patch)
|
||||
|
||||
@@ -16,7 +16,8 @@ import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { KnowledgeRetrievalConfig } from "@/lib/api";
|
||||
import type { KnowledgeRetrievalConfig, SystemToolKind } from "@/lib/api";
|
||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
||||
import { normalizeTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
import { NodeSelect, ToolOptionPicker } from "./controls";
|
||||
@@ -38,6 +39,7 @@ export function AgentNodePanel({
|
||||
asrOptions,
|
||||
ttsOptions,
|
||||
visionOptions,
|
||||
dynamicVariableOptions,
|
||||
}: {
|
||||
draft: WorkflowNodeData;
|
||||
set: (key: string, val: unknown) => void;
|
||||
@@ -49,6 +51,7 @@ export function AgentNodePanel({
|
||||
asrOptions: ModelOption[];
|
||||
ttsOptions: ModelOption[];
|
||||
visionOptions: ModelOption[];
|
||||
dynamicVariableOptions: ModelOption[];
|
||||
}) {
|
||||
const inheritsGlobal = draft.inheritGlobalConfig !== false;
|
||||
const knowledgeConfig: KnowledgeRetrievalConfig = {
|
||||
@@ -101,6 +104,27 @@ export function AgentNodePanel({
|
||||
turnConfig: agentTurnConfig,
|
||||
});
|
||||
};
|
||||
const toggleSystemTool = (kind: SystemToolKind, enabled: boolean) => {
|
||||
const current = draft.systemTools ?? [];
|
||||
const systemTools = enabled
|
||||
? [...new Set([...current, kind])]
|
||||
: current.filter((item) => item !== kind);
|
||||
setPatch({
|
||||
systemTools,
|
||||
...(!enabled && kind === "update_state"
|
||||
? { stateVariableNames: [] }
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
const toggleStateVariable = (name: string, enabled: boolean) => {
|
||||
const current = draft.stateVariableNames ?? [];
|
||||
set(
|
||||
"stateVariableNames",
|
||||
enabled
|
||||
? [...new Set([...current, name])]
|
||||
: current.filter((item) => item !== name),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelAnchorNavigation
|
||||
@@ -109,6 +133,7 @@ export function AgentNodePanel({
|
||||
{ id: "scope", label: "配置范围" },
|
||||
{ id: "prompt", label: inheritsGlobal ? "任务" : "提示词" },
|
||||
{ id: "entry", label: "进入行为" },
|
||||
{ id: "system-tools", label: "系统工具" },
|
||||
...(!inheritsGlobal
|
||||
? [
|
||||
{ id: "models", label: "模型与语音" },
|
||||
@@ -186,6 +211,78 @@ export function AgentNodePanel({
|
||||
</SectionCard>
|
||||
</PanelAnchor>
|
||||
|
||||
<PanelAnchor id="system-tools">
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="系统工具"
|
||||
description="只对当前 Agent 生效的内置会话控制能力"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{SYSTEM_TOOL_OPTIONS.map((option) => {
|
||||
const enabled = (draft.systemTools ?? []).includes(option.value);
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className="rounded-xl border border-hairline bg-canvas-soft p-3.5"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{option.label}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleSystemTool(option.value, checked)
|
||||
}
|
||||
aria-label={`启用${option.label}`}
|
||||
/>
|
||||
</div>
|
||||
{option.value === "update_state" && enabled && (
|
||||
<div className="mt-3 border-t border-hairline pt-3">
|
||||
<div className="mb-2 text-xs font-medium text-foreground">
|
||||
允许更新的变量
|
||||
</div>
|
||||
{dynamicVariableOptions.length ? (
|
||||
<div className="space-y-2">
|
||||
{dynamicVariableOptions.map((variable) => (
|
||||
<div
|
||||
key={variable.value}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-hairline bg-background px-3 py-2"
|
||||
>
|
||||
<span className="truncate text-xs text-foreground">
|
||||
{variable.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={(draft.stateVariableNames ?? []).includes(
|
||||
variable.value,
|
||||
)}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleStateVariable(variable.value, checked)
|
||||
}
|
||||
aria-label={`允许更新${variable.value}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
请先在工作流的动态变量面板中声明变量。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</PanelAnchor>
|
||||
|
||||
{!inheritsGlobal && (
|
||||
<>
|
||||
<PanelAnchor id="models">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { ActionNodePanel } from "./ActionNodePanel";
|
||||
import { AgentNodePanel } from "./AgentNodePanel";
|
||||
import { MessageNodePanel } from "./MessageNodePanel";
|
||||
import { UpdateStateNodePanel } from "./UpdateStateNodePanel";
|
||||
import { NodeSelect, ToolOptionPicker } from "./controls";
|
||||
import {
|
||||
PanelAnchor,
|
||||
@@ -31,6 +32,7 @@ export function NodeSettingsPanel({
|
||||
asrOptions,
|
||||
ttsOptions,
|
||||
visionOptions,
|
||||
dynamicVariableOptions,
|
||||
workflowSettings,
|
||||
onChange,
|
||||
}: {
|
||||
@@ -43,6 +45,7 @@ export function NodeSettingsPanel({
|
||||
asrOptions: ModelOption[];
|
||||
ttsOptions: ModelOption[];
|
||||
visionOptions: ModelOption[];
|
||||
dynamicVariableOptions: ModelOption[];
|
||||
workflowSettings: WorkflowSettings;
|
||||
onChange: (patch: WorkflowNodeData) => void;
|
||||
}) {
|
||||
@@ -91,6 +94,8 @@ export function NodeSettingsPanel({
|
||||
]
|
||||
: spec.type === "action"
|
||||
? [{ id: "execution", label: "工具执行" }]
|
||||
: spec.type === "update_state"
|
||||
? [{ id: "state", label: "状态更新" }]
|
||||
: spec.type === "handoff"
|
||||
? [{ id: "handoff", label: "转交配置" }]
|
||||
: spec.type === "end"
|
||||
@@ -114,6 +119,7 @@ export function NodeSettingsPanel({
|
||||
setArgumentsJson={setArgumentsJson}
|
||||
setAssignmentsJson={setAssignmentsJson}
|
||||
commitActionJson={commitActionJson}
|
||||
dynamicVariableOptions={dynamicVariableOptions}
|
||||
/>
|
||||
</PanelAnchorNavigation>
|
||||
);
|
||||
@@ -130,6 +136,7 @@ export function NodeSettingsPanel({
|
||||
asrOptions={asrOptions}
|
||||
ttsOptions={ttsOptions}
|
||||
visionOptions={visionOptions}
|
||||
dynamicVariableOptions={dynamicVariableOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -262,6 +269,14 @@ export function NodeSettingsPanel({
|
||||
<MessageNodePanel draft={draft} set={set} setPatch={setPatch} />
|
||||
)}
|
||||
|
||||
{spec.type === "update_state" && (
|
||||
<UpdateStateNodePanel
|
||||
draft={draft}
|
||||
set={set}
|
||||
dynamicVariableOptions={dynamicVariableOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{spec.type === "handoff" && (
|
||||
<NodeSelect
|
||||
label="转交类型"
|
||||
@@ -306,6 +321,7 @@ function WorkflowNodePanelForm({
|
||||
setArgumentsJson,
|
||||
setAssignmentsJson,
|
||||
commitActionJson,
|
||||
dynamicVariableOptions,
|
||||
}: {
|
||||
spec: RuntimeNodeSpec;
|
||||
draft: WorkflowNodeData;
|
||||
@@ -318,6 +334,7 @@ function WorkflowNodePanelForm({
|
||||
setArgumentsJson: (value: string) => void;
|
||||
setAssignmentsJson: (value: string) => void;
|
||||
commitActionJson: (argumentsValue: string, assignmentsValue: string) => void;
|
||||
dynamicVariableOptions: ModelOption[];
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -341,6 +358,16 @@ function WorkflowNodePanelForm({
|
||||
<MessageNodePanel draft={draft} set={set} setPatch={setPatch} />
|
||||
)}
|
||||
|
||||
{spec.type === "update_state" && (
|
||||
<PanelAnchor id="state">
|
||||
<UpdateStateNodePanel
|
||||
draft={draft}
|
||||
set={set}
|
||||
dynamicVariableOptions={dynamicVariableOptions}
|
||||
/>
|
||||
</PanelAnchor>
|
||||
)}
|
||||
|
||||
{spec.type === "handoff" && (
|
||||
<PanelAnchor id="handoff">
|
||||
<SectionCard
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { Braces } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
import type { WorkflowNodeData } from "../specs";
|
||||
import type { ModelOption } from "../types";
|
||||
|
||||
export function UpdateStateNodePanel({
|
||||
draft,
|
||||
set,
|
||||
dynamicVariableOptions,
|
||||
}: {
|
||||
draft: WorkflowNodeData;
|
||||
set: (key: string, value: unknown) => void;
|
||||
dynamicVariableOptions: ModelOption[];
|
||||
}) {
|
||||
const [value, setValue] = useState(
|
||||
JSON.stringify(draft.assignments ?? {}, null, 2),
|
||||
);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const commit = () => {
|
||||
try {
|
||||
const assignments = JSON.parse(value) as Record<string, unknown>;
|
||||
if (
|
||||
!assignments ||
|
||||
Array.isArray(assignments) ||
|
||||
typeof assignments !== "object"
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
setError("");
|
||||
set("assignments", assignments);
|
||||
} catch {
|
||||
setError("变量赋值必须是 JSON 对象");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
icon={<Braces size={15} />}
|
||||
title="状态更新"
|
||||
description="一次性更新已声明的动态变量,然后继续执行后续节点"
|
||||
>
|
||||
<div>
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
变量赋值
|
||||
</div>
|
||||
<Textarea
|
||||
rows={8}
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={commit}
|
||||
placeholder={'{\n "customer_name": "张三",\n "confirmed": true\n}'}
|
||||
className="field-sizing-fixed min-h-44 resize-y border-hairline-strong bg-background font-mono text-xs text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
{error && <p className="mt-1.5 text-xs text-destructive">{error}</p>}
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={commit}>
|
||||
应用变量赋值
|
||||
</Button>
|
||||
<div className="rounded-xl border border-hairline bg-canvas-soft p-3 text-xs leading-5 text-muted-foreground">
|
||||
可用变量:
|
||||
{dynamicVariableOptions.length
|
||||
? dynamicVariableOptions.map((option) => option.label).join("、")
|
||||
: "暂无,请先在动态变量面板中声明"}
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
值支持字符串、数字、布尔值,也可使用完整占位符,例如
|
||||
{" {{source_name}}"}。
|
||||
</p>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { Circle, type LucideIcon } from "lucide-react";
|
||||
|
||||
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
|
||||
import type { NodeSpecDto, SystemToolKind, TurnConfig } from "@/lib/api";
|
||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
export type WorkflowNodeType =
|
||||
@@ -11,6 +11,7 @@ export type WorkflowNodeType =
|
||||
| "agent"
|
||||
| "message"
|
||||
| "action"
|
||||
| "update_state"
|
||||
| "handoff"
|
||||
| "end";
|
||||
export type ContextPolicy = "inherit" | "fresh";
|
||||
@@ -40,6 +41,8 @@ export type WorkflowNodeData = {
|
||||
contextPolicy?: ContextPolicy;
|
||||
inheritGlobalConfig?: boolean;
|
||||
entryMode?: AgentEntryMode;
|
||||
systemTools?: SystemToolKind[];
|
||||
stateVariableNames?: string[];
|
||||
toolIds?: string[];
|
||||
knowledgeBaseId?: string;
|
||||
knowledgeMode?: KnowledgeMode;
|
||||
@@ -56,6 +59,7 @@ export type WorkflowNodeData = {
|
||||
arguments?: Record<string, unknown>;
|
||||
resultAssignmentMode?: ActionResultAssignmentMode;
|
||||
resultAssignments?: Record<string, string>;
|
||||
assignments?: Record<string, unknown>;
|
||||
userInputPolicy?: ActionUserInputPolicy;
|
||||
speech?: string;
|
||||
/** Legacy field read only while an older graph is open in the editor. */
|
||||
@@ -279,6 +283,8 @@ export function defaultGraph(): WorkflowGraph {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
systemTools: [],
|
||||
stateVariableNames: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ export type WorkflowEditorProps = {
|
||||
};
|
||||
toolOptions?: ModelOption[];
|
||||
knowledgeOptions?: ModelOption[];
|
||||
dynamicVariableOptions: ModelOption[];
|
||||
dynamicVariablesOpen: boolean;
|
||||
onDynamicVariablesOpenChange: (open: boolean) => void;
|
||||
dynamicVariablesPanel: ReactNode;
|
||||
|
||||
Reference in New Issue
Block a user