feat: regenerate in Chat, agent and Chatflow app (#7661)

This commit is contained in:
Hash Brown
2024-09-22 03:15:11 +08:00
committed by GitHub
parent b32a7713e0
commit 8c51d06222
51 changed files with 606 additions and 181 deletions

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
@@ -44,6 +45,8 @@ const ChatWrapper = () => {
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
chatList,
chatListRef,
handleUpdateChatList,
handleSend,
handleStop,
isResponding,
@@ -63,11 +66,12 @@ const ChatWrapper = () => {
currentChatInstanceRef.current.handleStop = handleStop
}, [])
const doSend: OnSend = useCallback((message, files) => {
const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = {
query: message,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
}
if (appConfig?.file_upload?.image.enabled && files?.length)
@@ -83,6 +87,7 @@ const ChatWrapper = () => {
},
)
}, [
chatListRef,
appConfig,
currentConversationId,
currentConversationItem,
@@ -92,6 +97,23 @@ const ChatWrapper = () => {
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = prevMessages.at(-1)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const chatNode = useMemo(() => {
if (inputsForms.length) {
return (
@@ -148,6 +170,7 @@ const ChatWrapper = () => {
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}

View File

@@ -12,10 +12,10 @@ import produce from 'immer'
import type {
Callback,
ChatConfig,
ChatItem,
Feedback,
} from '../types'
import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList } from '../utils'
import {
delConversation,
fetchAppInfo,
@@ -34,7 +34,6 @@ import type {
AppData,
ConversationItem,
} from '@/models/share'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config'
import { useAppFavicon } from '@/hooks/use-app-favicon'
@@ -108,32 +107,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const appPrevChatList = useMemo(() => {
const data = appChatListData?.data || []
const chatList: ChatItem[] = []
if (currentConversationId && data.length) {
data.forEach((item: any) => {
chatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
chatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
})
})
}
return chatList
}, [appChatListData, currentConversationId])
const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
: [],
[appChatListData, currentConversationId],
)
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)

View File

@@ -35,6 +35,7 @@ type AnswerProps = {
chatAnswerContainerInner?: string
hideProcessDetail?: boolean
appData?: AppData
noChatInput?: boolean
}
const Answer: FC<AnswerProps> = ({
item,
@@ -48,6 +49,7 @@ const Answer: FC<AnswerProps> = ({
chatAnswerContainerInner,
hideProcessDetail,
appData,
noChatInput,
}) => {
const { t } = useTranslation()
const {
@@ -110,6 +112,7 @@ const Answer: FC<AnswerProps> = ({
question={question}
index={index}
showPromptLog={showPromptLog}
noChatInput={noChatInput}
/>
)
}

View File

@@ -7,6 +7,7 @@ import {
import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types'
import { useChatContext } from '../context'
import RegenerateBtn from '@/app/components/base/regenerate-btn'
import cn from '@/utils/classnames'
import CopyBtn from '@/app/components/base/copy-btn'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
@@ -28,6 +29,7 @@ type OperationProps = {
maxSize: number
contentWidth: number
hasWorkflowProcess: boolean
noChatInput?: boolean
}
const Operation: FC<OperationProps> = ({
item,
@@ -37,6 +39,7 @@ const Operation: FC<OperationProps> = ({
maxSize,
contentWidth,
hasWorkflowProcess,
noChatInput,
}) => {
const { t } = useTranslation()
const {
@@ -45,6 +48,7 @@ const Operation: FC<OperationProps> = ({
onAnnotationEdited,
onAnnotationRemoved,
onFeedback,
onRegenerate,
} = useChatContext()
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
const {
@@ -159,12 +163,13 @@ const Operation: FC<OperationProps> = ({
</div>
)
}
{
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
}
{
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<Tooltip
popupContent={t('appDebug.operation.agree')}
>
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
<Tooltip popupContent={t('appDebug.operation.agree')}>
<div
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
onClick={() => handleFeedback('like')}

View File

@@ -12,6 +12,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
| 'answerIcon'
| 'allToolIcons'
| 'onSend'
| 'onRegenerate'
| 'onAnnotationEdited'
| 'onAnnotationAdded'
| 'onAnnotationRemoved'
@@ -36,6 +37,7 @@ export const ChatContextProvider = ({
answerIcon,
allToolIcons,
onSend,
onRegenerate,
onAnnotationEdited,
onAnnotationAdded,
onAnnotationRemoved,
@@ -51,6 +53,7 @@ export const ChatContextProvider = ({
answerIcon,
allToolIcons,
onSend,
onRegenerate,
onAnnotationEdited,
onAnnotationAdded,
onAnnotationRemoved,

View File

@@ -647,7 +647,8 @@ export const useChat = (
return {
chatList,
setChatList,
chatListRef,
handleUpdateChatList,
conversationId: conversationId.current,
isResponding,
setIsResponding,

View File

@@ -16,6 +16,7 @@ import type {
ChatConfig,
ChatItem,
Feedback,
OnRegenerate,
OnSend,
} from '../types'
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
@@ -42,6 +43,7 @@ export type ChatProps = {
onStopResponding?: () => void
noChatInput?: boolean
onSend?: OnSend
onRegenerate?: OnRegenerate
chatContainerClassName?: string
chatContainerInnerClassName?: string
chatFooterClassName?: string
@@ -67,6 +69,7 @@ const Chat: FC<ChatProps> = ({
appData,
config,
onSend,
onRegenerate,
chatList,
isResponding,
noStopResponding,
@@ -186,6 +189,7 @@ const Chat: FC<ChatProps> = ({
answerIcon={answerIcon}
allToolIcons={allToolIcons}
onSend={onSend}
onRegenerate={onRegenerate}
onAnnotationAdded={onAnnotationAdded}
onAnnotationEdited={onAnnotationEdited}
onAnnotationRemoved={onAnnotationRemoved}
@@ -219,6 +223,7 @@ const Chat: FC<ChatProps> = ({
showPromptLog={showPromptLog}
chatAnswerContainerInner={chatAnswerContainerInner}
hideProcessDetail={hideProcessDetail}
noChatInput={noChatInput}
/>
)
}

View File

@@ -95,6 +95,7 @@ export type IChatItem = {
// for agent log
conversationId?: string
input?: any
parentMessageId?: string
}
export type Metadata = {

View File

@@ -1 +1,2 @@
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
@@ -45,11 +46,13 @@ const ChatWrapper = () => {
} as ChatConfig
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
chatListRef,
chatList,
handleSend,
handleStop,
isResponding,
suggestedQuestions,
handleUpdateChatList,
} = useChat(
appConfig,
{
@@ -65,11 +68,12 @@ const ChatWrapper = () => {
currentChatInstanceRef.current.handleStop = handleStop
}, [])
const doSend: OnSend = useCallback((message, files) => {
const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = {
query: message,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || chatListRef.current.at(-1)?.id || null,
}
if (appConfig?.file_upload?.image.enabled && files?.length)
@@ -85,6 +89,7 @@ const ChatWrapper = () => {
},
)
}, [
chatListRef,
appConfig,
currentConversationId,
currentConversationItem,
@@ -94,6 +99,23 @@ const ChatWrapper = () => {
isInstalledApp,
appId,
])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)
if (index === -1)
return
const prevMessages = chatList.slice(0, index)
const question = prevMessages.pop()
const lastAnswer = prevMessages.at(-1)
if (!question)
return
handleUpdateChatList(prevMessages)
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
}, [chatList, handleUpdateChatList, doSend])
const chatNode = useMemo(() => {
if (inputsForms.length) {
return (
@@ -136,6 +158,7 @@ const ChatWrapper = () => {
chatFooterClassName='pb-4'
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}

View File

@@ -11,10 +11,10 @@ import { useLocalStorageState } from 'ahooks'
import produce from 'immer'
import type {
ChatConfig,
ChatItem,
Feedback,
} from '../types'
import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils'
import {
fetchAppInfo,
fetchAppMeta,
@@ -28,10 +28,8 @@ import type {
// AppData,
ConversationItem,
} from '@/models/share'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config'
import { getProcessedInputsFromUrlParams } from '@/app/components/base/chat/utils'
export const useEmbeddedChatbot = () => {
const isInstalledApp = false
@@ -75,32 +73,12 @@ export const useEmbeddedChatbot = () => {
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
const appPrevChatList = useMemo(() => {
const data = appChatListData?.data || []
const chatList: ChatItem[] = []
if (currentConversationId && data.length) {
data.forEach((item: any) => {
chatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
chatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
})
})
}
return chatList
}, [appChatListData, currentConversationId])
const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data)
: [],
[appChatListData, currentConversationId],
)
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
@@ -155,7 +133,7 @@ export const useEmbeddedChatbot = () => {
type: 'text-input',
}
})
}, [appParams])
}, [initInputs, appParams])
useEffect(() => {
// init inputs from url params

View File

@@ -63,7 +63,9 @@ export type ChatItem = IChatItem & {
conversationId?: string
}
export type OnSend = (message: string, files?: VisionFile[]) => void
export type OnSend = (message: string, files?: VisionFile[], last_answer?: ChatItem) => void
export type OnRegenerate = (chatItem: ChatItem) => void
export type Callback = {
onSuccess: () => void

View File

@@ -1,7 +1,11 @@
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
import { UUID_NIL } from './constants'
import type { ChatItem } from './types'
async function decodeBase64AndDecompress(base64String: string) {
const binaryString = atob(base64String)
const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
const decompressedStream = new Response(compressedUint8Array).body.pipeThrough(new DecompressionStream('gzip'))
const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
return new TextDecoder().decode(decompressedArrayBuffer)
}
@@ -15,6 +19,57 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
return inputs
}
function appendQAToChatList(chatList: ChatItem[], item: any) {
// we append answer first and then question since will reverse the whole chatList later
chatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
})
chatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
}
/**
* Computes the latest thread messages from all messages of the conversation.
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
*
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
* @returns An array of ChatItems representing the latest thread.
*/
function getPrevChatList(fetchedMessages: any[]) {
const ret: ChatItem[] = []
let nextMessageId = null
for (const item of fetchedMessages) {
if (!item.parent_message_id) {
appendQAToChatList(ret, item)
break
}
if (!nextMessageId) {
appendQAToChatList(ret, item)
nextMessageId = item.parent_message_id
}
else {
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
appendQAToChatList(ret, item)
nextMessageId = item.parent_message_id
}
}
}
return ret.reverse()
}
export {
getProcessedInputsFromUrlParams,
getPrevChatList,
}