mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 20:34:48 +08:00
Feat: When a Wait Node precedes a Message Node within a Loop Node, the outgoing message is split into two separate messages. (#14839)
### What problem does this PR solve? Feat: When a Wait Node precedes a Message Node within a Loop Node, the outgoing message is split into two separate messages. ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@@ -15,10 +15,9 @@ import {
|
||||
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { memo, useCallback, useContext } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { AgentChatContext } from '../context';
|
||||
import DebugContent from '../debug-content';
|
||||
import { useAwaitCompentData } from '../hooks/use-chat-logic';
|
||||
import { useAwaitComponentData } from '../hooks/use-chat-logic';
|
||||
import { useIsTaskMode } from '../hooks/use-get-begin-query';
|
||||
import { useGetFileIcon } from './use-get-file-icon';
|
||||
|
||||
@@ -43,13 +42,11 @@ function AgentChatBox() {
|
||||
useClickDrawer();
|
||||
useGetFileIcon();
|
||||
const { data: userInfo } = useFetchUserInfo();
|
||||
const { id: canvasId } = useParams();
|
||||
const { uploadAgentFile, loading } = useUploadAgentFileWithProgress();
|
||||
|
||||
const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({
|
||||
const { buildInputList, handleOk, isWaiting } = useAwaitComponentData({
|
||||
derivedMessages,
|
||||
sendFormMessage,
|
||||
canvasId: canvasId as string,
|
||||
});
|
||||
|
||||
const { setDerivedMessages } = useContext(AgentChatContext);
|
||||
@@ -125,9 +122,9 @@ function AgentChatBox() {
|
||||
<NextMessageInput
|
||||
value={value}
|
||||
sendLoading={sendLoading}
|
||||
disabled={isWaitting}
|
||||
sendDisabled={sendLoading || isWaitting}
|
||||
isUploading={loading || isWaitting}
|
||||
disabled={isWaiting}
|
||||
sendDisabled={sendLoading || isWaiting}
|
||||
isUploading={loading || isWaiting}
|
||||
resize="vertical"
|
||||
onPressEnter={handlePressEnter}
|
||||
onInputChange={handleInputChange}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { BeginId } from '../constant';
|
||||
import { MessageWaitSuffix } from '../constant/chat';
|
||||
import { AgentChatLogContext } from '../context';
|
||||
import { transferInputsArrayToObject } from '../form/begin-form/use-watch-change';
|
||||
import {
|
||||
@@ -39,6 +40,7 @@ import { useStopMessage } from '../hooks/use-stop-message';
|
||||
import { BeginQuery } from '../interface';
|
||||
import useGraphStore from '../store';
|
||||
import { receiveMessageError } from '../utils';
|
||||
import { shouldSplitMessage } from '../utils/chat';
|
||||
|
||||
export function findMessageFromList(eventList: IEventList) {
|
||||
const messageEventList = eventList.filter(
|
||||
@@ -363,7 +365,7 @@ export const useSendAgentMessage = ({
|
||||
);
|
||||
|
||||
const sendFormMessage = useCallback(
|
||||
async (body: { agent_id?: string; inputs: Record<string, BeginQuery> }) => {
|
||||
async (body: { inputs: Record<string, BeginQuery> }) => {
|
||||
addNewestOneQuestion({
|
||||
content: Object.entries(body.inputs)
|
||||
.map(([, val]) => `${val.name}: ${val.value}`)
|
||||
@@ -372,12 +374,21 @@ export const useSendAgentMessage = ({
|
||||
});
|
||||
await send({
|
||||
...body,
|
||||
...(isShared ? {} : { agent_id: agentId }),
|
||||
session_id: sessionId,
|
||||
...(releaseMode ? { release: releaseMode } : {}),
|
||||
});
|
||||
refetch?.();
|
||||
},
|
||||
[addNewestOneQuestion, refetch, releaseMode, send, sessionId],
|
||||
[
|
||||
addNewestOneQuestion,
|
||||
agentId,
|
||||
isShared,
|
||||
refetch,
|
||||
releaseMode,
|
||||
send,
|
||||
sessionId,
|
||||
],
|
||||
);
|
||||
|
||||
// reset session
|
||||
@@ -450,14 +461,31 @@ export const useSendAgentMessage = ({
|
||||
const answer = content || getLatestError(answerList);
|
||||
|
||||
if (answerList.length > 0) {
|
||||
addNewestOneAnswer({
|
||||
answer: answer ?? '',
|
||||
audio_binary: audio_binary,
|
||||
attachment: attachment as IAttachment,
|
||||
downloads,
|
||||
id: id,
|
||||
...inputAnswer,
|
||||
});
|
||||
const shouldSplit = shouldSplitMessage(answerList, content);
|
||||
|
||||
if (shouldSplit) {
|
||||
addNewestOneAnswer({
|
||||
answer: answer ?? '',
|
||||
audio_binary: audio_binary,
|
||||
attachment: attachment as IAttachment,
|
||||
downloads,
|
||||
id,
|
||||
});
|
||||
addNewestOneAnswer({
|
||||
answer: '',
|
||||
...inputAnswer,
|
||||
id: `${id}${MessageWaitSuffix}`,
|
||||
});
|
||||
} else {
|
||||
addNewestOneAnswer({
|
||||
answer: answer ?? '',
|
||||
audio_binary: audio_binary,
|
||||
attachment: attachment as IAttachment,
|
||||
downloads,
|
||||
id,
|
||||
...inputAnswer,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [answerList, addNewestOneAnswer]);
|
||||
|
||||
|
||||
1
web/src/pages/agent/constant/chat.ts
Normal file
1
web/src/pages/agent/constant/chat.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MessageWaitSuffix = '-wait';
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
} from '@/hooks/use-send-message';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { MessageWaitSuffix } from '../constant/chat';
|
||||
|
||||
export const ExcludeTypes = [
|
||||
MessageEventType.Message,
|
||||
MessageEventType.MessageEnd,
|
||||
];
|
||||
|
||||
const resolveMessageId = (messageId: string) =>
|
||||
messageId?.replace(new RegExp(`${MessageWaitSuffix}$`), '');
|
||||
|
||||
export function useCacheChatLog() {
|
||||
const [messageIdPool, setMessageIdPool] = useState<
|
||||
Record<string, IEventList>
|
||||
@@ -22,8 +26,9 @@ export function useCacheChatLog() {
|
||||
|
||||
const filterEventListByMessageId = useCallback(
|
||||
(messageId: string) => {
|
||||
return messageIdPool[messageId]?.filter(
|
||||
(x) => x.message_id === messageId,
|
||||
const resolvedId = resolveMessageId(messageId);
|
||||
return messageIdPool[resolvedId]?.filter(
|
||||
(x) => x.message_id === resolvedId,
|
||||
);
|
||||
},
|
||||
[messageIdPool],
|
||||
@@ -31,9 +36,8 @@ export function useCacheChatLog() {
|
||||
|
||||
const filterEventListByEventType = useCallback(
|
||||
(eventType: string) => {
|
||||
return messageIdPool[currentMessageId]?.filter(
|
||||
(x) => x.event === eventType,
|
||||
);
|
||||
const resolvedId = resolveMessageId(currentMessageId);
|
||||
return messageIdPool[resolvedId]?.filter((x) => x.event === eventType);
|
||||
},
|
||||
[messageIdPool, currentMessageId],
|
||||
);
|
||||
@@ -62,19 +66,20 @@ export function useCacheChatLog() {
|
||||
}, []);
|
||||
|
||||
const currentEventListWithoutMessage = useMemo(() => {
|
||||
const list = messageIdPool[currentMessageId]?.filter(
|
||||
const resolvedId = resolveMessageId(currentMessageId);
|
||||
const list = messageIdPool[resolvedId]?.filter(
|
||||
(x) =>
|
||||
x.message_id === currentMessageId &&
|
||||
ExcludeTypes.every((y) => y !== x.event),
|
||||
x.message_id === resolvedId && ExcludeTypes.every((y) => y !== x.event),
|
||||
);
|
||||
return list as INodeEvent[];
|
||||
}, [currentMessageId, messageIdPool]);
|
||||
|
||||
const currentEventListWithoutMessageById = useCallback(
|
||||
(messageId: string) => {
|
||||
const list = messageIdPool[messageId]?.filter(
|
||||
const resolvedId = resolveMessageId(messageId);
|
||||
const list = messageIdPool[resolvedId]?.filter(
|
||||
(x) =>
|
||||
x.message_id === messageId &&
|
||||
x.message_id === resolvedId &&
|
||||
ExcludeTypes.every((y) => y !== x.event),
|
||||
);
|
||||
return list as INodeEvent[];
|
||||
|
||||
@@ -6,14 +6,10 @@ import { BeginQuery } from '../interface';
|
||||
import { buildBeginQueryWithObject } from '../utils';
|
||||
type IAwaitCompentData = {
|
||||
derivedMessages: IMessage[];
|
||||
sendFormMessage: (params: {
|
||||
inputs: Record<string, BeginQuery>;
|
||||
agent_id: string;
|
||||
}) => void;
|
||||
canvasId: string;
|
||||
sendFormMessage: (params: { inputs: Record<string, BeginQuery> }) => void;
|
||||
};
|
||||
const useAwaitCompentData = (props: IAwaitCompentData) => {
|
||||
const { derivedMessages, sendFormMessage, canvasId } = props;
|
||||
const useAwaitComponentData = (props: IAwaitCompentData) => {
|
||||
const { derivedMessages, sendFormMessage } = props;
|
||||
|
||||
const getInputs = useCallback((message: Message) => {
|
||||
return get(message, 'data.inputs', {}) as Record<string, BeginQuery>;
|
||||
@@ -37,13 +33,12 @@ const useAwaitCompentData = (props: IAwaitCompentData) => {
|
||||
const nextInputs = buildBeginQueryWithObject(inputs, values);
|
||||
sendFormMessage({
|
||||
inputs: nextInputs,
|
||||
agent_id: canvasId,
|
||||
});
|
||||
},
|
||||
[getInputs, sendFormMessage, canvasId],
|
||||
[getInputs, sendFormMessage],
|
||||
);
|
||||
|
||||
const isWaitting = useMemo(() => {
|
||||
const isWaiting = useMemo(() => {
|
||||
const temp = derivedMessages?.some((message, i) => {
|
||||
const flag =
|
||||
message.role === MessageType.Assistant &&
|
||||
@@ -53,7 +48,7 @@ const useAwaitCompentData = (props: IAwaitCompentData) => {
|
||||
});
|
||||
return temp;
|
||||
}, [derivedMessages]);
|
||||
return { getInputs, buildInputList, handleOk, isWaitting };
|
||||
return { getInputs, buildInputList, handleOk, isWaiting };
|
||||
};
|
||||
|
||||
export { useAwaitCompentData };
|
||||
export { useAwaitComponentData };
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from '@/lib/utils';
|
||||
import i18n, { changeLanguageAsync } from '@/locales/config';
|
||||
import DebugContent from '@/pages/agent/debug-content';
|
||||
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
|
||||
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
|
||||
import { useAwaitComponentData } from '@/pages/agent/hooks/use-chat-logic';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
@@ -64,10 +64,9 @@ const ChatContainer = () => {
|
||||
resetSession,
|
||||
} = useSendNextSharedMessage(addEventList);
|
||||
|
||||
const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({
|
||||
const { buildInputList, handleOk, isWaiting } = useAwaitComponentData({
|
||||
derivedMessages,
|
||||
sendFormMessage,
|
||||
canvasId: conversationId as string,
|
||||
});
|
||||
const sendDisabled = useSendButtonDisabled(value);
|
||||
|
||||
@@ -191,8 +190,8 @@ const ChatContainer = () => {
|
||||
<NextMessageInput
|
||||
isShared
|
||||
value={value}
|
||||
disabled={hasError || isWaitting}
|
||||
sendDisabled={sendDisabled || isWaitting}
|
||||
disabled={hasError || isWaiting}
|
||||
sendDisabled={sendDisabled || isWaiting}
|
||||
resize="vertical"
|
||||
conversationId={conversationId}
|
||||
onInputChange={handleInputChange}
|
||||
@@ -200,7 +199,7 @@ const ChatContainer = () => {
|
||||
sendLoading={sendLoading}
|
||||
stopOutputMessage={stopOutputMessage}
|
||||
onUpload={handleUploadFile}
|
||||
isUploading={loading || isWaitting}
|
||||
isUploading={loading || isWaiting}
|
||||
></NextMessageInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { IEventList, MessageEventType } from '@/hooks/use-send-message';
|
||||
import { IMessage, IReference } from '@/interfaces/database/chat';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@@ -18,3 +19,36 @@ export const buildAgentMessageItemReference = (
|
||||
|
||||
return reference ?? { doc_aggs: [], chunks: [], total: 0 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the message should be split into two separate entries:
|
||||
* one for the assistant's answer and one for the user input prompt.
|
||||
*
|
||||
* A split is needed when all of the following are true:
|
||||
* 1. The event list contains a `MessageEnd` event.
|
||||
* 2. The event list contains a `UserInputs` event.
|
||||
* 3. The `MessageEnd` event occurs before the `UserInputs` event.
|
||||
* 4. There is actual message content (`content` is truthy).
|
||||
*
|
||||
* @param eventList - The list of SSE events received from the server.
|
||||
* @param content - The assistant's message content extracted from the events.
|
||||
* @returns `true` if the message should be split, otherwise `false`.
|
||||
*/
|
||||
export function shouldSplitMessage(
|
||||
eventList: IEventList,
|
||||
content?: string,
|
||||
): boolean {
|
||||
const messageEndIndex = eventList.findIndex(
|
||||
(x) => x.event === MessageEventType.MessageEnd,
|
||||
);
|
||||
const userInputsIndex = eventList.findIndex(
|
||||
(x) => x.event === MessageEventType.UserInputs,
|
||||
);
|
||||
|
||||
return (
|
||||
messageEndIndex !== -1 &&
|
||||
userInputsIndex !== -1 &&
|
||||
messageEndIndex < userInputsIndex &&
|
||||
!!content
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user