utils.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { addFileInfos, sortAgentSorts } from '../../tools/utils'
  2. import { UUID_NIL } from './constants'
  3. import type { IChatItem } from './chat/type'
  4. import type { ChatItem, ChatItemInTree } from './types'
  5. import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
  6. async function decodeBase64AndDecompress(base64String: string) {
  7. const binaryString = atob(base64String)
  8. const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
  9. const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
  10. const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
  11. return new TextDecoder().decode(decompressedArrayBuffer)
  12. }
  13. function getProcessedInputsFromUrlParams(): Record<string, any> {
  14. const urlParams = new URLSearchParams(window.location.search)
  15. const inputs: Record<string, any> = {}
  16. urlParams.forEach(async (value, key) => {
  17. inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value))
  18. })
  19. return inputs
  20. }
  21. function getLastAnswer(chatList: ChatItem[]) {
  22. for (let i = chatList.length - 1; i >= 0; i--) {
  23. const item = chatList[i]
  24. if (item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement)
  25. return item
  26. }
  27. return null
  28. }
  29. function appendQAToChatList(chatList: ChatItem[], item: any) {
  30. // we append answer first and then question since will reverse the whole chatList later
  31. const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
  32. chatList.push({
  33. id: item.id,
  34. content: item.answer,
  35. agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
  36. feedback: item.feedback,
  37. isAnswer: true,
  38. citation: item.retriever_resources,
  39. message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
  40. })
  41. const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
  42. chatList.push({
  43. id: `question-${item.id}`,
  44. content: item.query,
  45. isAnswer: false,
  46. message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
  47. })
  48. }
  49. /**
  50. * Computes the latest thread messages from all messages of the conversation.
  51. * Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
  52. *
  53. * @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.
  54. * @returns An array of ChatItems representing the latest thread.
  55. */
  56. function getPrevChatList(fetchedMessages: any[]) {
  57. const ret: ChatItem[] = []
  58. let nextMessageId = null
  59. for (const item of fetchedMessages) {
  60. if (!item.parent_message_id) {
  61. appendQAToChatList(ret, item)
  62. break
  63. }
  64. if (!nextMessageId) {
  65. appendQAToChatList(ret, item)
  66. nextMessageId = item.parent_message_id
  67. }
  68. else {
  69. if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
  70. appendQAToChatList(ret, item)
  71. nextMessageId = item.parent_message_id
  72. }
  73. }
  74. }
  75. return ret.reverse()
  76. }
  77. function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
  78. const map: Record<string, ChatItemInTree> = {}
  79. const rootNodes: ChatItemInTree[] = []
  80. const childrenCount: Record<string, number> = {}
  81. let lastAppendedLegacyAnswer: ChatItemInTree | null = null
  82. for (let i = 0; i < allMessages.length; i += 2) {
  83. const question = allMessages[i]!
  84. const answer = allMessages[i + 1]!
  85. const isLegacy = question.parentMessageId === UUID_NIL
  86. const parentMessageId = isLegacy
  87. ? (lastAppendedLegacyAnswer?.id || '')
  88. : (question.parentMessageId || '')
  89. // Process question
  90. childrenCount[parentMessageId] = (childrenCount[parentMessageId] || 0) + 1
  91. const questionNode: ChatItemInTree = {
  92. ...question,
  93. children: [],
  94. }
  95. map[question.id] = questionNode
  96. // Process answer
  97. childrenCount[question.id] = 1
  98. const answerNode: ChatItemInTree = {
  99. ...answer,
  100. children: [],
  101. siblingIndex: isLegacy ? 0 : childrenCount[parentMessageId] - 1,
  102. }
  103. map[answer.id] = answerNode
  104. // Connect question and answer
  105. questionNode.children!.push(answerNode)
  106. // Append to parent or add to root
  107. if (isLegacy) {
  108. if (!lastAppendedLegacyAnswer)
  109. rootNodes.push(questionNode)
  110. else
  111. lastAppendedLegacyAnswer.children!.push(questionNode)
  112. lastAppendedLegacyAnswer = answerNode
  113. }
  114. else {
  115. if (
  116. !parentMessageId
  117. || !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
  118. )
  119. rootNodes.push(questionNode)
  120. else
  121. map[parentMessageId]?.children!.push(questionNode)
  122. }
  123. }
  124. return rootNodes
  125. }
  126. function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): ChatItemInTree[] {
  127. let ret: ChatItemInTree[] = []
  128. let targetNode: ChatItemInTree | undefined
  129. // find path to the target message
  130. const stack = tree.toReversed().map(rootNode => ({
  131. node: rootNode,
  132. path: [rootNode],
  133. }))
  134. while (stack.length > 0) {
  135. const { node, path } = stack.pop()!
  136. if (
  137. node.id === targetMessageId
  138. || (!targetMessageId && !node.children?.length && !stack.length) // if targetMessageId is not provided, we use the last message in the tree as the target
  139. ) {
  140. targetNode = node
  141. ret = path.map((item, index) => {
  142. if (!item.isAnswer)
  143. return item
  144. const parentAnswer = path[index - 2]
  145. const siblingCount = !parentAnswer ? tree.length : parentAnswer.children!.length
  146. const prevSibling = !parentAnswer ? tree[item.siblingIndex! - 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! - 1]?.children?.[0].id
  147. const nextSibling = !parentAnswer ? tree[item.siblingIndex! + 1]?.children?.[0]?.id : parentAnswer.children![item.siblingIndex! + 1]?.children?.[0].id
  148. return { ...item, siblingCount, prevSibling, nextSibling }
  149. })
  150. break
  151. }
  152. if (node.children) {
  153. for (let i = node.children.length - 1; i >= 0; i--) {
  154. stack.push({
  155. node: node.children[i],
  156. path: [...path, node.children[i]],
  157. })
  158. }
  159. }
  160. }
  161. // append all descendant messages to the path
  162. if (targetNode) {
  163. const stack = [targetNode]
  164. while (stack.length > 0) {
  165. const node = stack.pop()!
  166. if (node !== targetNode)
  167. ret.push(node)
  168. if (node.children?.length) {
  169. const lastChild = node.children.at(-1)!
  170. if (!lastChild.isAnswer) {
  171. stack.push(lastChild)
  172. continue
  173. }
  174. const parentAnswer = ret.at(-2)
  175. const siblingCount = parentAnswer?.children?.length
  176. const prevSibling = parentAnswer?.children?.at(-2)?.children?.[0]?.id
  177. stack.push({ ...lastChild, siblingCount, prevSibling })
  178. }
  179. }
  180. }
  181. return ret
  182. }
  183. export {
  184. getProcessedInputsFromUrlParams,
  185. getPrevChatList,
  186. getLastAnswer,
  187. buildChatItemTree,
  188. getThreadMessages,
  189. }