ForcePilot/web/src/utils/messageProcessor.js

425 lines
14 KiB
JavaScript
Raw Normal View History

/**
* 消息处理工具类
*/
export class MessageProcessor {
/**
* 将工具结果与消息合并
* @param {Array} msgs - 消息数组
* @returns {Array} 合并后的消息数组
*/
static convertToolResultToMessages(msgs) {
const toolResponseMap = new Map()
// 构建工具响应映射
for (const item of msgs) {
if (item.type === 'tool') {
// 使用多种可能的ID字段来匹配工具调用
const toolCallId = item.tool_call_id || item.id
if (toolCallId) {
toolResponseMap.set(toolCallId, item)
}
}
}
// 合并工具调用和响应
const convertedMsgs = msgs.map((item) => {
if (item.type === 'ai' && item.tool_calls && item.tool_calls.length > 0) {
return {
...item,
tool_calls: item.tool_calls.map((toolCall) => {
const toolResponse = toolResponseMap.get(toolCall.id)
return {
...toolCall,
tool_call_result: toolResponse || null
}
})
}
}
return item
})
return convertedMsgs
}
/**
* 将服务器历史记录转换为对话格式
* @param {Array} serverHistory - 服务器历史记录
* @returns {Array} 对话数组
*/
static convertServerHistoryToMessages(serverHistory) {
// Filter out standalone 'tool' messages since tool results are already in AI messages' tool_calls
// Backend new storage: tool results are embedded in AI messages' tool_calls array with tool_call_result field
const filteredHistory = serverHistory.filter(
(item) =>
item.type !== 'tool' &&
!(item.type === 'human' && item.extra_metadata?.source === 'ask_user_question_resume')
)
// 按照对话分组
const conversations = []
let currentConv = null
for (const item of filteredHistory) {
if (item.type === 'human') {
// Start new conversation, finalize previous one
if (currentConv) {
// Find the last AI message and mark it as final
for (let i = currentConv.messages.length - 1; i >= 0; i--) {
if (currentConv.messages[i].type === 'ai') {
currentConv.messages[i].isLast = true
currentConv.status = 'finished'
break
}
}
}
currentConv = {
messages: [item],
status: 'loading'
}
conversations.push(currentConv)
} else if (item.type === 'ai' && currentConv) {
currentConv.messages.push(item)
}
}
// Mark the last conversation as finished
if (currentConv && currentConv.messages.length > 0) {
// Find the last AI message and mark it as final
for (let i = currentConv.messages.length - 1; i >= 0; i--) {
if (currentConv.messages[i].type === 'ai') {
currentConv.messages[i].isLast = true
currentConv.status = 'finished'
break
}
}
}
return conversations
}
/**
* 提取一轮对话中所有知识库检索块
* @param {Object} conv - 单轮对话
* @param {Array} databases - 知识库列表
* @returns {Array} 归一化后的检索块
*/
static extractKnowledgeChunksFromConversation(conv, databases = []) {
if (!conv || !Array.isArray(conv.messages) || conv.messages.length === 0) return []
const databaseNames = new Set(
2026-03-04 04:13:05 +08:00
(databases || [])
.map((db) => db?.name)
.filter((name) => typeof name === 'string' && name.trim())
)
if (databaseNames.size === 0) return []
const normalizedChunks = []
const dedupSet = new Set()
const appendChunk = (chunk, kbName) => {
if (!chunk || typeof chunk !== 'object') return
const content = typeof chunk.content === 'string' ? chunk.content.trim() : ''
if (!content) return
const metadata = chunk.metadata && typeof chunk.metadata === 'object' ? chunk.metadata : {}
const dedupKey =
metadata.chunk_id && typeof metadata.chunk_id === 'string'
? `${kbName}::${metadata.chunk_id}`
: `${kbName}::${content}`
if (dedupSet.has(dedupKey)) return
dedupSet.add(dedupKey)
const score = typeof chunk.score === 'number' ? chunk.score : null
normalizedChunks.push({
kb_name: kbName,
content,
score,
metadata: {
source: metadata.source || '',
file_id: metadata.file_id || '',
chunk_id: metadata.chunk_id || '',
2026-03-04 04:13:05 +08:00
chunk_index: metadata.chunk_index
}
})
}
const parseToolResultContent = (content) => {
if (Array.isArray(content)) return content
if (content && typeof content === 'object') return content
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return null
}
}
return null
}
for (const msg of conv.messages) {
if (!msg || msg.type !== 'ai' || !Array.isArray(msg.tool_calls)) continue
for (const toolCall of msg.tool_calls) {
const kbName = toolCall?.name || toolCall?.function?.name
if (!databaseNames.has(kbName)) continue
const content = toolCall?.tool_call_result?.content
const parsed = parseToolResultContent(content)
if (!parsed) continue
// Milvus / Dify: 直接是 chunks 数组
if (Array.isArray(parsed)) {
for (const chunk of parsed) appendChunk(chunk, kbName)
continue
}
const wrappedChunks = parsed?.data?.chunks
if (Array.isArray(wrappedChunks)) {
for (const chunk of wrappedChunks) appendChunk(chunk, kbName)
}
}
}
normalizedChunks.sort((a, b) => {
const scoreA = typeof a.score === 'number' ? a.score : Number.NEGATIVE_INFINITY
const scoreB = typeof b.score === 'number' ? b.score : Number.NEGATIVE_INFINITY
return scoreB - scoreA
})
return normalizedChunks
}
/**
* 提取一轮对话中的网络搜索来源
* @param {Object} conv - 单轮对话
* @returns {Array} 归一化后的网络来源
*/
static extractWebSourcesFromConversation(conv) {
if (!conv || !Array.isArray(conv.messages) || conv.messages.length === 0) return []
const webSources = []
const dedupSet = new Set()
const parseToolResultContent = (content) => {
if (Array.isArray(content)) return content
if (content && typeof content === 'object') return content
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return null
}
}
return null
}
for (const msg of conv.messages) {
if (!msg || msg.type !== 'ai' || !Array.isArray(msg.tool_calls)) continue
for (const toolCall of msg.tool_calls) {
const toolName = (toolCall?.name || toolCall?.function?.name || '').toLowerCase()
if (!toolName.includes('tavily_search')) continue
const content = toolCall?.tool_call_result?.content
const parsed = parseToolResultContent(content)
const results = Array.isArray(parsed?.results) ? parsed.results : []
if (results.length === 0) continue
for (const item of results) {
const title = typeof item?.title === 'string' ? item.title.trim() : ''
const url = typeof item?.url === 'string' ? item.url.trim() : ''
if (!title || !url) continue
if (dedupSet.has(url)) continue
dedupSet.add(url)
webSources.push({
tool_name: toolCall?.name || toolCall?.function?.name || '网络搜索',
title,
url,
score: typeof item?.score === 'number' ? item.score : null,
content: typeof item?.content === 'string' ? item.content : '',
2026-03-04 04:13:05 +08:00
published_date: typeof item?.published_date === 'string' ? item.published_date : ''
})
}
}
}
webSources.sort((a, b) => {
const scoreA = typeof a.score === 'number' ? a.score : Number.NEGATIVE_INFINITY
const scoreB = typeof b.score === 'number' ? b.score : Number.NEGATIVE_INFINITY
return scoreB - scoreA
})
return webSources
}
/**
* 提取单个消息中的来源
* @param {Object} message - 消息对象
* @param {Array} databases - 知识库列表
* @returns {{knowledgeChunks: Array, webSources: Array}}
*/
static extractSourcesFromMessage(message, databases = []) {
if (!message || message.type !== 'ai') return { knowledgeChunks: [], webSources: [] }
// 复用提取逻辑,通过构建临时对话对象
const mockConv = { messages: [message] }
return {
knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(mockConv, databases),
2026-03-04 04:13:05 +08:00
webSources: MessageProcessor.extractWebSourcesFromConversation(mockConv)
}
}
/**
* 提取一轮对话中的全部来源知识库+网络搜索
* @param {Object} conv - 单轮对话
* @param {Array} databases - 知识库列表
* @returns {{knowledgeChunks: Array, webSources: Array}}
*/
static extractSourcesFromConversation(conv, databases = []) {
return {
knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(conv, databases),
2026-03-04 04:13:05 +08:00
webSources: MessageProcessor.extractWebSourcesFromConversation(conv)
}
}
/**
* 解析助手消息正文与推理内容保持渲染和列表拆分使用同一套规则
* @param {Object} message - AI 消息对象
* @returns {{content: string, reasoningContent: string}}
*/
static parseAssistantMessageBody(message) {
let content = typeof message?.content === 'string' ? message.content.trim() : ''
let reasoningContent = message?.additional_kwargs?.reasoning_content || ''
if (!reasoningContent && content) {
const thinkRegex = /<think>(.*?)<\/think>|<think>(.*?)$/s
const thinkMatch = content.match(thinkRegex)
if (thinkMatch) {
reasoningContent = (thinkMatch[1] || thinkMatch[2] || '').trim()
content = content.replace(thinkMatch[0], '').trim()
}
}
return { content, reasoningContent }
}
/**
* 合并消息块
* @param {Array} chunks - 消息块数组
* @returns {Object|null} 合并后的消息
*/
static mergeMessageChunk(chunks) {
if (chunks.length === 0) return null
// 深拷贝第一个chunk作为结果
const result = JSON.parse(JSON.stringify(chunks[0]))
// 处理用户消息的内容格式 - 确保显示纯文本
if (result.type === 'human' || result.role === 'user') {
// 如果content是数组格式LangChain多模态消息提取文本部分
if (Array.isArray(result.content)) {
const textPart = result.content.find((item) => item.type === 'text')
result.content = textPart ? textPart.text : ''
} else {
result.content = result.content || ''
}
} else {
result.content = result.content || ''
}
// 合并后续chunks
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i]
// 合并内容
if (chunk.content) {
result.content += chunk.content
}
// 合并reasoning_content
if (chunk.reasoning_content) {
if (!result.reasoning_content) {
result.reasoning_content = ''
}
result.reasoning_content += chunk.reasoning_content
}
// 合并additional_kwargs中的reasoning_content
if (chunk.additional_kwargs?.reasoning_content) {
if (!result.additional_kwargs) result.additional_kwargs = {}
if (!result.additional_kwargs.reasoning_content) {
result.additional_kwargs.reasoning_content = ''
}
result.additional_kwargs.reasoning_content += chunk.additional_kwargs.reasoning_content
}
// 合并tool_calls (处理新的数据结构)
MessageProcessor._mergeToolCalls(result, chunk)
}
// 处理AIMessageChunk类型
if (result.type === 'AIMessageChunk') {
result.type = 'ai'
}
return result
}
/**
* 合并工具调用
* @private
* @param {Object} result - 结果对象
* @param {Object} chunk - 当前块
*/
static _mergeToolCalls(result, chunk) {
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
// 确保 result 有 tool_calls 数组
if (!result.tool_calls) result.tool_calls = []
for (const toolCallChunk of chunk.tool_call_chunks) {
// 使用 index 来标识工具调用(因为可能有多个工具调用)
const existingToolCallIndex = result.tool_calls.findIndex(
(t) => t.index === toolCallChunk.index
)
if (existingToolCallIndex !== -1) {
// 合并相同index的tool call
const existingToolCall = result.tool_calls[existingToolCallIndex]
// 更新名称和ID如果存在
if (toolCallChunk.name && !existingToolCall.function?.name) {
if (!existingToolCall.function) existingToolCall.function = {}
existingToolCall.function.name = toolCallChunk.name
}
if (toolCallChunk.id && !existingToolCall.id) {
existingToolCall.id = toolCallChunk.id
}
// 合并参数
if (toolCallChunk.args) {
if (!existingToolCall.function) existingToolCall.function = {}
if (!existingToolCall.function.arguments) existingToolCall.function.arguments = ''
existingToolCall.function.arguments += toolCallChunk.args
}
} else {
// 添加新的tool call
const newToolCall = {
index: toolCallChunk.index,
id: toolCallChunk.id,
function: {
name: toolCallChunk.name || null,
arguments: toolCallChunk.args || ''
}
}
result.tool_calls.push(newToolCall)
}
}
}
}
}
export default MessageProcessor