mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-04-20 05:26:57 +08:00
[feature](weaapp) add agent
This commit is contained in:
109
webapp/packages/supersonic-fe/src/pages/Agent/AgentModal.tsx
Normal file
109
webapp/packages/supersonic-fe/src/pages/Agent/AgentModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Form, Modal, Input, Button, Switch } from 'antd';
|
||||
import { AgentType } from './type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './style.less';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { uuid } from '@/utils/utils';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TextArea } = Input;
|
||||
|
||||
type Props = {
|
||||
editAgent?: AgentType;
|
||||
onSaveAgent: (agent: AgentType) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const AgentModal: React.FC<Props> = ({ editAgent, onSaveAgent, onCancel }) => {
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (editAgent) {
|
||||
form.setFieldsValue({ ...editAgent, enableSearch: editAgent.enableSearch !== 0 });
|
||||
if (editAgent.examples) {
|
||||
setExamples(editAgent.examples.map((question) => ({ id: uuid(), question })));
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [editAgent]);
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 14 },
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaveLoading(true);
|
||||
await onSaveAgent({
|
||||
id: editAgent?.id,
|
||||
...(editAgent || {}),
|
||||
...values,
|
||||
examples: examples.map((example) => example.question),
|
||||
enableSearch: values.enableSearch ? 1 : 0,
|
||||
});
|
||||
setSaveLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={editAgent ? '编辑助理' : '新建助理'}
|
||||
confirmLoading={saveLoading}
|
||||
width={800}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form {...layout} form={form} initialValues={{ enableSearch: true }}>
|
||||
<FormItem name="name" label="名称" rules={[{ required: true, message: '请输入助理名称' }]}>
|
||||
<Input placeholder="请输入助理名称" />
|
||||
</FormItem>
|
||||
<FormItem name="enableSearch" label="支持联想" valuePropName="checked">
|
||||
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
|
||||
</FormItem>
|
||||
<FormItem name="examples" label="示例问题">
|
||||
<div className={styles.paramsSection}>
|
||||
{examples.map((example) => {
|
||||
const { id, question } = example;
|
||||
return (
|
||||
<div className={styles.filterRow} key={id}>
|
||||
<Input
|
||||
placeholder="示例问题"
|
||||
value={question}
|
||||
className={styles.questionExample}
|
||||
onChange={(e) => {
|
||||
example.question = e.target.value;
|
||||
setExamples([...examples]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setExamples(examples.filter((item) => item.id !== id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setExamples([...examples, { id: uuid() }]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增示例问题
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem name="description" label="描述">
|
||||
<TextArea placeholder="请输入助理描述" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentModal;
|
||||
144
webapp/packages/supersonic-fe/src/pages/Agent/AgentsSection.tsx
Normal file
144
webapp/packages/supersonic-fe/src/pages/Agent/AgentsSection.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Popconfirm, Switch } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './style.less';
|
||||
import { AgentType } from './type';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
type Props = {
|
||||
agents: AgentType[];
|
||||
currentAgent?: AgentType;
|
||||
loading: boolean;
|
||||
onSelectAgent: (agent: AgentType) => void;
|
||||
onDeleteAgent: (id: number) => void;
|
||||
onEditAgent: (agent?: AgentType) => void;
|
||||
onSaveAgent: (agent: AgentType, noTip?: boolean) => Promise<void>;
|
||||
};
|
||||
|
||||
const AgentsSection: React.FC<Props> = ({
|
||||
agents,
|
||||
currentAgent,
|
||||
onSelectAgent,
|
||||
onDeleteAgent,
|
||||
onEditAgent,
|
||||
onSaveAgent,
|
||||
}) => {
|
||||
// const [searchName, setSearchName] = useState('');
|
||||
const [showAgents, setShowAgents] = useState<AgentType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowAgents(agents);
|
||||
}, [agents]);
|
||||
|
||||
return (
|
||||
<div className={styles.agentsSection}>
|
||||
{/* <div className={styles.sectionTitle}>问答助理</div> */}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.searchBar}>
|
||||
{/* <Search
|
||||
placeholder="请输入助理名称搜索"
|
||||
className={styles.searchControl}
|
||||
value={searchName}
|
||||
onChange={(e) => {
|
||||
setSearchName(e.target.value);
|
||||
}}
|
||||
onSearch={(value) => {
|
||||
setShowAgents(
|
||||
agents.filter((agent) =>
|
||||
agent.name?.trim().toLowerCase().includes(value.trim().toLowerCase()),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onEditAgent(undefined);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新建助理
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.agentsContainer}>
|
||||
{showAgents.map((agent) => {
|
||||
const agentItemClass = classNames(styles.agentItem, {
|
||||
[styles.agentActive]: agent.id === currentAgent?.id,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={agentItemClass}
|
||||
key={agent.id}
|
||||
onClick={() => {
|
||||
onSelectAgent(agent);
|
||||
}}
|
||||
>
|
||||
<UserOutlined className={styles.agentIcon} />
|
||||
<div className={styles.agentContent}>
|
||||
<div className={styles.agentNameBar}>
|
||||
<div className={styles.agentName}>{agent.name}</div>
|
||||
<div className={styles.operateIcons}>
|
||||
<EditOutlined
|
||||
className={styles.operateIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditAgent(agent);
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
onCancel={(e) => {
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDeleteAgent(agent.id!);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined
|
||||
className={styles.operateIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.bottomBar}>
|
||||
<div className={styles.agentDescription} title={agent.description}>
|
||||
{agent.description}
|
||||
</div>
|
||||
<div className={styles.toggleStatus}>
|
||||
{agent.status === 0 ? (
|
||||
'已禁用'
|
||||
) : (
|
||||
<span className={styles.online}>已启用</span>
|
||||
)}
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size="small"
|
||||
defaultChecked={agent.status === 1}
|
||||
onChange={(value) => {
|
||||
onSaveAgent({ ...agent, status: value ? 1 : 0 }, true);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsSection;
|
||||
264
webapp/packages/supersonic-fe/src/pages/Agent/ToolModal.tsx
Normal file
264
webapp/packages/supersonic-fe/src/pages/Agent/ToolModal.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Form, Modal, Input, Select, Button } from 'antd';
|
||||
import {
|
||||
AgentToolType,
|
||||
AgentToolTypeEnum,
|
||||
AGENT_TOOL_TYPE_LIST,
|
||||
MetricOptionType,
|
||||
MetricType,
|
||||
ModelType,
|
||||
QUERY_MODE_LIST,
|
||||
} from './type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import styles from './style.less';
|
||||
import { getLeafList, uuid } from '@/utils/utils';
|
||||
import { getMetricList, getModelList } from './service';
|
||||
import { PluginType } from '../ChatPlugin/type';
|
||||
import { getPluginList } from '../ChatPlugin/service';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
type Props = {
|
||||
editTool?: AgentToolType;
|
||||
onSaveTool: (tool: AgentToolType) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const ToolModal: React.FC<Props> = ({ editTool, onSaveTool, onCancel }) => {
|
||||
const [toolType, setToolType] = useState<AgentToolTypeEnum>();
|
||||
const [modelList, setModelList] = useState<ModelType[]>([]);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [examples, setExamples] = useState<{ id: string; question?: string }[]>([]);
|
||||
const [metricOptions, setMetricOptions] = useState<MetricOptionType[]>([]);
|
||||
const [modelMetricList, setModelMetricList] = useState<MetricType[]>([]);
|
||||
const [plugins, setPlugins] = useState<PluginType[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const initModelList = async () => {
|
||||
const res = await getModelList();
|
||||
setModelList([{ id: -1, name: '默认' }, ...getLeafList(res.data)]);
|
||||
};
|
||||
|
||||
const initPluginList = async () => {
|
||||
const res = await getPluginList({});
|
||||
setPlugins(res.data || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initModelList();
|
||||
initPluginList();
|
||||
}, []);
|
||||
|
||||
const initModelMetrics = async (params: any) => {
|
||||
const res = await getMetricList(params[0].modelId);
|
||||
setModelMetricList(res.data.list);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editTool) {
|
||||
form.setFieldsValue({ ...editTool, plugins: editTool.plugins?.[0] });
|
||||
setToolType(editTool.type);
|
||||
setExamples(
|
||||
(editTool.exampleQuestions || []).map((item) => ({ id: uuid(), question: item })),
|
||||
);
|
||||
setMetricOptions(editTool.metricOptions || []);
|
||||
if (editTool.metricOptions && editTool.metricOptions.length > 0) {
|
||||
initModelMetrics(editTool.metricOptions || []);
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [editTool]);
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 14 },
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaveLoading(true);
|
||||
await onSaveTool({
|
||||
id: editTool?.id,
|
||||
...values,
|
||||
exampleQuestions: examples.map((item) => item.question).filter((item) => item),
|
||||
plugins: values.plugins ? [values.plugins] : undefined,
|
||||
metricOptions: metricOptions.map((item) => ({ modelId: values.modelId, ...item })),
|
||||
});
|
||||
setSaveLoading(false);
|
||||
};
|
||||
|
||||
const updateMetricList = async (value: number) => {
|
||||
if (modelMetricList[value]) {
|
||||
return;
|
||||
}
|
||||
const res = await getMetricList(value);
|
||||
setModelMetricList(res.data.list);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={editTool ? '编辑工具' : '新建工具'}
|
||||
confirmLoading={saveLoading}
|
||||
width={800}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form {...layout} form={form}>
|
||||
<FormItem name="type" label="类型" rules={[{ required: true, message: '请选择工具类型' }]}>
|
||||
<Select
|
||||
options={AGENT_TOOL_TYPE_LIST}
|
||||
placeholder="请选择工具类型"
|
||||
onChange={setToolType}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="name" label="名称">
|
||||
<Input placeholder="请输入工具名称" />
|
||||
</FormItem>
|
||||
{toolType === AgentToolTypeEnum.DSL && (
|
||||
<>
|
||||
<FormItem name="modelIds" label="主题域">
|
||||
<Select
|
||||
options={modelList.map((model) => ({ label: model.name, value: model.id }))}
|
||||
placeholder="请选择主题域"
|
||||
mode="multiple"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="exampleQuestions" label="示例问题">
|
||||
<div className={styles.paramsSection}>
|
||||
{examples.map((example) => {
|
||||
const { id, question } = example;
|
||||
return (
|
||||
<div className={styles.filterRow} key={id}>
|
||||
<Input
|
||||
placeholder="示例问题"
|
||||
value={question}
|
||||
className={styles.questionExample}
|
||||
onChange={(e) => {
|
||||
example.question = e.target.value;
|
||||
setExamples([...examples]);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setExamples(examples.filter((item) => item.id !== id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setExamples([...examples, { id: uuid() }]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增示例问题
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
{toolType === AgentToolTypeEnum.INTERPRET && (
|
||||
<>
|
||||
<FormItem name="modelId" label="主题域">
|
||||
<Select
|
||||
options={modelList.map((model) => ({ label: model.name, value: model.id }))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
placeholder="请选择主题域"
|
||||
onChange={(value) => {
|
||||
setMetricOptions([...metricOptions]);
|
||||
updateMetricList(value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="params" label="指标">
|
||||
<div className={styles.paramsSection}>
|
||||
{metricOptions.map((filter: any) => {
|
||||
return (
|
||||
<div className={styles.filterRow} key={filter.id}>
|
||||
<Select
|
||||
placeholder="请选择指标,需先选择主题域"
|
||||
options={(modelMetricList || []).map((metric) => ({
|
||||
label: metric.name,
|
||||
value: `${metric.id}`,
|
||||
}))}
|
||||
showSearch
|
||||
className={styles.filterParamValueField}
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string)
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
allowClear
|
||||
value={filter.metricId}
|
||||
onChange={(value) => {
|
||||
filter.metricId = value;
|
||||
setMetricOptions([...metricOptions]);
|
||||
}}
|
||||
/>
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
setMetricOptions(metricOptions.filter((item) => item.id !== filter.id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMetricOptions([
|
||||
...metricOptions,
|
||||
{ id: uuid(), metricId: undefined, modelId: undefined },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新增指标
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
{toolType === AgentToolTypeEnum.PLUGIN && (
|
||||
<FormItem name="plugins" label="插件">
|
||||
<Select
|
||||
placeholder="请选择插件"
|
||||
options={plugins.map((plugin) => ({ label: plugin.name, value: plugin.id }))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={(value) => {
|
||||
const plugin = plugins.find((item) => item.id === value);
|
||||
if (plugin) {
|
||||
form.setFieldsValue({ name: plugin.name });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
{toolType === AgentToolTypeEnum.RULE && (
|
||||
<FormItem name="queryModes" label="查询模式">
|
||||
<Select
|
||||
placeholder="请选择查询模式"
|
||||
options={QUERY_MODE_LIST}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
filterOption={(input, option) =>
|
||||
((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolModal;
|
||||
196
webapp/packages/supersonic-fe/src/pages/Agent/ToolsSection.tsx
Normal file
196
webapp/packages/supersonic-fe/src/pages/Agent/ToolsSection.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { uuid } from '@/utils/utils';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ToolOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Empty, Popconfirm, Space, Switch, Tag } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import styles from './style.less';
|
||||
import ToolModal from './ToolModal';
|
||||
import { AgentToolType, AgentType, AGENT_TOOL_TYPE_LIST } from './type';
|
||||
|
||||
type Props = {
|
||||
currentAgent?: AgentType;
|
||||
onSaveAgent: (agent: AgentType, noTip?: boolean) => Promise<void>;
|
||||
onEditAgent: (agent?: AgentType) => void;
|
||||
goBack: () => void;
|
||||
};
|
||||
|
||||
const ToolsSection: React.FC<Props> = ({ currentAgent, onSaveAgent, onEditAgent, goBack }) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editTool, setEditTool] = useState<AgentToolType>();
|
||||
|
||||
const agentConfig = currentAgent?.agentConfig ? JSON.parse(currentAgent.agentConfig as any) : {};
|
||||
|
||||
const saveAgent = async (agent: AgentType) => {
|
||||
await onSaveAgent(agent);
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const onSaveTool = async (tool: AgentToolType) => {
|
||||
const newAgentConfig = agentConfig || ({} as any);
|
||||
if (!newAgentConfig.tools) {
|
||||
newAgentConfig.tools = [];
|
||||
}
|
||||
if (tool.id) {
|
||||
const index = newAgentConfig.tools.findIndex((item: AgentToolType) => item.id === tool.id);
|
||||
newAgentConfig.tools[index] = tool;
|
||||
} else {
|
||||
newAgentConfig.tools.push({ ...tool, id: uuid() });
|
||||
}
|
||||
await saveAgent({
|
||||
...currentAgent,
|
||||
agentConfig: JSON.stringify(newAgentConfig) as any,
|
||||
});
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const onDeleteTool = async (tool: AgentToolType) => {
|
||||
const newAgentConfig = agentConfig || ({} as any);
|
||||
if (!newAgentConfig.tools) {
|
||||
newAgentConfig.tools = [];
|
||||
}
|
||||
newAgentConfig.tools = newAgentConfig.tools.filter(
|
||||
(item: AgentToolType) => item.id !== tool.id,
|
||||
);
|
||||
await saveAgent({
|
||||
...currentAgent,
|
||||
agentConfig: JSON.stringify(newAgentConfig) as any,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.toolsSection}>
|
||||
<div className={styles.toolsSectionTitleBar}>
|
||||
<ArrowLeftOutlined className={styles.backIcon} onClick={goBack} />
|
||||
<div className={styles.agentTitle}>{currentAgent?.name}</div>
|
||||
<div className={styles.toggleStatus}>
|
||||
{currentAgent?.status === 0 ? '已禁用' : <span className={styles.online}>已启用</span>}
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size="small"
|
||||
defaultChecked={currentAgent?.status === 1}
|
||||
onChange={(value) => {
|
||||
onSaveAgent({ ...currentAgent, status: value ? 1 : 0 }, true);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.basicInfo}>
|
||||
<div className={styles.basicInfoTitle}>
|
||||
基本信息
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onEditAgent(currentAgent);
|
||||
}}
|
||||
>
|
||||
修改基本信息
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.infoContent}>
|
||||
<div className={styles.infoItem}>
|
||||
示例问题:
|
||||
<Space>
|
||||
{currentAgent?.examples?.map((item) => (
|
||||
<Tag key={item}>{item}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<div className={styles.infoItem}>描述:{currentAgent?.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.toolSection}>
|
||||
<div className={styles.toolSectionTitleBar}>
|
||||
<div className={styles.toolSectionTitle}>工具</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setEditTool(undefined);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<PlusOutlined /> 新增工具
|
||||
</Button>
|
||||
</div>
|
||||
{agentConfig?.tools && agentConfig?.tools?.length > 0 ? (
|
||||
<div className={styles.toolsContent}>
|
||||
{agentConfig.tools.map((tool: AgentToolType) => {
|
||||
const toolType = AGENT_TOOL_TYPE_LIST.find((item) => item.value === tool.type)?.label;
|
||||
return (
|
||||
<div
|
||||
className={styles.toolItem}
|
||||
key={tool.id}
|
||||
onClick={() => {
|
||||
setEditTool(tool);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<ToolOutlined className={styles.toolIcon} />
|
||||
<div className={styles.toolContent}>
|
||||
<div className={styles.toolTopSection}>
|
||||
<div className={styles.toolType}>{tool.name || toolType}</div>
|
||||
<div className={styles.toolOperateIcons}>
|
||||
<EditOutlined
|
||||
className={styles.toolOperateIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditTool(tool);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定删除吗?"
|
||||
onCancel={(e) => {
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDeleteTool(tool);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined
|
||||
className={styles.toolOperateIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.toolDesc} title={toolType}>
|
||||
{toolType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyHolder}>
|
||||
<Empty description={`【${currentAgent?.name}】暂无工具,请新增工具`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{modalVisible && (
|
||||
<ToolModal
|
||||
editTool={editTool}
|
||||
onSaveTool={onSaveTool}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsSection;
|
||||
94
webapp/packages/supersonic-fe/src/pages/Agent/index.tsx
Normal file
94
webapp/packages/supersonic-fe/src/pages/Agent/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import AgentsSection from './AgentsSection';
|
||||
import AgentModal from './AgentModal';
|
||||
import { deleteAgent, getAgentList, saveAgent } from './service';
|
||||
import styles from './style.less';
|
||||
import ToolsSection from './ToolsSection';
|
||||
import { AgentType } from './type';
|
||||
|
||||
const Agent = () => {
|
||||
const [agents, setAgents] = useState<AgentType[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentAgent, setCurrentAgent] = useState<AgentType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editAgent, setEditAgent] = useState<AgentType>();
|
||||
|
||||
const updateData = async () => {
|
||||
setLoading(true);
|
||||
const res = await getAgentList();
|
||||
setLoading(false);
|
||||
setAgents(res.data || []);
|
||||
if (!res.data?.length) {
|
||||
return;
|
||||
}
|
||||
if (currentAgent) {
|
||||
const agent = res.data.find((item) => item.id === currentAgent.id);
|
||||
if (agent) {
|
||||
setCurrentAgent(agent);
|
||||
} else {
|
||||
setCurrentAgent(res.data[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateData();
|
||||
}, []);
|
||||
|
||||
const onSaveAgent = async (agent: AgentType, noTip?: boolean) => {
|
||||
await saveAgent(agent);
|
||||
if (!noTip) {
|
||||
message.success('保存成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
updateData();
|
||||
};
|
||||
|
||||
const onDeleteAgent = async (id: number) => {
|
||||
await deleteAgent(id);
|
||||
message.success('删除成功');
|
||||
updateData();
|
||||
};
|
||||
|
||||
const onEditAgent = (agent?: AgentType) => {
|
||||
setEditAgent(agent);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.agent}>
|
||||
{!currentAgent ? (
|
||||
<AgentsSection
|
||||
agents={agents}
|
||||
currentAgent={currentAgent}
|
||||
loading={loading}
|
||||
onSelectAgent={setCurrentAgent}
|
||||
onEditAgent={onEditAgent}
|
||||
onDeleteAgent={onDeleteAgent}
|
||||
onSaveAgent={onSaveAgent}
|
||||
/>
|
||||
) : (
|
||||
<ToolsSection
|
||||
currentAgent={currentAgent}
|
||||
onSaveAgent={onSaveAgent}
|
||||
onEditAgent={onEditAgent}
|
||||
goBack={() => {
|
||||
setCurrentAgent(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modalVisible && (
|
||||
<AgentModal
|
||||
editAgent={editAgent}
|
||||
onSaveAgent={onSaveAgent}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Agent;
|
||||
36
webapp/packages/supersonic-fe/src/pages/Agent/service.ts
Normal file
36
webapp/packages/supersonic-fe/src/pages/Agent/service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { request } from "umi";
|
||||
import { AgentType, MetricType, ModelType } from "./type";
|
||||
|
||||
export function getAgentList() {
|
||||
return request<Result<AgentType[]>>('/api/chat/agent/getAgentList');
|
||||
}
|
||||
|
||||
export function saveAgent(agent: AgentType) {
|
||||
return request<Result<any>>('/api/chat/agent', {
|
||||
method: agent?.id ? 'PUT' : 'POST',
|
||||
data: {...agent, status: agent.status !== undefined ? agent.status : 1},
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAgent(id: number) {
|
||||
return request<Result<any>>(`/api/chat/agent/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export function getModelList() {
|
||||
return request<Result<ModelType[]>>('/api/chat/conf/modelList', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function getMetricList(modelId: number) {
|
||||
return request<Result<{list: MetricType[]}>>('/api/semantic/metric/queryMetric', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
modelIds: [modelId],
|
||||
current: 1,
|
||||
pageSize: 2000
|
||||
}
|
||||
});
|
||||
}
|
||||
292
webapp/packages/supersonic-fe/src/pages/Agent/style.less
Normal file
292
webapp/packages/supersonic-fe/src/pages/Agent/style.less
Normal file
@@ -0,0 +1,292 @@
|
||||
.agent {
|
||||
// background: #fff;
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
.agentsSection {
|
||||
padding: 20px 40px;
|
||||
background: #fff;
|
||||
height: calc(100vh - 48px);
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
.searchBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.searchControl {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.agentsContainer {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.agentItem {
|
||||
display: flex;
|
||||
width: 290px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&.agentActive {
|
||||
border-color: var(--chat-blue);
|
||||
}
|
||||
|
||||
.agentIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: var(--chat-blue);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.agentContent {
|
||||
margin-left: 12px;
|
||||
flex: 1;
|
||||
|
||||
.agentNameBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.agentName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.operateIcons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
.operateIcon {
|
||||
color: var(--text-color-fourth);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
width: 210px;
|
||||
|
||||
.agentDescription {
|
||||
width: 120px;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-color-third)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggleStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
font-size: 12px;
|
||||
|
||||
.online {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.toolsSection {
|
||||
.toolsSectionTitleBar {
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 20px;
|
||||
|
||||
.backIcon {
|
||||
font-size: 20px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.agentTitle {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.paramsSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
.filterRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
|
||||
.filterParamName {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.filterParamValueField {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.questionExample {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.basicInfo {
|
||||
margin: 20px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
|
||||
.basicInfoTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.infoContent {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolSection {
|
||||
margin: 20px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
|
||||
.toolSectionTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 30px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
|
||||
.toolSectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyHolder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.toolsContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 20px 20px 30px;
|
||||
|
||||
.toolItem {
|
||||
width: 300px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.toolIcon {
|
||||
font-size: 24px;
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
|
||||
.toolContent {
|
||||
flex: 1;
|
||||
|
||||
.toolTopSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.toolType {
|
||||
flex: 1;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.toolOperateIcons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
|
||||
.toolOperateIcon {
|
||||
color: var(--text-color-third);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolDesc {
|
||||
margin-top: 2px;
|
||||
width: 220px;
|
||||
font-size: 13px;
|
||||
color: var(--text-color-third);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
webapp/packages/supersonic-fe/src/pages/Agent/type.ts
Normal file
119
webapp/packages/supersonic-fe/src/pages/Agent/type.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
export type MetricOptionType = {
|
||||
id: string;
|
||||
metricId?: number;
|
||||
modelId?: number;
|
||||
}
|
||||
|
||||
export enum AgentToolTypeEnum {
|
||||
RULE = 'RULE',
|
||||
DSL = 'DSL',
|
||||
PLUGIN = 'PLUGIN',
|
||||
INTERPRET = 'INTERPRET'
|
||||
}
|
||||
|
||||
export enum QueryModeEnum {
|
||||
ENTITY_DETAIL = 'ENTITY_DETAIL',
|
||||
ENTITY_LIST_FILTER = 'ENTITY_LIST_FILTER',
|
||||
ENTITY_ID = 'ENTITY_ID',
|
||||
METRIC_ENTITY = 'METRIC_ENTITY',
|
||||
METRIC_FILTER = 'METRIC_FILTER',
|
||||
METRIC_GROUPBY = 'METRIC_GROUPBY',
|
||||
METRIC_MODEL = 'METRIC_MODEL',
|
||||
METRIC_ORDERBY = 'METRIC_ORDERBY'
|
||||
}
|
||||
|
||||
export const AGENT_TOOL_TYPE_LIST = [
|
||||
{
|
||||
label: '规则',
|
||||
value: AgentToolTypeEnum.RULE
|
||||
},
|
||||
{
|
||||
label: 'LLM语义解析',
|
||||
value: AgentToolTypeEnum.DSL
|
||||
},
|
||||
{
|
||||
label: '指标解读',
|
||||
value: AgentToolTypeEnum.INTERPRET
|
||||
},
|
||||
{
|
||||
label: '插件',
|
||||
value: AgentToolTypeEnum.PLUGIN
|
||||
},
|
||||
]
|
||||
|
||||
export const QUERY_MODE_LIST = [
|
||||
{
|
||||
label: '实体明细(查询维度信息)',
|
||||
value: QueryModeEnum.ENTITY_DETAIL
|
||||
},
|
||||
{
|
||||
label: '实体圈选',
|
||||
value: QueryModeEnum.ENTITY_LIST_FILTER
|
||||
},
|
||||
{
|
||||
label: '实体查询(按ID查询)',
|
||||
value: QueryModeEnum.ENTITY_ID
|
||||
},
|
||||
{
|
||||
label: '指标查询(带实体)',
|
||||
value: QueryModeEnum.METRIC_ENTITY
|
||||
},
|
||||
{
|
||||
label: '指标查询(带条件)',
|
||||
value: QueryModeEnum.METRIC_FILTER
|
||||
},
|
||||
{
|
||||
label: '指标查询(按维度分组)',
|
||||
value: QueryModeEnum.METRIC_GROUPBY
|
||||
},
|
||||
{
|
||||
label: '指标查询(不带条件)',
|
||||
value: QueryModeEnum.METRIC_MODEL
|
||||
},
|
||||
{
|
||||
label: '按指标排序',
|
||||
value: QueryModeEnum.METRIC_ORDERBY
|
||||
}
|
||||
];
|
||||
|
||||
export type AgentToolType = {
|
||||
id?: string;
|
||||
type: AgentToolTypeEnum;
|
||||
name: string;
|
||||
queryModes?: QueryModeEnum[];
|
||||
plugins?: number[];
|
||||
metricOptions?: MetricOptionType[];
|
||||
exampleQuestions?: string[];
|
||||
modelIds?: number[];
|
||||
}
|
||||
|
||||
export type AgentConfigType = {
|
||||
tools: AgentToolType[];
|
||||
}
|
||||
|
||||
export type AgentType = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
createdBy?: string;
|
||||
updatedBy?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
examples?: string[];
|
||||
status?: 0 | 1;
|
||||
enableSearch?: 0 | 1;
|
||||
agentConfig?: AgentConfigType;
|
||||
}
|
||||
|
||||
export type ModelType = {
|
||||
id: number | string;
|
||||
parentId: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type MetricType = {
|
||||
id: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
Reference in New Issue
Block a user