perf: agent skill editor

This commit is contained in:
archer 2025-12-22 19:13:49 +08:00
parent 21b8bcace5
commit 1f100276f6
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
17 changed files with 483 additions and 391 deletions

View File

@ -3,10 +3,10 @@ import type { AppDeleteJobData } from './index';
import { findAppAndAllChildren, deleteAppDataProcessor } from '../controller';
import { addLog } from '../../../common/system/log';
import { batchRun } from '@fastgpt/global/common/system/utils';
import type { AppSchema } from '@fastgpt/global/core/app/type';
import type { AppSchemaType } from '@fastgpt/global/core/app/type';
import { MongoApp } from '../schema';
const deleteApps = async ({ teamId, apps }: { teamId: string; apps: AppSchema[] }) => {
const deleteApps = async ({ teamId, apps }: { teamId: string; apps: AppSchemaType[] }) => {
const results = await batchRun(
apps,
async (app) => {

View File

@ -9,7 +9,6 @@ import type {
DispatchNodeResultType,
ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type';
import { getLLMModel } from '../../../../ai/model';
import { getNodeErrResponse, getHistories } from '../../utils';
import type { AIChatItemValueItemType, ChatItemType } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
@ -23,16 +22,14 @@ import { systemSubInfo } from './sub/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { dispatchPlanAgent, dispatchReplanAgent } from './sub/plan';
import { getFileInputPrompt, readFileTool } from './sub/file/utils';
import { getFileInputPrompt } from './sub/file/utils';
import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/global/core/ai/type';
import type { AgentPlanType } from './sub/plan/type';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { stepCall } from './master/call';
import { addLog } from '../../../../../common/system/log';
import { matchSkillForPlan } from './skillMatcher';
import { matchSkillForId, matchSkillForPlan } from './skillMatcher';
import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type';
import type { GetSubAppInfoFnType, SubAppRuntimeType } from './type';
import { agentSkillToToolRuntime } from './sub/tool/utils';
import type { SubAppRuntimeType } from './type';
import { getSubapps } from './utils';
export type DispatchAgentModuleProps = ModuleDispatchProps<{
@ -84,7 +81,6 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
isAskAgent = true
}
} = props;
const agentModel = getLLMModel(model);
const chatHistories = getHistories(history, histories);
const historiesMessages = chats2GPTMessages({
messages: chatHistories,
@ -135,22 +131,22 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
// Get sub apps
let { completionTools, subAppsMap } = await getSubapps({
let { completionTools: agentCompletionTools, subAppsMap: agentSubAppsMap } = await getSubapps({
tools: selectedTools,
tmbId: runningAppInfo.tmbId,
lang,
filesMap
});
const getSubAppInfo = (id: string) => {
const toolNode = subAppsMap.get(id) || systemSubInfo[id];
const toolNode = agentSubAppsMap.get(id) || systemSubInfo[id];
return {
name: toolNode?.name || '',
avatar: toolNode?.avatar || '',
toolDescription: toolNode?.toolDescription || toolNode?.name || ''
};
};
console.log(JSON.stringify(completionTools, null, 2), 'topAgent completionTools');
console.log(subAppsMap, 'topAgent subAppsMap');
// console.log(JSON.stringify(completionTools, null, 2), 'topAgent completionTools');
// console.log(subAppsMap, 'topAgent subAppsMap');
/* ===== AI Start ===== */
@ -175,7 +171,62 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
if (taskIsComplexity) {
/* ===== Plan Agent ===== */
let currentSkillId: string | undefined = matchedSkillId;
const mergeSkill = ({
skillCompletionTools,
skillSubAppsMap
}: {
skillCompletionTools: ChatCompletionTool[];
skillSubAppsMap: Map<string, SubAppRuntimeType>;
}) => {
// 将 skill 的 completionTools 和 subAppsMap 合并到 topAgent如果重复则以 skill 的为准。
agentCompletionTools = skillCompletionTools.concat(
agentCompletionTools.filter(
(item) =>
!skillCompletionTools.some((item2) => item2.function.name === item.function.name)
)
);
[...skillSubAppsMap].forEach(([id, item]) => {
agentSubAppsMap.set(id, item);
});
console.log(JSON.stringify(agentCompletionTools, null, 2), 'merge completionTools');
console.log(agentSubAppsMap, 'merge subAppsMap');
};
const skillMatch = async () => {
const matchResult = await matchSkillForPlan({
teamId: runningUserInfo.teamId,
tmbId: runningAppInfo.tmbId,
appId: runningAppInfo.id,
userInput: lastInteractive ? interactiveInput : userChatInput,
messages: historiesMessages, // 传入完整的对话历史
model,
lang
});
if (matchResult.matched) {
matchedSkillId = String(matchResult.skill._id);
mergeSkill({
skillCompletionTools: matchResult.completionTools,
skillSubAppsMap: matchResult.subAppsMap
});
// 可选: 推送匹配信息给前端
workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: `📋 找到参考技能: ${matchResult.systemPrompt}`
})
});
return {
skillPrompt: matchResult.systemPrompt
};
}
addLog.debug(`未匹配到 skill原因: ${matchResult.reason}`);
return {
skillPrompt: ''
};
};
const planCallFn = async () => {
// 点了确认。此时肯定有 agentPlans
if (
@ -185,58 +236,16 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
) {
planHistoryMessages = undefined;
} else {
// 🆕 执行 Skill 匹配(仅在 isPlanStep 且没有 planHistoryMessages 时)
let skillSystemPrompt: string | undefined;
// match skill
const matchResult = await matchSkillForPlan({
teamId: runningUserInfo.teamId,
tmbId: runningAppInfo.tmbId,
appId: runningAppInfo.id,
userInput: lastInteractive ? interactiveInput : userChatInput,
messages: historiesMessages, // 传入完整的对话历史
model,
lang
});
if (matchResult.matched) {
skillSystemPrompt = matchResult.systemPrompt;
currentSkillId = String(matchResult.skill._id);
// 将 skill 的 completionTools 和 subAppsMap 合并到topAgent如果重复则以 skill 的为准。
completionTools = matchResult.completionTools.concat(
completionTools.filter(
(item) =>
!matchResult.completionTools.some(
(item2) => item2.function.name === item.function.name
)
)
);
[...matchResult.subAppsMap].forEach(([id, item]) => {
subAppsMap.set(id, item);
});
console.log(JSON.stringify(completionTools, null, 2), 'merge completionTools');
console.log(subAppsMap, 'merge subAppsMap');
// 可选: 推送匹配信息给前端
workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: `📋 找到参考技能: ${matchResult.systemPrompt}`
})
});
} else {
addLog.debug(`未匹配到 skill原因: ${matchResult.reason}`);
}
const { skillPrompt } = await skillMatch();
const { answerText, plan, completeMessages, usages, interactiveResponse } =
await dispatchPlanAgent({
historyMessages: planHistoryMessages || historiesMessages,
userInput: lastInteractive ? interactiveInput : userChatInput,
interactive: lastInteractive,
completionTools,
completionTools: agentCompletionTools,
getSubAppInfo,
// TODO: 需要区分systemprompt 需要替换成 role 和 target 么?
systemPrompt: skillSystemPrompt || systemPrompt,
systemPrompt: skillPrompt || systemPrompt,
model,
temperature,
top_p: aiChatTopP,
@ -301,7 +310,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
[DispatchNodeResponseKeyEnum.memories]: {
[planMessagesKey]: filterMemoryMessages(completeMessages),
[agentPlanKey]: agentPlan,
[skillMatchKey]: currentSkillId
[skillMatchKey]: matchedSkillId
},
[DispatchNodeResponseKeyEnum.interactive]: interactiveResponse
};
@ -326,7 +335,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
userInput: lastInteractive ? interactiveInput : userChatInput,
plan,
interactive: lastInteractive,
completionTools,
completionTools: agentCompletionTools,
getSubAppInfo,
systemPrompt,
model,
@ -420,41 +429,26 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
if (result) return result;
}
// 如果有保存的 skill id恢复 skill 的 tools
else if (matchedSkillId) {
addLog.debug(`恢复 skill tools, skill id: ${matchedSkillId}`);
const skill = await matchSkillForId({
id: matchedSkillId,
tmbId: runningAppInfo.tmbId,
lang
});
if (skill) {
mergeSkill({
skillCompletionTools: skill.skillTools,
skillSubAppsMap: skill.skillSubAppsMap
});
}
}
addLog.debug(`Start master agent`, {
agentPlan: JSON.stringify(agentPlan, null, 2)
});
// 如果有保存的 skill id恢复 skill 的 tools
if (matchedSkillId) {
addLog.debug(`恢复 skill tools, skill id: ${matchedSkillId}`);
try {
const { MongoAiSkill } = await import('../../../../ai/skill/schema');
const skill = await MongoAiSkill.findById(matchedSkillId).lean();
if (skill && skill.tools) {
const { completionTools: skillTools, subAppsMap: skillSubAppsMap } = await getSubapps({
tools: skill.tools,
tmbId: runningAppInfo.tmbId,
lang
});
// 合并 skill 的 tools 到 completionTools
completionTools = skillTools.concat(
completionTools.filter(
(item) => !skillTools.some((item2) => item2.function.name === item.function.name)
)
);
[...skillSubAppsMap].forEach(([id, item]) => {
subAppsMap.set(id, item);
});
addLog.debug(`成功恢复 skill tools`);
}
} catch (error) {
addLog.error(`恢复 skill tools 失败:`, error);
}
}
/* ===== Master agent, 逐步执行 plan ===== */
if (!agentPlan) return Promise.reject('没有 plan');
@ -470,10 +464,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
...props,
getSubAppInfo,
steps: agentPlan.steps, // 传入所有步骤,而不仅仅是未执行的步骤
completionTools,
completionTools: agentCompletionTools,
step,
filesMap,
subAppsMap
subAppsMap: agentSubAppsMap
});
// Merge response
@ -513,7 +507,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
[agentPlanKey]: agentPlan,
[planMessagesKey]: undefined,
[replanMessagesKey]: undefined,
[skillMatchKey]: currentSkillId
[skillMatchKey]: matchedSkillId
},
[DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: {

View File

@ -260,22 +260,26 @@ export const stepCall = async ({
usages
};
} else if (tool.type === 'workflow' || tool.type === 'toolWorkflow') {
const fn = tool.type === 'workflow' ? dispatchApp : dispatchPlugin;
// const fn = tool.type === 'workflow' ? dispatchApp : dispatchPlugin;
const { response, usages } = await fn({
...props,
node,
workflowStreamResponse: childWorkflowStreamResponse,
callParams: {
appId: node.pluginId,
version: node.version,
...requestParams
}
});
// const { response, usages } = await fn({
// ...props,
// node,
// workflowStreamResponse: childWorkflowStreamResponse,
// callParams: {
// appId: node.pluginId,
// version: node.version,
// ...requestParams
// }
// });
// return {
// response,
// usages
// };
return {
response,
usages
response: 'Can not find the tool',
usages: []
};
} else {
return {

View File

@ -5,73 +5,11 @@ import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/gl
import { getLLMModel } from '../../../../ai/model';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { agentSkillToToolRuntime } from './sub/tool/utils';
import type { SubAppRuntimeType } from './type';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { getSubapps } from './utils';
import { addLog } from '../../../../../common/system/log';
/**
* Skill Tools
* MatcherService.ts match
*/
export const buildSkillTools = (skills: AiSkillSchemaType[]) => {
const skillCompletionTools: ChatCompletionTool[] = [];
const skillsMap: Record<string, AiSkillSchemaType> = {};
for (const skill of skills) {
// 生成唯一函数名
const functionName = getNanoid(6);
skill.name = functionName;
skillsMap[functionName] = skill;
if (skill.description) {
skillCompletionTools.push({
type: 'function',
function: {
name: functionName,
description: skill.description,
parameters: {
type: 'object',
properties: {},
required: []
}
}
});
}
}
return { skillCompletionTools, skillsMap };
};
/**
* Skill SystemPrompt
* skill XML
*/
export const formatSkillAsSystemPrompt = (skill: AiSkillSchemaType): string => {
const lines = ['<reference_skill>', `**参考技能**: ${skill.name}`, ''];
if (skill.description) {
lines.push(`**描述**: ${skill.description}`, '');
}
if (skill.steps && skill.steps.trim()) {
lines.push(`**步骤信息**:`, skill.steps, '');
}
lines.push(
'**说明**:',
'1. 以上是用户之前保存的类似任务的执行框架',
'2. 请参考该技能的宏观阶段划分和资源方向',
'3. 根据当前用户的具体需求,调整和优化框架',
'4. 保持阶段的逻辑性和方向的清晰性',
'',
'</reference_skill>'
);
return lines.join('\n');
};
/**
*
* MatcherService.ts match
@ -107,6 +45,67 @@ export const matchSkillForPlan = async ({
subAppsMap: Map<string, SubAppRuntimeType>;
}
> => {
/**
* Skill Tools
* MatcherService.ts match
*/
const buildSkillTools = (skills: AiSkillSchemaType[]) => {
const skillCompletionTools: ChatCompletionTool[] = [];
const skillsMap: Record<string, AiSkillSchemaType> = {};
for (const skill of skills) {
// 生成唯一函数名
const functionName = getNanoid(6);
skill.name = functionName;
skillsMap[functionName] = skill;
if (skill.description) {
skillCompletionTools.push({
type: 'function',
function: {
name: functionName,
description: skill.description,
parameters: {
type: 'object',
properties: {},
required: []
}
}
});
}
}
return { skillCompletionTools, skillsMap };
};
/**
* Skill SystemPrompt
* skill XML
*/
const formatSkillAsSystemPrompt = (skill: AiSkillSchemaType): string => {
const lines = ['<reference_skill>', `**参考技能**: ${skill.name}`, ''];
if (skill.description) {
lines.push(`**描述**: ${skill.description}`, '');
}
if (skill.steps && skill.steps.trim()) {
lines.push(`**步骤信息**:`, skill.steps, '');
}
lines.push(
'**说明**:',
'1. 以上是用户之前保存的类似任务的执行框架',
'2. 请参考该技能的宏观阶段划分和资源方向',
'3. 根据当前用户的具体需求,调整和优化框架',
'4. 保持阶段的逻辑性和方向的清晰性',
'',
'</reference_skill>'
);
return lines.join('\n');
};
addLog.debug('matchSkillForPlan start');
const modelData = getLLMModel(model);
@ -224,3 +223,26 @@ export const matchSkillForPlan = async ({
};
}
};
export const matchSkillForId = async ({
id,
tmbId,
lang
}: {
id: string;
tmbId: string;
lang?: localeType;
}) => {
const skill = await MongoAiSkill.findById(id).lean();
if (!skill || !skill.tools) return;
const { completionTools: skillTools, subAppsMap: skillSubAppsMap } = await getSubapps({
tools: skill.tools,
tmbId,
lang
});
return {
skillTools,
skillSubAppsMap
};
};

View File

@ -54,3 +54,15 @@ const MyIconButton = ({
};
export default MyIconButton;
export const MyDeleteIconButton = ({ onClick, ...props }: Omit<Props, 'icon'>) => {
return (
<MyIconButton
hoverBg="red.50"
hoverColor="red.600"
onClick={onClick}
{...props}
icon={'delete'}
/>
);
};

View File

@ -451,7 +451,6 @@
"core.dataset.My Dataset": "My Dataset",
"core.dataset.Query extension intro": "Enabling the question optimization function can improve the accuracy of Dataset searches during continuous conversations. After enabling this function, when performing Dataset searches, the AI will complete the missing information of the question based on the conversation history.",
"core.dataset.Quote Length": "Quote Content Length",
"core.dataset.Read Dataset": "View Dataset Details",
"core.dataset.Set Website Config": "Start Configuring",
"core.dataset.Start export": "Export Started",
"core.dataset.Text collection": "Text Dataset",

View File

@ -454,7 +454,6 @@
"core.dataset.My Dataset": "我的知识库",
"core.dataset.Query extension intro": "开启问题优化功能,可以提高提高连续对话时,知识库搜索的精度。开启该功能后,在进行知识库搜索时,会根据对话记录,利用 AI 补全问题缺失的信息。",
"core.dataset.Quote Length": "引用内容长度",
"core.dataset.Read Dataset": "查看知识库详情",
"core.dataset.Set Website Config": "开始配置",
"core.dataset.Start export": "已开始导出",
"core.dataset.Text collection": "文本数据集",

View File

@ -450,7 +450,6 @@
"core.dataset.My Dataset": "我的知識庫",
"core.dataset.Query extension intro": "開啟問題最佳化功能,可以提高連續對話時知識庫搜尋的準確度。開啟此功能後,在進行知識庫搜尋時,系統會根據對話記錄,利用 AI 補充問題中缺少的資訊。",
"core.dataset.Quote Length": "引用內容長度",
"core.dataset.Read Dataset": "檢視知識庫詳細資料",
"core.dataset.Set Website Config": "開始設定",
"core.dataset.Start export": "已開始匯出",
"core.dataset.Text collection": "文字資料集",

View File

@ -10,9 +10,13 @@ import type {
SkillAgentParamsType
} from '@fastgpt/global/core/chat/helperBot/skillAgent/type';
export type HelperBotRefType = {
restartChat: () => void;
};
export type HelperBotProps = {
emptyDom?: ReactNode;
fileSelectConfig?: AppFileSelectConfigType;
ChatBoxRef: React.ForwardedRef<HelperBotRefType>;
} & (
| {
type: typeof HelperBotTypeEnum.topAgent;
@ -29,6 +33,7 @@ type HelperBotContextType = HelperBotProps & {};
export const HelperBotContext = createContext<HelperBotContextType>({
type: HelperBotTypeEnum.topAgent,
ChatBoxRef: null,
metadata: {
role: '',
taskObject: '',

View File

@ -1,17 +1,11 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import HelperBotContextProvider, { type HelperBotProps } from './context';
import type {
AIChatItemValueItemType,
ChatItemType,
ChatSiteItemType
} from '@fastgpt/global/core/chat/type';
import HelperBotContextProvider, { type HelperBotRefType, type HelperBotProps } from './context';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import type { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRecords';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import type { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { Box } from '@chakra-ui/react';
import HumanItem from './components/HumanItem';
import AIItem from './components/AIItem';
@ -37,7 +31,7 @@ import { streamFetch } from '@/web/common/api/fetch';
import type { generatingMessageProps } from '../ChatContainer/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotProps) => {
const { toast } = useToast();
const { t } = useTranslation();
@ -59,8 +53,7 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
const requestParams = useMemoEnhance(() => {
return {
chatId,
type,
metadata
type
};
}, []);
@ -368,6 +361,14 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
setIsChatting(false);
});
useImperativeHandle(ChatBoxRef, () => ({
restartChat() {
abortRequest();
setChatRecords([]);
setChatId(getNanoid(12));
}
}));
return (
<MyBox display={'flex'} flexDirection={'column'} h={'100%'} position={'relative'}>
<ScrollData

View File

@ -1,11 +1,14 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSafeState } from 'ahooks';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import type {
AppFormEditFormType,
SelectedToolItemType
} from '@fastgpt/global/core/app/formEdit/type';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../context';
import { useChatTest } from '../../useChatTest';
@ -20,10 +23,10 @@ import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/const
import type { Form2WorkflowFnType } from '../FormComponent/type';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import HelperBot from '@/components/core/chat/HelperBot';
import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { loadGeneratedTools } from './utils';
type Props = {
appForm: AppFormEditFormType;
@ -36,6 +39,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
const { toast } = useToast();
const [activeTab, setActiveTab] = useSafeState<'helper' | 'chat_debug'>('helper');
const HelperBotRef = useRef<HelperBotRefType>(null);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
@ -126,7 +130,11 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
restartChat();
if (activeTab === 'helper') {
HelperBotRef.current?.restartChat();
} else {
restartChat();
}
}}
/>
</MyTooltip>
@ -134,27 +142,14 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
<Box flex={1}>
{activeTab === 'helper' && (
<HelperBot
ChatBoxRef={HelperBotRef}
type={HelperBotTypeEnum.topAgent}
metadata={topAgentMetadata}
onApply={async (formData) => {
const targetToolIds = formData.tools || [];
const newTools: FlowNodeTemplateType[] = [];
const failedToolIds: string[] = [];
const results = await Promise.all(
targetToolIds.map((toolId: string) =>
getToolPreviewNode({ appId: toolId })
.then((tool) => ({ status: 'fulfilled' as const, toolId, tool }))
.catch((error) => ({ status: 'rejected' as const, toolId, error }))
)
);
results.forEach((result) => {
if (result.status === 'fulfilled') {
newTools.push(result.tool);
} else if (result.status === 'rejected') {
failedToolIds.push(result.toolId);
}
const newTools = await loadGeneratedTools({
newToolIds: formData.tools || [],
existsTools: appForm.selectedTools,
fileSelectConfig: appForm.chatConfig.fileSelectConfig
});
setAppForm((prev) => {

View File

@ -32,6 +32,7 @@ import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons';
import { getAiSkillDetail } from '@/web/core/ai/skill/api';
import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@ -298,45 +299,69 @@ const EditForm = ({
similarity={appForm.dataset.similarity}
limit={appForm.dataset.limit}
usingReRank={appForm.dataset.usingReRank}
datasetSearchUsingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
usingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
/>
</Box>
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item.datasetId} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
<Flex
key={item.datasetId}
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={'base'}
_hover={{
'& .controler': {
display: 'flex'
}
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
</Flex>
</MyTooltip>
{item.name}
</Box>
{/* Icon */}
<Box className="controler" display={['flex', 'none']} alignItems={'center'}>
<MyIconButton
icon={'common/viewLight'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
}
/>
<MyDeleteIconButton
onClick={() => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets:
state.dataset.datasets?.filter(
(pre) => pre.datasetId !== item.datasetId
) || []
}
}));
}}
/>
</Box>
</Flex>
))}
</Grid>
</Box>

View File

@ -1,6 +1,6 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { SkillEditType, SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
@ -8,11 +8,8 @@ import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type
import MyBox from '@fastgpt/web/components/common/MyBox';
import HelperBot from '@/components/core/chat/HelperBot';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import {
validateToolConfiguration,
getToolConfigStatus
} from '@fastgpt/global/core/app/formEdit/utils';
import { loadGeneratedTools } from '../utils';
import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context';
type Props = {
topAgentSelectedTools?: SelectedToolItemType[];
@ -22,6 +19,7 @@ type Props = {
};
const ChatTest = ({ topAgentSelectedTools = [], skill, appForm, onAIGenerate }: Props) => {
const { t } = useTranslation();
const ChatBoxRef = useRef<HelperBotRefType>(null);
const skillAgentMetadata = useMemo(() => {
return {
@ -56,85 +54,40 @@ const ChatTest = ({ topAgentSelectedTools = [], skill, appForm, onAIGenerate }:
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
ChatBoxRef.current?.restartChat();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<HelperBot
ChatBoxRef={ChatBoxRef}
type={HelperBotTypeEnum.skillAgent}
metadata={skillAgentMetadata}
onApply={async (generatedSkillData) => {
console.log(generatedSkillData, 222);
// 1. 提取所有步骤中的工具 ID去重仅保留 type='tool'
const allToolIds = new Set<string>();
// 1. 计算新的 tool
const newToolIds: string[] = [];
generatedSkillData.execution_plan.steps.forEach((step) => {
step.expectedTools?.forEach((tool) => {
if (tool.type === 'tool') {
allToolIds.add(tool.id);
const exists = skill.selectedTools.find((t) => t.pluginId === tool.id);
if (exists) return;
newToolIds.push(tool.id);
}
});
});
// 2. 分类工具:已有的和新的
const existingTools: SelectedToolItemType[] = [];
const newToolIds: string[] = [];
Array.from(allToolIds).forEach((toolId) => {
const existingTool = skill.selectedTools.find((t) => t.pluginId === toolId);
if (existingTool) {
existingTools.push(existingTool);
} else {
newToolIds.push(toolId);
}
});
// 3. 并行获取新工具详情
const newTools: SelectedToolItemType[] = [];
const newTools = await loadGeneratedTools({
newToolIds,
existsTools: skill.selectedTools,
topAgentSelectedTools,
fileSelectConfig: appForm.chatConfig.fileSelectConfig
});
if (newToolIds.length > 0) {
const results = await Promise.all(
newToolIds.map((toolId: string) =>
getToolPreviewNode({ appId: toolId })
.then((tool) => ({ status: 'fulfilled' as const, toolId, tool }))
.catch((error) => ({ status: 'rejected' as const, toolId, error }))
)
);
results.forEach((result) => {
if (result.status !== 'fulfilled') return;
const tool = result.tool;
// 验证工具配置
const toolValid = validateToolConfiguration({
toolTemplate: tool,
canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile,
canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg
});
if (toolValid) {
// 添加与 top 相同工具的配置
const topTool = topAgentSelectedTools.find(
(item) => item.pluginId === tool.pluginId
);
if (topTool) {
tool.inputs.forEach((input) => {
const topInput = topTool.inputs.find((topIn) => topIn.key === input.key);
if (topInput) {
input.value = topInput.value;
}
});
}
newTools.push({
...tool,
configStatus: getToolConfigStatus(tool).status
});
}
});
}
// 3. 构建 stepsText保持原有逻辑
// 4. 构建 stepsText
const stepsText = generatedSkillData.execution_plan.steps
.map((step, index) => {
let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`;
@ -148,12 +101,12 @@ const ChatTest = ({ topAgentSelectedTools = [], skill, appForm, onAIGenerate }:
})
.join('\n\n');
// 4. 应用生成的数据,以 AI 生成的工具列表为准
// 5. 应用生成的数据,以 AI 生成的工具列表为准
onAIGenerate({
name: generatedSkillData.plan_analysis.name || skill.name,
description: generatedSkillData.plan_analysis.description || skill.description,
stepsText: stepsText,
selectedTools: [...existingTools, ...newTools]
selectedTools: newTools
});
}}
/>

View File

@ -11,6 +11,7 @@ import {
IconButton,
Textarea
} from '@chakra-ui/react';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config';
import type { SelectedToolItemType, SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import { useRouter } from 'next/router';
@ -265,38 +266,57 @@ const EditForm = ({
</Flex>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item.datasetId} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
<Flex
key={item.datasetId}
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={'base'}
_hover={{
'& .controler': {
display: 'flex'
}
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
</Flex>
</MyTooltip>
{item.name}
</Box>
{/* Icon */}
<Box className="controler" display={['flex', 'none']} alignItems={'center'}>
<MyIconButton
icon={'common/viewLight'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
}
/>
<MyDeleteIconButton
onClick={() => {
setValue(
'dataset.list',
selectDatasets?.filter((pre) => pre.datasetId !== item.datasetId) || [],
{ shouldDirty: true }
);
}}
/>
</Box>
</Flex>
))}
</Grid>
</Box>

View File

@ -1,4 +1,4 @@
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
import type { AppChatConfigType } from '@fastgpt/global/core/app/type';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import type {
@ -28,7 +28,12 @@ import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { Input_Template_File_Link } from '@fastgpt/global/core/workflow/template/input';
import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils';
import {
getToolConfigStatus,
validateToolConfiguration
} from '@fastgpt/global/core/app/formEdit/utils';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config';
/* format app nodes to edit form */
export const appWorkflow2AgentForm = ({
@ -232,3 +237,56 @@ export function agentForm2AppWorkflow(
chatConfig: data.chatConfig
};
}
export const loadGeneratedTools = async ({
newToolIds,
existsTools = [],
topAgentSelectedTools = [],
fileSelectConfig
}: {
newToolIds: string[]; // 新的,完整的 toolId
existsTools?: SelectedToolItemType[];
topAgentSelectedTools?: SelectedToolItemType[];
fileSelectConfig?: AppFileSelectConfigType;
}): Promise<SelectedToolItemType[]> => {
const results = (
await Promise.all(
newToolIds.map<Promise<SelectedToolItemType | undefined>>(async (toolId: string) => {
// 已经存在的工具,直接返回
const existTool = existsTools.find((tool) => tool.pluginId === toolId);
if (existTool) {
return existTool;
}
// 新工具,需要与已配置的 tool 进行 input 合并
const tool = await getToolPreviewNode({ appId: toolId });
// 验证工具配置
const toolValid = validateToolConfiguration({
toolTemplate: tool,
canSelectFile: fileSelectConfig?.canSelectFile,
canSelectImg: fileSelectConfig?.canSelectImg
});
if (!toolValid) {
return;
}
const topTool = topAgentSelectedTools.find((item) => item.pluginId === toolId);
if (topTool) {
tool.inputs.forEach((input) => {
const topInput = topTool.inputs.find((topIn) => topIn.key === input.key);
if (topInput) {
input.value = topInput.value;
}
});
}
return {
...tool,
configStatus: getToolConfigStatus(tool).status
};
})
)
).filter((item) => item !== undefined);
return results;
};

View File

@ -33,6 +33,7 @@ import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@ -289,38 +290,62 @@ const EditForm = ({
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item.datasetId} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
<Flex
key={item.datasetId}
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={'base'}
_hover={{
'& .controler': {
display: 'flex'
}
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
</Flex>
</MyTooltip>
{item.name}
</Box>
{/* Icon */}
<Box className="controler" display={['flex', 'none']} alignItems={'center'}>
<MyIconButton
icon={'common/viewLight'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
}
/>
<MyDeleteIconButton
onClick={() => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets:
state.dataset.datasets?.filter(
(pre) => pre.datasetId !== item.datasetId
) || []
}
}));
}}
/>
</Box>
</Flex>
))}
</Grid>
</Box>

View File

@ -9,13 +9,14 @@ import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoAppChatLog } from '@fastgpt/service/core/app/logs/chatLogsSchema';
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatFileTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
const createMockProps = (
overrides?: Partial<Props>,
@ -41,7 +42,6 @@ const createMockProps = (
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: 'Hello, how are you?'
}
@ -52,7 +52,6 @@ const createMockProps = (
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: 'I am doing well, thank you!'
}
@ -142,9 +141,8 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.file,
file: {
type: 'image',
type: ChatFileTypeEnum.image,
name: 'test.jpg',
url: 'https://example.com/test.jpg',
key: 'file-key-123'
@ -192,7 +190,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Response' }
}
],
@ -224,7 +221,7 @@ describe('pushChatRecords', () => {
});
it('should handle dataset search node with quoteList', async () => {
const quote = {
const quote: SearchDataResponseItemType = {
id: 'quote-1',
chunkIndex: 0,
datasetId: 'dataset-1',
@ -242,7 +239,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Based on the search results...' }
}
],
@ -531,7 +527,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Hello' }
}
]
@ -555,7 +550,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Hello' }
}
]
@ -580,7 +574,6 @@ describe('pushChatRecords', () => {
dataId: 'data-id-1',
value: [
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'userSelect',
params: {
@ -599,7 +592,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Option 1' }
}
]
@ -639,7 +631,6 @@ describe('pushChatRecords', () => {
dataId: 'data-id-1',
value: [
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'userInput',
params: {
@ -664,7 +655,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: JSON.stringify({ username: 'john_doe' }) }
}
]
@ -705,11 +695,9 @@ describe('pushChatRecords', () => {
dataId: 'data-id-1',
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Payment required' }
},
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'paymentPause',
params: {}
@ -729,8 +717,6 @@ describe('pushChatRecords', () => {
});
// PaymentPause is removed, and AI response is appended
expect(chatItem?.value.length).toBeGreaterThan(0);
// The first value should be text, last one should be from AI response
expect(chatItem?.value[0].type).toBe(ChatItemValueTypeEnum.text);
});
it('should merge AI response values', async () => {
@ -744,11 +730,9 @@ describe('pushChatRecords', () => {
dataId: 'data-id-1',
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'First response' }
},
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'userSelect',
params: { options: ['A', 'B'] }
@ -763,7 +747,6 @@ describe('pushChatRecords', () => {
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: { content: 'Second response' }
}
],
@ -797,7 +780,6 @@ describe('pushChatRecords', () => {
durationSeconds: 1.5,
value: [
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'userSelect',
params: { options: ['A', 'B'] }
@ -840,7 +822,6 @@ describe('pushChatRecords', () => {
dataId: 'data-id-1',
value: [
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
type: 'userSelect',
params: { options: ['A', 'B'] }