User select node (#2397)

* 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 <heheer@sealos.io>
This commit is contained in:
Archer
2024-08-15 12:27:04 +08:00
committed by GitHub
parent f8b8fcc172
commit fdeb1590d7
51 changed files with 1060 additions and 184 deletions

View File

@@ -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';
@@ -33,6 +34,7 @@ export type ChatCompletionMessageParam = (
| CustomChatCompletionUserMessageParam
) & {
dataId?: string;
interactive?: InteractiveNodeResponseItemType;
};
export type SdkChatCompletionMessageParam = SdkChatCompletionMessageParam;

View File

@@ -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: ''
});
}
});
}
@@ -254,6 +261,12 @@ export const GPTMessages2Chats = (
]
});
}
} else if (item.interactive) {
value.push({
//@ts-ignore
type: ChatItemValueTypeEnum.interactive,
interactive: item.interactive
});
}
}

View File

@@ -24,7 +24,8 @@ export enum ChatFileTypeEnum {
export enum ChatItemValueTypeEnum {
text = 'text',
file = 'file',
tool = 'tool'
tool = 'tool',
interactive = 'interactive'
}
export enum ChatSourceEnum {

View File

@@ -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;
@@ -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;
@@ -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 */

View File

@@ -3,6 +3,7 @@ export enum FlowNodeTemplateTypeEnum {
ai = 'ai',
function = 'function',
tools = 'tools',
interactive = 'interactive',
search = 'search',
multimodal = 'multimodal',
@@ -123,7 +124,9 @@ export enum NodeInputKeyEnum {
codeType = 'codeType', // js|py
// read files
fileUrlList = 'fileUrlList'
fileUrlList = 'fileUrlList',
// user select
userSelectOptions = 'userSelectOptions'
}
export enum NodeOutputKeyEnum {
@@ -162,7 +165,11 @@ export enum NodeOutputKeyEnum {
// plugin
pluginStart = 'pluginStart',
ifElseResult = 'ifElseResult'
// if else
ifElseResult = 'ifElseResult',
//user select
selectResult = 'selectResult'
}
export enum VariableInputEnum {

View File

@@ -118,7 +118,8 @@ export enum FlowNodeTypeEnum {
code = 'code',
textEditor = 'textEditor',
customFeedback = 'customFeedback',
readFiles = 'readFiles'
readFiles = 'readFiles',
userSelect = 'userSelect'
}
// node IO value type

View File

@@ -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 {
@@ -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 = [

View File

@@ -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';
@@ -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 = {
@@ -153,6 +155,9 @@ export type DispatchNodeResponseType = {
// read files
readFilesResult?: string;
readFiles?: ReadFileNodeResponse;
// user select
userSelectResult?: string;
};
export type DispatchNodeResultType<T> = {

View File

@@ -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;
@@ -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,
@@ -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,
@@ -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
};
})
};
});
}

View File

@@ -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,
@@ -51,7 +52,8 @@ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [
SystemConfigNode,
WorkflowStart,
...systemNodes,
CustomFeedbackNode
CustomFeedbackNode,
UserSelectNode
];
/* plugin flow module templates */
export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [

View File

@@ -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
}
]
};

View File

@@ -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;

View File

@@ -66,6 +66,8 @@ export type FlowNodeTemplateType = FlowNodeCommonType & {
// action
forbidDelete?: boolean; // forbid delete
unique?: boolean;
diagram?: string; // diagram url
};
export type NodeTemplateListItemType = {