支持插件 API routing 结构化配置
parent
31eba7234a
commit
a03cc82dd0
|
|
@ -33,6 +33,12 @@ export interface SkillDetail extends SkillBrief {
|
||||||
config: string;
|
config: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExternalToolApiRouting {
|
||||||
|
summary: string;
|
||||||
|
useWhen: string[];
|
||||||
|
doNotUseWhen?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExternalToolApi {
|
export interface ExternalToolApi {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -41,6 +47,7 @@ export interface ExternalToolApi {
|
||||||
path: string;
|
path: string;
|
||||||
headers?: Record<string, string> | null;
|
headers?: Record<string, string> | null;
|
||||||
parametersSchema: Record<string, unknown>;
|
parametersSchema: Record<string, unknown>;
|
||||||
|
routing: ExternalToolApiRouting;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,4 +127,3 @@ export const AgentAPI = {
|
||||||
deletePlugin: (agentId: string, pluginId: string) =>
|
deletePlugin: (agentId: string, pluginId: string) =>
|
||||||
api.delete(`/agents/${agentId}/plugins/${pluginId}`).then((r) => r.data)
|
api.delete(`/agents/${agentId}/plugins/${pluginId}`).then((r) => r.data)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CopyOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import { CopyOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space } from 'antd';
|
import { App as AntApp, Button, Card, Form, Input, Modal, Select, Space } from 'antd';
|
||||||
import { AgentAPI, ExternalToolApi, ExternalToolPlugin, ExternalToolPluginPayload } from '../api';
|
import { AgentAPI, ExternalToolApi, ExternalToolApiRouting, ExternalToolPlugin, ExternalToolPluginPayload } from '../api';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -10,9 +10,12 @@ interface Props {
|
||||||
onSaved?: () => void | Promise<void>;
|
onSaved?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolApiFormValue extends Omit<ExternalToolApi, 'headers' | 'parametersSchema'> {
|
interface ToolApiRoutingFormValue extends ExternalToolApiRouting {}
|
||||||
|
|
||||||
|
interface ToolApiFormValue extends Omit<ExternalToolApi, 'headers' | 'parametersSchema' | 'routing'> {
|
||||||
headers?: string;
|
headers?: string;
|
||||||
parametersSchema: string;
|
parametersSchema: string;
|
||||||
|
routing: ToolApiRoutingFormValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolPluginFormValue extends Omit<ExternalToolPluginPayload, 'authConfig' | 'apis'> {
|
interface ToolPluginFormValue extends Omit<ExternalToolPluginPayload, 'authConfig' | 'apis'> {
|
||||||
|
|
@ -27,6 +30,11 @@ const EMPTY_API: ToolApiFormValue = {
|
||||||
path: '',
|
path: '',
|
||||||
headers: '{}',
|
headers: '{}',
|
||||||
parametersSchema: JSON.stringify({ type: 'object', properties: {} }, null, 2),
|
parametersSchema: JSON.stringify({ type: 'object', properties: {} }, null, 2),
|
||||||
|
routing: {
|
||||||
|
summary: '',
|
||||||
|
useWhen: [''],
|
||||||
|
doNotUseWhen: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseJsonObject(value: string | undefined, fieldName: string, optional = false) {
|
function parseJsonObject(value: string | undefined, fieldName: string, optional = false) {
|
||||||
|
|
@ -83,6 +91,53 @@ function parseJsonObject(value: string | undefined, fieldName: string, optional
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStringList(values?: string[]) {
|
||||||
|
return Array.from(new Set((values ?? []).map((item) => item?.trim()).filter((item): item is string => Boolean(item))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTrimmedText(message: string) {
|
||||||
|
return async (_: unknown, value: string | undefined) => {
|
||||||
|
if (!value?.trim()) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRoutingList(message: string, min = 0) {
|
||||||
|
return async (_: unknown, value: string[] | undefined) => {
|
||||||
|
const normalized = normalizeStringList(value);
|
||||||
|
if (normalized.length < min) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((value ?? []).some((item) => !item?.trim())) {
|
||||||
|
throw new Error('列表项不能为空');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRouting(value: ToolApiRoutingFormValue | undefined, apiName: string): ExternalToolApiRouting {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`API ${apiName} 的路由规则不能为空`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = value.summary?.trim();
|
||||||
|
if (!summary) {
|
||||||
|
throw new Error(`API ${apiName} 的路由摘要不能为空`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWhen = normalizeStringList(value.useWhen);
|
||||||
|
if (useWhen.length < 1) {
|
||||||
|
throw new Error(`API ${apiName} 的 useWhen 至少保留一项`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary,
|
||||||
|
useWhen,
|
||||||
|
doNotUseWhen: normalizeStringList(value.doNotUseWhen),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function ExternalToolEditor({ open, agentId, plugin, onClose, onSaved }: Props) {
|
export default function ExternalToolEditor({ open, agentId, plugin, onClose, onSaved }: Props) {
|
||||||
const { message } = AntApp.useApp();
|
const { message } = AntApp.useApp();
|
||||||
const [form] = Form.useForm<ToolPluginFormValue>();
|
const [form] = Form.useForm<ToolPluginFormValue>();
|
||||||
|
|
@ -104,6 +159,7 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
|
||||||
path: item.path.trim(),
|
path: item.path.trim(),
|
||||||
headers: parseJsonObject(item.headers, `API ${item.name} 的请求头`, true),
|
headers: parseJsonObject(item.headers, `API ${item.name} 的请求头`, true),
|
||||||
parametersSchema: parseJsonObject(item.parametersSchema, `API ${item.name} 的依赖参数`),
|
parametersSchema: parseJsonObject(item.parametersSchema, `API ${item.name} 的依赖参数`),
|
||||||
|
routing: normalizeRouting(item.routing, item.name.trim() || '未命名 API'),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,6 +209,11 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
|
||||||
path: item.path,
|
path: item.path,
|
||||||
headers: JSON.stringify(item.headers || {}, null, 2),
|
headers: JSON.stringify(item.headers || {}, null, 2),
|
||||||
parametersSchema: JSON.stringify(item.parametersSchema || { type: 'object', properties: {} }, null, 2),
|
parametersSchema: JSON.stringify(item.parametersSchema || { type: 'object', properties: {} }, null, 2),
|
||||||
|
routing: {
|
||||||
|
summary: item.routing?.summary || '',
|
||||||
|
useWhen: item.routing?.useWhen?.length ? item.routing.useWhen : [''],
|
||||||
|
doNotUseWhen: item.routing?.doNotUseWhen || [],
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
|
@ -259,11 +320,15 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="description(描述)"
|
label="description(描述)"
|
||||||
name={[field.name, 'description']}
|
name={[field.name, 'description']}
|
||||||
rules={[{ required: true, message: '请输入描述' }]}
|
rules={[{ validator: validateTrimmedText('请输入描述') }]}
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
|
<Input.TextArea rows={2} placeholder="描述调用时机和工具能力" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="path(API 地址)" name={[field.name, 'path']} rules={[{ required: true, message: '请输入 API 地址' }]}>
|
<Form.Item
|
||||||
|
label="path(API 地址)"
|
||||||
|
name={[field.name, 'path']}
|
||||||
|
rules={[{ validator: validateTrimmedText('请输入 API 地址') }]}
|
||||||
|
>
|
||||||
<Input placeholder="/v1/products/hot-selling" />
|
<Input placeholder="/v1/products/hot-selling" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div className="agent-editor-tool-grid">
|
<div className="agent-editor-tool-grid">
|
||||||
|
|
@ -278,6 +343,77 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
|
||||||
<Input.TextArea rows={7} className="agent-editor-code-input" />
|
<Input.TextArea rows={7} className="agent-editor-code-input" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
<Card size="small" title="routing(路由规则)">
|
||||||
|
<Form.Item
|
||||||
|
label="summary"
|
||||||
|
name={[field.name, 'routing', 'summary']}
|
||||||
|
rules={[{ validator: validateTrimmedText('请输入路由摘要') }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="查询商品维度数据" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.List name={[field.name, 'routing', 'useWhen']} rules={[{ validator: validateRoutingList('至少添加一条 useWhen', 1) }]}>
|
||||||
|
{(routingFields, { add: addUseWhen, remove: removeUseWhen }, { errors }) => (
|
||||||
|
<Space direction="vertical" size={8} className="agent-editor-tool-list">
|
||||||
|
<div className="agent-editor-tool-list-header">
|
||||||
|
<div>
|
||||||
|
<strong>useWhen</strong>
|
||||||
|
<div className="agent-editor-tool-help">至少一项,描述什么情况下应该调用这个 API。</div>
|
||||||
|
</div>
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addUseWhen('')}>
|
||||||
|
添加条件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{routingFields.map((routingField) => (
|
||||||
|
<Space key={routingField.key} align="start" className="agent-editor-tool-list">
|
||||||
|
<Form.Item
|
||||||
|
name={routingField.name}
|
||||||
|
className="flex-1 mb-0"
|
||||||
|
rules={[{ validator: validateTrimmedText('条件不能为空') }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="用户明确要求商品数据" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="text"
|
||||||
|
icon={<MinusCircleOutlined />}
|
||||||
|
onClick={() => removeUseWhen(routingField.name)}
|
||||||
|
disabled={routingFields.length <= 1}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
<Form.List name={[field.name, 'routing', 'doNotUseWhen']} rules={[{ validator: validateRoutingList('列表项不能为空') }]}>
|
||||||
|
{(routingFields, { add: addDoNotUseWhen, remove: removeDoNotUseWhen }, { errors }) => (
|
||||||
|
<Space direction="vertical" size={8} className="agent-editor-tool-list" style={{ marginTop: 16 }}>
|
||||||
|
<div className="agent-editor-tool-list-header">
|
||||||
|
<div>
|
||||||
|
<strong>doNotUseWhen</strong>
|
||||||
|
<div className="agent-editor-tool-help">可为空,描述什么情况下不要调用这个 API。</div>
|
||||||
|
</div>
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={() => addDoNotUseWhen('')}>
|
||||||
|
添加条件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{routingFields.map((routingField) => (
|
||||||
|
<Space key={routingField.key} align="start" className="agent-editor-tool-list">
|
||||||
|
<Form.Item
|
||||||
|
name={routingField.name}
|
||||||
|
className="flex-1 mb-0"
|
||||||
|
rules={[{ validator: validateTrimmedText('条件不能为空') }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="当前问题只需要其他维度数据" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button danger type="text" icon={<MinusCircleOutlined />} onClick={() => removeDoNotUseWhen(routingField.name)} />
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</Card>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue