feat: 流式输出支持 - 思考内容流式显示后折叠,回答内容流式输出
This commit is contained in:
64
main_v2.py
64
main_v2.py
@@ -638,39 +638,71 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str):
|
|||||||
# 获取对话历史
|
# 获取对话历史
|
||||||
history = conv_service.get_conversation_history(conversation_id, limit=agent_config['agent'].get('max_history', 20))
|
history = conv_service.get_conversation_history(conversation_id, limit=agent_config['agent'].get('max_history', 20))
|
||||||
|
|
||||||
# 调用LLM
|
# 使用流式调用LLM
|
||||||
try:
|
try:
|
||||||
if agent_config['agent'].get('enable_thinking') and enable_thinking:
|
thinking_buffer = ""
|
||||||
await websocket.send_json({
|
content_buffer = ""
|
||||||
"type": "thinking_start",
|
in_thinking = False
|
||||||
"conversation_id": conversation_id
|
|
||||||
})
|
|
||||||
|
|
||||||
response, thinking_content = await llm_service.chat(
|
# 流式生成
|
||||||
|
async for chunk in llm_service.chat_stream(
|
||||||
messages=history,
|
messages=history,
|
||||||
provider_config=agent_config['provider'],
|
provider_config=agent_config['provider'],
|
||||||
agent_config=agent_config['agent'],
|
agent_config=agent_config['agent'],
|
||||||
enable_thinking=enable_thinking
|
enable_thinking=enable_thinking
|
||||||
)
|
):
|
||||||
|
chunk_type = chunk.get('type', 'content')
|
||||||
|
chunk_text = chunk.get('text', '')
|
||||||
|
|
||||||
|
if chunk_type == 'thinking':
|
||||||
|
if not in_thinking:
|
||||||
|
in_thinking = True
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "thinking_start",
|
||||||
|
"conversation_id": conversation_id
|
||||||
|
})
|
||||||
|
thinking_buffer += chunk_text
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "thinking_stream",
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"text": chunk_text
|
||||||
|
})
|
||||||
|
elif chunk_type == 'content':
|
||||||
|
if in_thinking:
|
||||||
|
in_thinking = False
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "thinking_end",
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"content": thinking_buffer # 发送完整思考内容用于折叠
|
||||||
|
})
|
||||||
|
content_buffer += chunk_text
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "content_stream",
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"text": chunk_text
|
||||||
|
})
|
||||||
|
|
||||||
if thinking_content:
|
# 如果思考没结束就结束了,发送思考结束
|
||||||
|
if in_thinking:
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "thinking_content",
|
"type": "thinking_end",
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"content": thinking_content
|
"content": thinking_buffer
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 发送完成通知
|
||||||
await websocket.send_json({
|
await websocket.send_json({
|
||||||
"type": "thinking_end",
|
"type": "stream_end",
|
||||||
"conversation_id": conversation_id
|
"conversation_id": conversation_id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 保存AI回复
|
||||||
assistant_msg = conv_service.add_message(
|
assistant_msg = conv_service.add_message(
|
||||||
conversation_id=conversation.id,
|
conversation_id=conversation.id,
|
||||||
role='assistant',
|
role='assistant',
|
||||||
content=response,
|
content=content_buffer,
|
||||||
source='web',
|
source='web',
|
||||||
thinking_content=thinking_content,
|
thinking_content=thinking_buffer if thinking_buffer else None,
|
||||||
agent_id=current_agent_id,
|
agent_id=current_agent_id,
|
||||||
model_used=agent_config['provider'].get('default_model')
|
model_used=agent_config['provider'].get('default_model')
|
||||||
)
|
)
|
||||||
@@ -681,8 +713,8 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str):
|
|||||||
"message": {
|
"message": {
|
||||||
"id": assistant_msg.id,
|
"id": assistant_msg.id,
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": response,
|
"content": content_buffer,
|
||||||
"thinking_content": thinking_content,
|
"thinking_content": thinking_buffer if thinking_buffer else None,
|
||||||
"source": "web",
|
"source": "web",
|
||||||
"agent_id": current_agent_id,
|
"agent_id": current_agent_id,
|
||||||
"agent_name": agent_config['agent'].get('display_name'),
|
"agent_name": agent_config['agent'].get('display_name'),
|
||||||
|
|||||||
@@ -349,7 +349,6 @@
|
|||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'history':
|
case 'history':
|
||||||
displayHistory(data.messages);
|
displayHistory(data.messages);
|
||||||
// 更新Agent选择框为该对话使用的Agent
|
|
||||||
if (data.agent_id) {
|
if (data.agent_id) {
|
||||||
currentAgentId = data.agent_id;
|
currentAgentId = data.agent_id;
|
||||||
updateAgentSelect(data.agent_id);
|
updateAgentSelect(data.agent_id);
|
||||||
@@ -368,14 +367,23 @@
|
|||||||
currentAgentId = data.agent_id;
|
currentAgentId = data.agent_id;
|
||||||
console.log('Agent已切换:', data.agent_name);
|
console.log('Agent已切换:', data.agent_name);
|
||||||
break;
|
break;
|
||||||
|
// 流式输出处理
|
||||||
case 'thinking_start':
|
case 'thinking_start':
|
||||||
showThinkingIndicator();
|
createStreamingMessage('thinking');
|
||||||
break;
|
break;
|
||||||
case 'thinking_content':
|
case 'thinking_stream':
|
||||||
updateThinkingContent(data.content);
|
appendStreamText('thinking', data.text);
|
||||||
break;
|
break;
|
||||||
case 'thinking_end':
|
case 'thinking_end':
|
||||||
hideThinkingIndicator();
|
collapseThinking(data.content);
|
||||||
|
createStreamingMessage('content');
|
||||||
|
break;
|
||||||
|
case 'content_stream':
|
||||||
|
appendStreamText('content', data.text);
|
||||||
|
break;
|
||||||
|
case 'stream_end':
|
||||||
|
finishStreaming();
|
||||||
|
document.getElementById('sendBtn').disabled = false;
|
||||||
break;
|
break;
|
||||||
case 'user_message':
|
case 'user_message':
|
||||||
appendMessage('user', data.message.content, data.message.source);
|
appendMessage('user', data.message.content, data.message.source);
|
||||||
@@ -385,46 +393,129 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'assistant_message':
|
case 'assistant_message':
|
||||||
appendMessage('assistant', data.message.content, data.message.source, data.message.thinking_content, data.message.agent_name);
|
// 流式完成后,更新最终消息(添加Agent信息等)
|
||||||
document.getElementById('sendBtn').disabled = false;
|
updateFinalMessage(data.message);
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
alert(data.message);
|
showError(data.message);
|
||||||
document.getElementById('sendBtn').disabled = false;
|
document.getElementById('sendBtn').disabled = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示思考指示器
|
// 创建流式消息容器
|
||||||
function showThinkingIndicator() {
|
function createStreamingMessage(type) {
|
||||||
const container = document.getElementById('messagesContainer');
|
const container = document.getElementById('messagesContainer');
|
||||||
const thinking = document.createElement('div');
|
container.querySelector('.welcome')?.remove();
|
||||||
thinking.id = 'thinkingIndicator';
|
|
||||||
thinking.className = 'message assistant';
|
const existingStream = document.getElementById('streaming-message');
|
||||||
thinking.innerHTML = `
|
if (existingStream && type === 'content') return; // 回答区域已存在
|
||||||
<div class="message-avatar">🤔</div>
|
|
||||||
<div class="message-content" style="color:#667eea;">
|
const div = document.createElement('div');
|
||||||
<span class="loading">正在思考</span>
|
div.id = 'streaming-message';
|
||||||
</div>
|
div.className = 'message assistant';
|
||||||
`;
|
|
||||||
container.appendChild(thinking);
|
if (type === 'thinking') {
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="message-avatar">🤔</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="thinking-stream" id="thinking-stream" style="background:#f8f9fa;border-left:3px solid #667eea;padding:12px;font-size:14px;color:#667eea;">
|
||||||
|
<strong>💭 思考过程</strong><br>
|
||||||
|
<span id="thinking-text"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="message-avatar">🤖</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div id="thinking-collapsed" style="display:none;"></div>
|
||||||
|
<span id="content-text"></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
container.appendChild(div);
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新思考内容
|
// 追加流式文本
|
||||||
function updateThinkingContent(content) {
|
function appendStreamText(type, text) {
|
||||||
const indicator = document.getElementById('thinkingIndicator');
|
const textEl = document.getElementById(type === 'thinking' ? 'thinking-text' : 'content-text');
|
||||||
if (indicator) {
|
if (textEl) {
|
||||||
indicator.querySelector('.message-content').innerHTML = `
|
textEl.textContent += text;
|
||||||
<div class="thinking-content">${formatContent(content)}</div>
|
const container = document.getElementById('messagesContainer');
|
||||||
`;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏思考指示器
|
// 折叠思考内容
|
||||||
function hideThinkingIndicator() {
|
function collapseThinking(content) {
|
||||||
const indicator = document.getElementById('thinkingIndicator');
|
const streamEl = document.getElementById('thinking-stream');
|
||||||
if (indicator) indicator.remove();
|
if (streamEl) {
|
||||||
|
// 创建折叠版本
|
||||||
|
const collapsedDiv = document.getElementById('thinking-collapsed');
|
||||||
|
if (collapsedDiv) {
|
||||||
|
collapsedDiv.style.display = 'block';
|
||||||
|
collapsedDiv.innerHTML = `
|
||||||
|
<div class="thinking-collapsed" onclick="toggleThinkingBlock(this)" style="cursor:pointer;background:#f0f0f0;padding:8px 12px;border-radius:4px;margin-bottom:8px;font-size:13px;color:#667eea;">
|
||||||
|
<i class="ri-lightbulb-line"></i> 思考过程 <span class="toggle-hint">(点击展开)</span>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-expanded" style="display:none;background:#f8f9fa;border-left:3px solid #667eea;padding:12px;font-size:14px;margin-bottom:12px;">
|
||||||
|
${formatContent(content)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
streamEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开/折叠思考内容
|
||||||
|
function toggleThinkingBlock(el) {
|
||||||
|
const expanded = el.parentElement.querySelector('.thinking-expanded');
|
||||||
|
const hint = el.querySelector('.toggle-hint');
|
||||||
|
if (expanded.style.display === 'none') {
|
||||||
|
expanded.style.display = 'block';
|
||||||
|
hint.textContent = '(点击折叠)';
|
||||||
|
} else {
|
||||||
|
expanded.style.display = 'none';
|
||||||
|
hint.textContent = '(点击展开)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成流式输出
|
||||||
|
function finishStreaming() {
|
||||||
|
const streamMsg = document.getElementById('streaming-message');
|
||||||
|
if (streamMsg) {
|
||||||
|
streamMsg.id = ''; // 移除临时ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最终消息(添加Agent信息)
|
||||||
|
function updateFinalMessage(msg) {
|
||||||
|
const messages = document.querySelectorAll('.message.assistant');
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
if (lastMsg && msg.agent_name) {
|
||||||
|
const contentDiv = lastMsg.querySelector('.message-content');
|
||||||
|
if (contentDiv && !contentDiv.querySelector('.agent-info')) {
|
||||||
|
const agentInfo = document.createElement('div');
|
||||||
|
agentInfo.className = 'agent-info';
|
||||||
|
agentInfo.style.cssText = 'font-size:12px;color:#999;margin-top:8px;';
|
||||||
|
agentInfo.innerHTML = `<i class="ri-robot-line"></i> ${msg.agent_name}`;
|
||||||
|
contentDiv.appendChild(agentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误
|
||||||
|
function showError(msg) {
|
||||||
|
const container = document.getElementById('messagesContainer');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message assistant';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="message-avatar">❌</div>
|
||||||
|
<div class="message-content" style="color:#dc3545;">${msg}</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空消息
|
// 清空消息
|
||||||
|
|||||||
Reference in New Issue
Block a user