mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
feat: load tool in agent
This commit is contained in:
parent
5097d25379
commit
a384e644cb
|
|
@ -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 */]
|
||||
},
|
||||
// ...
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
};
|
||||
|
|
@ -171,7 +171,7 @@ export enum NodeInputKeyEnum {
|
|||
aiTaskObject = 'aiTaskObject',
|
||||
|
||||
// agent
|
||||
subApps = 'subApps',
|
||||
selectedTools = 'agent_selectedTools',
|
||||
skills = 'skills',
|
||||
isAskAgent = 'isAskAgent',
|
||||
isPlanAgent = 'isPlanAgent',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
你是一个智能流程优化专家,专门负责在已完成的任务步骤基础上,追加生成优化步骤来完善整个流程,确保任务目标的完美达成。
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) || []
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(保持原有逻辑)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue