ForcePilot/web/src/utils/messageProcessor.js

303 lines
9.3 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');
// 按照对话分组
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 {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);
}
}
}
}
/**
* 处理流式响应数据块
* @param {Object} data - 响应数据
* @param {Object} onGoingConv - 进行中的对话对象
* @param {Object} state - 状态对象
* @param {Function} getAgentHistory - 获取历史记录函数
* @param {Function} handleError - 错误处理函数
*/
static async processResponseChunk(data, onGoingConv, state, getAgentHistory, handleError) {
try {
switch (data.status) {
case 'init':
// 代表服务端收到请求并返回第一个响应
state.waitingServerResponse = false;
onGoingConv.msgChunks[data.request_id] = [data.msg];
break;
case 'loading':
if (data.msg.id) {
if (!onGoingConv.msgChunks[data.msg.id]) {
onGoingConv.msgChunks[data.msg.id] = [];
}
onGoingConv.msgChunks[data.msg.id].push(data.msg);
}
break;
case 'error':
console.error("流式处理出错:", data.message);
handleError(new Error(data.message), 'stream');
break;
case 'finished':
await getAgentHistory();
break;
default:
console.warn('未知的响应状态:', data.status);
}
} catch (error) {
handleError(error, 'stream');
}
}
/**
* 处理流式响应
* @param {Response} response - 响应对象
* @param {Function} processChunk - 处理块的函数
* @param {Function} scrollToBottom - 滚动到底部函数
* @param {Function} handleError - 错误处理函数
*/
static async handleStreamResponse(response, processChunk, scrollToBottom, handleError) {
try {
const reader = response.body.getReader();
let buffer = '';
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后一行可能不完整的内容
for (const line of lines) {
if (line.trim()) {
try {
const data = JSON.parse(line.trim());
await processChunk(data);
} catch (e) {
console.debug('解析JSON出错:', e.message);
}
}
}
await scrollToBottom();
}
// 处理缓冲区中可能剩余的内容
if (buffer.trim()) {
try {
const data = JSON.parse(buffer.trim());
await processChunk(data);
} catch (e) {
console.warn('最终缓冲区内容无法解析:', buffer);
}
}
} catch (error) {
handleError(error, 'stream');
}
}
}
export default MessageProcessor;