mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 04:06:18 +00:00
V4.9.6 feature (#4565)
* Dashboard submenu (#4545) * add app submenu (#4452) * add app submenu * fix * width & i18n * optimize submenu code (#4515) * optimize submenu code * fix * fix * fix * fix ts * perf: dashboard sub menu * doc --------- Co-authored-by: heheer <heheer@sealos.io> * feat: value format test * doc * Mcp export (#4555) * feat: mcp server * feat: mcp server * feat: mcp server build * update doc * perf: path selector (#4556) * perf: path selector * fix: docker file path * perf: add image endpoint to dataset search (#4557) * perf: add image endpoint to dataset search * fix: mcp_server url * human in loop (#4558) * Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552) * feat: add LoopInteractive definition * feat: Support LoopInteractive type and update related logic * fix: Refactor loop handling logic and improve output value initialization * feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses * feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling * refactor: Remove redundant comments in mergeChatResponseData for clarity * perf: loop interactive * perf: human in loop --------- Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com> * mcp server ui * integrate mcp (#4549) * integrate mcp * delete unused code * fix ts * bug fix * fix * support whole mcp tools * add try catch * fix * fix * fix ts * fix test * fix ts * fix: interactive in v1 completions * doc * fix: router path * fix mcp integrate (#4563) * fix mcp integrate * fix ui * fix: mcp ux * feat: mcp call title * remove repeat loading * fix mcp tools avatar (#4564) * fix * fix avatar * fix update version * update doc * fix: value format * close server and remove cache * perf: avatar --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
@@ -86,3 +86,19 @@ export async function findAppAndAllChildren({
|
||||
|
||||
return [app, ...childDatasets];
|
||||
}
|
||||
|
||||
export const getAppBasicInfoByIds = async ({ teamId, ids }: { teamId: string; ids: string[] }) => {
|
||||
const apps = await MongoApp.find(
|
||||
{
|
||||
teamId,
|
||||
_id: { $in: ids }
|
||||
},
|
||||
'_id name avatar'
|
||||
).lean();
|
||||
|
||||
return apps.map((item) => ({
|
||||
id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar
|
||||
}));
|
||||
};
|
||||
|
@@ -1,6 +1,11 @@
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { appData2FlowNodeIO, pluginData2FlowNodeIO } from '@fastgpt/global/core/workflow/utils';
|
||||
import {
|
||||
appData2FlowNodeIO,
|
||||
pluginData2FlowNodeIO,
|
||||
toolData2FlowNodeIO,
|
||||
toolSetData2FlowNodeIO
|
||||
} from '@fastgpt/global/core/workflow/utils';
|
||||
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { getHandleConfig } from '@fastgpt/global/core/workflow/template/utils';
|
||||
@@ -128,11 +133,41 @@ export async function getChildAppPreviewNode({
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput
|
||||
);
|
||||
|
||||
const isTool =
|
||||
!!app.workflow.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool) &&
|
||||
app.workflow.nodes.length === 1;
|
||||
|
||||
const isToolSet =
|
||||
!!app.workflow.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet) &&
|
||||
app.workflow.nodes.length === 1;
|
||||
|
||||
const { flowNodeType, nodeIOConfig } = (() => {
|
||||
if (isToolSet)
|
||||
return {
|
||||
flowNodeType: FlowNodeTypeEnum.toolSet,
|
||||
nodeIOConfig: toolSetData2FlowNodeIO({ nodes: app.workflow.nodes })
|
||||
};
|
||||
if (isTool)
|
||||
return {
|
||||
flowNodeType: FlowNodeTypeEnum.tool,
|
||||
nodeIOConfig: toolData2FlowNodeIO({ nodes: app.workflow.nodes })
|
||||
};
|
||||
if (isPlugin)
|
||||
return {
|
||||
flowNodeType: FlowNodeTypeEnum.pluginModule,
|
||||
nodeIOConfig: pluginData2FlowNodeIO({ nodes: app.workflow.nodes })
|
||||
};
|
||||
return {
|
||||
flowNodeType: FlowNodeTypeEnum.appModule,
|
||||
nodeIOConfig: appData2FlowNodeIO({ chatConfig: app.workflow.chatConfig })
|
||||
};
|
||||
})();
|
||||
|
||||
return {
|
||||
id: getNanoid(),
|
||||
pluginId: app.id,
|
||||
templateType: app.templateType,
|
||||
flowNodeType: isPlugin ? FlowNodeTypeEnum.pluginModule : FlowNodeTypeEnum.appModule,
|
||||
flowNodeType,
|
||||
avatar: app.avatar,
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
@@ -141,11 +176,13 @@ export async function getChildAppPreviewNode({
|
||||
showStatus: app.showStatus,
|
||||
isTool: true,
|
||||
version: app.version,
|
||||
sourceHandle: getHandleConfig(true, true, true, true),
|
||||
targetHandle: getHandleConfig(true, true, true, true),
|
||||
...(isPlugin
|
||||
? pluginData2FlowNodeIO({ nodes: app.workflow.nodes })
|
||||
: appData2FlowNodeIO({ chatConfig: app.workflow.chatConfig }))
|
||||
sourceHandle: isToolSet
|
||||
? getHandleConfig(false, false, false, false)
|
||||
: getHandleConfig(true, true, true, true),
|
||||
targetHandle: isToolSet
|
||||
? getHandleConfig(false, false, false, false)
|
||||
: getHandleConfig(true, true, true, true),
|
||||
...nodeIOConfig
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ import axios from 'axios';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
|
||||
import { i18nT } from '../../../web/i18n/utils';
|
||||
import { addLog } from '../../common/system/log';
|
||||
import { getImageBase64 } from '../../common/file/image/utils';
|
||||
import { addEndpointToImageUrl, getImageBase64 } from '../../common/file/image/utils';
|
||||
|
||||
export const filterGPTMessageByMaxContext = async ({
|
||||
messages = [],
|
||||
@@ -87,26 +87,17 @@ export const loadRequestMessages = async ({
|
||||
useVision?: boolean;
|
||||
origin?: string;
|
||||
}) => {
|
||||
const replaceLinkUrl = (text: string) => {
|
||||
const baseURL = process.env.FE_DOMAIN;
|
||||
if (!baseURL) return text;
|
||||
// 匹配 /api/system/img/xxx.xx 的图片链接,并追加 baseURL
|
||||
return text.replace(
|
||||
/(?<!https?:\/\/[^\s]*)(?:\/api\/system\/img\/[^\s.]*\.[^\s]*)/g,
|
||||
(match) => `${baseURL}${match}`
|
||||
);
|
||||
};
|
||||
const parseSystemMessage = (
|
||||
content: string | ChatCompletionContentPartText[]
|
||||
): string | ChatCompletionContentPartText[] | undefined => {
|
||||
if (typeof content === 'string') {
|
||||
if (!content) return;
|
||||
return replaceLinkUrl(content);
|
||||
return addEndpointToImageUrl(content);
|
||||
}
|
||||
|
||||
const arrayContent = content
|
||||
.filter((item) => item.text)
|
||||
.map((item) => ({ ...item, text: replaceLinkUrl(item.text) }));
|
||||
.map((item) => ({ ...item, text: addEndpointToImageUrl(item.text) }));
|
||||
if (arrayContent.length === 0) return;
|
||||
return arrayContent;
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
@@ -70,7 +70,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
|
||||
appData.modules,
|
||||
getWorkflowEntryNodeIds(appData.modules)
|
||||
),
|
||||
runtimeEdges: initWorkflowEdgeStatus(appData.edges),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(appData.edges),
|
||||
histories: chatHistories,
|
||||
query: runtimePrompt2ChatsValue({
|
||||
files,
|
||||
|
@@ -22,7 +22,7 @@ import { formatModelChars2Points } from '../../../../../support/wallet/usage/uti
|
||||
import { getHistoryPreview } from '@fastgpt/global/core/chat/utils';
|
||||
import { runToolWithFunctionCall } from './functionCall';
|
||||
import { runToolWithPromptCall } from './promptCall';
|
||||
import { replaceVariable } from '@fastgpt/global/common/string/tools';
|
||||
import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools';
|
||||
import { getMultiplePrompt, Prompt_Tool_Call } from './constants';
|
||||
import { filterToolResponseToPreview } from './utils';
|
||||
import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
@@ -188,6 +188,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
if (toolModel.toolChoice) {
|
||||
return runToolWithToolChoice({
|
||||
...props,
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
toolNodes,
|
||||
toolModel,
|
||||
maxRunToolTimes: 30,
|
||||
@@ -198,6 +200,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
if (toolModel.functionCall) {
|
||||
return runToolWithFunctionCall({
|
||||
...props,
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
toolNodes,
|
||||
toolModel,
|
||||
messages: adaptMessages,
|
||||
@@ -226,6 +230,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
|
||||
|
||||
return runToolWithPromptCall({
|
||||
...props,
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
toolNodes,
|
||||
toolModel,
|
||||
messages: adaptMessages,
|
||||
|
@@ -17,6 +17,7 @@ import { MongoDataset } from '../../../dataset/schema';
|
||||
import { i18nT } from '../../../../../web/i18n/utils';
|
||||
import { filterDatasetsByTmbId } from '../../../dataset/utils';
|
||||
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
|
||||
import { addEndpointToImageUrl } from '../../../../common/file/image/utils';
|
||||
|
||||
type DatasetSearchProps = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType;
|
||||
@@ -246,7 +247,7 @@ export async function dispatchDatasetSearch(
|
||||
[DispatchNodeResponseKeyEnum.toolResponses]: searchRes.map((item) => ({
|
||||
sourceName: item.sourceName,
|
||||
updateTime: item.updateTime,
|
||||
content: `${item.q}\n${item.a}`.trim()
|
||||
content: addEndpointToImageUrl(`${item.q}\n${item.a}`.trim())
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
@@ -37,7 +37,8 @@ import { dispatchQueryExtension } from './tools/queryExternsion';
|
||||
import { dispatchRunPlugin } from './plugin/run';
|
||||
import { dispatchPluginInput } from './plugin/runInput';
|
||||
import { dispatchPluginOutput } from './plugin/runOutput';
|
||||
import { formatHttpError, removeSystemVariable, valueTypeFormat } from './utils';
|
||||
import { formatHttpError, removeSystemVariable, rewriteRuntimeWorkFlow } from './utils';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import {
|
||||
filterWorkflowEdges,
|
||||
checkNodeRunStatus,
|
||||
@@ -74,6 +75,7 @@ import { dispatchFormInput } from './interactive/formInput';
|
||||
import { dispatchToolParams } from './agent/runTool/toolParams';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { filterModuleTypeList } from '@fastgpt/global/core/chat/utils';
|
||||
import { dispatchRunTool } from './plugin/runTool';
|
||||
|
||||
const callbackMap: Record<FlowNodeTypeEnum, Function> = {
|
||||
[FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart,
|
||||
@@ -104,6 +106,7 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
|
||||
[FlowNodeTypeEnum.loopStart]: dispatchLoopStart,
|
||||
[FlowNodeTypeEnum.loopEnd]: dispatchLoopEnd,
|
||||
[FlowNodeTypeEnum.formInput]: dispatchFormInput,
|
||||
[FlowNodeTypeEnum.tool]: dispatchRunTool,
|
||||
|
||||
// none
|
||||
[FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig,
|
||||
@@ -111,6 +114,7 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
|
||||
[FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(),
|
||||
[FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(),
|
||||
[FlowNodeTypeEnum.comment]: () => Promise.resolve(),
|
||||
[FlowNodeTypeEnum.toolSet]: () => Promise.resolve(),
|
||||
|
||||
[FlowNodeTypeEnum.runApp]: dispatchAppRequest // abandoned
|
||||
};
|
||||
@@ -136,6 +140,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
...props
|
||||
} = data;
|
||||
|
||||
rewriteRuntimeWorkFlow(runtimeNodes, runtimeEdges);
|
||||
|
||||
// 初始化深度和自动增加深度,避免无限嵌套
|
||||
if (!props.workflowDispatchDeep) {
|
||||
props.workflowDispatchDeep = 1;
|
||||
@@ -643,9 +649,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
|
||||
) {
|
||||
props.workflowStreamResponse?.({
|
||||
event: SseResponseEventEnum.flowNodeResponse,
|
||||
data: {
|
||||
...formatResponseData
|
||||
}
|
||||
data: formatResponseData
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -17,19 +17,25 @@ type Response = DispatchNodeResultType<{
|
||||
export const dispatchWorkflowStart = (props: Record<string, any>): Response => {
|
||||
const {
|
||||
query,
|
||||
variables,
|
||||
params: { userChatInput }
|
||||
} = props as UserChatInputProps;
|
||||
|
||||
const { text, files } = chatValue2RuntimePrompt(query);
|
||||
|
||||
const queryFiles = files
|
||||
.map((item) => {
|
||||
return item?.url ?? '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
const variablesFiles: string[] = Array.isArray(variables?.fileUrlList)
|
||||
? variables.fileUrlList
|
||||
: [];
|
||||
|
||||
return {
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {},
|
||||
[NodeInputKeyEnum.userChatInput]: text || userChatInput,
|
||||
[NodeOutputKeyEnum.userFiles]: files
|
||||
.map((item) => {
|
||||
return item?.url ?? '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
[NodeOutputKeyEnum.userFiles]: [...queryFiles, ...variablesFiles]
|
||||
// [NodeInputKeyEnum.inputFiles]: files
|
||||
};
|
||||
};
|
||||
|
@@ -8,12 +8,18 @@ import { dispatchWorkFlow } from '..';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { AIChatItemValueItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
LoopInteractive,
|
||||
WorkflowInteractiveResponseType
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { storeEdges2RuntimeEdges } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.loopInputArray]: Array<any>;
|
||||
[NodeInputKeyEnum.childrenNodeIdList]: string[];
|
||||
}>;
|
||||
type Response = DispatchNodeResultType<{
|
||||
[DispatchNodeResponseKeyEnum.interactive]?: LoopInteractive;
|
||||
[NodeOutputKeyEnum.loopArray]: Array<any>;
|
||||
}>;
|
||||
|
||||
@@ -21,6 +27,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
const {
|
||||
params,
|
||||
runtimeEdges,
|
||||
lastInteractive,
|
||||
runtimeNodes,
|
||||
node: { name }
|
||||
} = props;
|
||||
@@ -29,6 +36,8 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
if (!Array.isArray(loopInputArray)) {
|
||||
return Promise.reject('Input value is not an array');
|
||||
}
|
||||
|
||||
// Max loop times
|
||||
const maxLength = process.env.WORKFLOW_MAX_LOOP_TIMES
|
||||
? Number(process.env.WORKFLOW_MAX_LOOP_TIMES)
|
||||
: 50;
|
||||
@@ -36,34 +45,63 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
return Promise.reject(`Input array length cannot be greater than ${maxLength}`);
|
||||
}
|
||||
|
||||
const outputValueArr = [];
|
||||
const loopDetail: ChatHistoryItemResType[] = [];
|
||||
const interactiveData =
|
||||
lastInteractive?.type === 'loopInteractive' ? lastInteractive?.params : undefined;
|
||||
const lastIndex = interactiveData?.currentIndex;
|
||||
|
||||
const outputValueArr = interactiveData ? interactiveData.loopResult : [];
|
||||
const loopResponseDetail: ChatHistoryItemResType[] = [];
|
||||
let assistantResponses: AIChatItemValueItemType[] = [];
|
||||
let totalPoints = 0;
|
||||
let newVariables: Record<string, any> = props.variables;
|
||||
|
||||
let interactiveResponse: WorkflowInteractiveResponseType | undefined = undefined;
|
||||
let index = 0;
|
||||
|
||||
for await (const item of loopInputArray.filter(Boolean)) {
|
||||
runtimeNodes.forEach((node) => {
|
||||
if (
|
||||
childrenNodeIdList.includes(node.nodeId) &&
|
||||
node.flowNodeType === FlowNodeTypeEnum.loopStart
|
||||
) {
|
||||
node.isEntry = true;
|
||||
node.inputs.forEach((input) => {
|
||||
if (input.key === NodeInputKeyEnum.loopStartInput) {
|
||||
input.value = item;
|
||||
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
|
||||
input.value = index++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Skip already looped
|
||||
if (lastIndex && index < lastIndex) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// It takes effect only once in current loop
|
||||
const isInteractiveResponseIndex = !!interactiveData && index === interactiveData?.currentIndex;
|
||||
|
||||
// Init entry
|
||||
if (isInteractiveResponseIndex) {
|
||||
runtimeNodes.forEach((node) => {
|
||||
if (interactiveData?.childrenResponse?.entryNodeIds.includes(node.nodeId)) {
|
||||
node.isEntry = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
runtimeNodes.forEach((node) => {
|
||||
if (!childrenNodeIdList.includes(node.nodeId)) return;
|
||||
|
||||
// Init interactive response
|
||||
if (node.flowNodeType === FlowNodeTypeEnum.loopStart) {
|
||||
node.isEntry = true;
|
||||
node.inputs.forEach((input) => {
|
||||
if (input.key === NodeInputKeyEnum.loopStartInput) {
|
||||
input.value = item;
|
||||
} else if (input.key === NodeInputKeyEnum.loopStartIndex) {
|
||||
input.value = index + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
const response = await dispatchWorkFlow({
|
||||
...props,
|
||||
lastInteractive: interactiveData?.childrenResponse,
|
||||
variables: newVariables,
|
||||
runtimeEdges: cloneDeep(runtimeEdges)
|
||||
runtimeNodes,
|
||||
runtimeEdges: cloneDeep(
|
||||
storeEdges2RuntimeEdges(runtimeEdges, interactiveData?.childrenResponse)
|
||||
)
|
||||
});
|
||||
|
||||
const loopOutputValue = response.flowResponses.find(
|
||||
@@ -71,8 +109,10 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
)?.loopOutputValue;
|
||||
|
||||
// Concat runtime response
|
||||
outputValueArr.push(loopOutputValue);
|
||||
loopDetail.push(...response.flowResponses);
|
||||
if (!response.workflowInteractiveResponse) {
|
||||
outputValueArr.push(loopOutputValue);
|
||||
}
|
||||
loopResponseDetail.push(...response.flowResponses);
|
||||
assistantResponses.push(...response.assistantResponses);
|
||||
totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0);
|
||||
|
||||
@@ -81,15 +121,32 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
|
||||
...newVariables,
|
||||
...response.newVariables
|
||||
};
|
||||
|
||||
// handle interactive response
|
||||
if (response.workflowInteractiveResponse) {
|
||||
interactiveResponse = response.workflowInteractiveResponse;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[DispatchNodeResponseKeyEnum.interactive]: interactiveResponse
|
||||
? {
|
||||
type: 'loopInteractive',
|
||||
params: {
|
||||
currentIndex: index - 1,
|
||||
childrenResponse: interactiveResponse,
|
||||
loopResult: outputValueArr
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
[DispatchNodeResponseKeyEnum.assistantResponses]: assistantResponses,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
totalPoints,
|
||||
loopInput: loopInputArray,
|
||||
loopResult: outputValueArr,
|
||||
loopDetail: loopDetail
|
||||
loopDetail: loopResponseDetail,
|
||||
mergeSignId: props.node.nodeId
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
|
||||
{
|
||||
|
@@ -5,7 +5,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
|
||||
import { getChildAppRuntimeById } from '../../../app/plugin/controller';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
@@ -101,7 +101,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
|
||||
}).value,
|
||||
chatConfig: {},
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(plugin.edges)
|
||||
runtimeEdges: storeEdges2RuntimeEdges(plugin.edges)
|
||||
});
|
||||
const output = flowResponses.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput);
|
||||
if (output) {
|
||||
|
@@ -5,7 +5,8 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
rewriteNodeOutputByHistories,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
@@ -107,9 +108,15 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
|
||||
lastInteractive?.type === 'childrenInteractive'
|
||||
? lastInteractive.params.childrenResponse
|
||||
: undefined;
|
||||
const entryNodeIds = getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined);
|
||||
const runtimeNodes = storeNodes2RuntimeNodes(nodes, entryNodeIds);
|
||||
const runtimeEdges = initWorkflowEdgeStatus(edges, childrenInteractive);
|
||||
const runtimeNodes = rewriteNodeOutputByHistories(
|
||||
storeNodes2RuntimeNodes(
|
||||
nodes,
|
||||
getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined)
|
||||
),
|
||||
childrenInteractive
|
||||
);
|
||||
|
||||
const runtimeEdges = storeEdges2RuntimeEdges(edges, childrenInteractive);
|
||||
const theQuery = childrenInteractive
|
||||
? query
|
||||
: runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput });
|
||||
@@ -170,7 +177,8 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
|
||||
totalPoints: usagePoints,
|
||||
query: userChatInput,
|
||||
textOutput: text,
|
||||
pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined
|
||||
pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined,
|
||||
mergeSignId: props.node.nodeId
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
|
||||
{
|
||||
|
60
packages/service/core/workflow/dispatch/plugin/runTool.ts
Normal file
60
packages/service/core/workflow/dispatch/plugin/runTool.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
DispatchNodeResultType,
|
||||
ModuleDispatchProps
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
type RunToolProps = ModuleDispatchProps<{
|
||||
toolData: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
type RunToolResponse = DispatchNodeResultType<{
|
||||
[NodeOutputKeyEnum.rawResponse]: any;
|
||||
}>;
|
||||
|
||||
export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolResponse> => {
|
||||
const {
|
||||
params,
|
||||
node: { avatar }
|
||||
} = props;
|
||||
|
||||
const { toolData, ...restParams } = params;
|
||||
const { name: toolName, url } = toolData;
|
||||
|
||||
const client = new Client({
|
||||
name: 'FastGPT-MCP-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const result = await (async () => {
|
||||
try {
|
||||
const transport = new SSEClientTransport(new URL(url));
|
||||
await client.connect(transport);
|
||||
|
||||
return await client.callTool({
|
||||
name: toolName,
|
||||
arguments: restParams
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running MCP tool:', error);
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: {
|
||||
toolRes: result,
|
||||
moduleLogo: avatar
|
||||
},
|
||||
[DispatchNodeResponseKeyEnum.toolResponses]: result,
|
||||
[NodeOutputKeyEnum.rawResponse]: result
|
||||
};
|
||||
};
|
@@ -10,7 +10,8 @@ import {
|
||||
SseResponseEventEnum
|
||||
} from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import axios from 'axios';
|
||||
import { formatHttpError, valueTypeFormat } from '../utils';
|
||||
import { formatHttpError } from '../utils';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
|
||||
import { addLog } from '../../../../common/system/log';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
|
@@ -2,7 +2,7 @@ import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import axios from 'axios';
|
||||
import { valueTypeFormat } from '../utils';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools';
|
||||
import { addLog } from '../../../../common/system/log';
|
||||
import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
|
@@ -10,8 +10,9 @@ import {
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
|
||||
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { removeSystemVariable, valueTypeFormat } from '../utils';
|
||||
import { removeSystemVariable } from '../utils';
|
||||
import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
||||
type Props = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.updateList]: TUpdateListItem[];
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
RuntimeEdgeItemType,
|
||||
RuntimeNodeItemType,
|
||||
SystemVariablesType
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { responseWrite } from '../../../common/response';
|
||||
@@ -14,7 +15,8 @@ import { NextApiResponse } from 'next';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import json5 from 'json5';
|
||||
import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
export const getWorkflowResponseWrite = ({
|
||||
res,
|
||||
@@ -104,102 +106,6 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
|
||||
return [...systemHistories, ...filterHistories];
|
||||
};
|
||||
|
||||
/* value type format */
|
||||
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
|
||||
// 1. 基础条件检查
|
||||
if (value === undefined || value === null) return;
|
||||
if (!type || type === WorkflowIOValueTypeEnum.any) return value;
|
||||
|
||||
// 2. 如果值已经符合目标类型,直接返回
|
||||
if (
|
||||
(type === WorkflowIOValueTypeEnum.string && typeof value === 'string') ||
|
||||
(type === WorkflowIOValueTypeEnum.number && typeof value === 'number') ||
|
||||
(type === WorkflowIOValueTypeEnum.boolean && typeof value === 'boolean') ||
|
||||
(type === WorkflowIOValueTypeEnum.object &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value)) ||
|
||||
(type.startsWith('array') && Array.isArray(value))
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3. 处理JSON字符串
|
||||
if (type === WorkflowIOValueTypeEnum.object || type.startsWith('array')) {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const trimmedValue = value.trim();
|
||||
const isJsonLike =
|
||||
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
|
||||
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'));
|
||||
|
||||
if (isJsonLike) {
|
||||
try {
|
||||
const parsed = json5.parse(trimmedValue);
|
||||
|
||||
// 解析结果与目标类型匹配时使用解析后的值
|
||||
if (
|
||||
(Array.isArray(parsed) && type.startsWith('array')) ||
|
||||
(type === WorkflowIOValueTypeEnum.object &&
|
||||
typeof parsed === 'object' &&
|
||||
!Array.isArray(parsed))
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
// 解析失败时继续使用原始值
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 按类型处理
|
||||
// 4.1 数组类型
|
||||
if (type.startsWith('array')) {
|
||||
// 数组类型的特殊处理:字符串转为单元素数组
|
||||
if (type === WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
// 其他值包装为数组
|
||||
return [value];
|
||||
}
|
||||
|
||||
// 4.2 基本类型转换
|
||||
if (type === WorkflowIOValueTypeEnum.string) {
|
||||
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
|
||||
if (type === WorkflowIOValueTypeEnum.number) {
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
if (type === WorkflowIOValueTypeEnum.boolean) {
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase() === 'true';
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
// 4.3 复杂对象类型处理
|
||||
if (
|
||||
[
|
||||
WorkflowIOValueTypeEnum.object,
|
||||
WorkflowIOValueTypeEnum.chatHistory,
|
||||
WorkflowIOValueTypeEnum.datasetQuote,
|
||||
WorkflowIOValueTypeEnum.selectApp,
|
||||
WorkflowIOValueTypeEnum.selectDataset
|
||||
].includes(type) &&
|
||||
typeof value !== 'object'
|
||||
) {
|
||||
try {
|
||||
return json5.parse(value);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 默认返回原值
|
||||
return value;
|
||||
};
|
||||
|
||||
export const checkQuoteQAValue = (quoteQA?: SearchDataResponseItemType[]) => {
|
||||
if (!quoteQA) return undefined;
|
||||
if (quoteQA.length === 0) {
|
||||
@@ -252,3 +158,53 @@ export const formatHttpError = (error: any) => {
|
||||
status: error?.status
|
||||
};
|
||||
};
|
||||
|
||||
export const rewriteRuntimeWorkFlow = (
|
||||
nodes: RuntimeNodeItemType[],
|
||||
edges: RuntimeEdgeItemType[]
|
||||
) => {
|
||||
const toolSetNodes = nodes.filter((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet);
|
||||
|
||||
if (toolSetNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeIdsToRemove = new Set<string>();
|
||||
|
||||
for (const toolSetNode of toolSetNodes) {
|
||||
nodeIdsToRemove.add(toolSetNode.nodeId);
|
||||
const toolList =
|
||||
toolSetNode.inputs.find((input) => input.key === 'toolSetData')?.value?.toolList || [];
|
||||
const url = toolSetNode.inputs.find((input) => input.key === 'toolSetData')?.value?.url;
|
||||
|
||||
const incomingEdges = edges.filter((edge) => edge.target === toolSetNode.nodeId);
|
||||
|
||||
for (const tool of toolList) {
|
||||
const newToolNode = getMCPToolRuntimeNode({ avatar: toolSetNode.avatar, tool, url });
|
||||
|
||||
nodes.push({ ...newToolNode, name: `${toolSetNode.name} / ${tool.name}` });
|
||||
|
||||
for (const inEdge of incomingEdges) {
|
||||
edges.push({
|
||||
source: inEdge.source,
|
||||
target: newToolNode.nodeId,
|
||||
sourceHandle: inEdge.sourceHandle,
|
||||
targetHandle: 'selectedTools',
|
||||
status: inEdge.status
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
if (nodeIdsToRemove.has(nodes[i].nodeId)) {
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = edges.length - 1; i >= 0; i--) {
|
||||
if (nodeIdsToRemove.has(edges[i].target)) {
|
||||
edges.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user