Skip to content

Commit 2212dcd

Browse files
c121914yusd0ric4
andauthored
Test interactive (#4509)
* 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 * remove some invalid code --------- Co-authored-by: Theresa <[email protected]>
1 parent b5568d4 commit 2212dcd

File tree

14 files changed

+142
-93
lines changed

14 files changed

+142
-93
lines changed

docSite/content/zh-cn/docs/development/upgrading/495.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ weight: 795
1111
## 🚀 新增内容
1212

1313
1. 团队成员权限细分,可分别控制是否可创建在根目录应用/知识库以及 API Key
14+
2. 支持交互节点在嵌套工作流中使用。
1415

1516
## ⚙️ 优化
1617

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: 33 additions & 24 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) => {
@@ -34,7 +46,9 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
3446
1. Get the interactive data
3547
2. Check that the workflow starts at the interaction node
3648
*/
37-
export const getLastInteractiveValue = (histories: ChatItemType[]) => {
49+
export const getLastInteractiveValue = (
50+
histories: ChatItemType[]
51+
): WorkflowInteractiveResponseType | undefined => {
3852
const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI);
3953

4054
if (lastAIMessage) {
@@ -45,7 +59,11 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
4559
lastValue.type !== ChatItemValueTypeEnum.interactive ||
4660
!lastValue.interactive
4761
) {
48-
return null;
62+
return;
63+
}
64+
65+
if (lastValue.interactive.type === 'childrenInteractive') {
66+
return lastValue.interactive;
4967
}
5068

5169
// Check is user select
@@ -62,38 +80,29 @@ export const getLastInteractiveValue = (histories: ChatItemType[]) => {
6280
}
6381
}
6482

65-
return null;
83+
return;
6684
};
6785

6886
export const initWorkflowEdgeStatus = (
69-
edges: StoreEdgeItemType[] | RuntimeEdgeItemType[],
70-
histories?: ChatItemType[]
87+
edges: StoreEdgeItemType[],
88+
lastInteractive?: WorkflowInteractiveResponseType
7189
): 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-
90+
if (lastInteractive) {
91+
const memoryEdges = lastInteractive.memoryEdges || [];
7692
if (memoryEdges && memoryEdges.length > 0) {
7793
return memoryEdges;
7894
}
7995
}
8096

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

89100
export const getWorkflowEntryNodeIds = (
90101
nodes: (StoreNodeItemType | RuntimeNodeItemType)[],
91-
histories?: ChatItemType[]
102+
lastInteractive?: WorkflowInteractiveResponseType
92103
) => {
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-
104+
if (lastInteractive) {
105+
const entryNodeIds = lastInteractive.entryNodeIds || [];
97106
if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) {
98107
return entryNodeIds;
99108
}
@@ -396,10 +405,10 @@ export const textAdaptGptResponse = ({
396405

397406
/* Update runtimeNode's outputs with interactive data from history */
398407
export function rewriteNodeOutputByHistories(
399-
histories: ChatItemType[],
400-
runtimeNodes: RuntimeNodeItemType[]
408+
runtimeNodes: RuntimeNodeItemType[],
409+
lastInteractive?: InteractiveNodeResponseType
401410
) {
402-
const interactive = getLastInteractiveValue(histories);
411+
const interactive = lastInteractive;
403412
if (!interactive?.nodeOutputs) {
404413
return runtimeNodes;
405414
}

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

Lines changed: 12 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,6 +21,13 @@ 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;
@@ -62,5 +67,9 @@ type UserInputInteractive = InteractiveNodeType & {
6267
submitted?: boolean;
6368
};
6469
};
65-
export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive;
70+
71+
export type InteractiveNodeResponseType =
72+
| UserSelectInteractive
73+
| UserInputInteractive
74+
| ChildrenInteractive;
6675
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: 11 additions & 8 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) {
@@ -82,31 +83,33 @@ export const setUserSelectResultToHistories = (
8283
i !== item.value.length - 1 ||
8384
val.type !== ChatItemValueTypeEnum.interactive ||
8485
!val.interactive
85-
)
86+
) {
8687
return val;
88+
}
8789

88-
if (val.interactive.type === 'userSelect') {
90+
const finalInteractive = extractDeepestInteractive(val.interactive);
91+
if (finalInteractive.type === 'userSelect') {
8992
return {
9093
...val,
9194
interactive: {
92-
...val.interactive,
95+
...finalInteractive,
9396
params: {
94-
...val.interactive.params,
95-
userSelectedVal: val.interactive.params.userSelectOptions.find(
97+
...finalInteractive.params,
98+
userSelectedVal: finalInteractive.params.userSelectOptions.find(
9699
(item) => item.value === interactiveVal
97100
)?.value
98101
}
99102
}
100103
};
101104
}
102105

103-
if (val.interactive.type === 'userInput') {
106+
if (finalInteractive.type === 'userInput') {
104107
return {
105108
...val,
106109
interactive: {
107-
...val.interactive,
110+
...finalInteractive,
108111
params: {
109-
...val.interactive.params,
112+
...finalInteractive.params,
110113
submitted: true
111114
}
112115
}

0 commit comments

Comments
 (0)