feat: plan interactive type

This commit is contained in:
archer
2025-09-15 10:07:04 +08:00
parent 97198abe7e
commit 04b7153c5e
18 changed files with 383 additions and 119 deletions

View File

@@ -118,9 +118,9 @@ export type ChatItemValueItemType =
| UserChatItemValueItemType
| SystemChatItemValueItemType
| AIChatItemValueItemType;
export type ChatItemMergeType = UserChatItemType | SystemChatItemType | AIChatItemType;
export type ChatItemObjItemType = UserChatItemType | SystemChatItemType | AIChatItemType;
export type ChatItemSchema = ChatItemMergeType & {
export type ChatItemSchema = ChatItemObjItemType & {
dataId: string;
chatId: string;
userId: string;
@@ -145,12 +145,12 @@ export type ResponseTagItemType = {
toolCiteLinks?: ToolCiteLinksType[];
};
export type ChatItemType = ChatItemMergeType & {
export type ChatItemType = ChatItemObjItemType & {
dataId?: string;
} & ResponseTagItemType;
// Frontend type
export type ChatSiteItemType = ChatItemMergeType & {
export type ChatSiteItemType = ChatItemObjItemType & {
_id?: string;
dataId: string;
status: `${ChatStatusEnum}`;

View File

@@ -41,3 +41,6 @@ export const needReplaceReferenceInputTypeList = [
FlowNodeInputTypeEnum.addInputParam,
FlowNodeInputTypeEnum.custom
] as string[];
// Interactive
export const ConfirmPlanAgentText = 'CONFIRM';

View File

@@ -197,14 +197,32 @@ export const getLastInteractiveValue = (
// Check is user select
if (
lastValue.interactive.type === 'userSelect' &&
!lastValue.interactive.params.userSelectedVal
(lastValue.interactive.type === 'userSelect' ||
lastValue.interactive.type === 'agentPlanAskUserSelect') &&
!lastValue.interactive?.params?.userSelectedVal
) {
return lastValue.interactive;
}
// Check is user input
if (lastValue.interactive.type === 'userInput' && !lastValue.interactive.params.submitted) {
if (
(lastValue.interactive.type === 'userInput' ||
lastValue.interactive.type === 'agentPlanAskUserForm') &&
!lastValue.interactive?.params?.submitted
) {
return lastValue.interactive;
}
// Agent plan check
if (
lastValue.interactive.type === 'agentPlanCheck' &&
!lastValue.interactive?.params?.confirmed
) {
return lastValue.interactive;
}
// Agent plan ask query
if (lastValue.interactive.type === 'agentPlanAskQuery') {
return lastValue.interactive;
}
}

View File

@@ -1,10 +1,10 @@
import type { NodeOutputItemType } from '../../../../chat/type';
import type { FlowNodeOutputItemType } from '../../../type/io';
import type { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
import type { WorkflowIOValueTypeEnum } from 'core/workflow/constants';
import type { ChatCompletionMessageParam } from '../../../../ai/type';
import type { RuntimeEdgeItemType } from '../../../type/edge';
type InteractiveBasicType = {
export type InteractiveBasicType = {
entryNodeIds: string[];
memoryEdges: RuntimeEdgeItemType[];
nodeOutputs: NodeOutputItemType[];
@@ -40,12 +40,26 @@ type LoopInteractive = InteractiveNodeType & {
};
};
// Agent Interactive
export type AgentPlanCheckInteractive = InteractiveNodeType & {
type: 'agentPlanCheck';
params: {
confirmed?: boolean;
};
};
export type AgentPlanAskQueryInteractive = InteractiveNodeType & {
type: 'agentPlanAskQuery';
params: {
content: string;
};
};
export type UserSelectOptionItemType = {
key: string;
value: string;
};
type UserSelectInteractive = InteractiveNodeType & {
type: 'userSelect';
export type UserSelectInteractive = InteractiveNodeType & {
type: 'userSelect' | 'agentPlanAskUserSelect';
params: {
description: string;
userSelectOptions: UserSelectOptionItemType[];
@@ -71,8 +85,8 @@ export type UserInputFormItemType = {
// select
list?: { label: string; value: string }[];
};
type UserInputInteractive = InteractiveNodeType & {
type: 'userInput';
export type UserInputInteractive = InteractiveNodeType & {
type: 'userInput' | 'agentPlanAskUserForm';
params: {
description: string;
inputForm: UserInputFormItemType[];
@@ -84,6 +98,8 @@ export type InteractiveNodeResponseType =
| UserSelectInteractive
| UserInputInteractive
| ChildrenInteractive
| LoopInteractive;
| LoopInteractive
| AgentPlanCheckInteractive
| AgentPlanAskQueryInteractive;
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;

View File

@@ -314,10 +314,12 @@ export const updateInteractiveChat = async ({
// Update interactive value
const interactiveValue = chatItem.value[chatItem.value.length - 1];
if (!interactiveValue || !interactiveValue.interactive?.params) {
if (!interactiveValue || !interactiveValue.interactive) {
return;
}
interactiveValue.interactive.params = interactiveValue.interactive.params || {};
// Update interactive value
const parsedUserInteractiveVal = (() => {
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userContent.value);
try {
@@ -337,7 +339,7 @@ export const updateInteractiveChat = async ({
if (finalInteractive.type === 'userSelect') {
finalInteractive.params.userSelectedVal = parsedUserInteractiveVal;
} else if (
finalInteractive.type === 'userInput' &&
(finalInteractive.type === 'userInput' || finalInteractive.type === 'agentPlanAskUserForm') &&
typeof parsedUserInteractiveVal === 'object'
) {
finalInteractive.params.inputForm = finalInteractive.params.inputForm.map((item) => {
@@ -350,6 +352,8 @@ export const updateInteractiveChat = async ({
: item;
});
finalInteractive.params.submitted = true;
} else if (finalInteractive.type === 'agentPlanCheck') {
finalInteractive.params.confirmed = true;
}
if (aiResponse.customFeedbacks) {

View File

@@ -1,4 +1,8 @@
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import {
DispatchNodeResponseKeyEnum,
SseResponseEventEnum
@@ -41,7 +45,10 @@ import { dispatchPlanAgent } from './sub/plan';
import { dispatchModelAgent } from './sub/model';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { getSubApps, rewriteSubAppsToolset } from './sub';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { dispatchTool } from './sub/tool';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getMasterAgentDefaultPrompt } from './constants';
@@ -98,34 +105,33 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
planAgentConfig
}
} = props;
const memoryKey = `${runningAppInfo.id}-${nodeId}`;
const agentModel = getLLMModel(model);
const chatHistories = getHistories(history, histories);
const runtimeSubApps = await rewriteSubAppsToolset({
subApps: subApps.map<RuntimeNodeItemType>((node) => {
const masterMessagesKey = `masterMessages-${nodeId}`;
const planMessagesKey = `planMessages-${nodeId}`;
// Get history messages
const { masterHistoryMessages, planHistoryMessages } = (() => {
const lastHistory = chatHistories[chatHistories.length - 1];
if (lastHistory && lastHistory.obj === ChatRoleEnum.AI) {
return {
nodeId: node.id,
name: node.name,
avatar: node.avatar,
intro: node.intro,
toolDescription: node.toolDescription,
flowNodeType: node.flowNodeType,
showStatus: node.showStatus,
isEntry: false,
inputs: node.inputs,
outputs: node.outputs,
pluginId: node.pluginId,
version: node.version,
toolConfig: node.toolConfig,
catchError: node.catchError
masterHistoryMessages: (lastHistory.memories?.[masterMessagesKey] ||
[]) as ChatCompletionMessageParam[],
planHistoryMessages: (lastHistory.memories?.[planMessagesKey] ||
[]) as ChatCompletionMessageParam[]
};
}),
lang
});
}
return {
masterHistoryMessages: [],
planHistoryMessages: []
};
})();
// Check interactive entry
props.node.isEntry = false;
try {
const agentModel = getLLMModel(model);
const chatHistories = getHistories(history, histories);
// Get files
const fileUrlInput = inputs.find((item) => item.key === NodeInputKeyEnum.fileUrlList);
if (!fileUrlInput || !fileUrlInput.value || fileUrlInput.value.length === 0) {
@@ -138,34 +144,55 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
histories: chatHistories
});
// Get sub apps
const runtimeSubApps = await rewriteSubAppsToolset({
subApps: subApps.map<RuntimeNodeItemType>((node) => {
return {
nodeId: node.id,
name: node.name,
avatar: node.avatar,
intro: node.intro,
toolDescription: node.toolDescription,
flowNodeType: node.flowNodeType,
showStatus: node.showStatus,
isEntry: false,
inputs: node.inputs,
outputs: node.outputs,
pluginId: node.pluginId,
version: node.version,
toolConfig: node.toolConfig,
catchError: node.catchError
};
}),
lang
});
const subAppList = getSubApps({
subApps: runtimeSubApps,
addReadFileTool: Object.keys(filesMap).length > 0
});
// Init tool params
const toolNodesMap = new Map(runtimeSubApps.map((item) => [item.nodeId, item]));
const getToolInfo = (id: string) => {
const toolNode = toolNodesMap.get(id) || systemSubInfo[id];
const subAppsMap = new Map(runtimeSubApps.map((item) => [item.nodeId, item]));
const getSubAppInfo = (id: string) => {
const toolNode = subAppsMap.get(id) || systemSubInfo[id];
return {
name: toolNode?.name || '',
avatar: toolNode?.avatar || ''
};
};
// Get master request messages
const systemMessages = chats2GPTMessages({
messages: getSystemPrompt_ChatItemType(systemPrompt || getMasterAgentDefaultPrompt()),
reserveId: false
});
const historyMessages: ChatCompletionMessageParam[] = (() => {
const lastHistory = chatHistories[chatHistories.length - 1];
if (lastHistory && lastHistory.obj === ChatRoleEnum.AI && lastHistory.memories?.[memoryKey]) {
return lastHistory.memories?.[memoryKey];
if (masterHistoryMessages && masterHistoryMessages.length > 0) {
return masterHistoryMessages;
}
return chats2GPTMessages({ messages: chatHistories, reserveId: false });
})();
const userMessages = chats2GPTMessages({
messages: [
{
@@ -180,8 +207,80 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
const requestMessages = [...systemMessages, ...historyMessages, ...userMessages];
// Check interactive entry
props.node.isEntry = false;
// TODO: 执行 plan function(只有lastInteractive userselect/userInput 时候,才不需要进入 plan)
if (lastInteractive?.type !== 'userSelect' && lastInteractive?.type !== 'userInput') {
// const planResponse = xxxx
// requestMessages.push(一组 toolcall)
workflowStreamResponse?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: '这是 plan'
})
});
return {
[DispatchNodeResponseKeyEnum.memories]: {
[planMessagesKey]: [
{
role: 'user',
content: '测试'
},
{
role: 'assistant',
content: '测试'
}
]
},
// Mock返回 plan check
// [DispatchNodeResponseKeyEnum.interactive]: {
// type: 'agentPlanCheck',
// params: {}
// }
// Mock: 返回 plan user select
// [DispatchNodeResponseKeyEnum.interactive]: {
// type: 'agentPlanAskUserSelect',
// params: {
// description: '测试',
// userSelectOptions: [
// {
// key: 'test',
// value: '测试'
// },
// {
// key: 'test2',
// value: '测试2'
// }
// ]
// }
// },
// Mock: 返回 plan user input
// [DispatchNodeResponseKeyEnum.interactive]: {
// type: 'agentPlanAskUserForm',
// params: {
// description: '测试',
// inputForm: [
// {
// type: FlowNodeInputTypeEnum.input,
// key: 'test1',
// label: '测试1',
// value: '',
// valueType: WorkflowIOValueTypeEnum.string,
// required: true
// }
// ]
// }
// }
// Mock: 返回 plan user query
[DispatchNodeResponseKeyEnum.interactive]: {
type: 'agentPlanAskQuery',
params: {
content: '请提供 xxxxx'
}
}
};
}
const dispatchFlowResponse: ChatHistoryItemResType[] = [];
// console.log(JSON.stringify(requestMessages, null, 2));
@@ -200,7 +299,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
userKey: externalProvider.openaiAccount,
isAborted: res ? () => res.closed : undefined,
getToolInfo,
getToolInfo: getSubAppInfo,
onReasoning({ text }) {
workflowStreamResponse?.({
@@ -219,15 +318,15 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
},
onToolCall({ call }) {
const toolNode = getToolInfo(call.function.name);
const subApp = getSubAppInfo(call.function.name);
workflowStreamResponse?.({
id: call.id,
event: SseResponseEventEnum.toolCall,
data: {
tool: {
id: `${nodeId}/${call.function.name}`,
toolName: toolNode?.name || call.function.name,
toolAvatar: toolNode?.avatar || '',
toolName: subApp?.name || call.function.name,
toolAvatar: subApp?.avatar || '',
functionName: call.function.name,
params: call.function.arguments ?? ''
}
@@ -350,7 +449,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
}
// User Sub App
else {
const node = toolNodesMap.get(toolId);
const node = subAppsMap.get(toolId);
if (!node) {
return {
response: 'Can not find the tool',
@@ -497,7 +596,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
},
// TODO: 需要对 memoryMessages 单独建表存储
[DispatchNodeResponseKeyEnum.memories]: {
[memoryKey]: filterMemoryMessages(completeMessages)
[masterMessagesKey]: filterMemoryMessages(completeMessages),
[planMessagesKey]: [] // TODO: plan messages 需要记录
},
[DispatchNodeResponseKeyEnum.assistantResponses]: previewAssistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: {

View File

@@ -72,7 +72,7 @@ export const getSubApps = ({
}): ChatCompletionTool[] => {
// System Tools: Plan Agent, stop sign, model agent.
const systemTools: ChatCompletionTool[] = [
PlanAgentTool,
// PlanAgentTool,
...(addReadFileTool ? [readFileTool] : [])
// ModelAgentTool
// StopAgentTool,

View File

@@ -31,6 +31,7 @@ type DispatchPlanAgentResponse = {
export const dispatchPlanAgent = async ({
messages,
tools,
model,
customSystemPrompt,

View File

@@ -23,6 +23,7 @@
"config_input_guide": "Set Up Input Guide",
"config_input_guide_lexicon": "Set Up Lexicon",
"config_input_guide_lexicon_title": "Set Up Lexicon",
"confirm_plan_agent": "Please confirm whether the change plan meets expectations. If you need to modify it, you can send the modification requirements in the input box at the bottom.",
"content_empty": "No Content",
"contextual": "{{num}} Contexts",
"contextual_preview": "Contextual Preview {{num}} Items",

View File

@@ -23,6 +23,7 @@
"config_input_guide": "配置输入引导",
"config_input_guide_lexicon": "配置词库",
"config_input_guide_lexicon_title": "配置词库",
"confirm_plan_agent": "请确认改计划是否符合预期,如需修改,可在底部输入框中发送修改要求。",
"content_empty": "内容为空",
"contextual": "{{num}}条上下文",
"contextual_preview": "上下文预览 {{num}} 条",

View File

@@ -23,6 +23,7 @@
"config_input_guide": "設定輸入導引",
"config_input_guide_lexicon": "設定詞彙庫",
"config_input_guide_lexicon_title": "設定詞彙庫",
"confirm_plan_agent": "請確認改計劃是否符合預期,如需修改,可在底部輸入框中發送修改要求。",
"content_empty": "無內容",
"contextual": "{{num}} 筆上下文",
"contextual_preview": "上下文預覽 {{num}} 筆",

View File

@@ -37,9 +37,9 @@ import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import {
checkIsInteractiveByHistories,
getInteractiveStatus,
formatChatValue2InputType,
setUserSelectResultToHistories
rewriteHistoriesByInteractiveResponse
} from './utils';
import { ChatTypeEnum, textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
@@ -149,7 +149,10 @@ const ChatBox = ({
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
// Workflow running, there are user input or selection
const isInteractive = useMemo(() => checkIsInteractiveByHistories(chatRecords), [chatRecords]);
const { interactiveType, canSendQuery } = useMemo(
() => getInteractiveStatus(chatRecords),
[chatRecords]
);
const showExternalVariable = useMemo(() => {
const map: Record<string, boolean> = {
@@ -338,7 +341,7 @@ const ChatBox = ({
})();
const updateValue: AIChatItemValueItemType = cloneDeep(item.value[updateIndex]);
updateValue.id = responseValueId;
console.log(event, tool, updateValue);
if (event === SseResponseEventEnum.flowNodeResponse && nodeResponse) {
return {
...item,
@@ -544,8 +547,8 @@ const ChatBox = ({
text = '',
files = [],
history = chatRecords,
interactiveType,
autoTTSResponse = false,
isInteractivePrompt = false,
hideInUI = false
}) => {
variablesForm.handleSubmit(
@@ -638,9 +641,13 @@ const ChatBox = ({
// Update histories(Interactive input does not require new session rounds)
setChatRecords(
isInteractivePrompt
interactiveType
? // 把交互的结果存储到对话记录中,交互模式下,不需要新的会话轮次
setUserSelectResultToHistories(newChatList.slice(0, -2), text)
rewriteHistoriesByInteractiveResponse({
histories: newChatList,
interactiveType: interactiveType,
interactiveVal: text
})
: newChatList
);
@@ -698,7 +705,7 @@ const ChatBox = ({
});
setTimeout(() => {
if (!checkIsInteractiveByHistories(newChatHistories)) {
if (!getInteractiveStatus(newChatHistories).interactiveType) {
createQuestionGuide();
}
@@ -982,7 +989,7 @@ const ChatBox = ({
abortRequest('leave');
}, [chatId, appId, abortRequest, setValue]);
const canSendPrompt = onStartChat && chatStarted && active && !isInteractive;
const canSendPrompt = onStartChat && chatStarted && active && canSendQuery;
// Add listener
useEffect(() => {
@@ -995,9 +1002,12 @@ const ChatBox = ({
};
window.addEventListener('message', windowMessage);
const fn: SendPromptFnType = (e) => {
if (canSendPrompt || e.isInteractivePrompt) {
sendPrompt(e);
const fn = ({ focus = false, ...e }: ChatBoxInputType & { focus?: boolean }) => {
if (canSendPrompt || focus) {
sendPrompt({
...e,
interactiveType
});
}
};
eventBus.on(EventNameEnum.sendQuestion, fn);
@@ -1011,7 +1021,7 @@ const ChatBox = ({
eventBus.off(EventNameEnum.sendQuestion);
eventBus.off(EventNameEnum.editQuestion);
};
}, [isReady, resetInputVal, sendPrompt, canSendPrompt]);
}, [isReady, resetInputVal, sendPrompt, canSendPrompt, interactiveType]);
// Auto send prompt
useDebounceEffect(

View File

@@ -3,6 +3,7 @@ import type { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import type { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemValueItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import type { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
export type UserInputFileItemType = {
id: string;
@@ -25,7 +26,7 @@ export type ChatBoxInputFormType = {
export type ChatBoxInputType = {
text?: string;
files?: UserInputFileItemType[];
isInteractivePrompt?: boolean;
interactiveType?: InteractiveNodeResponseType['type'];
hideInUI?: boolean;
};

View File

@@ -7,6 +7,8 @@ import { type ChatBoxInputType, type UserInputFileItemType } from './type';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import type { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ConfirmPlanAgentText } from '@fastgpt/global/core/workflow/runtime/constants';
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) {
@@ -43,36 +45,83 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat
};
};
export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[]) => {
export const getInteractiveStatus = (
chatHistories: ChatSiteItemType[]
): {
interactiveType: InteractiveNodeResponseType['type'] | undefined;
canSendQuery: boolean;
} => {
const lastAIHistory = chatHistories[chatHistories.length - 1];
if (!lastAIHistory) return false;
if (!lastAIHistory)
return {
interactiveType: undefined,
canSendQuery: true
};
const lastMessageValue = lastAIHistory.value[
lastAIHistory.value.length - 1
] as AIChatItemValueItemType;
if (lastMessageValue && !!lastMessageValue?.interactive?.params) {
const params = lastMessageValue.interactive.params;
// 如果用户选择了,则不认为是交互模式(可能是上一轮以交互结尾,发起的新的一轮对话)
if ('userSelectOptions' in params) {
return !params.userSelectedVal;
} else if ('inputForm' in params) {
return !params.submitted;
if (!lastMessageValue || !lastMessageValue.interactive) {
return {
interactiveType: undefined,
canSendQuery: true
};
}
const interactive = lastMessageValue.interactive;
if (interactive.params) {
if (interactive.type === 'userSelect' || interactive.type === 'agentPlanAskUserSelect') {
return {
interactiveType: !!interactive.params.userSelectedVal ? undefined : 'userSelect',
canSendQuery: !!interactive.params.userSelectedVal
};
}
if (interactive.type === 'userInput' || interactive.type === 'agentPlanAskUserForm') {
return {
interactiveType: !!interactive.params.submitted ? undefined : 'userInput',
canSendQuery: !!interactive.params.submitted
};
}
if (interactive.type === 'agentPlanCheck') {
return {
interactiveType: !!interactive.params.confirmed ? undefined : 'agentPlanCheck',
canSendQuery: true
};
}
if (interactive.type === 'agentPlanAskQuery') {
return {
interactiveType: 'agentPlanAskQuery',
canSendQuery: true
};
}
}
return false;
return {
interactiveType: undefined,
canSendQuery: true
};
};
export const setUserSelectResultToHistories = (
histories: ChatSiteItemType[],
interactiveVal: string
): ChatSiteItemType[] => {
if (histories.length === 0) return histories;
export const rewriteHistoriesByInteractiveResponse = ({
histories,
interactiveVal,
interactiveType
}: {
histories: ChatSiteItemType[];
interactiveVal: string;
interactiveType: InteractiveNodeResponseType['type'];
}): ChatSiteItemType[] => {
const formatHistories = (() => {
if (interactiveType === 'agentPlanCheck' && interactiveVal !== ConfirmPlanAgentText) {
return histories;
}
return histories.slice(0, -2);
})();
// @ts-ignore
return histories.map((item, i) => {
if (i !== histories.length - 1) return item;
const newHistories = formatHistories.map((item, i) => {
if (i !== formatHistories.length - 1) return item;
const value = item.value.map((val, i) => {
if (i !== item.value.length - 1) {
@@ -81,7 +130,10 @@ export const setUserSelectResultToHistories = (
if (!('interactive' in val) || !val.interactive) return val;
const finalInteractive = extractDeepestInteractive(val.interactive);
if (finalInteractive.type === 'userSelect') {
if (
finalInteractive.type === 'userSelect' ||
finalInteractive.type === 'agentPlanAskUserSelect'
) {
return {
...val,
interactive: {
@@ -96,7 +148,10 @@ export const setUserSelectResultToHistories = (
};
}
if (finalInteractive.type === 'userInput') {
if (
finalInteractive.type === 'userInput' ||
finalInteractive.type === 'agentPlanAskUserForm'
) {
return {
...val,
interactive: {
@@ -108,12 +163,28 @@ export const setUserSelectResultToHistories = (
}
};
}
if (finalInteractive.type === 'agentPlanCheck') {
return {
...val,
interactive: {
...finalInteractive,
params: {
confirmed: true
}
}
};
}
return val;
});
return {
...item,
status: ChatStatusEnum.loading,
value
};
} as ChatSiteItemType;
});
return newHistories;
};

View File

@@ -19,19 +19,25 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import type {
InteractiveBasicType,
InteractiveNodeResponseType,
UserInputInteractive,
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { isEqual } from 'lodash';
import { useTranslation } from 'next-i18next';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import {
SelectOptionsComponent,
FormInputComponent,
AgentPlanCheckComponent
} from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { type OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext';
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
import { useCreation } from 'ahooks';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
import { ConfirmPlanAgentText } from '@fastgpt/global/core/workflow/runtime/constants';
const accordionButtonStyle = {
w: 'auto',
@@ -215,21 +221,21 @@ ${response}`}
(prevProps, nextProps) => isEqual(prevProps, nextProps)
);
const onSendPrompt = (e: { text: string; isInteractivePrompt: boolean }) =>
eventBus.emit(EventNameEnum.sendQuestion, e);
const onSendPrompt = (text: string) =>
eventBus.emit(EventNameEnum.sendQuestion, {
text,
focus: true
});
const RenderUserSelectInteractive = React.memo(function RenderInteractive({
interactive
}: {
interactive: InteractiveBasicType & UserSelectInteractive;
interactive: UserSelectInteractive;
}) {
return (
<SelectOptionsComponent
interactiveParams={interactive.params}
onSelect={(value) => {
onSendPrompt({
text: value,
isInteractivePrompt: true
});
onSendPrompt(value);
}}
/>
);
@@ -237,7 +243,7 @@ const RenderUserSelectInteractive = React.memo(function RenderInteractive({
const RenderUserFormInteractive = React.memo(function RenderFormInput({
interactive
}: {
interactive: InteractiveBasicType & UserInputInteractive;
interactive: UserInputInteractive;
}) {
const { t } = useTranslation();
@@ -261,10 +267,7 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
}
});
onSendPrompt({
text: JSON.stringify(finalData),
isInteractivePrompt: true
});
onSendPrompt(JSON.stringify(finalData));
},
[interactive.params.inputForm]
);
@@ -329,12 +332,25 @@ const AIResponseBox = ({
);
}
if ('interactive' in value && value.interactive) {
const finalInteractive = extractDeepestInteractive(value.interactive);
if (finalInteractive.type === 'userSelect') {
return <RenderUserSelectInteractive interactive={finalInteractive} />;
const interactive = extractDeepestInteractive(value.interactive);
if (interactive.type === 'userSelect' || interactive.type === 'agentPlanAskUserSelect') {
return <RenderUserSelectInteractive interactive={interactive} />;
}
if (finalInteractive.type === 'userInput') {
return <RenderUserFormInteractive interactive={finalInteractive} />;
if (interactive.type === 'userInput' || interactive.type === 'agentPlanAskUserForm') {
return <RenderUserFormInteractive interactive={interactive} />;
}
if (interactive.type === 'agentPlanCheck') {
return (
<AgentPlanCheckComponent
interactiveParams={interactive.params}
onConfirm={() => {
onSendPrompt(ConfirmPlanAgentText);
}}
/>
);
}
if (interactive.type === 'agentPlanAskQuery') {
return <Box>{interactive.params.content}</Box>;
}
}

View File

@@ -3,16 +3,18 @@ import { Box, Flex } from '@chakra-ui/react';
import { Controller, useForm, type UseFormHandleSubmit } from 'react-hook-form';
import Markdown from '@/components/Markdown';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import {
type UserInputFormItemType,
type UserInputInteractive,
type UserSelectInteractive,
type UserSelectOptionItemType
import type {
AgentPlanCheckInteractive,
UserInputFormItemType,
UserInputInteractive,
UserSelectInteractive,
UserSelectOptionItemType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import InputRender from '@/components/core/app/formRender';
import { nodeInputTypeToInputType } from '@/components/core/app/formRender/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio';
import { useTranslation } from 'next-i18next';
const DescriptionBox = React.memo(function DescriptionBox({
description
@@ -133,3 +135,23 @@ export const FormInputComponent = React.memo(function FormInputComponent({
</Box>
);
});
// Agent interactive
export const AgentPlanCheckComponent = React.memo(function AgentPlanCheckComponent({
interactiveParams,
onConfirm
}: {
interactiveParams: AgentPlanCheckInteractive['params'];
onConfirm: () => void;
}) {
const { t } = useTranslation();
return interactiveParams?.confirmed ? (
// TODO临时 UI
<Box></Box>
) : (
<Box>
<Box>{t('chat:confirm_plan_agent')}</Box>
<Button onClick={onConfirm}>{t('common:Confirm')}</Button>
</Box>
);
});

View File

@@ -139,7 +139,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
const newHistories = concatHistories(histories, chatMessages);
const interactive = getLastInteractiveValue(newHistories) || undefined;
const interactive = getLastInteractiveValue(newHistories);
// Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, interactive));
if (isPlugin) {

View File

@@ -319,8 +319,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return ChatSourceEnum.online;
})();
const isInteractiveRequest = !!getLastInteractiveValue(histories);
const newTitle = isPlugin
? variables.cTime ?? formatTime2YMDHM()
: getChatTitleFromChatMessage(userQuestion);
@@ -333,6 +331,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
memories: system_memories
};
const isInteractiveRequest = !!getLastInteractiveValue(histories);
const saveChatId = chatId || getNanoid(24);
const params = {
chatId: saveChatId,