diff --git a/.claude/design/agentv1-detailed.md b/.claude/design/agentv1-detailed.md index 707a1e386..eee58d811 100644 --- a/.claude/design/agentv1-detailed.md +++ b/.claude/design/agentv1-detailed.md @@ -146,7 +146,7 @@ type AgentNodeInputType = { [NodeInputKeyEnum.fileUrlList]?: string[]; // 工具配置 - [NodeInputKeyEnum.subApps]?: FlowNodeTemplateType[]; + [NodeInputKeyEnum.selectedTools]?: FlowNodeTemplateType[]; // 模式配置 [NodeInputKeyEnum.isPlanAgent]?: boolean; @@ -370,7 +370,7 @@ async function executePlanStep(params: { temperature: params.temperature, stream: params.stream, top_p: params.top_p, - subApps: buildSubAppTools(params.toolNodes) + agent_selectedTools: buildSubAppTools(params.toolNodes) }, // 工具调用处理器 @@ -1711,7 +1711,7 @@ describe('Agent End-to-End Flow', () => { systemPrompt: '你是一个智能助手', userChatInput: '帮我查找最新的 AI 新闻并总结', isPlanAgent: true, - subApps: [/* mock sub apps */] + agent_selectedTools: [/* mock sub apps */] }, // ... 其他参数 }); @@ -1746,7 +1746,7 @@ describe('Agent End-to-End Flow', () => { userChatInput: '帮我制定旅行计划', isPlanAgent: true, isAskAgent: true, - subApps: [] + agent_selectedTools: [] }, // ... }); @@ -1780,7 +1780,7 @@ describe('Agent Performance', () => { model: 'gpt-4', userChatInput: '执行一个包含 5 个步骤的复杂任务', isPlanAgent: true, - subApps: [/* 5 个 sub apps */] + agent_selectedTools: [/* 5 个 sub apps */] }, // ... }); diff --git a/packages/global/core/app/formEdit/utils.ts b/packages/global/core/app/formEdit/utils.ts new file mode 100644 index 000000000..7b07b3cab --- /dev/null +++ b/packages/global/core/app/formEdit/utils.ts @@ -0,0 +1,135 @@ +import { NodeInputKeyEnum } from '../../workflow/constants'; +import { FlowNodeInputTypeEnum } from '../../workflow/node/constant'; +import type { FlowNodeTemplateType } from '../../workflow/type/node'; + +/* Invalid tool check + 1. Reference type. but not tool description; + 2. Has dataset select + 3. Has dynamic external data +*/ +export const validateToolConfiguration = ({ + toolTemplate, + canSelectFile, + canSelectImg +}: { + toolTemplate: FlowNodeTemplateType; + canSelectFile?: boolean; + canSelectImg?: boolean; +}): boolean => { + // 检查文件上传配置 + const oneFileInput = + toolTemplate.inputs.filter((input) => + input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) + ).length === 1; + + const canUploadFile = canSelectFile || canSelectImg; + + const hasValidFileInput = oneFileInput && !!canUploadFile; + + // 检查是否有无效的输入配置 + const hasInvalidInput = toolTemplate.inputs.some( + (input) => + // 引用类型但没有工具描述 + (input.renderTypeList.length === 1 && + input.renderTypeList[0] === FlowNodeInputTypeEnum.reference && + !input.toolDescription) || + // 包含数据集选择 + input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) || + // 包含动态输入参数 + input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) || + // 文件选择但配置无效 + (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !hasValidFileInput) + ); + + if (hasInvalidInput) { + return false; + } + + return true; +}; + +export const checkNeedsUserConfiguration = (toolTemplate: FlowNodeTemplateType): boolean => { + const formRenderTypesMap: Record = { + [FlowNodeInputTypeEnum.input]: true, + [FlowNodeInputTypeEnum.textarea]: true, + [FlowNodeInputTypeEnum.numberInput]: true, + [FlowNodeInputTypeEnum.password]: true, + [FlowNodeInputTypeEnum.switch]: true, + [FlowNodeInputTypeEnum.select]: true, + [FlowNodeInputTypeEnum.JSONEditor]: true, + [FlowNodeInputTypeEnum.timePointSelect]: true, + [FlowNodeInputTypeEnum.timeRangeSelect]: true + }; + return ( + (toolTemplate.inputs.length > 0 && + toolTemplate.inputs.some((input) => { + // 有工具描述的不需要配置 + if (input.toolDescription) return false; + // 禁用流的不需要配置 + if (input.key === NodeInputKeyEnum.forbidStream) return false; + // 系统输入配置需要配置 + if (input.key === NodeInputKeyEnum.systemInputConfig) return true; + + // 检查是否包含表单类型的输入 + return input.renderTypeList.some((type) => formRenderTypesMap[type]); + })) || + false + ); +}; + +/** + * Get the configuration status of a tool + * Checks if tool needs configuration and whether all required fields are filled + * @param toolTemplate - The tool template to check + * @returns 'active' if tool is ready to use, 'waitingForConfig' if configuration needed + */ +export const getToolConfigStatus = ( + toolTemplate: FlowNodeTemplateType +): { + needConfig: boolean; + status: 'active' | 'waitingForConfig'; +} => { + // Check if tool needs configuration + const needsConfig = checkNeedsUserConfiguration(toolTemplate); + if (!needsConfig) { + return { + needConfig: false, + status: 'active' + }; + } + + // For tools that need config, check if all required fields have values + const formRenderTypesMap: Record = { + [FlowNodeInputTypeEnum.input]: true, + [FlowNodeInputTypeEnum.textarea]: true, + [FlowNodeInputTypeEnum.numberInput]: true, + [FlowNodeInputTypeEnum.password]: true, + [FlowNodeInputTypeEnum.switch]: true, + [FlowNodeInputTypeEnum.select]: true, + [FlowNodeInputTypeEnum.JSONEditor]: true, + [FlowNodeInputTypeEnum.timePointSelect]: true, + [FlowNodeInputTypeEnum.timeRangeSelect]: true + }; + + // Find all inputs that need configuration + const configInputs = toolTemplate.inputs.filter((input) => { + if (input.toolDescription) return false; + if (input.key === NodeInputKeyEnum.forbidStream) return false; + if (input.key === NodeInputKeyEnum.systemInputConfig) return true; + return input.renderTypeList.some((type) => formRenderTypesMap[type]); + }); + + // Check if all required fields are filled + const allConfigured = configInputs.every((input) => { + const value = input.value; + if (value === undefined || value === null || value === '') return false; + if (Array.isArray(value) && value.length === 0) return false; + if (typeof value === 'object' && Object.keys(value).length === 0) return false; + return true; + }); + + return { + needConfig: !allConfigured, + status: allConfigured ? 'active' : 'waitingForConfig' + }; +}; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index cfa0ca4a3..efb195824 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -171,7 +171,7 @@ export enum NodeInputKeyEnum { aiTaskObject = 'aiTaskObject', // agent - subApps = 'subApps', + selectedTools = 'agent_selectedTools', skills = 'skills', isAskAgent = 'isAskAgent', isPlanAgent = 'isPlanAgent', diff --git a/packages/global/core/workflow/template/system/agent/index.ts b/packages/global/core/workflow/template/system/agent/index.ts index 6d125a51e..a0b7a581d 100644 --- a/packages/global/core/workflow/template/system/agent/index.ts +++ b/packages/global/core/workflow/template/system/agent/index.ts @@ -44,7 +44,7 @@ export const AgentNode: FlowNodeTemplateType = { Input_Template_System_Prompt, Input_Template_History, { - key: NodeInputKeyEnum.subApps, + key: NodeInputKeyEnum.selectedTools, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', valueType: WorkflowIOValueTypeEnum.object diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index a48728cb2..ad22ecf41 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -10,6 +10,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import type { localeType } from '@fastgpt/global/common/i18n/type'; +import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type'; export async function listAppDatasetDataByTeamIdAndDatasetIds({ teamId, @@ -46,76 +47,144 @@ export async function rewriteAppWorkflowToDetail({ }) { const datasetIdSet = new Set(); + const loadToolNode = async ({ id, versionId }: { id: string; versionId?: string }) => { + const { source, pluginId } = splitCombineToolId(id); + + try { + const [preview] = await Promise.all([ + getChildAppPreviewNode({ + appId: id, + versionId, + lang + }), + ...(source === AppToolSourceEnum.personal + ? [ + authAppByTmbId({ + tmbId: ownerTmbId, + appId: pluginId, + per: ReadPermissionVal + }) + ] + : []) + ]); + + return { + success: true, + data: preview + }; + } catch (error) { + return { + success: false, + error: getErrText(error) + }; + } + }; + /* Add node(App Type) versionlabel and latest sign ==== */ await Promise.all( nodes.map(async (node) => { - if (!node.pluginId) return; - const { source, pluginId } = splitCombineToolId(node.pluginId); + // Tool node + if (node.pluginId) { + const result = await loadToolNode({ id: node.pluginId, versionId: node.version }); + if (result.success) { + const preview = result.data!; + node.pluginData = { + name: preview.name, + avatar: preview.avatar, + status: preview.status, + diagram: preview.diagram, + userGuide: preview.userGuide, + courseUrl: preview.courseUrl + }; + node.versionLabel = preview.versionLabel; + node.isLatestVersion = preview.isLatestVersion; + node.version = preview.version; - try { - const [preview] = await Promise.all([ - getChildAppPreviewNode({ - appId: node.pluginId, - versionId: node.version, - lang - }), - ...(source === AppToolSourceEnum.personal - ? [ - authAppByTmbId({ - tmbId: ownerTmbId, - appId: pluginId, - per: ReadPermissionVal - }) - ] - : []) - ]); + node.currentCost = preview.currentCost; + node.systemKeyCost = preview.systemKeyCost; + node.hasTokenFee = preview.hasTokenFee; + node.hasSystemSecret = preview.hasSystemSecret; + node.isFolder = preview.isFolder; - node.pluginData = { - name: preview.name, - avatar: preview.avatar, - status: preview.status, - diagram: preview.diagram, - userGuide: preview.userGuide, - courseUrl: preview.courseUrl - }; - node.versionLabel = preview.versionLabel; - node.isLatestVersion = preview.isLatestVersion; - node.version = preview.version; + node.toolConfig = preview.toolConfig; + node.toolDescription = preview.toolDescription; - node.currentCost = preview.currentCost; - node.systemKeyCost = preview.systemKeyCost; - node.hasTokenFee = preview.hasTokenFee; - node.hasSystemSecret = preview.hasSystemSecret; - node.isFolder = preview.isFolder; + // Latest version + if (!node.version) { + const inputsMap = new Map(node.inputs.map((item) => [item.key, item])); + const outputsMap = new Map(node.outputs.map((item) => [item.key, item])); - node.toolConfig = preview.toolConfig; - node.toolDescription = preview.toolDescription; - - // Latest version - if (!node.version) { - const inputsMap = new Map(node.inputs.map((item) => [item.key, item])); - const outputsMap = new Map(node.outputs.map((item) => [item.key, item])); - - node.inputs = preview.inputs.map((item) => { - const input = inputsMap.get(item.key); - return { - ...item, - value: input?.value, - selectedTypeIndex: input?.selectedTypeIndex - }; - }); - node.outputs = preview.outputs.map((item) => { - const output = outputsMap.get(item.key); - return { - ...item, - value: output?.value - }; - }); + node.inputs = preview.inputs.map((item) => { + const input = inputsMap.get(item.key); + return { + ...item, + value: input?.value, + selectedTypeIndex: input?.selectedTypeIndex + }; + }); + node.outputs = preview.outputs.map((item) => { + const output = outputsMap.get(item.key); + return { + ...item, + value: output?.value + }; + }); + } + } else { + node.pluginData = { + error: result.error + }; } - } catch (error) { - node.pluginData = { - error: getErrText(error) - }; + } + // Agent, parse subapp + if (node.flowNodeType === FlowNodeTypeEnum.agent) { + const tools = (node.inputs.find((item) => item.key === NodeInputKeyEnum.selectedTools) + ?.value || []) as SkillToolType[]; + const nodes = await Promise.all( + tools.map(async (tool) => { + const result = await loadToolNode({ id: tool.id }); + if (result.success) { + const data = result.data!; + // Merge saved config back into inputs + const mergedInputs = data.inputs.map((input) => ({ + ...input, + value: + tool.config && tool.config[input.key] !== undefined + ? tool.config[input.key] // Use saved config value + : input.value // Keep default value + })); + + return { + ...data, + inputs: mergedInputs + }; + } else { + return { + id: tool.id, + templateType: 'personalTool' as const, + flowNodeType: FlowNodeTypeEnum.tool, + name: 'Invalid', + avatar: '', + intro: '', + showStatus: false, + weight: 0, + isTool: true, + version: 'v1', + inputs: [], + outputs: [], + configStatus: 'invalid' as const, + pluginData: { + error: result.error + } + }; + } + }) + ); + node.inputs.forEach((input) => { + if (input.key === NodeInputKeyEnum.selectedTools) { + input.value = nodes; + } + }); } }) ); diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index e0d8abba3..a00d4326a 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -7,8 +7,7 @@ import { } from '@fastgpt/global/core/workflow/runtime/constants'; import type { DispatchNodeResultType, - ModuleDispatchProps, - RuntimeNodeItemType + ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { getLLMModel } from '../../../../ai/model'; import { getNodeErrResponse, getHistories } from '../../utils'; @@ -19,24 +18,22 @@ import { chatValue2RuntimePrompt, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; -import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils'; import { filterMemoryMessages } from '../utils'; import { systemSubInfo } from './sub/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { dispatchPlanAgent, dispatchReplanAgent } from './sub/plan'; -import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; -import { getSubApps, rewriteSubAppsToolset } from './sub'; -import { getFileInputPrompt } from './sub/file/utils'; -import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; +import { getFileInputPrompt, readFileTool } 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 type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { addLog } from '../../../../../common/system/log'; -import { checkTaskComplexity } from './master/taskComplexity'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; import { 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 { getSubapps } from './utils'; export type DispatchAgentModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.history]?: ChatItemType[]; @@ -48,7 +45,7 @@ export type DispatchAgentModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.aiChatTemperature]?: number; [NodeInputKeyEnum.aiChatTopP]?: number; - [NodeInputKeyEnum.subApps]?: FlowNodeTemplateType[]; + [NodeInputKeyEnum.selectedTools]?: SkillToolType[]; [NodeInputKeyEnum.isAskAgent]?: boolean; [NodeInputKeyEnum.isPlanAgent]?: boolean; }>; @@ -82,7 +79,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise fileUrlList: fileLinks, temperature, aiChatTopP, - subApps = [], + agent_selectedTools: selectedTools = [], isPlanAgent = true, isAskAgent = true } @@ -135,11 +132,22 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }); // Get sub apps - const { subAppList, subAppsMap, getSubAppInfo } = await useSubApps({ - subApps, + let { completionTools, subAppsMap } = await getSubapps({ + tools: selectedTools, + tmbId: runningAppInfo.tmbId, lang, filesMap }); + const getSubAppInfo = (id: string) => { + const toolNode = subAppsMap.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'); /* ===== AI Start ===== */ @@ -164,7 +172,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise if (taskIsComplexity) { /* ===== Plan Agent ===== */ - const planCallFn = async (referencePlanSystemPrompt?: string) => { + const planCallFn = async (skillSystemPrompt?: string) => { // 点了确认。此时肯定有 agentPlans if ( lastInteractive?.type === 'agentPlanCheck' && @@ -178,9 +186,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise historyMessages: planHistoryMessages || historiesMessages, userInput: lastInteractive ? interactiveInput : userChatInput, interactive: lastInteractive, - subAppList, + completionTools, getSubAppInfo, - systemPrompt: referencePlanSystemPrompt || systemPrompt, + // TODO: 需要区分?systemprompt 需要替换成 role 和 target 么? + systemPrompt: skillSystemPrompt || systemPrompt, model, temperature, top_p: aiChatTopP, @@ -269,7 +278,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise userInput: lastInteractive ? interactiveInput : userChatInput, plan, interactive: lastInteractive, - subAppList, + completionTools, getSubAppInfo, systemPrompt, model, @@ -351,24 +360,39 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise // Replan step: 已有 plan,且有 replan 历史消息 const isReplanStep = isPlanAgent && agentPlan && replanMessages; - // 🆕 执行 Skill 匹配(仅在 isPlanStep 且没有 planHistoryMessages 时) - let matchedSkillSystemPrompt: string | undefined; - console.log('planHistoryMessages', planHistoryMessages); // 执行 Plan/replan if (isPlanStep) { + // 🆕 执行 Skill 匹配(仅在 isPlanStep 且没有 planHistoryMessages 时) + let skillSystemPrompt: string | undefined; // match skill - addLog.debug('尝试匹配用户的历史 skills'); const matchResult = await matchSkillForPlan({ teamId: runningUserInfo.teamId, + tmbId: runningAppInfo.tmbId, appId: runningAppInfo.id, userInput: lastInteractive ? interactiveInput : userChatInput, messages: historiesMessages, // 传入完整的对话历史 - model + model, + lang }); - if (matchResult.matched && matchResult.systemPrompt) { - addLog.debug(`匹配到 skill: ${matchResult.skill?.name}`); - matchedSkillSystemPrompt = matchResult.systemPrompt; + + if (matchResult.matched) { + skillSystemPrompt = matchResult.systemPrompt; + + // 将 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?.({ @@ -381,7 +405,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise addLog.debug(`未匹配到 skill,原因: ${matchResult.reason}`); } - const result = await planCallFn(matchedSkillSystemPrompt); + const result = await planCallFn(skillSystemPrompt); // 有 result 代表 plan 有交互响应(check/ask) if (result) return result; } else if (isReplanStep) { @@ -410,7 +434,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...props, getSubAppInfo, steps: agentPlan.steps, // 传入所有步骤,而不仅仅是未执行的步骤 - subAppList, + completionTools, step, filesMap, subAppsMap @@ -475,57 +499,3 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise return getNodeErrResponse({ error }); } }; - -export const useSubApps = async ({ - subApps, - lang, - filesMap -}: { - subApps: FlowNodeTemplateType[]; - lang?: localeType; - filesMap: Record; -}) => { - // Get sub apps - const runtimeSubApps = await rewriteSubAppsToolset({ - subApps: subApps.map((node) => { - return { - nodeId: node.id, - name: node.name, - avatar: node.avatar, - intro: node.intro, - toolDescription: node.toolDescription, - flowNodeType: node.flowNodeType, - showStatus: node.showStatus, - isEntry: false, - inputs: node.inputs, - outputs: node.outputs, - pluginId: node.pluginId, - version: node.version, - toolConfig: node.toolConfig, - catchError: node.catchError - }; - }), - lang - }); - - const subAppList = getSubApps({ - subApps: runtimeSubApps, - addReadFileTool: Object.keys(filesMap).length > 0 - }); - - const subAppsMap = new Map(runtimeSubApps.map((item) => [item.nodeId, item])); - const getSubAppInfo = (id: string) => { - const toolNode = subAppsMap.get(id) || systemSubInfo[id]; - return { - name: toolNode?.name || '', - avatar: toolNode?.avatar || '', - toolDescription: toolNode?.toolDescription || toolNode?.name || '' - }; - }; - - return { - subAppList, - subAppsMap, - getSubAppInfo - }; -}; 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 888a131ee..3f99d04a3 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts @@ -4,7 +4,7 @@ import { chats2GPTMessages, runtimePrompt2ChatsValue } from '@fastgpt/global/cor import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { addFilePrompt2Input } from '../sub/file/utils'; import type { AgentPlanStepType } from '../sub/plan/type'; -import type { GetSubAppInfoFnType } from '../type'; +import type { GetSubAppInfoFnType, SubAppRuntimeType } from '../type'; import { getMasterAgentSystemPrompt } from '../constants'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -30,7 +30,7 @@ import { getResponseSummary } from './responseSummary'; export const stepCall = async ({ getSubAppInfo, - subAppList, + completionTools, steps, step, filesMap, @@ -38,11 +38,11 @@ export const stepCall = async ({ ...props }: DispatchAgentModuleProps & { getSubAppInfo: GetSubAppInfoFnType; - subAppList: ChatCompletionTool[]; + completionTools: ChatCompletionTool[]; steps: AgentPlanStepType[]; step: AgentPlanStepType; filesMap: Record; - subAppsMap: Map; + subAppsMap: Map; }) => { const { res, @@ -107,7 +107,7 @@ export const stepCall = async ({ }); // console.log( // 'Step call requestMessages', - // JSON.stringify({ requestMessages, subAppList }, null, 2) + // JSON.stringify({ requestMessages, completionTools }, null, 2) // ); const { assistantMessages, inputTokens, outputTokens, subAppUsages, interactiveResponse } = @@ -119,7 +119,7 @@ export const stepCall = async ({ temperature, stream, top_p: aiChatTopP, - tools: subAppList + tools: completionTools }, userKey: externalProvider.openaiAccount, @@ -219,8 +219,8 @@ export const stepCall = async ({ } // User Sub App else { - const node = subAppsMap.get(toolId); - if (!node) { + const tool = subAppsMap.get(toolId); + if (!tool) { return { response: 'Can not find the tool', usages: [] @@ -237,47 +237,18 @@ export const stepCall = async ({ } // Get params - const requestParams = (() => { - const params: Record = toolCallParams; + const requestParams = { + ...tool.params, + ...toolCallParams + }; - node.inputs.forEach((input) => { - if (input.key in toolCallParams) { - return; - } - // Skip some special key - if ( - [ - NodeInputKeyEnum.childrenNodeIdList, - NodeInputKeyEnum.systemInputConfig - ].includes(input.key as NodeInputKeyEnum) - ) { - params[input.key] = input.value; - return; - } - - // replace {{$xx.xx$}} and {{xx}} variables - let value = replaceEditorVariable({ - text: input.value, - nodes: runtimeNodes, - variables - }); - - // replace reference variables - value = getReferenceVariableValue({ - value, - nodes: runtimeNodes, - variables - }); - - params[input.key] = valueTypeFormat(value, input.valueType); - }); - - return params; - })(); - - if (node.flowNodeType === FlowNodeTypeEnum.tool) { + if (tool.type === 'tool') { const { response, usages } = await dispatchTool({ - node, + tool: { + name: tool.name, + version: tool.version, + toolConfig: tool.toolConfig + }, params: requestParams, runningUserInfo, runningAppInfo, @@ -288,12 +259,8 @@ export const stepCall = async ({ response, usages }; - } else if ( - node.flowNodeType === FlowNodeTypeEnum.appModule || - node.flowNodeType === FlowNodeTypeEnum.pluginModule - ) { - const fn = - node.flowNodeType === FlowNodeTypeEnum.appModule ? dispatchApp : dispatchPlugin; + } else if (tool.type === 'workflow' || tool.type === 'toolWorkflow') { + const fn = tool.type === 'workflow' ? dispatchApp : dispatchPlugin; const { response, usages } = await fn({ ...props, diff --git a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts index f928694ff..6b321b01d 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts @@ -3,64 +3,45 @@ import type { AiSkillSchemaType } from '@fastgpt/global/core/ai/skill/type'; import { createLLMResponse } from '../../../../ai/llm/request'; import type { ChatCompletionMessageParam, ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import { getLLMModel } from '../../../../ai/model'; - -/** - * 生成唯一函数名 - * 参考 MatcherService.ts 的 _generateUniqueFunctionName - */ -const generateUniqueFunctionName = (skill: AiSkillSchemaType): string => { - let baseName = skill.name || skill._id.toString(); - - // 清理名称 - let cleanName = baseName.replace(/[^a-zA-Z0-9_]/g, '_'); - - if (cleanName && !/^[a-zA-Z_]/.test(cleanName)) { - cleanName = 'skill_' + cleanName; - } else if (!cleanName) { - cleanName = 'skill_unknown'; - } - - const timestampSuffix = Date.now().toString().slice(-6); - // return `${cleanName}_${timestampSuffix}`; - return `${cleanName}`; -}; +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[] -): { - tools: ChatCompletionTool[]; - skillsMap: Record; -} => { - const tools: ChatCompletionTool[] = []; +export const buildSkillTools = (skills: AiSkillSchemaType[]) => { + const skillCompletionTools: ChatCompletionTool[] = []; const skillsMap: Record = {}; for (const skill of skills) { // 生成唯一函数名 - const functionName = generateUniqueFunctionName(skill); + const functionName = getNanoid(6); + skill.name = functionName; skillsMap[functionName] = skill; - // 构建 description - let description = skill.description || 'No description available'; - - tools.push({ - type: 'function', - function: { - name: functionName, - description: description, - parameters: { - type: 'object', - properties: {}, - required: [] + if (skill.description) { + skillCompletionTools.push({ + type: 'function', + function: { + name: functionName, + description: skill.description, + parameters: { + type: 'object', + properties: {}, + required: [] + } } - } - }); + }); + } } - return { tools, skillsMap }; + return { skillCompletionTools, skillsMap }; }; /** @@ -100,19 +81,35 @@ export const matchSkillForPlan = async ({ appId, userInput, messages, - model + model, + tmbId, + lang }: { teamId: string; appId: string; userInput: string; messages?: ChatCompletionMessageParam[]; model: string; -}): Promise<{ - matched: boolean; - skill?: AiSkillSchemaType; - systemPrompt?: string; - reason?: string; -}> => { + + tmbId: string; + lang?: localeType; +}): Promise< + | { + matched: false; + reason: string; + } + | { + matched: true; + reason?: string; + skill: AiSkillSchemaType; + systemPrompt: string; + completionTools: ChatCompletionTool[]; + subAppsMap: Map; + } +> => { + addLog.debug('matchSkillForPlan start'); + const modelData = getLLMModel(model); + try { const skills = await MongoAiSkill.find({ teamId, @@ -126,11 +123,9 @@ export const matchSkillForPlan = async ({ return { matched: false, reason: 'No skills available' }; } - const { tools, skillsMap } = buildSkillTools(skills); + const { skillCompletionTools, skillsMap } = buildSkillTools(skills); - console.debug('tools', tools); - - const modelData = getLLMModel(model); + console.debug('skill tools', skillCompletionTools); // 4. 调用 LLM Tool Calling 进行匹配 // 构建系统提示词,指导 LLM 选择相似的任务 @@ -178,7 +173,7 @@ export const matchSkillForPlan = async ({ body: { model: modelData.model, messages: allMessages, - tools, + tools: skillCompletionTools, tool_choice: 'auto', toolCallMode: modelData.toolChoice ? 'toolChoice' : 'prompt', stream: false @@ -205,10 +200,19 @@ export const matchSkillForPlan = async ({ const matchedSkill = skillsMap[functionName]; const systemPrompt = formatSkillAsSystemPrompt(matchedSkill); + // Get tools + const { completionTools, subAppsMap } = await getSubapps({ + tools: matchedSkill.tools, + tmbId, + lang + }); + return { matched: true, skill: matchedSkill, - systemPrompt + systemPrompt, + completionTools, + subAppsMap }; } } @@ -221,7 +225,7 @@ export const matchSkillForPlan = async ({ console.error('Error during skill matching:', error); return { matched: false, - reason: error.message || 'Unknown error' + reason: getErrText(error) }; } }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/index.ts index 5a2cd7ae6..54c436729 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/index.ts @@ -15,130 +15,3 @@ import { MongoApp } from '../../../../../app/schema'; import { getMCPChildren } from '../../../../../app/mcp'; import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/utils'; import type { localeType } from '@fastgpt/global/common/i18n/type'; - -export const rewriteSubAppsToolset = ({ - subApps, - lang -}: { - subApps: RuntimeNodeItemType[]; - lang?: localeType; -}) => { - return Promise.all( - subApps.map(async (node) => { - if (node.flowNodeType === FlowNodeTypeEnum.toolSet) { - const systemToolId = node.toolConfig?.systemToolSet?.toolId; - const mcpToolsetVal = node.toolConfig?.mcpToolSet ?? node.inputs[0].value; - if (systemToolId) { - const children = await getSystemToolRunTimeNodeFromSystemToolset({ - toolSetNode: node, - lang - }); - return children; - } else if (mcpToolsetVal) { - const app = await MongoApp.findOne({ _id: node.pluginId }).lean(); - if (!app) return []; - const toolList = await getMCPChildren(app); - - const parentId = mcpToolsetVal.toolId ?? node.pluginId; - const children = toolList.map((tool, index) => { - const newToolNode = getMCPToolRuntimeNode({ - avatar: node.avatar, - tool, - // New ?? Old - parentId - }); - newToolNode.nodeId = `${parentId}${index}`; // ID 不能随机,否则下次生成时候就和之前的记录对不上 - newToolNode.name = `${node.name}/${tool.name}`; - - return newToolNode; - }); - - return children; - } - return []; - } else { - return [node]; - } - }) - ).then((res) => res.flat()); -}; -export const getSubApps = ({ - subApps, - addReadFileTool -}: { - subApps: RuntimeNodeItemType[]; - addReadFileTool?: boolean; -}): ChatCompletionTool[] => { - // System Tools: Plan Agent, stop sign, model agent. - const systemTools: ChatCompletionTool[] = [ - // PlanAgentTool, - ...(addReadFileTool ? [readFileTool] : []) - // ModelAgentTool - // StopAgentTool, - ]; - - // Node Tools - const unitNodeTools = subApps.filter( - (item, index, array) => array.findIndex((app) => app.pluginId === item.pluginId) === index - ); - - const nodeTools = unitNodeTools.map((item) => { - const toolParams: FlowNodeInputItemType[] = []; - let jsonSchema: JSONSchemaInputType | undefined; - - for (const input of item.inputs) { - if (input.toolDescription) { - toolParams.push(input); - } - - if (input.key === NodeInputKeyEnum.toolData) { - jsonSchema = (input.value as McpToolDataType).inputSchema; - } - } - - const description = JSON.stringify({ - type: item.flowNodeType, - name: item.name, - intro: item.toolDescription || item.intro - }); - - if (jsonSchema) { - return { - type: 'function', - function: { - name: item.nodeId, - description, - parameters: jsonSchema - } - }; - } - - const properties: Record = {}; - toolParams.forEach((param) => { - const jsonSchema = param.valueType - ? valueTypeJsonSchemaMap[param.valueType] || toolValueTypeList[0].jsonSchema - : toolValueTypeList[0].jsonSchema; - - properties[param.key] = { - ...jsonSchema, - description: param.toolDescription || '', - enum: param.enum?.split('\n').filter(Boolean) || undefined - }; - }); - - return { - type: 'function', - function: { - name: item.nodeId, - description, - parameters: { - type: 'object', - properties, - required: toolParams.filter((param) => param.required).map((param) => param.key) - } - } - }; - }); - - return [...systemTools, ...nodeTools]; -}; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts index 5420d0878..0c6c64f90 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts @@ -37,7 +37,7 @@ type DispatchPlanAgentProps = PlanAgentConfig & { referencePlans?: string; isTopPlanAgent: boolean; - subAppList: ChatCompletionTool[]; + completionTools: ChatCompletionTool[]; getSubAppInfo: GetSubAppInfoFnType; }; @@ -53,7 +53,7 @@ export const dispatchPlanAgent = async ({ historyMessages, userInput, interactive, - subAppList, + completionTools, getSubAppInfo, systemPrompt, model, @@ -69,7 +69,7 @@ export const dispatchPlanAgent = async ({ role: 'system', content: getPlanAgentSystemPrompt({ getSubAppInfo, - subAppList + completionTools }) }, ...historyMessages @@ -212,7 +212,7 @@ export const dispatchPlanAgent = async ({ export const dispatchReplanAgent = async ({ historyMessages, interactive, - subAppList, + completionTools, getSubAppInfo, userInput, plan, @@ -234,7 +234,7 @@ export const dispatchReplanAgent = async ({ role: 'system', content: getReplanAgentSystemPrompt({ getSubAppInfo, - subAppList + completionTools }) }, ...historyMessages diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts index c17002911..d2e0a4513 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts @@ -7,12 +7,12 @@ import { parseSystemPrompt } from '../../utils'; const getSubAppPrompt = ({ getSubAppInfo, - subAppList + completionTools }: { getSubAppInfo: GetSubAppInfoFnType; - subAppList: ChatCompletionTool[]; + completionTools: ChatCompletionTool[]; }) => { - return subAppList + return completionTools .map((app) => { const info = getSubAppInfo(app.function.name); if (!info) return ''; @@ -24,12 +24,12 @@ const getSubAppPrompt = ({ export const getPlanAgentSystemPrompt = ({ getSubAppInfo, - subAppList + completionTools }: { getSubAppInfo: GetSubAppInfoFnType; - subAppList: ChatCompletionTool[]; + completionTools: ChatCompletionTool[]; }) => { - const subAppPrompt = getSubAppPrompt({ getSubAppInfo, subAppList }); + const subAppPrompt = getSubAppPrompt({ getSubAppInfo, completionTools }); return ` 你是一个专业的主题计划构建专家,擅长将复杂的主题学习和探索过程转化为结构清晰、可执行的渐进式学习路径。你的规划方法强调: @@ -271,12 +271,12 @@ export const getUserContent = ({ export const getReplanAgentSystemPrompt = ({ getSubAppInfo, - subAppList + completionTools }: { getSubAppInfo: GetSubAppInfoFnType; - subAppList: ChatCompletionTool[]; + completionTools: ChatCompletionTool[]; }) => { - const subAppPrompt = getSubAppPrompt({ getSubAppInfo, subAppList }); + const subAppPrompt = getSubAppPrompt({ getSubAppInfo, completionTools }); return ` 你是一个智能流程优化专家,专门负责在已完成的任务步骤基础上,追加生成优化步骤来完善整个流程,确保任务目标的完美达成。 diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts index 155dc75be..ec43ddc36 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts @@ -24,10 +24,13 @@ type SystemInputConfigType = { type: SystemToolSecretInputTypeEnum; value: StoreSecretValueType; }; -type Props = { - node: RuntimeNodeItemType; +export type Props = { + tool: { + name: string; + version?: string; + toolConfig: RuntimeNodeItemType['toolConfig']; + }; params: { - [NodeInputKeyEnum.toolData]?: McpToolDataType; [NodeInputKeyEnum.systemInputConfig]?: SystemInputConfigType; [key: string]: any; }; @@ -38,8 +41,8 @@ type Props = { }; export const dispatchTool = async ({ - node: { name, version, toolConfig }, - params: { system_input_config, system_toolData, ...params }, + tool: { name, version, toolConfig }, + params: { system_input_config, ...params }, runningUserInfo, runningAppInfo, variables, diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts new file mode 100644 index 000000000..7fd877d98 --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts @@ -0,0 +1,226 @@ +import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; +import type { localeType } from '@fastgpt/global/common/i18n/type'; +import { getChildAppPreviewNode } from '../../../../../../app/tool/controller'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { authAppByTmbId } from '../../../../../../../support/permission/app/auth'; +import { addLog } from '../../../../../../../common/system/log'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { getSystemToolRunTimeNodeFromSystemToolset } from '../../../../../../workflow/utils'; +import { MongoApp } from '../../../../../../app/schema'; +import { getMCPChildren } from '../../../../../../app/mcp'; +import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/utils'; +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; +import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; +import { + NodeInputKeyEnum, + toolValueTypeList, + valueTypeJsonSchemaMap +} from '@fastgpt/global/core/workflow/constants'; +import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; +import type { SubAppInitType } from '../type'; + +export const agentSkillToToolRuntime = async ({ + tools, + tmbId, + lang +}: { + tools: SkillToolType[]; + tmbId: string; + lang?: localeType; +}): Promise => { + const formatSchema = ({ + toolId, + inputs, + flowNodeType, + name, + toolDescription, + intro + }: { + toolId: string; + inputs: FlowNodeInputItemType[]; + flowNodeType: FlowNodeTypeEnum; + name: string; + toolDescription?: string; + intro?: string; + }): ChatCompletionTool => { + const toolParams: FlowNodeInputItemType[] = []; + let jsonSchema: JSONSchemaInputType | undefined; + + for (const input of inputs) { + if (input.toolDescription) { + toolParams.push(input); + } + + if (input.key === NodeInputKeyEnum.toolData) { + jsonSchema = (input.value as McpToolDataType).inputSchema; + } + } + + const description = JSON.stringify({ + type: flowNodeType, + name: name, + intro: toolDescription || intro + }); + + if (jsonSchema) { + return { + type: 'function', + function: { + name: toolId, + description, + parameters: jsonSchema + } + }; + } + + const properties: Record = {}; + toolParams.forEach((param) => { + const jsonSchema = param.valueType + ? valueTypeJsonSchemaMap[param.valueType] || toolValueTypeList[0].jsonSchema + : toolValueTypeList[0].jsonSchema; + + properties[param.key] = { + ...jsonSchema, + description: param.toolDescription || '', + enum: param.enum?.split('\n').filter(Boolean) || undefined + }; + }); + + return { + type: 'function', + function: { + name: toolId, + description, + parameters: { + type: 'object', + properties, + required: toolParams.filter((param) => param.required).map((param) => param.key) + } + } + }; + }; + + return Promise.all( + tools.map>(async (tool) => { + try { + const { source, pluginId } = splitCombineToolId(tool.id); + const [toolNode] = await Promise.all([ + getChildAppPreviewNode({ + appId: pluginId, + lang + }), + ...(source === AppToolSourceEnum.personal + ? [ + authAppByTmbId({ + tmbId, + appId: pluginId, + per: ReadPermissionVal + }) + ] + : []) + ]); + + const removePrefixId = pluginId.replace(`${source}-`, ''); + const requestToolId = `t${removePrefixId}`; + console.log(requestToolId); + + if (toolNode.flowNodeType === FlowNodeTypeEnum.toolSet) { + const systemToolId = toolNode.toolConfig?.systemToolSet?.toolId; + const mcpToolsetVal = toolNode.toolConfig?.mcpToolSet ?? toolNode.inputs[0].value; + if (systemToolId) { + const children = await getSystemToolRunTimeNodeFromSystemToolset({ + toolSetNode: { + toolConfig: toolNode.toolConfig, + inputs: toolNode.inputs, + nodeId: requestToolId + }, + lang + }); + + return children.map((child) => ({ + id: child.nodeId, + name: child.name, + version: child.version, + toolConfig: child.toolConfig, + params: tool.config, + requestSchema: formatSchema({ + toolId: child.nodeId, + inputs: child.inputs, + flowNodeType: child.flowNodeType, + name: child.name, + toolDescription: child.toolDescription, + intro: child.intro + }) + })); + } else if (mcpToolsetVal) { + const app = await MongoApp.findOne({ _id: toolNode.pluginId }).lean(); + if (!app) return []; + const toolList = await getMCPChildren(app); + + const parentId = mcpToolsetVal.toolId ?? toolNode.pluginId; + const children = toolList.map((tool, index) => { + const newToolNode = getMCPToolRuntimeNode({ + avatar: toolNode.avatar, + tool, + // New ?? Old + parentId + }); + newToolNode.nodeId = `${parentId}${index}`; // ID 不能随机,否则下次生成时候就和之前的记录对不上 + newToolNode.name = `${toolNode.name}/${tool.name}`; + + return newToolNode; + }); + + return children.map((child) => { + return { + id: child.nodeId, + name: child.name, + version: child.version, + toolConfig: child.toolConfig, + params: tool.config, + requestSchema: formatSchema({ + toolId: child.nodeId, + inputs: child.inputs, + flowNodeType: child.flowNodeType, + name: child.name, + toolDescription: child.toolDescription, + intro: child.intro + }) + }; + }); + } + + return []; + } else { + return [ + { + id: requestToolId, + name: toolNode.name, + version: toolNode.version, + toolConfig: toolNode.toolConfig, + params: tool.config, + requestSchema: formatSchema({ + toolId: requestToolId, + inputs: toolNode.inputs, + flowNodeType: toolNode.flowNodeType, + name: toolNode.name, + toolDescription: toolNode.toolDescription, + intro: toolNode.intro + }) + } + ]; + } + } catch (error) { + addLog.warn(`[Agent] tool load error`, { + toolId: tool.id, + error: getErrText(error) + }); + return []; + } + }) + ).then((res) => res.flat()); +}; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/type.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/type.ts new file mode 100644 index 000000000..6c0dc9c76 --- /dev/null +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/type.ts @@ -0,0 +1,21 @@ +import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import type { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; +import { NodeToolConfigTypeSchema } from '@fastgpt/global/core/workflow/type/node'; + +export type SubAppInitType = { + id: string; + name: string; + version?: string; + toolConfig?: RuntimeNodeItemType['toolConfig']; + requestSchema: ChatCompletionTool; + params: { + [NodeInputKeyEnum.systemInputConfig]?: { + type: SystemToolSecretInputTypeEnum; + value: StoreSecretValueType; + }; + [key: string]: any; + }; +}; diff --git a/packages/service/core/workflow/dispatch/ai/agent/type.ts b/packages/service/core/workflow/dispatch/ai/agent/type.ts index be7218172..91455b4cf 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/type.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/type.ts @@ -1,6 +1,8 @@ import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; +import z from 'zod'; +import { NodeToolConfigTypeSchema } from '@fastgpt/global/core/workflow/type/node'; export type ToolNodeItemType = RuntimeNodeItemType & { toolParams: RuntimeNodeItemType['inputs']; @@ -12,6 +14,18 @@ export type DispatchSubAppResponse = { usages?: ChatNodeUsageType[]; }; +export const SubAppRuntimeSchema = z.object({ + type: z.enum(['tool', 'file', 'workflow', 'toolWorkflow']), + id: z.string(), + name: z.string(), + avatar: z.string().optional(), + toolDescription: z.string().optional(), + version: z.string().optional(), + toolConfig: NodeToolConfigTypeSchema.optional(), + params: z.record(z.string(), z.any()).optional() +}); +export type SubAppRuntimeType = z.infer; + export type GetSubAppInfoFnType = (id: string) => { name: string; avatar: string; diff --git a/packages/service/core/workflow/dispatch/ai/agent/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/utils.ts index 38458d645..e1d3d4f0c 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/utils.ts @@ -1,3 +1,10 @@ +import type { localeType } from '@fastgpt/global/common/i18n/type'; +import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type'; +import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; +import type { GetSubAppInfoFnType, SubAppRuntimeType } from './type'; +import { agentSkillToToolRuntime } from './sub/tool/utils'; +import { readFileTool } from './sub/file/utils'; + /* 匹配 {{@toolId@}},转化成: @name 的格式。 */ @@ -29,3 +36,50 @@ export const parseSystemPrompt = ({ return processedPrompt; }; + +export const getSubapps = async ({ + tmbId, + tools, + lang, + filesMap = {} +}: { + tmbId: string; + tools: SkillToolType[]; + lang?: localeType; + filesMap?: Record; +}): Promise<{ + completionTools: ChatCompletionTool[]; + subAppsMap: Map; +}> => { + const subAppsMap = new Map(); + const completionTools: ChatCompletionTool[] = []; + + // File + if (Object.keys(filesMap).length > 0) { + completionTools.push(readFileTool); + } + + // Get tools + const formatTools = await agentSkillToToolRuntime({ + tools, + tmbId, + lang + }); + + formatTools.forEach((tool) => { + completionTools.push(tool.requestSchema); + subAppsMap.set(tool.id, { + type: 'tool', + id: tool.id, + name: tool.name, + version: tool.version, + toolConfig: tool.toolConfig, + params: tool.params + }); + }); + + return { + completionTools, + subAppsMap + }; +}; diff --git a/packages/service/core/workflow/utils.ts b/packages/service/core/workflow/utils.ts index 19cb98c8c..0704b6c74 100644 --- a/packages/service/core/workflow/utils.ts +++ b/packages/service/core/workflow/utils.ts @@ -34,7 +34,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ toolSetNode, lang = 'en' }: { - toolSetNode: RuntimeNodeItemType; + toolSetNode: Pick; lang?: localeType; }): Promise { const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId!; diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/Edit.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/Edit.tsx index b0fbc784c..f75e3809a 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/Edit.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/Edit.tsx @@ -12,8 +12,6 @@ import { type SimpleAppSnapshotType } from '../FormComponent/useSnapshots'; import { agentForm2AppWorkflow } from './utils'; import styles from '../FormComponent/styles.module.scss'; import dynamic from 'next/dynamic'; -import { getAiSkillDetail } from '@/web/core/ai/skill/api'; -import { useToast } from '@fastgpt/web/hooks/useToast'; const SkillEditForm = dynamic(() => import('./SkillEdit/EditForm'), { ssr: false }); const SKillChatTest = dynamic(() => import('./SkillEdit/ChatTest'), { ssr: false }); @@ -128,6 +126,7 @@ const Edit = ({ <> import('@/components/core/app/DatasetSelectModal')); const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal')); @@ -111,29 +109,18 @@ const EditForm = ({ if (skill.id) { const detail = await getAiSkillDetail({ id: skill.id }); - // Validate tools and determine their configuration status - const toolsWithStatus = (detail.tools || []) - .filter((tool) => { - // First, validate tool compatibility with current config - const isValid = validateToolConfiguration({ - toolTemplate: tool, - canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile, - canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg - }); - return isValid; - }) - .map((tool) => ({ - ...tool, - configStatus: getToolConfigStatus(tool) - })); - // Merge server data with local data onEditSkill({ id: detail._id, name: detail.name, description: detail.description || '', stepsText: detail.steps, - selectedTools: toolsWithStatus, + selectedTools: (detail.tools || []).map((tool) => { + return { + ...tool, + configStatus: getToolConfigStatus(tool).status + }; + }), dataset: { list: detail.datasets || [] } }); } else { @@ -141,7 +128,7 @@ const EditForm = ({ onEditSkill(skill); } }, - [onEditSkill, appForm.chatConfig.fileSelectConfig] + [onEditSkill] ); return ( @@ -269,7 +256,7 @@ const EditForm = ({ onRemoveTool={(id) => { setAppForm((state) => ({ ...state, - selectedTools: state.selectedTools?.filter((item) => item.id !== id) || [] + selectedTools: state.selectedTools?.filter((item) => item.pluginId !== id) || [] })); }} /> 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 a33084b30..f58a28362 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 @@ -8,16 +8,19 @@ 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 { useToast } from '@fastgpt/web/hooks/useToast'; import { getToolPreviewNode } from '@/web/core/app/api/tool'; -import { validateToolConfiguration, checkNeedsUserConfiguration } from '../utils'; +import { + validateToolConfiguration, + getToolConfigStatus +} from '@fastgpt/global/core/app/formEdit/utils'; type Props = { + topAgentSelectedTools?: SelectedToolItemType[]; skill: SkillEditType; appForm: AppFormEditFormType; onAIGenerate: (updates: Partial) => void; }; -const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => { +const ChatTest = ({ topAgentSelectedTools = [], skill, appForm, onAIGenerate }: Props) => { const { t } = useTranslation(); const skillAgentMetadata = useMemo(() => { @@ -68,7 +71,10 @@ const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => { const allToolIds = new Set(); generatedSkillData.execution_plan.steps.forEach((step) => { step.expectedTools?.forEach((tool) => { - if (tool.type === 'tool') { + if ( + tool.type === 'tool' && + !skill.selectedTools.find((t) => t.pluginId === tool.id) + ) { allToolIds.add(tool.id); } }); @@ -77,7 +83,6 @@ const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => { // 2. 并行获取工具详情 const targetToolIds = Array.from(allToolIds); const newTools: SelectedToolItemType[] = []; - const failedToolIds: string[] = []; if (targetToolIds.length > 0) { const results = await Promise.all( @@ -89,34 +94,35 @@ const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => { ); results.forEach((result) => { - if (result.status === 'fulfilled') { - // 验证工具配置 - const toolValid = validateToolConfiguration({ - toolTemplate: result.tool, - canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile, - canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg - }); + 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) { - // 判断是否需要用户配置,设置 configStatus - const needsConfig = checkNeedsUserConfiguration(result.tool); - newTools.push({ - ...result.tool, - configStatus: needsConfig ? 'waitingForConfig' : 'active' + if (toolValid) { + // 添加与 top 相同工具的配置 + const topTool = topAgentSelectedTools.find( + (item) => item.pluginId === tool.pluginId + ); + if (topTool) { + tool.inputs.forEach((input) => { + const topInput = topTool.inputs.find((input) => input.key === input.key); + if (topInput) { + input.value = topInput.value; + } }); - } else { - // 工具验证失败,记录失败 - failedToolIds.push(result.toolId); } - } else if (result.status === 'rejected') { - failedToolIds.push(result.toolId); + + newTools.push({ + ...tool, + configStatus: getToolConfigStatus(tool).status + }); } }); - - // 可选:提示用户哪些工具获取失败 - if (failedToolIds.length > 0) { - console.warn('部分工具获取失败:', failedToolIds); - } } // 3. 构建 stepsText(保持原有逻辑) 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 41415055b..e2d44a5f5 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 @@ -12,7 +12,7 @@ import { Textarea } from '@chakra-ui/react'; import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config'; -import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type'; +import type { SelectedToolItemType, SkillEditType } from '@fastgpt/global/core/app/formEdit/type'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; @@ -35,6 +35,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); type EditFormProps = { + topAgentSelectedTools: SelectedToolItemType[]; model: string; fileSelectConfig?: AppFileSelectConfigType; skill: SkillEditType; @@ -42,7 +43,14 @@ type EditFormProps = { onSave: (skill: SkillEditType) => void; }; -const EditForm = ({ model, fileSelectConfig, skill, onClose, onSave }: EditFormProps) => { +const EditForm = ({ + topAgentSelectedTools, + model, + fileSelectConfig, + skill, + onClose, + onSave +}: EditFormProps) => { const theme = useTheme(); const router = useRouter(); const { t } = useTranslation(); @@ -211,6 +219,7 @@ const EditForm = ({ model, fileSelectConfig, skill, onClose, onSave }: EditFormP {/* Tool select */} { - setValue('selectedTools', selectedTools?.filter((item) => item.id !== id) || [], { - shouldDirty: true - }); + setValue( + 'selectedTools', + selectedTools?.filter((item) => item.pluginId !== id) || [], + { + shouldDirty: true + } + ); }} /> diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/Row.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/Row.tsx index d79f32cdf..d783fd2e0 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/Row.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/Row.tsx @@ -38,7 +38,7 @@ const Row = ({ const { runAsync: handleEditSkill, loading: isEditingSkill } = useRequest2(onEditSkill, { manual: true }); - const { runAsync: handleDeleteSkill, loading: isDeletingSkill } = useRequest2( + const { runAsync: handleDeleteSkill } = useRequest2( async (skill: SkillEditType) => { await deleteAiSkill({ id: skill.id }); // Remove from local state diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx index 679e30fd9..5d01914a0 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/hooks/useSkillManager.tsx @@ -7,7 +7,10 @@ import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useTranslation } from 'next-i18next'; import { useCallback, useMemo, useState } from 'react'; -import { checkNeedsUserConfiguration, validateToolConfiguration } from '../utils'; +import { + getToolConfigStatus, + validateToolConfiguration +} from '@fastgpt/global/core/app/formEdit/utils'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { FlowNodeInputTypeEnum, @@ -176,11 +179,10 @@ export const useSkillManager = ({ return input; }) }; - const hasFormInput = checkNeedsUserConfiguration(tool); onUpdateOrAddTool({ ...tool, - configStatus: hasFormInput ? 'waitingForConfig' : 'active' + configStatus: getToolConfigStatus(tool).status }); return tool.id; @@ -275,8 +277,8 @@ export const useSkillManager = ({ if (!tool) return; if (isSubApp(tool.flowNodeType)) { - const hasFormInput = checkNeedsUserConfiguration(tool); - if (!hasFormInput) return; + const { needConfig } = getToolConfigStatus(tool); + if (!needConfig) return; setConfigTool(tool); } else { console.log('onClickSkill', id); 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 3cbbcbba7..9c6488d29 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts @@ -28,6 +28,7 @@ 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'; /* format app nodes to edit form */ export const appWorkflow2AgentForm = ({ @@ -52,9 +53,12 @@ export const appWorkflow2AgentForm = ({ defaultAppForm.aiSettings.maxHistories = inputMap.get(NodeInputKeyEnum.history); defaultAppForm.aiSettings.aiChatTopP = inputMap.get(NodeInputKeyEnum.aiChatTopP); - const subApps = inputMap.get(NodeInputKeyEnum.subApps) as FlowNodeTemplateType[]; - if (subApps) { - defaultAppForm.selectedTools = subApps; + const tools = inputMap.get(NodeInputKeyEnum.selectedTools) as FlowNodeTemplateType[]; + if (tools) { + defaultAppForm.selectedTools = tools.map((tool) => ({ + ...tool, + configStatus: getToolConfigStatus(tool).status + })); } } else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) { defaultAppForm.chatConfig = getAppChatConfig({ @@ -183,32 +187,27 @@ export function agentForm2AppWorkflow( value: [workflowStartNodeId, NodeInputKeyEnum.userChatInput] }, { - key: NodeInputKeyEnum.subApps, + key: NodeInputKeyEnum.selectedTools, renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window label: '', valueType: WorkflowIOValueTypeEnum.arrayObject, value: data.selectedTools.map((tool) => ({ - ...tool, - inputs: tool.inputs.map((input) => { - // Special key value - if (input.key === NodeInputKeyEnum.forbidStream) { - input.value = true; - } - // Special tool - if ( - tool.flowNodeType === FlowNodeTypeEnum.appModule && - input.key === NodeInputKeyEnum.history - ) { - return { - ...input, - value: data.aiSettings.maxHistories - }; - } - if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) { - input.value = [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]; - } - return input; - }) + id: tool.pluginId, + + config: tool.inputs.reduce( + (acc, input) => { + // Special tool + if ( + tool.flowNodeType === FlowNodeTypeEnum.appModule && + input.key === NodeInputKeyEnum.history + ) { + acc[input.key] = data.aiSettings.maxHistories; + } + acc[input.key] = input.value; + return acc; + }, + {} as Record + ) })) } ], @@ -233,126 +232,3 @@ export function agentForm2AppWorkflow( chatConfig: data.chatConfig }; } - -/* Invalid tool check - 1. Reference type. but not tool description; - 2. Has dataset select - 3. Has dynamic external data -*/ -export const validateToolConfiguration = ({ - toolTemplate, - canSelectFile, - canSelectImg -}: { - toolTemplate: FlowNodeTemplateType; - canSelectFile?: boolean; - canSelectImg?: boolean; -}): boolean => { - // 检查文件上传配置 - const oneFileInput = - toolTemplate.inputs.filter((input) => - input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) - ).length === 1; - - const canUploadFile = canSelectFile || canSelectImg; - - const hasValidFileInput = oneFileInput && !!canUploadFile; - - // 检查是否有无效的输入配置 - const hasInvalidInput = toolTemplate.inputs.some( - (input) => - // 引用类型但没有工具描述 - (input.renderTypeList.length === 1 && - input.renderTypeList[0] === FlowNodeInputTypeEnum.reference && - !input.toolDescription) || - // 包含数据集选择 - input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) || - // 包含动态输入参数 - input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) || - // 文件选择但配置无效 - (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !hasValidFileInput) - ); - - if (hasInvalidInput) { - return false; - } - - return true; -}; - -export const checkNeedsUserConfiguration = (toolTemplate: FlowNodeTemplateType): boolean => { - const formRenderTypesMap: Record = { - [FlowNodeInputTypeEnum.input]: true, - [FlowNodeInputTypeEnum.textarea]: true, - [FlowNodeInputTypeEnum.numberInput]: true, - [FlowNodeInputTypeEnum.password]: true, - [FlowNodeInputTypeEnum.switch]: true, - [FlowNodeInputTypeEnum.select]: true, - [FlowNodeInputTypeEnum.JSONEditor]: true, - [FlowNodeInputTypeEnum.timePointSelect]: true, - [FlowNodeInputTypeEnum.timeRangeSelect]: true - }; - return ( - (toolTemplate.inputs.length > 0 && - toolTemplate.inputs.some((input) => { - // 有工具描述的不需要配置 - if (input.toolDescription) return false; - // 禁用流的不需要配置 - if (input.key === NodeInputKeyEnum.forbidStream) return false; - // 系统输入配置需要配置 - if (input.key === NodeInputKeyEnum.systemInputConfig) return true; - - // 检查是否包含表单类型的输入 - return input.renderTypeList.some((type) => formRenderTypesMap[type]); - })) || - false - ); -}; - -/** - * Get the configuration status of a tool - * Checks if tool needs configuration and whether all required fields are filled - * @param toolTemplate - The tool template to check - * @returns 'active' if tool is ready to use, 'waitingForConfig' if configuration needed - */ -export const getToolConfigStatus = ( - toolTemplate: FlowNodeTemplateType -): 'active' | 'waitingForConfig' => { - // Check if tool needs configuration - const needsConfig = checkNeedsUserConfiguration(toolTemplate); - if (!needsConfig) { - return 'active'; - } - - // For tools that need config, check if all required fields have values - const formRenderTypesMap: Record = { - [FlowNodeInputTypeEnum.input]: true, - [FlowNodeInputTypeEnum.textarea]: true, - [FlowNodeInputTypeEnum.numberInput]: true, - [FlowNodeInputTypeEnum.password]: true, - [FlowNodeInputTypeEnum.switch]: true, - [FlowNodeInputTypeEnum.select]: true, - [FlowNodeInputTypeEnum.JSONEditor]: true, - [FlowNodeInputTypeEnum.timePointSelect]: true, - [FlowNodeInputTypeEnum.timeRangeSelect]: true - }; - - // Find all inputs that need configuration - const configInputs = toolTemplate.inputs.filter((input) => { - if (input.toolDescription) return false; - if (input.key === NodeInputKeyEnum.forbidStream) return false; - if (input.key === NodeInputKeyEnum.systemInputConfig) return true; - return input.renderTypeList.some((type) => formRenderTypesMap[type]); - }); - - // Check if all required fields are filled - const allConfigured = configInputs.every((input) => { - const value = input.value; - if (value === undefined || value === null || value === '') return false; - if (Array.isArray(value) && value.length === 0) return false; - if (typeof value === 'object' && Object.keys(value).length === 0) return false; - return true; - }); - - return allConfigured ? 'active' : 'waitingForConfig'; -}; diff --git a/projects/app/src/pageComponents/app/detail/Edit/FormComponent/ToolSelector/ToolSelect.tsx b/projects/app/src/pageComponents/app/detail/Edit/FormComponent/ToolSelector/ToolSelect.tsx index f5c30388a..51d413b1a 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/FormComponent/ToolSelector/ToolSelect.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/FormComponent/ToolSelector/ToolSelect.tsx @@ -8,21 +8,20 @@ import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type/conf import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type'; import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete'; import ToolSelectModal from './ToolSelectModal'; import Avatar from '@fastgpt/web/components/common/Avatar'; import ConfigToolModal from '../../component/ConfigToolModal'; -import { getWebLLMModel } from '@/web/common/system/utils'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { formatToolError } from '@fastgpt/global/core/app/utils'; import { PluginStatusEnum, PluginStatusMap } from '@fastgpt/global/core/plugin/type'; import MyTag from '@fastgpt/web/components/common/Tag/index'; -import { checkNeedsUserConfiguration } from '../../ChatAgent/utils'; +import { checkNeedsUserConfiguration } from '@fastgpt/global/core/app/formEdit/utils'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model'; const ToolSelect = ({ + topAgentSelectedTools, selectedModel, selectedTools = [], fileSelectConfig = {}, @@ -30,6 +29,7 @@ const ToolSelect = ({ onUpdateTool, onRemoveTool }: { + topAgentSelectedTools?: SelectedToolItemType[]; selectedModel: LLMModelItemType; selectedTools?: SelectedToolItemType[]; fileSelectConfig?: AppFileSelectConfigType; @@ -164,7 +164,7 @@ const ToolSelect = ({ hoverColor="red.600" onClick={(e) => { e.stopPropagation(); - onRemoveTool(item.id); + onRemoveTool(item.pluginId!); }} /> @@ -176,6 +176,7 @@ const ToolSelect = ({ {isOpenToolsSelect && ( void }) export default React.memo(ToolSelectModal); const RenderList = React.memo(function RenderList({ + topAgentSelectedTools = [], templates, type, onAddTool, @@ -273,9 +277,20 @@ const RenderList = React.memo(function RenderList({ }); } + // 添加与 top 相同工具的配置 + const topTool = topAgentSelectedTools.find((tool) => tool.pluginId === res.pluginId); + if (topTool) { + res.inputs.forEach((input) => { + const topInput = topTool.inputs.find((input) => input.key === input.key); + if (topInput) { + input.value = topInput.value; + } + }); + } + onAddTool({ ...res, - configStatus: checkNeedsUserConfiguration(res) ? 'waitingForConfig' : 'active' + configStatus: getToolConfigStatus(res).status }); } ); diff --git a/projects/app/src/pages/api/core/ai/skill/detail.ts b/projects/app/src/pages/api/core/ai/skill/detail.ts index 3267f42f1..19d81830c 100644 --- a/projects/app/src/pages/api/core/ai/skill/detail.ts +++ b/projects/app/src/pages/api/core/ai/skill/detail.ts @@ -7,13 +7,15 @@ import { type GetAiSkillDetailResponse } from '@fastgpt/global/openapi/core/ai/skill/api'; import { MongoAiSkill } from '@fastgpt/service/core/ai/skill/schema'; -import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import { authApp, authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getChildAppPreviewNode } from '@fastgpt/service/core/app/tool/controller'; import { getLocale } from '@fastgpt/service/common/middle/i18n'; import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { UserError } from '@fastgpt/global/common/error/utils'; +import { getErrText, UserError } from '@fastgpt/global/common/error/utils'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; async function handler( req: ApiRequestProps<{}, GetAiSkillDetailQueryType>, @@ -28,7 +30,7 @@ async function handler( } // Auth app with read permission - const { teamId } = await authApp({ + const { teamId, app } = await authApp({ req, appId: String(skill.appId), per: ReadPermissionVal, @@ -44,10 +46,23 @@ async function handler( const expandedTools: SelectedToolItemType[] = await Promise.all( (skill.tools || []).map(async (tool) => { try { - const toolNode = await getChildAppPreviewNode({ - appId: tool.id, - lang: getLocale(req) - }); + const { source, pluginId } = splitCombineToolId(tool.id); + + const [toolNode] = await Promise.all([ + getChildAppPreviewNode({ + appId: pluginId, + lang: getLocale(req) + }), + ...(source === AppToolSourceEnum.personal + ? [ + authAppByTmbId({ + tmbId: app.tmbId, + appId: pluginId, + per: ReadPermissionVal + }) + ] + : []) + ]); // Merge saved config back into inputs const mergedInputs = toolNode.inputs.map((input) => ({ @@ -68,7 +83,7 @@ async function handler( id: tool.id, templateType: 'personalTool' as const, flowNodeType: FlowNodeTypeEnum.tool, - name: 'Invalid Tool', + name: 'Invalid', avatar: '', intro: '', showStatus: false, @@ -77,7 +92,10 @@ async function handler( version: 'v1', inputs: [], outputs: [], - configStatus: 'invalid' as const + configStatus: 'invalid' as const, + pluginData: { + error: getErrText(error) + } }; } }) diff --git a/test/cases/service/core/chat/saveChat.test.ts b/test/cases/service/core/chat/saveChat.test.ts index 5fade7cf1..37b546bd4 100644 --- a/test/cases/service/core/chat/saveChat.test.ts +++ b/test/cases/service/core/chat/saveChat.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it, beforeEach } from 'vitest'; -import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat'; +import { + type Props, + pushChatRecords, + updateInteractiveChat +} from '@fastgpt/service/core/chat/saveChat'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; @@ -7,7 +11,6 @@ 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 { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import type { Props } from '@fastgpt/service/core/chat/saveChat'; 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'; @@ -61,7 +64,7 @@ const createMockProps = ( ...overrides }); -describe('saveChat', () => { +describe('pushChatRecords', () => { let testAppId: string; let testTeamId: string; let testTmbId: string; @@ -110,13 +113,13 @@ describe('saveChat', () => { testAppId = String(app._id); }); - describe('saveChat function', () => { + describe('pushChatRecords function', () => { it('should skip saving if chatId is empty', async () => { const props = createMockProps( { chatId: '' }, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const chatItems = await MongoChatItem.find({ appId: testAppId }); expect(chatItems).toHaveLength(0); @@ -127,7 +130,7 @@ describe('saveChat', () => { { chatId: 'NO_RECORD_HISTORIES' }, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const chatItems = await MongoChatItem.find({ appId: testAppId }); expect(chatItems).toHaveLength(0); @@ -151,7 +154,7 @@ describe('saveChat', () => { } }); - await saveChat(props); + await pushChatRecords(props); // Verify that the URL was removed expect(props.userContent.value[0].file?.url).toBe(''); @@ -160,7 +163,7 @@ describe('saveChat', () => { it('should create chat items and update chat record', async () => { const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId }); - await saveChat(props); + await pushChatRecords(props); // Check chat items were created const chatItems = await MongoChatItem.find({ appId: testAppId, chatId: props.chatId }); @@ -206,7 +209,7 @@ describe('saveChat', () => { } }); - await saveChat(props); + await pushChatRecords(props); const responses = await MongoChatItemResponse.find({ appId: testAppId, @@ -259,7 +262,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const responses = await MongoChatItemResponse.find({ appId: testAppId, @@ -291,7 +294,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const app = await MongoApp.findById(testAppId); expect(app?.updateTime).toBeDefined(); @@ -307,7 +310,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const updatedApp = await MongoApp.findById(testAppId); expect(updatedApp!.updateTime.getTime()).toBe(originalUpdateTime.getTime()); @@ -336,7 +339,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId }); expect(logs).toHaveLength(1); @@ -372,7 +375,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId }); expect(logs).toHaveLength(1); @@ -387,7 +390,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props1); + await pushChatRecords(props1); const props2 = createMockProps( { @@ -397,7 +400,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props2); + await pushChatRecords(props2); const chat = await MongoChat.findOne({ appId: testAppId, chatId: props1.chatId }); expect(chat?.metadata).toMatchObject({ @@ -415,7 +418,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const aiItem = await MongoChatItem.findOne({ appId: testAppId, @@ -478,7 +481,7 @@ describe('saveChat', () => { { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } ); - await saveChat(props); + await pushChatRecords(props); const aiItem = await MongoChatItem.findOne({ appId: testAppId,