Skip to content

Commit

Permalink
User select node (#2397)
Browse files Browse the repository at this point in the history
* feat: add user select node (#2300)

* feat: add user select node

* fix

* type

* fix

* fix

* fix

* perf: user select code

* perf: user select histories

* perf: i18n

---------

Co-authored-by: heheer <[email protected]>
  • Loading branch information
c121914yu and newfish-cmyk authored Aug 15, 2024
1 parent f8b8fcc commit fdeb159
Show file tree
Hide file tree
Showing 51 changed files with 1,056 additions and 180 deletions.
2 changes: 2 additions & 0 deletions packages/global/core/ai/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ChatCompletionUserMessageParam as SdkChatCompletionUserMessageParam
} from 'openai/resources';
import { ChatMessageTypeEnum } from './constants';
import { InteractiveNodeResponseItemType } from '../workflow/template/system/userSelect/type';

export * from 'openai/resources';

Expand All @@ -33,6 +34,7 @@ export type ChatCompletionMessageParam = (
| CustomChatCompletionUserMessageParam
) & {
dataId?: string;
interactive?: InteractiveNodeResponseItemType;
};
export type SdkChatCompletionMessageParam = SdkChatCompletionMessageParam;

Expand Down
13 changes: 13 additions & 0 deletions packages/global/core/chat/adapt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ export const chats2GPTMessages = ({
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: value.text.content
});
} else if (value.type === ChatItemValueTypeEnum.interactive) {
results = results.concat({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Assistant,
interactive: value.interactive,
content: ''
});
}
});
}
Expand Down Expand Up @@ -254,6 +261,12 @@ export const GPTMessages2Chats = (
]
});
}
} else if (item.interactive) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.interactive,
interactive: item.interactive
});
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/global/core/chat/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export enum ChatFileTypeEnum {
export enum ChatItemValueTypeEnum {
text = 'text',
file = 'file',
tool = 'tool'
tool = 'tool',
interactive = 'interactive'
}

export enum ChatSourceEnum {
Expand Down
11 changes: 10 additions & 1 deletion packages/global/core/chat/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d';
import { DatasetSearchModeEnum } from '../dataset/constants';
import { DispatchNodeResponseType } from '../workflow/runtime/type.d';
import { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type';
import { InteractiveNodeResponseItemType } from '../workflow/template/system/userSelect/type';

export type ChatSchema = {
_id: string;
Expand Down Expand Up @@ -67,11 +68,12 @@ export type SystemChatItemType = {
value: SystemChatItemValueItemType[];
};
export type AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.tool;
type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.tool | ChatItemValueTypeEnum.interactive;
text?: {
content: string;
};
tools?: ToolModuleResponseItemType[];
interactive?: InteractiveNodeResponseItemType;
};
export type AIChatItemType = {
obj: ChatRoleEnum.AI;
Expand Down Expand Up @@ -153,6 +155,13 @@ export type ChatHistoryItemResType = DispatchNodeResponseType & {
moduleName: string;
};

/* ---------- node outputs ------------ */
export type NodeOutputItemType = {
nodeId: string;
key: NodeOutputKeyEnum;
value: any;
};

/* One tool run response */
export type ToolRunResponseItemType = any;
/* tool module response */
Expand Down
11 changes: 9 additions & 2 deletions packages/global/core/workflow/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum FlowNodeTemplateTypeEnum {
ai = 'ai',
function = 'function',
tools = 'tools',
interactive = 'interactive',

search = 'search',
multimodal = 'multimodal',
Expand Down Expand Up @@ -123,7 +124,9 @@ export enum NodeInputKeyEnum {
codeType = 'codeType', // js|py

// read files
fileUrlList = 'fileUrlList'
fileUrlList = 'fileUrlList',
// user select
userSelectOptions = 'userSelectOptions'
}

export enum NodeOutputKeyEnum {
Expand Down Expand Up @@ -162,7 +165,11 @@ export enum NodeOutputKeyEnum {
// plugin
pluginStart = 'pluginStart',

ifElseResult = 'ifElseResult'
// if else
ifElseResult = 'ifElseResult',

//user select
selectResult = 'selectResult'
}

export enum VariableInputEnum {
Expand Down
3 changes: 2 additions & 1 deletion packages/global/core/workflow/node/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export enum FlowNodeTypeEnum {
code = 'code',
textEditor = 'textEditor',
customFeedback = 'customFeedback',
readFiles = 'readFiles'
readFiles = 'readFiles',
userSelect = 'userSelect'
}

// node IO value type
Expand Down
8 changes: 6 additions & 2 deletions packages/global/core/workflow/runtime/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export enum SseResponseEventEnum {
toolParams = 'toolParams', // tool params return
toolResponse = 'toolResponse', // tool response return
flowResponses = 'flowResponses', // sse response request
updateVariables = 'updateVariables'
updateVariables = 'updateVariables',

interactive = 'interactive' // user select
}

export enum DispatchNodeResponseKeyEnum {
Expand All @@ -19,7 +21,9 @@ export enum DispatchNodeResponseKeyEnum {
nodeDispatchUsages = 'nodeDispatchUsages', // the node bill.
childrenResponses = 'childrenResponses', // Some nodes make recursive calls that need to be returned
toolResponses = 'toolResponses', // The result is passed back to the tool node for use
assistantResponses = 'assistantResponses' // assistant response
assistantResponses = 'assistantResponses', // assistant response

interactive = 'INTERACTIVE' // is interactive
}

export const needReplaceReferenceInputTypeList = [
Expand Down
7 changes: 6 additions & 1 deletion packages/global/core/workflow/runtime/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
ChatItemType,
UserChatItemValueItemType,
ChatItemValueItemType,
ToolRunResponseItemType
ToolRunResponseItemType,
NodeOutputItemType
} from '../../chat/type';
import { FlowNodeInputItemType, FlowNodeOutputItemType } from '../type/io.d';
import { StoreNodeItemType } from '../type/node';
Expand All @@ -17,6 +18,7 @@ import { AppDetailType, AppSchema } from '../../app/type';
import { RuntimeNodeItemType } from '../runtime/type';
import { RuntimeEdgeItemType } from './edge';
import { ReadFileNodeResponse } from '../template/system/readFiles/type';
import { UserSelectOptionType } from '../template/system/userSelect/type';

/* workflow props */
export type ChatDispatchProps = {
Expand Down Expand Up @@ -153,6 +155,9 @@ export type DispatchNodeResponseType = {
// read files
readFilesResult?: string;
readFiles?: ReadFileNodeResponse;

// user select
userSelectResult?: string;
};

export type DispatchNodeResultType<T> = {
Expand Down
74 changes: 71 additions & 3 deletions packages/global/core/workflow/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { StoreEdgeItemType } from '../type/edge';
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
import { VARIABLE_NODE_ID } from '../constants';
import { isReferenceValue } from '../utils';
import { ReferenceValueProps } from '../type/io';
import { FlowNodeOutputItemType, ReferenceValueProps } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';

export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
let limit = 10;
Expand All @@ -25,7 +27,35 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number
return limit * 2;
};

export const initWorkflowEdgeStatus = (edges: StoreEdgeItemType[]): RuntimeEdgeItemType[] => {
export const getLastInteractiveValue = (histories: ChatItemType[]) => {
const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI);

if (lastAIMessage) {
const interactiveValue = lastAIMessage.value.find(
(v) => v.type === ChatItemValueTypeEnum.interactive
);

if (interactiveValue && 'interactive' in interactiveValue) {
return interactiveValue.interactive;
}
}

return null;
};

export const initWorkflowEdgeStatus = (
edges: StoreEdgeItemType[],
histories?: ChatItemType[]
): RuntimeEdgeItemType[] => {
// If there is a history, use the last interactive value
if (!!histories) {
const memoryEdges = getLastInteractiveValue(histories)?.memoryEdges;

if (memoryEdges && memoryEdges.length > 0) {
return memoryEdges;
}
}

return (
edges?.map((edge) => ({
...edge,
Expand All @@ -34,7 +64,19 @@ export const initWorkflowEdgeStatus = (edges: StoreEdgeItemType[]): RuntimeEdgeI
);
};

export const getDefaultEntryNodeIds = (nodes: (StoreNodeItemType | RuntimeNodeItemType)[]) => {
export const getWorkflowEntryNodeIds = (
nodes: (StoreNodeItemType | RuntimeNodeItemType)[],
histories?: ChatItemType[]
) => {
// If there is a history, use the last interactive entry node
if (!!histories) {
const entryNodeIds = getLastInteractiveValue(histories)?.entryNodeIds;

if (Array.isArray(entryNodeIds) && entryNodeIds.length > 0) {
return entryNodeIds;
}
}

const entryList = [
FlowNodeTypeEnum.systemConfig,
FlowNodeTypeEnum.workflowStart,
Expand Down Expand Up @@ -212,3 +254,29 @@ export const textAdaptGptResponse = ({
]
});
};

/* Update runtimeNode's outputs with interactive data from history */
export function rewriteNodeOutputByHistories(
histories: ChatItemType[],
runtimeNodes: RuntimeNodeItemType[]
) {
const interactive = getLastInteractiveValue(histories);
if (!interactive?.nodeOutputs) {
return runtimeNodes;
}

return runtimeNodes.map((node) => {
return {
...node,
outputs: node.outputs.map((output: FlowNodeOutputItemType) => {
return {
...output,
value:
interactive?.nodeOutputs?.find(
(item: NodeOutputItemType) => item.nodeId === node.nodeId && item.key === output.key
)?.value || output?.value
};
})
};
});
}
4 changes: 3 additions & 1 deletion packages/global/core/workflow/template/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CodeNode } from './system/sandbox';
import { TextEditorNode } from './system/textEditor';
import { CustomFeedbackNode } from './system/customFeedback';
import { ReadFilesNodes } from './system/readFiles';
import { UserSelectNode } from './system/userSelect/index';

const systemNodes: FlowNodeTemplateType[] = [
AiChatModule,
Expand All @@ -51,7 +52,8 @@ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [
SystemConfigNode,
WorkflowStart,
...systemNodes,
CustomFeedbackNode
CustomFeedbackNode,
UserSelectNode
];
/* plugin flow module templates */
export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [
Expand Down
62 changes: 62 additions & 0 deletions packages/global/core/workflow/template/system/userSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { i18nT } from '../../../../../../web/i18n/utils';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../../constants';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum,
FlowNodeTypeEnum
} from '../../../node/constant';
import { FlowNodeTemplateType } from '../../../type/node.d';
import { getHandleConfig } from '../../utils';

export const UserSelectNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.userSelect,
templateType: FlowNodeTemplateTypeEnum.interactive,
flowNodeType: FlowNodeTypeEnum.userSelect,
sourceHandle: getHandleConfig(false, false, false, false),
targetHandle: getHandleConfig(true, false, true, true),
avatar: 'core/workflow/template/userSelect',
diagram: '/imgs/app/userSelect.svg',
name: i18nT('app:workflow.user_select'),
intro: i18nT(`app:workflow.user_select_tip`),
showStatus: true,
version: '489',
inputs: [
{
key: NodeInputKeyEnum.description,
renderTypeList: [FlowNodeInputTypeEnum.textarea],
valueType: WorkflowIOValueTypeEnum.string,
label: i18nT('app:workflow.select_description')
},
{
key: NodeInputKeyEnum.userSelectOptions,
renderTypeList: [FlowNodeInputTypeEnum.custom],
valueType: WorkflowIOValueTypeEnum.any,
label: '',
value: [
{
value: 'Confirm',
key: 'option1'
},
{
value: 'Cancel',
key: 'option2'
}
]
}
],
outputs: [
{
id: NodeOutputKeyEnum.selectResult,
key: NodeOutputKeyEnum.selectResult,
required: true,
label: i18nT('app:workflow.select_result'),
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static
}
]
};
26 changes: 26 additions & 0 deletions packages/global/core/workflow/template/system/userSelect/type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NodeOutputItemType } from '../../../../chat/type';
import { FlowNodeOutputItemType } from '../../../type/io';
import { RuntimeEdgeItemType } from '../../../runtime/type';

export type UserSelectOptionItemType = {
key: string;
value: string;
};

type InteractiveBasicType = {
entryNodeIds: string[];
memoryEdges: RuntimeEdgeItemType[];
nodeOutputs: NodeOutputItemType[];
};
type UserSelectInteractive = {
type: 'userSelect';
params: {
// description: string;
userSelectOptions: UserSelectOptionItemType[];
userSelectedVal?: string;
};
};

export type InteractiveNodeResponseItemType = InteractiveBasicType & UserSelectInteractive;

export type UserInteractiveType = UserSelectInteractive;
2 changes: 2 additions & 0 deletions packages/global/core/workflow/type/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export type FlowNodeTemplateType = FlowNodeCommonType & {
// action
forbidDelete?: boolean; // forbid delete
unique?: boolean;

diagram?: string; // diagram url
};

export type NodeTemplateListItemType = {
Expand Down
Loading

0 comments on commit fdeb159

Please sign in to comment.