feat: load tool in agent

This commit is contained in:
archer 2025-12-19 10:08:30 +08:00
parent 5097d25379
commit a384e644cb
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
28 changed files with 912 additions and 655 deletions

View File

@ -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 */]
},
// ...
});

View File

@ -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<string, boolean> = {
[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<string, boolean> = {
[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'
};
};

View File

@ -171,7 +171,7 @@ export enum NodeInputKeyEnum {
aiTaskObject = 'aiTaskObject',
// agent
subApps = 'subApps',
selectedTools = 'agent_selectedTools',
skills = 'skills',
isAskAgent = 'isAskAgent',
isPlanAgent = 'isPlanAgent',

View File

@ -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

View File

@ -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<string>();
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;
}
});
}
})
);

View File

@ -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<string, string>;
}) => {
// Get sub apps
const runtimeSubApps = await rewriteSubAppsToolset({
subApps: subApps.map<RuntimeNodeItemType>((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
};
};

View File

@ -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<string, string>;
subAppsMap: Map<string, RuntimeNodeItemType>;
subAppsMap: Map<string, SubAppRuntimeType>;
}) => {
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<string, any> = 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,

View File

@ -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<string, AiSkillSchemaType>;
} => {
const tools: ChatCompletionTool[] = [];
export const buildSkillTools = (skills: AiSkillSchemaType[]) => {
const skillCompletionTools: ChatCompletionTool[] = [];
const skillsMap: Record<string, AiSkillSchemaType> = {};
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<string, SubAppRuntimeType>;
}
> => {
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)
};
}
};

View File

@ -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<ChatCompletionTool>((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<string, any> = {};
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];
};

View File

@ -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

View File

@ -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 `
<role>
@ -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 `<role>

View File

@ -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,

View File

@ -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<SubAppInitType[]> => {
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<string, any> = {};
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<Promise<SubAppInitType[]>>(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());
};

View File

@ -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;
};
};

View File

@ -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<typeof SubAppRuntimeSchema>;
export type GetSubAppInfoFnType = (id: string) => {
name: string;
avatar: string;

View File

@ -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<string, string>;
}): Promise<{
completionTools: ChatCompletionTool[];
subAppsMap: Map<string, SubAppRuntimeType>;
}> => {
const subAppsMap = new Map<string, SubAppRuntimeType>();
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
};
};

View File

@ -34,7 +34,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({
toolSetNode,
lang = 'en'
}: {
toolSetNode: RuntimeNodeItemType;
toolSetNode: Pick<RuntimeNodeItemType, 'toolConfig' | 'inputs' | 'nodeId'>;
lang?: localeType;
}): Promise<RuntimeNodeItemType[]> {
const systemToolId = toolSetNode.toolConfig?.systemToolSet?.toolId!;

View File

@ -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 = ({
<>
<Box overflowY={'auto'} minW={['auto', '580px']} flex={'1'} borderRight={'base'}>
<SkillEditForm
topAgentSelectedTools={appForm.selectedTools}
model={appForm.aiSettings.model}
fileSelectConfig={appForm.chatConfig.fileSelectConfig}
skill={editingSkill}
@ -137,6 +136,7 @@ const Edit = ({
</Box>
<Box flex={'2 0 0'} w={0} mb={3}>
<SKillChatTest
topAgentSelectedTools={appForm.selectedTools}
skill={editingSkill}
appForm={appForm}
onAIGenerate={handleAIGenerate}

View File

@ -18,7 +18,6 @@ import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
@ -31,9 +30,8 @@ import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import SkillRow from './SkillEdit/Row';
import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAiSkillDetail } from '@/web/core/ai/skill/api';
import { validateToolConfiguration, getToolConfigStatus } from './utils';
import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils';
const DatasetSelectModal = dynamic(() => 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) || []
}));
}}
/>

View File

@ -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<SkillEditType>) => 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<string>();
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保持原有逻辑

View File

@ -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 */}
<Box mt={5} px={3} py={4} borderTop={'base'}>
<ToolSelect
topAgentSelectedTools={topAgentSelectedTools}
selectedModel={selectedModel}
selectedTools={selectedTools}
fileSelectConfig={fileSelectConfig}
@ -225,9 +234,13 @@ const EditForm = ({ model, fileSelectConfig, skill, onClose, onSave }: EditFormP
);
}}
onRemoveTool={(id) => {
setValue('selectedTools', selectedTools?.filter((item) => item.id !== id) || [], {
shouldDirty: true
});
setValue(
'selectedTools',
selectedTools?.filter((item) => item.pluginId !== id) || [],
{
shouldDirty: true
}
);
}}
/>
</Box>

View File

@ -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

View File

@ -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);

View File

@ -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<string, any>
)
}))
}
],
@ -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<string, boolean> = {
[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<string, boolean> = {
[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';
};

View File

@ -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!);
}}
/>
</Box>
@ -176,6 +176,7 @@ const ToolSelect = ({
{isOpenToolsSelect && (
<ToolSelectModal
topAgentSelectedTools={topAgentSelectedTools}
selectedTools={selectedTools}
fileSelectConfig={fileSelectConfig}
selectedModel={selectedModel}

View File

@ -18,7 +18,7 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../../context';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
@ -29,7 +29,6 @@ import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model';
import { workflowStartNodeId } from '@/web/core/app/constants';
import CostTooltip from '@/components/core/app/tool/CostTooltip';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@ -37,9 +36,13 @@ import ToolTagFilterBox from '@fastgpt/web/components/core/plugin/tool/TagFilter
import { getPluginToolTags } from '@/web/core/plugin/toolTag/api';
import { useRouter } from 'next/router';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { checkNeedsUserConfiguration, validateToolConfiguration } from '../../ChatAgent/utils';
import {
getToolConfigStatus,
validateToolConfiguration
} from '@fastgpt/global/core/app/formEdit/utils';
type Props = {
topAgentSelectedTools?: SelectedToolItemType[];
selectedTools: FlowNodeTemplateType[];
fileSelectConfig: AppFormEditFormType['chatConfig']['fileSelectConfig'];
selectedModel: LLMModelItemType;
@ -239,6 +242,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => 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
});
}
);

View File

@ -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)
}
};
}
})

View File

@ -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,