Commit 40a8a809 by Archer Committed by GitHub

fix: load agent runtime tool schemas (#7077)

* fix: load agent runtime tool schemas

* perf: create api
parent f9ba442c
import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type'; import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type';
import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
import { getChildAppPreviewNode } from '../../../../../../app/tool/controller';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authAppByTmbId } from '../../../../../../../support/permission/app/auth'; import { authAppByTmbId } from '../../../../../../../support/permission/app/auth';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { getSystemToolRunTimeNodeFromSystemToolset } from '../../../../../../workflow/utils'; 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 { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/utils';
import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/utils'; import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/utils';
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type'; import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type';
...@@ -19,13 +20,44 @@ import { ...@@ -19,13 +20,44 @@ import {
toolValueTypeList, toolValueTypeList,
valueTypeJsonSchemaMap valueTypeJsonSchemaMap
} from '@fastgpt/global/core/workflow/constants'; } from '@fastgpt/global/core/workflow/constants';
import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import type {
McpToolConfigType,
McpToolDataType
} from '@fastgpt/global/core/app/tool/mcpTool/type';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type'; import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type';
import type { SubAppInitType } from '../type'; import type { SubAppInitType } from '../type';
import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils'; import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils';
import { getLogger, LogCategories } from '../../../../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../../common/logger';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import {
appData2FlowNodeIO,
pluginData2FlowNodeIO,
toolData2FlowNodeIO
} from '@fastgpt/global/core/workflow/utils';
import type { AppSchemaType } from '@fastgpt/global/core/app/type';
import { getAppVersionById } from '../../../../../../app/version/controller';
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { SystemToolRepo } from '../../../../../../app/tool/systemTool/systemTool.repo';
import { Output_Template_Error_Message } from '@fastgpt/global/core/workflow/template/output';
import type { NodeToolConfigType } from '@fastgpt/global/core/workflow/type/node';
type AgentRuntimeNode = RuntimeNodeItemType & {
currentCost?: number;
hasSystemSecret?: boolean;
hasTokenFee?: boolean;
systemKeyCost?: number;
};
/**
* 将 Agent 选择的工具配置转换成 LLM function calling 与 runtime 执行共用的工具描述。
*
* 这里刻意不调用面向前端预览的 getChildAppPreviewNode:Agent runtime 只关心鉴权后的执行
* 节点、toolConfig 和 JSON Schema。App 类工具统一读取当前发布版本;MCP/HTTP 工具集只使用
* 当前版本节点里保存的 toolList,不额外兼容旧版 MCP 数据源。
*/
export const getAgentRuntimeTools = async ({ export const getAgentRuntimeTools = async ({
tools, tools,
tmbId, tmbId,
...@@ -35,13 +67,312 @@ export const getAgentRuntimeTools = async ({ ...@@ -35,13 +67,312 @@ export const getAgentRuntimeTools = async ({
tmbId: string; tmbId: string;
lang?: localeType; lang?: localeType;
}): Promise<SubAppInitType[]> => { }): Promise<SubAppInitType[]> => {
// Agent 工具执行需要统一的错误输出口,方便 workflow runtime 收敛失败结果。
const appendErrorOutput = (outputs: RuntimeNodeItemType['outputs'] = []) => {
return outputs.some((item) => item.type === FlowNodeOutputTypeEnum.error)
? outputs
: [...outputs, Output_Template_Error_Message];
};
// toolsetData2FlowNodeIO 偏前端展示,会丢掉部分 schema 相关信息;runtime 直接保留节点 IO。
const getToolSetNodeIO = ({
nodes
}: {
nodes: AppSchemaType['modules'];
}): {
inputs: RuntimeNodeItemType['inputs'];
outputs: RuntimeNodeItemType['outputs'];
toolConfig?: NodeToolConfigType;
} => {
const toolSetNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
return {
inputs: toolSetNode?.inputs || [],
outputs: toolSetNode?.outputs || [],
toolConfig: toolSetNode?.toolConfig
};
};
/**
* 系统工具和商业工具来自 SystemToolRepo,不需要 App 鉴权和版本查询。
* associatedPluginId 表示这个系统能力最终应按插件工作流执行。
*/
const formatSystemToolNode = async ({
toolId,
nodeId,
source
}: {
toolId: string;
nodeId: string;
source: AppToolSourceEnum.systemTool | AppToolSourceEnum.commercial;
}): Promise<AgentRuntimeNode> => {
const systemToolRepo = SystemToolRepo.getInstance();
const toolDetail = await systemToolRepo.getSystemToolDetail({
pluginId: toolId,
lang,
source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : 'system'
});
const isWorkflowTool = !!toolDetail.associatedPluginId;
// secrets 是运行时私密配置,只放进隐藏 input,不能暴露成模型可填写参数。
const inputs = [
...(toolDetail.secrets?.length
? [
{
key: NodeInputKeyEnum.systemInputConfig,
label: '',
renderTypeList: [FlowNodeInputTypeEnum.hidden],
inputList: toolDetail.secrets
} satisfies FlowNodeInputItemType
]
: []),
...(toolDetail.inputs ?? [])
];
return {
nodeId,
pluginId: toolId,
flowNodeType: isWorkflowTool
? FlowNodeTypeEnum.pluginModule
: toolDetail.isToolSet
? FlowNodeTypeEnum.toolSet
: FlowNodeTypeEnum.tool,
avatar: toolDetail.avatar,
name: toolDetail.name,
intro: toolDetail.intro,
toolDescription: toolDetail.toolDescription,
version: '',
inputs,
outputs: appendErrorOutput(toolDetail.outputs ?? []),
currentCost: toolDetail.currentCost,
hasSystemSecret: toolDetail.hasSystemSecret,
hasTokenFee: toolDetail.hasTokenFee,
systemKeyCost: toolDetail.systemKeyCost,
...(isWorkflowTool
? {}
: {
toolConfig: {
...(toolDetail.isToolSet
? {
systemToolSet: {
toolId,
toolList:
toolDetail.children?.map((child) => ({
description: child.description ?? '',
name: child.name,
toolId: child.id
})) ?? []
}
}
: {
systemTool: {
toolId
}
})
}
})
};
};
// 当前 Agent 工具始终取 App 的当前发布版本;没有发布版本时由 controller 回落到 app.modules。
const getVersionNodes = async ({ app }: { app: AppSchemaType }) => {
const version = await getAppVersionById({
appId: String(app._id),
app
});
return {
...version,
nodes: version.nodes
};
};
/**
* 普通 App 需要根据当前版本节点形态判断运行时类型:
* - pluginInput: 插件工作流
* - 单 toolSet: MCP/HTTP/System toolset
* - 单 tool: 普通工具节点
* - 其他: 子应用 workflow
*/
const formatPersonalAppNode = async ({
app
}: {
app: AppSchemaType;
}): Promise<AgentRuntimeNode> => {
if (AppFolderTypeList.includes(app.type)) {
return Promise.reject(PluginErrEnum.unExist);
}
const version = await getVersionNodes({ app });
const nodes = version.nodes;
const baseNode = {
nodeId: String(app._id),
pluginId: String(app._id),
avatar: app.avatar,
name: parseI18nString(app.name, lang),
intro: parseI18nString(app.intro, lang),
version: '',
showStatus: true,
inputs: [],
outputs: []
};
// pluginInput 是插件工作流的显式标识,后续执行走 dispatchPlugin。
if (nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)) {
const nodeIO = pluginData2FlowNodeIO({ nodes });
return {
...baseNode,
flowNodeType: FlowNodeTypeEnum.pluginModule,
inputs: nodeIO.inputs,
outputs: appendErrorOutput(nodeIO.outputs)
};
}
const toolSetNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
if (toolSetNode && nodes.length === 1) {
// MCP/HTTP toolset 的 json schema 保存在 toolConfig.toolList 上,必须保留原始 toolConfig。
const nodeIO = getToolSetNodeIO({ nodes });
return {
...baseNode,
flowNodeType: FlowNodeTypeEnum.toolSet,
inputs: nodeIO.inputs,
outputs: appendErrorOutput(nodeIO.outputs),
toolConfig: nodeIO.toolConfig
};
}
const toolNode = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool);
if (toolNode && nodes.length === 1) {
// 单工具节点可能直接带 jsonSchema,优先作为 function parameters 使用。
const nodeIO = toolData2FlowNodeIO({ nodes });
return {
...baseNode,
flowNodeType: FlowNodeTypeEnum.tool,
inputs: nodeIO.inputs,
outputs: appendErrorOutput(nodeIO.outputs),
toolConfig: nodeIO.toolConfig,
jsonSchema: (toolNode as RuntimeNodeItemType).jsonSchema
};
}
const nodeIO = appData2FlowNodeIO({ chatConfig: version.chatConfig });
return {
...baseNode,
flowNodeType: FlowNodeTypeEnum.appModule,
inputs: nodeIO.inputs,
outputs: appendErrorOutput(nodeIO.outputs)
};
};
/**
* 解析单个 MCP 工具 id: mcp-${appId}/${toolName}。
* 只读取当前版本 toolConfig.mcpToolSet.toolList;不再通过 getMCPChildren 补旧版数据。
*/
const formatMcpToolNode = async ({
app,
pluginId
}: {
app: AppSchemaType;
pluginId: string;
}): Promise<AgentRuntimeNode> => {
const [, ...toolNameParts] = pluginId.split('/');
const toolName = toolNameParts.join('/');
const version = await getVersionNodes({ app });
const toolList = version.nodes[0]?.toolConfig?.mcpToolSet?.toolList ?? [];
const tool = toolList.find((item) => item.name === toolName);
if (!tool) return Promise.reject(PluginErrEnum.unExist);
const node = getMCPToolRuntimeNode({
nodeId: pluginId.replace(/[^a-zA-Z0-9_-]/g, ''),
toolSetId: String(app._id),
toolsetName: app.name,
avatar: app.avatar,
tool
});
// 单独选择子工具时,模型侧展示子工具名即可,不需要带 toolset 前缀。
return {
...node,
name: tool.name,
intro: tool.description
};
};
/**
* 解析单个 HTTP 工具 id: http-${appId}/${toolName}。
* HTTP function parameters 使用 requestSchema,而不是展示表单用的 inputSchema。
*/
const formatHttpToolNode = async ({
app,
pluginId
}: {
app: AppSchemaType;
pluginId: string;
}): Promise<AgentRuntimeNode> => {
const [, ...toolNameParts] = pluginId.split('/');
const toolName = toolNameParts.join('/');
const version = await getVersionNodes({ app });
const tool = version.nodes[0]?.toolConfig?.httpToolSet?.toolList.find(
(item) => item.name === toolName
);
if (!tool) return Promise.reject(PluginErrEnum.unExist);
const node = getHTTPToolRuntimeNode({
nodeId: pluginId.replace(/[^a-zA-Z0-9_-]/g, ''),
toolSetId: String(app._id),
toolsetName: app.name,
avatar: app.avatar,
tool
});
// 单独选择子工具时,模型侧展示子工具名即可,不需要带 toolset 前缀。
return {
...node,
name: tool.name,
intro: tool.description
};
};
const getRuntimeToolNode = async ({
source,
pluginId,
toolId,
app
}: {
source: AppToolSourceEnum;
pluginId: string;
toolId: string;
app?: AppSchemaType;
}): Promise<AgentRuntimeNode> => {
// Agent 运行时只需要节点执行和 schema 信息,不能依赖面向前端展示的 preview controller。
if (source === AppToolSourceEnum.systemTool || source === AppToolSourceEnum.commercial) {
return formatSystemToolNode({
toolId,
nodeId: pluginId,
source
});
}
if (!app) return Promise.reject(PluginErrEnum.unExist);
if (source === AppToolSourceEnum.mcp) {
return formatMcpToolNode({ app, pluginId });
}
if (source === AppToolSourceEnum.http) {
return formatHttpToolNode({ app, pluginId });
}
return formatPersonalAppNode({ app });
};
/**
* 生成 OpenAI-compatible function schema。
* schema 优先级:显式 jsonSchema > toolData.inputSchema > 带 toolDescription 的普通 inputs。
*/
const formatSchema = ({ const formatSchema = ({
toolId, toolId,
inputs, inputs,
flowNodeType, flowNodeType,
name, name,
toolDescription, toolDescription,
intro intro,
jsonSchema
}: { }: {
toolId: string; toolId: string;
inputs: FlowNodeInputItemType[]; inputs: FlowNodeInputItemType[];
...@@ -49,17 +380,19 @@ export const getAgentRuntimeTools = async ({ ...@@ -49,17 +380,19 @@ export const getAgentRuntimeTools = async ({
name: string; name: string;
toolDescription?: string; toolDescription?: string;
intro?: string; intro?: string;
jsonSchema?: JSONSchemaInputType;
}): ChatCompletionTool => { }): ChatCompletionTool => {
const toolParams: FlowNodeInputItemType[] = []; const toolParams: FlowNodeInputItemType[] = [];
let jsonSchema: JSONSchemaInputType | undefined; let schema = jsonSchema;
for (const input of inputs) { for (const input of inputs) {
// 没有 JSON Schema 的普通工具,用 toolDescription 标识哪些 input 可以交给模型填写。
if (input.toolDescription) { if (input.toolDescription) {
toolParams.push(input); toolParams.push(input);
} }
if (input.key === NodeInputKeyEnum.toolData) { if (!schema && input.key === NodeInputKeyEnum.toolData) {
jsonSchema = (input.value as McpToolDataType)?.inputSchema; schema = (input.value as McpToolDataType)?.inputSchema;
} }
} }
...@@ -68,15 +401,16 @@ export const getAgentRuntimeTools = async ({ ...@@ -68,15 +401,16 @@ export const getAgentRuntimeTools = async ({
name: name, name: name,
intro: toolDescription || intro intro: toolDescription || intro
}); });
const formatToolId = `t${toolId}`; // 仅数字开头的工具名需要补前缀,避免破坏 runtime 使用原始 tool id 反查工具。
const formatToolId = /^\d/.test(toolId) ? `t${toolId}` : toolId;
if (jsonSchema) { if (schema) {
return { return {
type: 'function', type: 'function',
function: { function: {
name: formatToolId, name: formatToolId,
description, description,
parameters: jsonSchema parameters: schema
} }
}; };
} }
...@@ -112,31 +446,37 @@ export const getAgentRuntimeTools = async ({ ...@@ -112,31 +446,37 @@ export const getAgentRuntimeTools = async ({
tools.map<Promise<SubAppInitType[]>>(async (tool) => { tools.map<Promise<SubAppInitType[]>>(async (tool) => {
try { try {
const { pluginId, authAppId, source } = splitCombineToolId(tool.id); const { pluginId, authAppId, source } = splitCombineToolId(tool.id);
// 工具间整体并发;单个 App 类工具必须先鉴权拿到 app,才能读取对应版本节点。
const authAppPromise = authAppId
? authAppByTmbId({
tmbId,
appId: authAppId,
per: ReadPermissionVal
})
: Promise.resolve(undefined);
const [toolNode] = await Promise.all([ const [authResult, toolNode] = await Promise.all([
getChildAppPreviewNode({ authAppPromise,
appId: tool.id, authAppPromise.then((authResult) =>
versionId: '', getRuntimeToolNode({
lang source,
}), pluginId,
...(authAppId app: authResult?.app,
? [ toolId: tool.id
authAppByTmbId({ })
tmbId, )
appId: authAppId,
per: ReadPermissionVal
})
]
: [])
]); ]);
// 1. Add config value to toolNode.inputs const authApp = authResult?.app;
// 合并用户在 Agent 工具面板里保存的配置;false/0/空字符串也是有效配置值。
toolNode.inputs.forEach((input) => { toolNode.inputs.forEach((input) => {
const value = tool.config[input.key]; if (Object.prototype.hasOwnProperty.call(tool.config, input.key)) {
if (value) { const value = tool.config[input.key];
input.value = value; input.value = value;
} }
}); });
// 2. Check config status
// 缺少必填运行配置时,不把该工具注册给模型,避免模型调用后才失败。
const configStatus = getToolConfigStatus({ const configStatus = getToolConfigStatus({
tool: toolNode tool: toolNode
}); });
...@@ -161,12 +501,33 @@ export const getAgentRuntimeTools = async ({ ...@@ -161,12 +501,33 @@ export const getAgentRuntimeTools = async ({
return 'tool'; return 'tool';
})(); })();
// toolset 展开后的子工具统一走 tool 执行;params 仍继承父工具配置。
const buildSubApp = (child: RuntimeNodeItemType, id = child.nodeId): SubAppInitType => ({
type: 'tool',
id,
name: child.name,
avatar: child.avatar,
version: child.version,
toolConfig: child.toolConfig,
params: tool.config,
requestSchema: formatSchema({
toolId: id,
inputs: child.inputs,
flowNodeType: child.flowNodeType,
name: child.name,
toolDescription: child.toolDescription,
intro: child.intro,
jsonSchema: child.jsonSchema
})
});
if (toolNode.flowNodeType === FlowNodeTypeEnum.toolSet) { if (toolNode.flowNodeType === FlowNodeTypeEnum.toolSet) {
const systemToolId = toolNode.toolConfig?.systemToolSet?.toolId; const systemToolId = toolNode.toolConfig?.systemToolSet?.toolId;
const mcpToolsetVal = toolNode.toolConfig?.mcpToolSet ?? toolNode.inputs[0]?.value; const mcpToolsetVal = toolNode.toolConfig?.mcpToolSet ?? toolNode.inputs[0]?.value;
const httpToolsetVal = toolNode.toolConfig?.httpToolSet; const httpToolsetVal = toolNode.toolConfig?.httpToolSet;
if (systemToolId) { if (systemToolId) {
// System toolset 的子工具由系统工具仓库展开,可能包含内置运行配置。
const children = await getSystemToolRunTimeNodeFromSystemToolset({ const children = await getSystemToolRunTimeNodeFromSystemToolset({
toolSetNode: { toolSetNode: {
toolConfig: toolNode.toolConfig, toolConfig: toolNode.toolConfig,
...@@ -176,29 +537,12 @@ export const getAgentRuntimeTools = async ({ ...@@ -176,29 +537,12 @@ export const getAgentRuntimeTools = async ({
lang lang
}); });
return children.map((child) => ({ return children.map((child) => buildSubApp(child));
type: 'tool',
id: child.nodeId,
name: child.name,
avatar: child.avatar,
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) { } else if (mcpToolsetVal) {
const app = await MongoApp.findOne({ _id: toolNode.pluginId }).lean(); // MCP toolset 已在当前版本节点保存 toolList;展开时不再触发 DB 查询。
if (!app) return []; const toolList: McpToolConfigType[] = mcpToolsetVal.toolList ?? [];
const toolList = await getMCPChildren(app);
const toolSetId = mcpToolsetVal.toolId || toolNode.pluginId; const toolSetId = mcpToolsetVal.toolId || toolNode.pluginId || pluginId;
const children = toolList.map((tool, index) => { const children = toolList.map((tool, index) => {
const newToolNode = getMCPToolRuntimeNode({ const newToolNode = getMCPToolRuntimeNode({
toolSetId, toolSetId,
...@@ -210,26 +554,9 @@ export const getAgentRuntimeTools = async ({ ...@@ -210,26 +554,9 @@ export const getAgentRuntimeTools = async ({
return newToolNode; return newToolNode;
}); });
return children.map((child) => { return children.map((child) => buildSubApp(child));
return {
type: 'tool',
id: child.nodeId,
name: child.name,
avatar: child.avatar,
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 (httpToolsetVal) { } else if (httpToolsetVal) {
// HTTP toolset 的 requestSchema 在 getHTTPToolRuntimeNode 中写入 jsonSchema。
const children = httpToolsetVal.toolList.map((tool: HttpToolConfigType, index) => { const children = httpToolsetVal.toolList.map((tool: HttpToolConfigType, index) => {
const newToolNode = getHTTPToolRuntimeNode({ const newToolNode = getHTTPToolRuntimeNode({
tool, tool,
...@@ -241,39 +568,12 @@ export const getAgentRuntimeTools = async ({ ...@@ -241,39 +568,12 @@ export const getAgentRuntimeTools = async ({
return newToolNode; return newToolNode;
}); });
return children.map((child) => { return children.map((child) => buildSubApp(child));
return {
type: 'tool',
id: child.nodeId,
name: child.name,
avatar: child.avatar,
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 []; return [];
} } else {
// OpenAI function name 不能包含斜杠等字符,runtime map 也使用同一份清洗后的 id。
// else if (source === AppToolSourceEnum.commercial) {
// const systemToolRepo = SystemToolRepo.getInstance();
// const detail = await systemToolRepo.getSystemToolDetail({
// pluginId
// })
// }
else {
const cleanedPluginId = pluginId.replace(/[^a-zA-Z0-9_-]/g, ''); const cleanedPluginId = pluginId.replace(/[^a-zA-Z0-9_-]/g, '');
return [ return [
...@@ -291,7 +591,8 @@ export const getAgentRuntimeTools = async ({ ...@@ -291,7 +591,8 @@ export const getAgentRuntimeTools = async ({
flowNodeType: toolNode.flowNodeType, flowNodeType: toolNode.flowNodeType,
name: toolNode.name, name: toolNode.name,
toolDescription: toolNode.toolDescription, toolDescription: toolNode.toolDescription,
intro: toolNode.intro intro: toolNode.intro,
jsonSchema: toolNode.jsonSchema
}) })
} }
]; ];
......
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getAgentRuntimeTools } from '@fastgpt/service/core/workflow/dispatch/ai/agent/sub/tool/utils';
import type { NodeToolConfigType } from '@fastgpt/global/core/workflow/type/node';
const { authAppByTmbIdMock, getAppVersionByIdMock } = vi.hoisted(() => ({
authAppByTmbIdMock: vi.fn(),
getAppVersionByIdMock: vi.fn()
}));
vi.mock('@fastgpt/service/support/permission/app/auth', () => ({
authAppByTmbId: authAppByTmbIdMock
}));
vi.mock('@fastgpt/service/core/app/version/controller', () => ({
getAppVersionById: getAppVersionByIdMock
}));
vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({
SystemToolRepo: {
getInstance: vi.fn(() => ({
getSystemToolDetail: vi.fn()
}))
}
}));
vi.mock('@fastgpt/service/common/logger', () => ({
LogCategories: {
MODULE: {
AI: {
AGENT: 'agent'
}
}
},
getLogger: vi.fn(() => ({
warn: vi.fn()
}))
}));
const mcpInputSchema = {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
}
},
required: ['query']
};
const httpInputSchema = {
type: 'object',
properties: {
keyword: {
type: 'string',
description: 'Keyword',
'x-tool-description': 'Keyword'
}
},
required: ['keyword']
};
const httpRequestSchema = {
type: 'object',
properties: {
body: {
type: 'object',
description: 'Raw request body'
}
},
required: ['body']
};
const mcpTool = {
name: 'search',
description: 'Search docs',
inputSchema: mcpInputSchema
};
const httpTool = {
name: 'create',
description: 'Create record',
path: '/records',
method: 'post',
inputSchema: httpInputSchema,
requestSchema: httpRequestSchema,
outputSchema: {
type: 'object',
properties: {}
}
};
const createToolsetApp = ({
id,
type,
toolConfig
}: {
id: string;
type: AppTypeEnum.mcpToolSet | AppTypeEnum.httpToolSet;
toolConfig: NodeToolConfigType;
}) => ({
_id: id,
teamId: 'team_1',
tmbId: 'tmb_1',
type,
name: `${id} name`,
avatar: `${id}.png`,
intro: `${id} intro`,
updateTime: new Date(),
modules: [
{
nodeId: 'toolset_node',
flowNodeType: FlowNodeTypeEnum.toolSet,
name: `${id} node`,
avatar: `${id}.png`,
intro: `${id} intro`,
inputs: [],
outputs: [],
toolConfig
}
],
edges: [],
chatConfig: {}
});
describe('getAgentRuntimeTools schema loading', () => {
beforeEach(() => {
vi.clearAllMocks();
authAppByTmbIdMock.mockImplementation(async ({ appId }: { appId: string }) => {
const app = appMap[appId];
if (!app) throw new Error(`missing app ${appId}`);
return { app };
});
getAppVersionByIdMock.mockImplementation(async ({ app }: { app: any }) => ({
versionId: '',
versionName: app.name,
nodes: app.modules,
edges: app.edges,
chatConfig: app.chatConfig
}));
});
const appMap: Record<string, any> = {
mcp_app: createToolsetApp({
id: 'mcp_app',
type: AppTypeEnum.mcpToolSet,
toolConfig: {
mcpToolSet: {
url: 'https://mcp.example.com',
headerSecret: {},
toolList: [mcpTool]
}
}
}),
'123_app': createToolsetApp({
id: '123_app',
type: AppTypeEnum.mcpToolSet,
toolConfig: {
mcpToolSet: {
url: 'https://mcp.example.com',
headerSecret: {},
toolList: [mcpTool]
}
}
}),
http_app: createToolsetApp({
id: 'http_app',
type: AppTypeEnum.httpToolSet,
toolConfig: {
httpToolSet: {
baseUrl: 'https://api.example.com',
headerSecret: {},
toolList: [httpTool]
}
}
})
};
it('loads MCP toolset children with their input schema', async () => {
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'mcp_app', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].requestSchema.function.name).toBe('mcp_app0');
expect(tools[0].requestSchema.function.parameters).toEqual(mcpInputSchema);
expect(tools[0].toolConfig?.mcpTool?.toolId).toBe('mcp-mcp_app/search');
});
it('loads a selected MCP tool with its input schema', async () => {
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'mcp-mcp_app/search', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].id).toBe('mcp_appsearch');
expect(tools[0].name).toBe('search');
expect(tools[0].requestSchema.function.name).toBe('mcp_appsearch');
expect(tools[0].requestSchema.function.parameters).toEqual(mcpInputSchema);
expect(tools[0].toolConfig?.mcpTool?.toolId).toBe('mcp-mcp_app/search');
});
it('prefixes tool function name only when the runtime tool id starts with a number', async () => {
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'mcp-123_app/search', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].id).toBe('123_appsearch');
expect(tools[0].requestSchema.function.name).toBe('t123_appsearch');
expect(tools[0].requestSchema.function.parameters).toEqual(mcpInputSchema);
});
it('loads HTTP toolset children with their request schema', async () => {
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'http_app', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].requestSchema.function.name).toBe('http_app0');
expect(tools[0].requestSchema.function.parameters).toEqual(httpRequestSchema);
expect(tools[0].requestSchema.function.parameters).not.toEqual(httpInputSchema);
expect(tools[0].toolConfig?.httpTool?.toolId).toBe('http-http_app/create');
});
it('loads a selected HTTP tool with its request schema', async () => {
const tools = await getAgentRuntimeTools({
tmbId: 'tmb_1',
tools: [{ id: 'http-http_app/create', config: {} }]
});
expect(tools).toHaveLength(1);
expect(tools[0].id).toBe('http_appcreate');
expect(tools[0].name).toBe('create');
expect(tools[0].requestSchema.function.name).toBe('http_appcreate');
expect(tools[0].requestSchema.function.parameters).toEqual(httpRequestSchema);
expect(tools[0].requestSchema.function.parameters).not.toEqual(httpInputSchema);
expect(tools[0].toolConfig?.httpTool?.toolId).toBe('http-http_app/create');
});
});
Subproject commit 35458b567a3b719987bdd4e9a18dc747a8b34495 Subproject commit 5151345e0b0c883f3b812194430598dfe3113e65
...@@ -53,8 +53,19 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) { ...@@ -53,8 +53,19 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) {
// 凭证校验 // 凭证校验
const { teamId, tmbId, userId, isRoot } = parentId const { teamId, tmbId, userId, isRoot } = parentId
? await authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true }) ? await authApp({
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); req,
appId: parentId,
authToken: true,
authApiKey: true,
per: WritePermissionVal
})
: await authUserPer({
req,
authToken: true,
authApiKey: true,
per: TeamAppCreatePermissionVal
});
// 上限校验 // 上限校验
await checkTeamAppTypeLimit({ await checkTeamAppTypeLimit({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment