mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
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:
2
packages/global/core/ai/type.d.ts
vendored
2
packages/global/core/ai/type.d.ts
vendored
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,8 @@ export enum ChatFileTypeEnum {
|
||||
export enum ChatItemValueTypeEnum {
|
||||
text = 'text',
|
||||
file = 'file',
|
||||
tool = 'tool'
|
||||
tool = 'tool',
|
||||
interactive = 'interactive'
|
||||
}
|
||||
|
||||
export enum ChatSourceEnum {
|
||||
|
11
packages/global/core/chat/type.d.ts
vendored
11
packages/global/core/chat/type.d.ts
vendored
@@ -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 */
|
||||
|
@@ -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 {
|
||||
|
@@ -118,7 +118,8 @@ export enum FlowNodeTypeEnum {
|
||||
code = 'code',
|
||||
textEditor = 'textEditor',
|
||||
customFeedback = 'customFeedback',
|
||||
readFiles = 'readFiles'
|
||||
readFiles = 'readFiles',
|
||||
userSelect = 'userSelect'
|
||||
}
|
||||
|
||||
// node IO value type
|
||||
|
@@ -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 = [
|
||||
|
@@ -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> = {
|
||||
|
@@ -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
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -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[] = [
|
||||
|
@@ -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
packages/global/core/workflow/template/system/userSelect/type.d.ts
vendored
Normal file
26
packages/global/core/workflow/template/system/userSelect/type.d.ts
vendored
Normal 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;
|
2
packages/global/core/workflow/type/node.d.ts
vendored
2
packages/global/core/workflow/type/node.d.ts
vendored
@@ -66,6 +66,8 @@ export type FlowNodeTemplateType = FlowNodeCommonType & {
|
||||
// action
|
||||
forbidDelete?: boolean; // forbid delete
|
||||
unique?: boolean;
|
||||
|
||||
diagram?: string; // diagram url
|
||||
};
|
||||
|
||||
export type NodeTemplateListItemType = {
|
||||
|
Reference in New Issue
Block a user