Skip to content

Commit e6f388e

Browse files
authored
Support nested node interaction (#4503)
* feat: Add a new InteractiveContext type and update InteractiveBasicType, adding an optional context property to support more complex interaction state management. * feat: Enhance workflow interactivity by adding InteractiveContext support and updating dispatch logic to manage nested contexts and entry nodes more effectively. * feat: Refactor dispatchWorkFlow to utilize InteractiveContext for improved context management * feat: Enhance entry node resolution by adding validation for entryNodeIds and recursive search in InteractiveContext * feat: Remove workflowDepth from InteractiveContext and update recovery logic to utilize parentContext for improved context management * feat: Update getWorkflowEntryNodeIds to use lastInteractive for improved context handling in runtime nodes * feat: Add lastInteractive support to enhance context management across workflow components * feat: Enhance interactive workflow by adding stopForInteractive flag and improving memory edge validation in runtime logic * feat: Refactor InteractiveContext by removing interactiveAppId and updating runtime edge handling in dispatchRunApp for improved context management * feat: Simplify runtime node and edge initialization in dispatchRunApp by using ternary operators for improved readability and maintainability * feat: Improve memory edge validation in initWorkflowEdgeStatus by adding detailed comments for better understanding of subset checks and recursive context searching * feat: Remove commented-out current level information from InteractiveContext for cleaner code and improved readability * feat: Simplify stopForInteractive check in dispatchWorkFlow for improved code clarity and maintainability * feat: Remove stopForInteractive handling and related references for improved code clarity and maintainability * feat: Add interactive response handling in dispatchRunAppNode for enhanced workflow interactivity * feat: Add context property to InteractiveBasicType and InteractiveNodeType for improved interactivity management * feat: remove comments * feat: Remove the node property from ChatDispatchProps to simplify type definitions * feat: Remove workflowInteractiveResponse from dispatchRunAppNode for cleaner code * feat: Refactor interactive value handling in chat history processing for improved clarity * feat: Simplify initWorkflowEdgeStatus logic for better readability and maintainability * feat: Add workflowInteractiveResponse to dispatchWorkFlow for enhanced functionality * feat: Enhance interactive response handling with nested children support * feat: Remove commented-out code for interactive node handling to improve clarity * feat: remove InteractiveContext type * feat: Refactor UserSelectInteractive and UserInputInteractive params for improved structure and clarity * feat: remove * feat: The front end supports extracting the deepest interaction parameters to enhance interaction processing * feat: The front end supports extracting the deepest interaction parameters to enhance interaction processing * fix: handle undefined interactive values in runtimeEdges and runtimeNodes initialization * fix: handle undefined interactive values in runtimeNodes and runtimeEdges initialization * fix: update runtimeNodes and runtimeEdges initialization to use last interactive value * fix: remove unused imports and replace getLastInteractiveValue with lastInteractive in runtimeEdges initialization * fix: import WorkflowInteractiveResponseType and handle lastInteractive as undefined in chatTest * feat: implement extractDeepestInteractive function and refactor usage in AIResponseBox and ChatBox utils * fix: refactor initWorkflowEdgeStatus and getWorkflowEntryNodeIds calls in dispatchRunAppNode for recovery handling * fix: ensure lastInteractive is handled consistently as undefined in runtimeEdges and runtimeNodes initialization * fix: update dispatchFormInput and dispatchUserSelect to use lastInteractive consistently * fix: update condition checks in dispatchFormInput and dispatchUserSelect to ensure lastInteractive type is validated correctly * fix: refactor dispatchRunAppNode to replace isRecovery with childrenInteractive for improved clarity in runtimeNodes and runtimeEdges initialization * refactor: streamline runtimeNodes and runtimeEdges initialization in dispatchRunAppNode for improved readability and maintainability * fix: update rewriteNodeOutputByHistories function to accept runtimeNodes and interactive as parameters for improved clarity * fix: simplify interactiveResponse assignment in dispatchWorkFlow for improved clarity * fix: update entryNodeIds check in getWorkflowEntryNodeIds to ensure it's an array for improved reliability
1 parent c9e12bb commit e6f388e

File tree

13 files changed

+132
-87
lines changed

13 files changed

+132
-87
lines changed

packages/global/core/workflow/runtime/type.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { WorkflowResponseType } from '../../../../service/core/workflow/dispatch
2323
import { AiChatQuoteRoleType } from '../template/system/aiChat/type';
2424
import { LafAccountType, OpenaiAccountType } from '../../../support/user/team/type';
2525
import { CompletionFinishReason } from '../../ai/type';
26-
26+
import { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
2727
export type ExternalProviderType = {
2828
openaiAccount?: OpenaiAccountType;
2929
externalWorkflowVariables?: Record<string, string>;
@@ -55,6 +55,7 @@ export type ChatDispatchProps = {
5555
variables: Record<string, any>; // global variable
5656
query: UserChatItemValueItemType[]; // trigger query
5757
chatConfig: AppSchema['chatConfig'];
58+
lastInteractive?: WorkflowInteractiveResponseType; // last interactive response
5859
stream: boolean;
5960
maxRunTimes: number;
6061
isToolCall?: boolean;

packages/global/core/workflow/runtime/utils.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
1010
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
1111
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
1212
import { replaceVariable, valToStr } from '../../../common/string/tools';
13-
13+
import {
14+
InteractiveNodeResponseType,
15+
WorkflowInteractiveResponseType
16+
} from '../template/system/interactive/type';
17+
18+
export const extractDeepestInteractive = (
19+
interactive: WorkflowInteractiveResponseType
20+
): WorkflowInteractiveResponseType => {
21+
if (interactive?.type === 'childrenInteractive' && interactive.params?.childrenResponse) {
22+
return extractDeepestInteractive(interactive.params.childrenResponse);
23+
}
24+
return interactive;
25+
};
1426
export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
1527
let limit = 10;
1628
nodes.forEach((node) => {
@@ -48,6 +60,10 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
4860
return null;
4961
}
5062

63+
if (lastValue.interactive.type === 'childrenInteractive') {
64+
return lastValue.interactive;
65+
}
66+
5167
// Check is user select
5268
if (
5369
lastValue.interactive.type === 'userSelect' &&
@@ -66,34 +82,25 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
6682
};
6783

6884
export const initWorkflowEdgeStatus = (
69-
edges: StoreEdgeItemType[] | RuntimeEdgeItemType[],
70-
histories?: ChatItemType[]
85+
edges: StoreEdgeItemType[],
86+
lastInteractive?: WorkflowInteractiveResponseType
7187
): RuntimeEdgeItemType[] => {
72-
// If there is a history, use the last interactive value
73-
if (histories && histories.length > 0) {
74-
const memoryEdges = getLastInteractiveValue(histories)?.memoryEdges;
75-
88+
if (lastInteractive) {
89+
const memoryEdges = lastInteractive.memoryEdges || [];
7690
if (memoryEdges && memoryEdges.length > 0) {
7791
return memoryEdges;
7892
}
7993
}
8094

81-
return (
82-
edges?.map((edge) => ({
83-
...edge,
84-
status: 'waiting'
85-
})) || []
86-
);
95+
return edges?.map((edge) => ({ ...edge, status: 'waiting' })) || [];
8796
};
8897

8998
export const getWorkflowEntryNodeIds = (
9099
nodes: (StoreNodeItemType | RuntimeNodeItemType)[],
91-
histories?: ChatItemType[]
100+
lastInteractive?: WorkflowInteractiveResponseType
92101
) => {
93-
// If there is a history, use the last interactive entry node
94-
if (histories && histories.length > 0) {
95-
const entryNodeIds = getLastInteractiveValue(histories)?.entryNodeIds;
96-
102+
if (lastInteractive) {
103+
const entryNodeIds = lastInteractive.entryNodeIds || [];
97104
if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) {
98105
return entryNodeIds;
99106
}
@@ -396,10 +403,10 @@ export const textAdaptGptResponse = ({
396403

397404
/* Update runtimeNode's outputs with interactive data from history */
398405
export function rewriteNodeOutputByHistories(
399-
histories: ChatItemType[],
400-
runtimeNodes: RuntimeNodeItemType[]
406+
runtimeNodes: RuntimeNodeItemType[],
407+
lastInteractive?: InteractiveNodeResponseType
401408
) {
402-
const interactive = getLastInteractiveValue(histories);
409+
const interactive = lastInteractive;
403410
if (!interactive?.nodeOutputs) {
404411
return runtimeNodes;
405412
}

packages/global/core/workflow/template/system/interactive/type.d.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { NodeOutputItemType } from '../../../../chat/type';
22
import type { FlowNodeOutputItemType } from '../../../type/io';
3-
import type { RuntimeEdgeItemType } from '../../../runtime/type';
43
import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
54
import { WorkflowIOValueTypeEnum } from 'core/workflow/constants';
65
import type { ChatCompletionMessageParam } from '../../../../ai/type';
@@ -9,7 +8,6 @@ type InteractiveBasicType = {
98
entryNodeIds: string[];
109
memoryEdges: RuntimeEdgeItemType[];
1110
nodeOutputs: NodeOutputItemType[];
12-
1311
toolParams?: {
1412
entryNodeIds: string[]; // 记录工具中,交互节点的 Id,而不是起始工作流的入口
1513
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
@@ -23,13 +21,21 @@ type InteractiveNodeType = {
2321
nodeOutputs?: NodeOutputItemType[];
2422
};
2523

24+
type ChildrenInteractive = InteractiveNodeType & {
25+
type: 'childrenInteractive';
26+
params: {
27+
childrenResponse?: WorkflowInteractiveResponseType;
28+
};
29+
};
30+
2631
export type UserSelectOptionItemType = {
2732
key: string;
2833
value: string;
2934
};
3035
type UserSelectInteractive = InteractiveNodeType & {
3136
type: 'userSelect';
3237
params: {
38+
childrenResponse?: WorkflowInteractiveResponseType;
3339
description: string;
3440
userSelectOptions: UserSelectOptionItemType[];
3541
userSelectedVal?: string;
@@ -57,10 +63,15 @@ export type UserInputFormItemType = {
5763
type UserInputInteractive = InteractiveNodeType & {
5864
type: 'userInput';
5965
params: {
66+
childrenResponse?: WorkflowInteractiveResponseType;
6067
description: string;
6168
inputForm: UserInputFormItemType[];
6269
submitted?: boolean;
6370
};
6471
};
65-
export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive;
72+
73+
export type InteractiveNodeResponseType =
74+
| UserSelectInteractive
75+
| UserInputInteractive
76+
| ChildrenInteractive;
6677
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;

packages/service/core/workflow/dispatch/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
374374
};
375375

376376
// Tool call, not need interactive response
377-
if (!props.isToolCall) {
377+
if (!props.isToolCall && !props.runningAppInfo.isChildApp) {
378378
props.workflowStreamResponse?.({
379379
event: SseResponseEventEnum.interactive,
380380
data: { interactive: interactiveResult }
@@ -721,7 +721,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
721721
entryNodeIds: nodeInteractiveResponse.entryNodeIds,
722722
interactiveResponse: nodeInteractiveResponse.interactiveResponse
723723
});
724-
chatAssistantResponse.push(interactiveAssistant);
724+
if (!props.runningAppInfo.isChildApp) {
725+
chatAssistantResponse.push(interactiveAssistant);
726+
}
725727
return interactiveAssistant.interactive;
726728
}
727729
})();

packages/service/core/workflow/dispatch/interactive/formInput.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type {
1010
UserInputInteractive
1111
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
1212
import { addLog } from '../../../../common/system/log';
13-
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
1413

1514
type Props = ModuleDispatchProps<{
1615
[NodeInputKeyEnum.description]: string;
@@ -29,13 +28,13 @@ export const dispatchFormInput = async (props: Props): Promise<FormInputResponse
2928
histories,
3029
node,
3130
params: { description, userInputForms },
32-
query
31+
query,
32+
lastInteractive
3333
} = props;
3434
const { isEntry } = node;
35-
const interactive = getLastInteractiveValue(histories);
3635

3736
// Interactive node is not the entry node, return interactive result
38-
if (!isEntry || interactive?.type !== 'userInput') {
37+
if (!isEntry || lastInteractive?.type !== 'userInput') {
3938
return {
4039
[DispatchNodeResponseKeyEnum.interactive]: {
4140
type: 'userInput',

packages/service/core/workflow/dispatch/interactive/userSelect.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type {
1010
UserSelectOptionItemType
1111
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
1212
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
13-
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
1413

1514
type Props = ModuleDispatchProps<{
1615
[NodeInputKeyEnum.description]: string;
@@ -27,13 +26,13 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
2726
histories,
2827
node,
2928
params: { description, userSelectOptions },
30-
query
29+
query,
30+
lastInteractive
3131
} = props;
3232
const { nodeId, isEntry } = node;
33-
const interactive = getLastInteractiveValue(histories);
3433

3534
// Interactive node is not the entry node, return interactive result
36-
if (!isEntry || interactive?.type !== 'userSelect') {
35+
if (!isEntry || lastInteractive?.type !== 'userSelect') {
3736
return {
3837
[DispatchNodeResponseKeyEnum.interactive]: {
3938
type: 'userSelect',

packages/service/core/workflow/dispatch/plugin/runApp.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { authAppByTmbId } from '../../../../support/permission/app/auth';
1818
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
1919
import { getAppVersionById } from '../../../app/version/controller';
2020
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
21+
import { ChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
2122

2223
type Props = ModuleDispatchProps<{
2324
[NodeInputKeyEnum.userChatInput]: string;
@@ -27,6 +28,7 @@ type Props = ModuleDispatchProps<{
2728
[NodeInputKeyEnum.fileUrlList]?: string[];
2829
}>;
2930
type Response = DispatchNodeResultType<{
31+
[DispatchNodeResponseKeyEnum.interactive]?: ChildrenInteractive;
3032
[NodeOutputKeyEnum.answerText]: string;
3133
[NodeOutputKeyEnum.history]: ChatItemType[];
3234
}>;
@@ -36,6 +38,7 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
3638
runningAppInfo,
3739
histories,
3840
query,
41+
lastInteractive,
3942
node: { pluginId: appId, version },
4043
workflowStreamResponse,
4144
params,
@@ -100,31 +103,38 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
100103
appId: String(appData._id)
101104
};
102105

103-
const { flowResponses, flowUsages, assistantResponses, runTimes } = await dispatchWorkFlow({
104-
...props,
105-
// Rewrite stream mode
106-
...(system_forbid_stream
107-
? {
108-
stream: false,
109-
workflowStreamResponse: undefined
110-
}
111-
: {}),
112-
runningAppInfo: {
113-
id: String(appData._id),
114-
teamId: String(appData.teamId),
115-
tmbId: String(appData.tmbId),
116-
isChildApp: true
117-
},
118-
runtimeNodes: storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)),
119-
runtimeEdges: initWorkflowEdgeStatus(edges),
120-
histories: chatHistories,
121-
variables: childrenRunVariables,
122-
query: runtimePrompt2ChatsValue({
123-
files: userInputFiles,
124-
text: userChatInput
125-
}),
126-
chatConfig
127-
});
106+
const childrenInteractive = lastInteractive?.params?.childrenResponse;
107+
const entryNodeIds = getWorkflowEntryNodeIds(nodes, childrenInteractive || undefined);
108+
const runtimeNodes = storeNodes2RuntimeNodes(nodes, entryNodeIds);
109+
const runtimeEdges = initWorkflowEdgeStatus(edges, childrenInteractive);
110+
const theQuery = childrenInteractive
111+
? query
112+
: runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput });
113+
114+
const { flowResponses, flowUsages, assistantResponses, runTimes, workflowInteractiveResponse } =
115+
await dispatchWorkFlow({
116+
...props,
117+
lastInteractive: childrenInteractive,
118+
// Rewrite stream mode
119+
...(system_forbid_stream
120+
? {
121+
stream: false,
122+
workflowStreamResponse: undefined
123+
}
124+
: {}),
125+
runningAppInfo: {
126+
id: String(appData._id),
127+
teamId: String(appData.teamId),
128+
tmbId: String(appData.tmbId),
129+
isChildApp: true
130+
},
131+
runtimeNodes,
132+
runtimeEdges,
133+
histories: chatHistories,
134+
variables: childrenRunVariables,
135+
query: theQuery,
136+
chatConfig
137+
});
128138

129139
const completeMessages = chatHistories.concat([
130140
{
@@ -142,6 +152,14 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
142152
const usagePoints = flowUsages.reduce((sum, item) => sum + (item.totalPoints || 0), 0);
143153

144154
return {
155+
[DispatchNodeResponseKeyEnum.interactive]: workflowInteractiveResponse
156+
? {
157+
type: 'childrenInteractive',
158+
params: {
159+
childrenResponse: workflowInteractiveResponse
160+
}
161+
}
162+
: undefined,
145163
assistantResponses: system_forbid_stream ? [] : assistantResponses,
146164
[DispatchNodeResponseKeyEnum.runTimes]: runTimes,
147165
[DispatchNodeResponseKeyEnum.nodeResponse]: {

projects/app/src/components/core/chat/ChatContainer/ChatBox/utils.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { ChatBoxInputType, UserInputFileItemType } from './type';
77
import { getFileIcon } from '@fastgpt/global/common/file/icon';
88
import { ChatItemValueTypeEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
9+
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
910

1011
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
1112
if (!value) {
@@ -84,29 +85,29 @@ export const setUserSelectResultToHistories = (
8485
!val.interactive
8586
)
8687
return val;
87-
88-
if (val.interactive.type === 'userSelect') {
88+
const finalInteractive = extractDeepestInteractive(val.interactive);
89+
if (finalInteractive.type === 'userSelect') {
8990
return {
9091
...val,
9192
interactive: {
92-
...val.interactive,
93+
...finalInteractive,
9394
params: {
94-
...val.interactive.params,
95-
userSelectedVal: val.interactive.params.userSelectOptions.find(
96-
(item) => item.value === interactiveVal
95+
...finalInteractive.params,
96+
userSelectedVal: finalInteractive.params.userSelectOptions.find(
97+
(item: { value: string }) => item.value === interactiveVal
9798
)?.value
9899
}
99100
}
100101
};
101102
}
102103

103-
if (val.interactive.type === 'userInput') {
104+
if (finalInteractive.type === 'userInput') {
104105
return {
105106
...val,
106107
interactive: {
107-
...val.interactive,
108+
...finalInteractive,
108109
params: {
109-
...val.interactive.params,
110+
...finalInteractive.params,
110111
submitted: true
111112
}
112113
}

0 commit comments

Comments
 (0)