2024-09-22 03:15:11 +08:00
import { addFileInfos , sortAgentSorts } from '../../tools/utils'
import { UUID_NIL } from './constants'
2024-10-24 12:09:46 +08:00
import type { IChatItem } from './chat/type'
import type { ChatItem , ChatItemInTree } from './types'
2024-10-21 10:32:37 +08:00
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
2024-09-22 03:15:11 +08:00
2024-07-18 21:54:16 +08:00
async function decodeBase64AndDecompress ( base64String : string ) {
const binaryString = atob ( base64String )
const compressedUint8Array = Uint8Array . from ( binaryString , char = > char . charCodeAt ( 0 ) )
2024-09-22 03:15:11 +08:00
const decompressedStream = new Response ( compressedUint8Array ) . body ? . pipeThrough ( new DecompressionStream ( 'gzip' ) )
2024-07-18 21:54:16 +08:00
const decompressedArrayBuffer = await new Response ( decompressedStream ) . arrayBuffer ( )
return new TextDecoder ( ) . decode ( decompressedArrayBuffer )
}
function getProcessedInputsFromUrlParams ( ) : Record < string , any > {
const urlParams = new URLSearchParams ( window . location . search )
const inputs : Record < string , any > = { }
urlParams . forEach ( async ( value , key ) = > {
inputs [ key ] = await decodeBase64AndDecompress ( decodeURIComponent ( value ) )
} )
return inputs
}
2024-09-23 18:44:09 +08:00
function getLastAnswer ( chatList : ChatItem [ ] ) {
for ( let i = chatList . length - 1 ; i >= 0 ; i -- ) {
const item = chatList [ i ]
2024-10-25 14:06:33 +08:00
if ( item . isAnswer && ! item . id . startsWith ( 'answer-placeholder-' ) && ! item . isOpeningStatement )
2024-09-23 18:44:09 +08:00
return item
}
return null
}
2024-09-22 03:15:11 +08:00
function appendQAToChatList ( chatList : ChatItem [ ] , item : any ) {
// we append answer first and then question since will reverse the whole chatList later
2024-10-21 10:32:37 +08:00
const answerFiles = item . message_files ? . filter ( ( file : any ) = > file . belongs_to === 'assistant' ) || [ ]
2024-09-22 03:15:11 +08:00
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 ,
2024-10-21 10:32:37 +08:00
message_files : getProcessedFilesFromResponse ( answerFiles . map ( ( item : any ) = > ( { . . . item , related_id : item.id } ) ) ) ,
2024-09-22 03:15:11 +08:00
} )
2024-10-21 10:32:37 +08:00
const questionFiles = item . message_files ? . filter ( ( file : any ) = > file . belongs_to === 'user' ) || [ ]
2024-09-22 03:15:11 +08:00
chatList . push ( {
id : ` question- ${ item . id } ` ,
content : item.query ,
isAnswer : false ,
2024-10-21 10:32:37 +08:00
message_files : getProcessedFilesFromResponse ( questionFiles . map ( ( item : any ) = > ( { . . . item , related_id : item.id } ) ) ) ,
2024-09-22 03:15:11 +08:00
} )
}
/ * *
* 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 ( )
}
2024-10-24 12:09:46 +08:00
function buildChatItemTree ( allMessages : IChatItem [ ] ) : ChatItemInTree [ ] {
const map : Record < string , ChatItemInTree > = { }
const rootNodes : ChatItemInTree [ ] = [ ]
const childrenCount : Record < string , number > = { }
let lastAppendedLegacyAnswer : ChatItemInTree | null = null
for ( let i = 0 ; i < allMessages . length ; i += 2 ) {
const question = allMessages [ i ] !
const answer = allMessages [ i + 1 ] !
const isLegacy = question . parentMessageId === UUID_NIL
const parentMessageId = isLegacy
? ( lastAppendedLegacyAnswer ? . id || '' )
: ( question . parentMessageId || '' )
// Process question
childrenCount [ parentMessageId ] = ( childrenCount [ parentMessageId ] || 0 ) + 1
const questionNode : ChatItemInTree = {
. . . question ,
children : [ ] ,
}
map [ question . id ] = questionNode
// Process answer
childrenCount [ question . id ] = 1
const answerNode : ChatItemInTree = {
. . . answer ,
children : [ ] ,
siblingIndex : isLegacy ? 0 : childrenCount [ parentMessageId ] - 1 ,
}
map [ answer . id ] = answerNode
// Connect question and answer
questionNode . children ! . push ( answerNode )
// Append to parent or add to root
if ( isLegacy ) {
if ( ! lastAppendedLegacyAnswer )
rootNodes . push ( questionNode )
else
lastAppendedLegacyAnswer . children ! . push ( questionNode )
lastAppendedLegacyAnswer = answerNode
}
else {
2024-11-21 14:12:01 +08:00
if (
! parentMessageId
|| ! allMessages . some ( item = > item . id === parentMessageId ) // parent message might not be fetched yet, in this case we will append the question to the root nodes
)
2024-10-24 12:09:46 +08:00
rootNodes . push ( questionNode )
else
map [ parentMessageId ] ? . children ! . push ( questionNode )
}
}
return rootNodes
}
function getThreadMessages ( tree : ChatItemInTree [ ] , targetMessageId? : string ) : ChatItemInTree [ ] {
let ret : ChatItemInTree [ ] = [ ]
let targetNode : ChatItemInTree | undefined
// find path to the target message
const stack = tree . toReversed ( ) . map ( rootNode = > ( {
node : rootNode ,
path : [ rootNode ] ,
} ) )
while ( stack . length > 0 ) {
const { node , path } = stack . pop ( ) !
if (
node . id === targetMessageId
|| ( ! targetMessageId && ! node . children ? . length && ! stack . length ) // if targetMessageId is not provided, we use the last message in the tree as the target
) {
targetNode = node
ret = path . map ( ( item , index ) = > {
if ( ! item . isAnswer )
return item
const parentAnswer = path [ index - 2 ]
const siblingCount = ! parentAnswer ? tree.length : parentAnswer.children ! . length
const prevSibling = ! parentAnswer ? tree [ item . siblingIndex ! - 1 ] ? . children ? . [ 0 ] ? . id : parentAnswer.children ! [ item . siblingIndex ! - 1 ] ? . children ? . [ 0 ] . id
const nextSibling = ! parentAnswer ? tree [ item . siblingIndex ! + 1 ] ? . children ? . [ 0 ] ? . id : parentAnswer.children ! [ item . siblingIndex ! + 1 ] ? . children ? . [ 0 ] . id
return { . . . item , siblingCount , prevSibling , nextSibling }
} )
break
}
if ( node . children ) {
for ( let i = node . children . length - 1 ; i >= 0 ; i -- ) {
stack . push ( {
node : node.children [ i ] ,
path : [ . . . path , node . children [ i ] ] ,
} )
}
}
}
// append all descendant messages to the path
if ( targetNode ) {
const stack = [ targetNode ]
while ( stack . length > 0 ) {
const node = stack . pop ( ) !
if ( node !== targetNode )
ret . push ( node )
if ( node . children ? . length ) {
const lastChild = node . children . at ( - 1 ) !
if ( ! lastChild . isAnswer ) {
stack . push ( lastChild )
continue
}
const parentAnswer = ret . at ( - 2 )
const siblingCount = parentAnswer ? . children ? . length
const prevSibling = parentAnswer ? . children ? . at ( - 2 ) ? . children ? . [ 0 ] ? . id
stack . push ( { . . . lastChild , siblingCount , prevSibling } )
}
}
}
return ret
}
2024-07-18 21:54:16 +08:00
export {
getProcessedInputsFromUrlParams ,
2024-09-22 03:15:11 +08:00
getPrevChatList ,
2024-10-24 12:09:46 +08:00
getLastAnswer ,
buildChatItemTree ,
getThreadMessages ,
2024-07-18 21:54:16 +08:00
}