perf: sub agent output

This commit is contained in:
archer
2025-09-09 20:24:43 +08:00
parent bec13053aa
commit 843388ce44
5 changed files with 199 additions and 140 deletions

View File

@@ -9,7 +9,7 @@ import type {
RuntimeNodeItemType
} from '@fastgpt/global/core/workflow/runtime/type';
import { getLLMModel } from '../../../../ai/model';
import { getNodeErrResponse, getHistories } from '../../utils';
import { getNodeErrResponse, getHistories, getWorkflowChildResponseWrite } from '../../utils';
import { runAgentCall } from '../../../../ai/llm/agentCall';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { type ChatItemType } from '@fastgpt/global/core/chat/type';
@@ -48,7 +48,7 @@ import { getMasterAgentDefaultPrompt } from './constants';
import { addFilePrompt2Input, getFileInputPrompt } from './sub/file/utils';
import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { dispatchFileRead } from './sub/file';
import { dispatchApp } from './sub/app';
import { dispatchApp, dispatchPlugin } from './sub/app';
export type DispatchAgentModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[];
@@ -252,6 +252,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
handleToolResponse: async ({ call, messages }) => {
const toolId = call.function.name;
const childWorkflowStreamResponse = getWorkflowChildResponseWrite({
id: call.id,
fn: workflowStreamResponse
});
const {
response,
@@ -276,8 +280,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
stream,
onStreaming({ text }) {
//TODO: 需要一个新的 plan sse event
workflowStreamResponse?.({
id: call.id,
childWorkflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {
@@ -312,8 +315,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
systemPrompt,
task,
onStreaming({ text }) {
workflowStreamResponse?.({
id: call.id,
childWorkflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {
@@ -419,18 +421,32 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
runningUserInfo,
runningAppInfo,
variables,
workflowStreamResponse
workflowStreamResponse: childWorkflowStreamResponse
});
return {
response,
usages,
isEnd: false
};
} else if (node.flowNodeType === FlowNodeTypeEnum.appModule) {
const { response, usages } = await dispatchApp({
} else if (
node.flowNodeType === FlowNodeTypeEnum.appModule ||
node.flowNodeType === FlowNodeTypeEnum.pluginModule
) {
const fn =
node.flowNodeType === FlowNodeTypeEnum.appModule ? dispatchApp : dispatchPlugin;
console.log(requestParams, 22);
const { response, usages } = await fn({
...props,
node
node,
// stream: false,
workflowStreamResponse: undefined,
callParams: {
appId: node.pluginId,
version: node.version,
...requestParams
}
});
return {
response,
usages,
@@ -455,8 +471,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
// Push stream response
if (streamResponse) {
workflowStreamResponse?.({
id: call.id,
childWorkflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {

View File

@@ -1,17 +1,11 @@
import type { DispatchSubAppResponse } from '../../type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import {
filterSystemVariables,
getHistories
} from '../../../../../../../core/workflow/dispatch/utils';
import { filterSystemVariables } from '../../../../../../../core/workflow/dispatch/utils';
import { authAppByTmbId } from '../../../../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../../../../../core/app/version/controller';
import { getRunningUserInfoByTmbId } from '../../../../../../../support/user/team/utils';
import { runWorkflow } from '../../../../../../../core/workflow/dispatch';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
getWorkflowEntryNodeIds,
rewriteNodeOutputByHistories,
@@ -19,145 +13,181 @@ import {
storeNodes2RuntimeNodes,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
import { chatValue2RuntimePrompt, runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getUserChatInfoAndAuthTeamPoints } from '../../../../../../../support/permission/auth/team';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getChildAppRuntimeById } from '../../../../../../app/plugin/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
export type appConfigType = {
variables: Record<string, any>;
type Props = ModuleDispatchProps<{}> & {
callParams: {
appId?: string;
version?: string;
[key: string]: any;
};
};
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number;
[NodeInputKeyEnum.fileUrlList]?: string[];
[NodeInputKeyEnum.forbidStream]?: boolean;
[NodeInputKeyEnum.fileUrlList]?: string[];
}>;
export const dispatchApp = async (props: Props): Promise<DispatchSubAppResponse> => {
const {
runningAppInfo,
histories,
query,
lastInteractive,
node: { pluginId: appId, version },
workflowStreamResponse,
params,
variables,
stream
} = props;
const {
system_forbid_stream = false,
userChatInput,
history,
fileUrlList,
...childrenAppVariables
} = params;
const { files } = chatValue2RuntimePrompt(query);
const userInputFiles = (() => {
if (fileUrlList) {
return fileUrlList.map((url) => parseUrlToFileType(url)).filter(Boolean);
}
// Adapt version 4.8.13 upgrade
return files;
})();
if (!userChatInput && !userInputFiles) {
return Promise.reject(new Error('Input is empty'));
}
if (!appId) {
return Promise.reject(new Error('pluginId is empty'));
}
try {
// Auth the app by tmbId(Not the user, but the workflow user)
const { app: appData } = await authAppByTmbId({
appId: appId,
tmbId: runningAppInfo.tmbId,
per: ReadPermissionVal
});
const { nodes, edges, chatConfig } = await getAppVersionById({
callParams: {
appId,
versionId: version,
app: appData
});
const childStreamResponse = system_forbid_stream ? false : stream;
// Auto line
if (childStreamResponse) {
workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: '\n'
})
});
version,
userChatInput,
system_forbid_stream,
history,
fileUrlList,
...data
}
} = props;
const chatHistories = getHistories(history, histories);
if (!appId) {
return Promise.reject(new Error('AppId is empty'));
}
// Rewrite children app variables
const systemVariables = filterSystemVariables(variables);
const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(appData.tmbId);
const childrenRunVariables = {
...systemVariables,
...childrenAppVariables,
histories: chatHistories,
appId: String(appData._id),
...(externalProvider ? externalProvider.externalWorkflowVariables : {})
// Auth the app by tmbId(Not the user, but the workflow user)
const { app: appData } = await authAppByTmbId({
appId,
tmbId: runningAppInfo.tmbId,
per: ReadPermissionVal
});
const { nodes, edges, chatConfig } = await getAppVersionById({
appId,
versionId: version,
app: appData
});
// Rewrite children app variables
const systemVariables = filterSystemVariables(variables);
const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(appData.tmbId);
const childrenRunVariables = {
...systemVariables,
histories: [],
appId: String(appData._id),
...data,
...(externalProvider ? externalProvider.externalWorkflowVariables : {})
};
const runtimeNodes = rewriteNodeOutputByHistories(
storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes))
);
const runtimeEdges = storeEdges2RuntimeEdges(edges);
const { assistantResponses, flowUsages } = await runWorkflow({
...props,
runningAppInfo: {
id: String(appData._id),
teamId: String(appData.teamId),
tmbId: String(appData.tmbId),
isChildApp: true
},
runningUserInfo: await getRunningUserInfoByTmbId(appData.tmbId),
runtimeNodes,
runtimeEdges,
histories: [],
variables: childrenRunVariables,
query: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: userChatInput
}
}
],
chatConfig
});
const { text } = chatValue2RuntimePrompt(assistantResponses);
return {
response: text,
usages: flowUsages
};
};
export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppResponse> => {
const {
runningAppInfo,
callParams: { appId, version, system_forbid_stream, ...data }
} = props;
if (!appId) {
return Promise.reject(new Error('AppId is empty'));
}
// Auth the app by tmbId(Not the user, but the workflow user)
const {
app: { tmbId }
} = await authAppByTmbId({
appId,
tmbId: runningAppInfo.tmbId,
per: ReadPermissionVal
});
const plugin = await getChildAppRuntimeById({ id: appId, versionId: version });
const outputFilterMap =
plugin.nodes
.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput)
?.inputs.reduce<Record<string, boolean>>((acc, cur) => {
acc[cur.key] = cur.isToolOutput === false ? false : true;
return acc;
}, {}) ?? {};
const runtimeNodes = storeNodes2RuntimeNodes(
plugin.nodes,
getWorkflowEntryNodeIds(plugin.nodes)
).map((node) => {
// Update plugin input value
if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) {
return {
...node,
showStatus: false,
inputs: node.inputs.map((input) => ({
...input,
value: data[input.key] ?? input.value
}))
};
}
return {
...node,
showStatus: false
};
});
const childrenInteractive =
lastInteractive?.type === 'childrenInteractive'
? lastInteractive.params.childrenResponse
: undefined;
const runtimeNodes = rewriteNodeOutputByHistories(
storeNodes2RuntimeNodes(
nodes,
getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined)
),
childrenInteractive
);
const { externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const runtimeVariables = {
...filterSystemVariables(props.variables),
appId: String(plugin.id),
...(externalProvider ? externalProvider.externalWorkflowVariables : {})
};
const runtimeEdges = storeEdges2RuntimeEdges(edges, childrenInteractive);
const theQuery = childrenInteractive
? query
: runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput });
const { assistantResponses } = await runWorkflow({
const { flowResponses, flowUsages, assistantResponses, runTimes, system_memories } =
await runWorkflow({
...props,
lastInteractive: childrenInteractive,
// Rewrite stream mode
...(system_forbid_stream
? {
stream: false,
workflowStreamResponse: undefined
}
: {}),
runningAppInfo: {
id: String(appData._id),
teamId: String(appData.teamId),
tmbId: String(appData.tmbId),
id: String(plugin.id),
// 如果系统插件有 teamId 和 tmbId则使用系统插件的 teamId 和 tmbId管理员指定了插件作为系统插件
teamId: plugin.teamId || runningAppInfo.teamId,
tmbId: plugin.tmbId || runningAppInfo.tmbId,
isChildApp: true
},
runningUserInfo: await getRunningUserInfoByTmbId(appData.tmbId),
variables: runtimeVariables,
query: getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(plugin.nodes),
variables: runtimeVariables
}).value,
chatConfig: {},
runtimeNodes,
runtimeEdges,
histories: chatHistories,
variables: childrenRunVariables,
query: theQuery,
chatConfig
runtimeEdges: storeEdges2RuntimeEdges(plugin.edges)
});
const output = flowResponses.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput);
const response = output?.pluginOutput ? JSON.stringify(output?.pluginOutput) : 'No output';
const { text } = chatValue2RuntimePrompt(assistantResponses);
return {
response: text,
usages: []
};
} catch (error) {
return Promise.reject('dispatch app failed');
}
return {
response,
usages: flowUsages
};
};

View File

@@ -70,6 +70,18 @@ export const getWorkflowResponseWrite = ({
};
return fn;
};
export const getWorkflowChildResponseWrite = ({
id,
fn
}: {
id: string;
fn?: WorkflowResponseType;
}): WorkflowResponseType | undefined => {
if (!fn) return;
return (e: Parameters<WorkflowResponseType>[0]) => {
return fn({ ...e, id });
};
};
export const filterToolNodeIdByEdges = ({
nodeId,

View File

@@ -318,6 +318,7 @@ const ChatBox = ({
}
} else if (event === SseResponseEventEnum.toolCall && tool) {
const val: AIChatItemValueItemType = {
id: responseValueId,
type: ChatItemValueTypeEnum.tool,
tools: [tool]
};

View File

@@ -20,6 +20,7 @@ import SecretInputModal, {
} from '@/pageComponents/app/plugin/SecretInputModal';
import { SystemToolInputTypeMap } from '@fastgpt/global/core/app/systemTool/constants';
import { useBoolean } from 'ahooks';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const ConfigToolModal = ({
configTool,
@@ -30,7 +31,7 @@ const ConfigToolModal = ({
onCloseConfigTool: () => void;
onAddTool: (tool: AppFormEditFormType['selectedTools'][number]) => void;
}) => {
const { t } = useTranslation();
const { t } = useSafeTranslation();
const [isOpenSecretModal, { setTrue: setTrueSecretModal, setFalse: setFalseSecretModal }] =
useBoolean(false);
@@ -86,8 +87,8 @@ const ConfigToolModal = ({
<Box key={input.key} _notLast={{ mb: 4 }}>
<Flex alignItems={'center'} mb={1}>
{input.required && <Box color={'red.500'}>*</Box>}
<FormLabel>{input.label}</FormLabel>
{input.description && <QuestionTip ml={1} label={input.description} />}
<FormLabel>{t(input.label)}</FormLabel>
{input.description && <QuestionTip ml={1} label={t(input.description)} />}
</Flex>
{input.key === NodeInputKeyEnum.systemInputConfig && input.inputList ? (