diff --git a/packages/service/core/app/delete/processor.ts b/packages/service/core/app/delete/processor.ts index 264fcd269..dd17dcf73 100644 --- a/packages/service/core/app/delete/processor.ts +++ b/packages/service/core/app/delete/processor.ts @@ -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) => { diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 2ce569dea..63d150a1c 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -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; + }) => { + // 将 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]: { diff --git a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts index 6f7272b6f..9d00be33f 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts @@ -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 { diff --git a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts index 4f31b7c2c..016124dfe 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts @@ -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 = {}; - - 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 = ['', `**参考技能**: ${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. 保持阶段的逻辑性和方向的清晰性', - '', - '' - ); - - return lines.join('\n'); -}; - /** * 主匹配函数 * 参考 MatcherService.ts 的 match 方法 @@ -107,6 +45,67 @@ export const matchSkillForPlan = async ({ subAppsMap: Map; } > => { + /** + * 构建 Skill Tools 数组 + * 参考 MatcherService.ts 的 match 函数 + */ + const buildSkillTools = (skills: AiSkillSchemaType[]) => { + const skillCompletionTools: ChatCompletionTool[] = []; + const skillsMap: Record = {}; + + 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 = ['', `**参考技能**: ${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. 保持阶段的逻辑性和方向的清晰性', + '', + '' + ); + + 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 + }; +}; diff --git a/packages/web/components/common/Icon/button.tsx b/packages/web/components/common/Icon/button.tsx index 800740f91..de2b22a78 100644 --- a/packages/web/components/common/Icon/button.tsx +++ b/packages/web/components/common/Icon/button.tsx @@ -54,3 +54,15 @@ const MyIconButton = ({ }; export default MyIconButton; + +export const MyDeleteIconButton = ({ onClick, ...props }: Omit) => { + return ( + + ); +}; diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 8543abe0a..4a827d892 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -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", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 56fcaa0a6..7f937c33d 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -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": "文本数据集", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index f78d3678a..3d728212b 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -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": "文字資料集", diff --git a/projects/app/src/components/core/chat/HelperBot/context.tsx b/projects/app/src/components/core/chat/HelperBot/context.tsx index 6f418b41f..b490c1db8 100644 --- a/projects/app/src/components/core/chat/HelperBot/context.tsx +++ b/projects/app/src/components/core/chat/HelperBot/context.tsx @@ -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; } & ( | { type: typeof HelperBotTypeEnum.topAgent; @@ -29,6 +33,7 @@ type HelperBotContextType = HelperBotProps & {}; export const HelperBotContext = createContext({ type: HelperBotTypeEnum.topAgent, + ChatBoxRef: null, metadata: { role: '', taskObject: '', diff --git a/projects/app/src/components/core/chat/HelperBot/index.tsx b/projects/app/src/components/core/chat/HelperBot/index.tsx index 2a0c36900..aa5ed323d 100644 --- a/projects/app/src/components/core/chat/HelperBot/index.tsx +++ b/projects/app/src/components/core/chat/HelperBot/index.tsx @@ -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 ( ('helper'); + const HelperBotRef = useRef(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(); + } }} /> @@ -134,27 +142,14 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props {activeTab === 'helper' && ( { - 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) => { diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx index 6a2e8707e..21e2798a5 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx @@ -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} /> )} {selectDatasets.map((item) => ( - - - router.push({ - pathname: '/dataset/detail', - query: { - datasetId: item.datasetId - } - }) + + + - - - {item.name} - - - + {item.name} + + + {/* Icon */} + + + router.push({ + pathname: '/dataset/detail', + query: { + datasetId: item.datasetId + } + }) + } + /> + { + setAppForm((state) => ({ + ...state, + dataset: { + ...state.dataset, + datasets: + state.dataset.datasets?.filter( + (pre) => pre.datasetId !== item.datasetId + ) || [] + } + })); + }} + /> + + ))} diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx index c3c6ee004..593ec3edc 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx @@ -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(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(); }} /> { console.log(generatedSkillData, 222); - // 1. 提取所有步骤中的工具 ID(去重,仅保留 type='tool') - const allToolIds = new Set(); + // 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 }); }} /> diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx index e2d44a5f5..a6c556f8b 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx @@ -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 = ({ {selectDatasets.map((item) => ( - - - router.push({ - pathname: '/dataset/detail', - query: { - datasetId: item.datasetId - } - }) + + + - - - {item.name} - - - + {item.name} + + + {/* Icon */} + + + router.push({ + pathname: '/dataset/detail', + query: { + datasetId: item.datasetId + } + }) + } + /> + { + setValue( + 'dataset.list', + selectDatasets?.filter((pre) => pre.datasetId !== item.datasetId) || [], + { shouldDirty: true } + ); + }} + /> + + ))} diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts index 9c6488d29..70b545b19 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts @@ -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 => { + const results = ( + await Promise.all( + newToolIds.map>(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; +}; diff --git a/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/EditForm.tsx b/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/EditForm.tsx index 59a2b0713..3f4c1dbc6 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/EditForm.tsx @@ -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 = ({ )} {selectDatasets.map((item) => ( - - - router.push({ - pathname: '/dataset/detail', - query: { - datasetId: item.datasetId - } - }) + + + - - - {item.name} - - - + {item.name} + + + {/* Icon */} + + + router.push({ + pathname: '/dataset/detail', + query: { + datasetId: item.datasetId + } + }) + } + /> + { + setAppForm((state) => ({ + ...state, + dataset: { + ...state.dataset, + datasets: + state.dataset.datasets?.filter( + (pre) => pre.datasetId !== item.datasetId + ) || [] + } + })); + }} + /> + + ))} diff --git a/test/cases/service/core/chat/saveChat.test.ts b/test/cases/service/core/chat/saveChat.test.ts index 37b546bd4..17d5ec7e2 100644 --- a/test/cases/service/core/chat/saveChat.test.ts +++ b/test/cases/service/core/chat/saveChat.test.ts @@ -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, @@ -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'] }