Przeglądaj źródła

fastapi+sse+agent对话

linzhaoxin319319 1 miesiąc temu
rodzic
commit
81aadf75f0
1 zmienionych plików z 265 dodań i 155 usunięć
  1. 265 155
      林兆新/2/sse_app.py

+ 265 - 155
林兆新/2/sse_app.py

@@ -33,8 +33,7 @@ except ImportError as e:
 # 加载环境变量
 dotenv.load_dotenv()
 
-# 全局消息队列
-message_queue: List[Dict[str, Any]] = []
+# 移除消息队列,现在使用直接SSE流
 
 # 全局Agent实例和Memory
 global_agent = None
@@ -423,6 +422,25 @@ async def home():
                 40% { content: '..'; }
                 50% { content: '...'; }
             }
+            
+            /* 打字机光标效果 */
+            .typing-cursor {
+                display: inline-block;
+                background-color: #333;
+                width: 2px;
+                height: 1em;
+                margin-left: 1px;
+                animation: blink 1s infinite;
+            }
+            
+            .message.bot .typing-cursor {
+                background-color: #333;
+            }
+            
+            @keyframes blink {
+                0%, 50% { opacity: 1; }
+                51%, 100% { opacity: 0; }
+            }
         </style>
     </head>
     <body>
@@ -448,7 +466,7 @@ async def home():
                      type="text" 
                      id="messageInput" 
                      class="message-input" 
-                     placeholder="输入您的消息... (输入 '记忆' 查看我的记忆状态)"
+                     placeholder="输入您的消息... (现在支持打字机效果!输入 '记忆' 查看记忆状态)"
                      maxlength="500"
                  >
                  <button id="sendButton" class="send-button">发送</button>
@@ -456,8 +474,8 @@ async def home():
         </div>
         
         <script>
-            let eventSource = null;
-            let isConnected = false;
+            // 全局变量
+            let currentBotMessageElement = null;  // 当前正在构建的机器人消息元素
             
             const messagesContainer = document.getElementById('messagesContainer');
             const messageInput = document.getElementById('messageInput');
@@ -474,61 +492,103 @@ async def home():
                 sendButton.disabled = false;
                 messageInput.focus();
                 
-                                 // 显示欢迎消息
-                 addMessage('🎉 欢迎使用AI实时对话窗口!我是您的智能助手,具有记忆功能,可以记住我们的对话历史。请开始聊天吧~', 'system');
+                // 显示欢迎消息
+                addMessage('🎉 欢迎使用AI实时对话窗口!我是您的智能助手,具有记忆功能和打字机效果。我会一个词一个词地回复您,就像真人打字一样!请开始聊天吧~', 'system');
                 
-                // 连接SSE
-                connectSSE();
+                // 更新状态
+                statusBar.textContent = '✅ 已就绪 - 请开始对话';
+                statusBar.style.background = '#d4edda';
+                statusBar.style.color = '#155724';
             });
             
-            // 连接SSE
-            function connectSSE() {
-                try {
-                    console.log('连接SSE...');
-                    eventSource = new EventSource('/sse/chat');
+            // 创建单次SSE连接用于获取回复
+            function createSSEConnection(message) {
+                return new Promise((resolve, reject) => {
+                    const encodedMessage = encodeURIComponent(message);
+                    const eventSource = new EventSource(`/api/chat?message=${encodedMessage}`);
                     
                     eventSource.onopen = function() {
-                        console.log('SSE连接成功');
-                        isConnected = true;
-                        statusBar.textContent = '✅ 已连接 - 消息将实时更新';
-                        statusBar.style.background = '#d4edda';
-                        statusBar.style.color = '#155724';
+                        console.log('SSE连接已建立');
+                        statusBar.textContent = '🔄 正在处理消息...';
+                        statusBar.style.background = '#fff3cd';
+                        statusBar.style.color = '#856404';
                     };
                     
                     eventSource.onmessage = function(event) {
                         try {
                             const data = JSON.parse(event.data);
-                            handleMessage(data);
+                            handleSSEMessage(data);
+                            
+                            // 当收到complete消息时关闭连接
+                            if (data.type === 'complete') {
+                                eventSource.close();
+                                resolve();
+                            }
                         } catch (e) {
                             console.error('消息解析错误:', e);
+                            eventSource.close();
+                            reject(e);
                         }
                     };
                     
                     eventSource.onerror = function() {
                         console.log('SSE连接失败');
-                        isConnected = false;
-                        statusBar.textContent = '⚠️ 连接失败 - 使用离线模式';
+                        eventSource.close();
+                        statusBar.textContent = '❌ 连接失败';
                         statusBar.style.background = '#f8d7da';
                         statusBar.style.color = '#721c24';
+                        reject(new Error('SSE连接失败'));
                     };
-                    
-                } catch (error) {
-                    console.error('SSE初始化错误:', error);
-                    statusBar.textContent = '📱 离线模式';
-                }
+                });
             }
             
-            // 处理消息
-            function handleMessage(data) {
-                console.log('收到消息:', data);
+            // 处理SSE消息
+            function handleSSEMessage(data) {
+                console.log('收到SSE消息:', data);
                 
                 switch(data.type) {
+                    case 'status':
+                        // 更新状态栏显示处理进度
+                        statusBar.textContent = data.message;
+                        statusBar.style.background = '#fff3cd';
+                        statusBar.style.color = '#856404';
+                        break;
+                    case 'bot_message_start':
+                        // 开始新的机器人回复(打字机效果)
+                        hideTypingIndicator();
+                        currentBotMessageElement = createEmptyBotMessage();
+                        break;
+                    case 'bot_message_token':
+                        // 添加单个单词到当前机器人回复
+                        if (currentBotMessageElement) {
+                            appendTokenToBotMessage(currentBotMessageElement, data.token);
+                        }
+                        break;
+                    case 'bot_message_end':
+                        // 完成机器人回复
+                        if (currentBotMessageElement) {
+                            removeTypingCursor(currentBotMessageElement);
+                        }
+                        currentBotMessageElement = null;
+                        console.log('✅ 打字机效果回复完成:', data.complete_message);
+                        break;
                     case 'bot_message':
+                        // 兼容旧版本的完整消息模式
                         hideTypingIndicator();
                         addMessage(data.message, 'bot');
                         break;
-                    case 'system_message':
-                        addMessage(data.message, 'system');
+                    case 'complete':
+                        // 回复完成,更新状态
+                        statusBar.textContent = '✅ 已就绪 - 请开始对话';
+                        statusBar.style.background = '#d4edda';
+                        statusBar.style.color = '#155724';
+                        break;
+                    case 'error':
+                        hideTypingIndicator();
+                        addMessage('❌ 错误: ' + data.message, 'system');
+                        statusBar.textContent = '❌ 处理失败';
+                        statusBar.style.background = '#f8d7da';
+                        statusBar.style.color = '#721c24';
                         break;
                 }
             }
@@ -555,6 +615,61 @@ async def home():
                 messagesContainer.scrollTop = messagesContainer.scrollHeight;
             }
             
+            // 创建空的机器人消息元素(用于打字机效果)
+            function createEmptyBotMessage() {
+                console.log('创建空的机器人消息元素');
+                
+                const messageDiv = document.createElement('div');
+                messageDiv.className = 'message bot';
+                
+                const bubbleDiv = document.createElement('div');
+                bubbleDiv.className = 'message-bubble';
+                
+                // 添加打字光标
+                const cursor = document.createElement('span');
+                cursor.className = 'typing-cursor';
+                bubbleDiv.appendChild(cursor);
+                
+                const timeDiv = document.createElement('div');
+                timeDiv.className = 'message-time';
+                timeDiv.textContent = new Date().toLocaleTimeString();
+                
+                messageDiv.appendChild(bubbleDiv);
+                messageDiv.appendChild(timeDiv);
+                
+                messagesContainer.appendChild(messageDiv);
+                messagesContainer.scrollTop = messagesContainer.scrollHeight;
+                
+                return messageDiv;
+            }
+            
+            // 向机器人消息添加单词(打字机效果)
+            function appendTokenToBotMessage(messageElement, token) {
+                if (!messageElement) return;
+                
+                const bubbleDiv = messageElement.querySelector('.message-bubble');
+                const cursor = bubbleDiv.querySelector('.typing-cursor');
+                
+                if (bubbleDiv && cursor) {
+                    // 在光标前插入单词
+                    const textNode = document.createTextNode(token);
+                    bubbleDiv.insertBefore(textNode, cursor);
+                    
+                    // 滚动到底部,保持跟踪打字进度
+                    messagesContainer.scrollTop = messagesContainer.scrollHeight;
+                }
+            }
+            
+            // 移除打字光标
+            function removeTypingCursor(messageElement) {
+                if (!messageElement) return;
+                
+                const cursor = messageElement.querySelector('.typing-cursor');
+                if (cursor) {
+                    cursor.remove();
+                }
+            }
+            
             // 显示输入指示器
             function showTypingIndicator() {
                 typingIndicator.style.display = 'block';
@@ -608,59 +723,49 @@ async def home():
                  }
              }
             
-                         // 发送消息
-             async function sendMessage() {
-                 const message = messageInput.value.trim();
-                 console.log('准备发送消息:', message);
-                 
-                 if (!message) {
-                     console.log('消息为空');
-                     return;
-                 }
-                 
-                 // 检查特殊命令
-                 if (message === '记忆' || message.toLowerCase() === 'memory') {
-                     console.log('处理记忆查询命令');
-                     addMessage(message, 'user');
-                     messageInput.value = '';
-                     await showMemoryStatus();
-                     return;
-                 }
-                 
-                 // 立即显示用户消息
-                 addMessage(message, 'user');
-                 messageInput.value = '';
-                 
-                 // 禁用输入
-                 messageInput.disabled = true;
-                 sendButton.disabled = true;
-                 
-                 // 显示输入指示器
-                 showTypingIndicator();
+            // 发送消息
+            async function sendMessage() {
+                const message = messageInput.value.trim();
+                console.log('准备发送消息:', message);
+                
+                if (!message) {
+                    console.log('消息为空');
+                    return;
+                }
+                
+                // 检查特殊命令
+                if (message === '记忆' || message.toLowerCase() === 'memory') {
+                    console.log('处理记忆查询命令');
+                    addMessage(message, 'user');
+                    messageInput.value = '';
+                    await showMemoryStatus();
+                    return;
+                }
+                
+                // 立即显示用户消息
+                addMessage(message, 'user');
+                messageInput.value = '';
+                
+                // 禁用输入
+                messageInput.disabled = true;
+                sendButton.disabled = true;
+                
+                // 显示输入指示器
+                showTypingIndicator();
                 
                 try {
-                    const response = await fetch('/api/chat', {
-                        method: 'POST',
-                        headers: {
-                            'Content-Type': 'application/json',
-                        },
-                        body: JSON.stringify({
-                            message: message
-                        })
-                    });
-                    
-                    const result = await response.json();
-                    console.log('发送结果:', result);
-                    
-                    if (!result.success) {
-                        hideTypingIndicator();
-                        addMessage('❌ 发送失败: ' + result.error, 'system');
-                    }
+                    // 使用SSE获取回复
+                    console.log('发送SSE请求...');
+                    await createSSEConnection(message);
+                    console.log('SSE对话完成');
                     
                 } catch (error) {
-                    console.error('发送错误:', error);
+                    console.error('SSE对话错误:', error);
                     hideTypingIndicator();
-                    addMessage('❌ 网络错误: ' + error.message, 'system');
+                    addMessage('❌ 对话失败: ' + error.message, 'system');
+                    statusBar.textContent = '❌ 对话失败';
+                    statusBar.style.background = '#f8d7da';
+                    statusBar.style.color = '#721c24';
                 } finally {
                     // 重新启用输入
                     messageInput.disabled = false;
@@ -679,11 +784,9 @@ async def home():
                 }
             });
             
-            // 页面关闭时断开连接
+            // 页面关闭时的清理工作
             window.addEventListener('beforeunload', function() {
-                if (eventSource) {
-                    eventSource.close();
-                }
+                console.log('页面即将关闭');
             });
         </script>
     </body>
@@ -691,59 +794,19 @@ async def home():
     """
     return HTMLResponse(content=html_content)
 
-# SSE消息流生成器
-async def generate_chat_stream():
-    """生成聊天消息流"""
-    client_id = f"client_{int(time.time())}"
-    last_message_count = 0
-    
-    try:
-        print(f"新的SSE连接: {client_id}")
-        
-        while True:
-            # 检查新消息
-            if len(message_queue) > last_message_count:
-                for i in range(last_message_count, len(message_queue)):
-                    message = message_queue[i]
-                    yield f"data: {json.dumps(message, ensure_ascii=False)}\n\n"
-                
-                last_message_count = len(message_queue)
-            
-            await asyncio.sleep(0.1)  # 100ms检查间隔
-            
-    except Exception as e:
-        print(f"SSE流错误: {e}")
-    finally:
-        print(f"SSE连接断开: {client_id}")
-
-@app.get("/sse/chat")
-async def sse_chat():
-    """SSE聊天端点"""
-    return StreamingResponse(
-        generate_chat_stream(),
-        media_type="text/event-stream",
-        headers={
-            "Cache-Control": "no-cache",
-            "Connection": "keep-alive",
-            "Access-Control-Allow-Origin": "*",
-        }
-    )
+# 旧的SSE接口已被移除,现在使用 /api/chat 直接SSE接口
 
-@app.post("/api/chat")
-async def chat_api(request: Request):
-    """处理聊天消息"""
+async def generate_chat_response(user_message: str):
+    """生成聊天回复的SSE流"""
     global global_agent, global_user_id
     
     try:
-        data = await request.json()
-        user_message = data.get("message", "").strip()
-        
-        if not user_message:
-            return {"success": False, "error": "消息不能为空"}
-        
         print(f"📨 收到用户消息: {user_message}")
         print(f"🤖 Agent可用性: {global_agent is not None}")
         
+        # 发送开始处理消息
+        yield f"data: {json.dumps({'type': 'status', 'message': '正在处理您的消息...'}, ensure_ascii=False)}\n\n"
+        
         # 模拟思考时间
         await asyncio.sleep(random.uniform(0.5, 1.5))
         
@@ -753,6 +816,7 @@ async def chat_api(request: Request):
         if global_agent:
             try:
                 print("🤖 调用Memory Agent生成回复...")
+                yield f"data: {json.dumps({'type': 'status', 'message': '🤖 正在生成智能回复...'}, ensure_ascii=False)}\n\n"
                 
                 # 异步调用Agent处理消息,传入user_id以关联记忆
                 response = await call_agent_async(global_agent, user_message, global_user_id)
@@ -779,50 +843,96 @@ async def chat_api(request: Request):
             bot_reply = "抱歉,我暂时无法回复。请稍后再试。"
             print(f"⚠️ 使用默认回复: {bot_reply}")
         
-        # 添加机器人回复到消息队列
-        bot_message = {
-            "type": "bot_message",
-            "message": bot_reply,
-            "timestamp": datetime.now().isoformat()
-        }
-        message_queue.append(bot_message)
+        # 发送打字机效果的回复
+        print(f"⌨️ 开始打字机效果发送回复...")
         
-        print(f"📤 机器人回复已添加到消息队列: {bot_reply[:50]}...")
+        # 发送开始打字消息
+        yield f"data: {json.dumps({'type': 'bot_message_start', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n"
         
-        return {
-            "success": True,
-            "message": "消息已处理",
+        # 按单词逐个发送,实现打字机效果
+        import re
+        # 使用正则表达式分割文本为单词,包含标点符号
+        words = re.findall(r'\S+|\s+', bot_reply)
+        
+        for i, word in enumerate(words):
+            # 发送单个单词
+            word_message = {
+                "type": "bot_message_token",
+                "token": word,
+                "position": i
+            }
+            yield f"data: {json.dumps(word_message, ensure_ascii=False)}\n\n"
+            
+            # 控制打字速度:根据单词类型调整停顿时间
+            if word.strip() == '':
+                # 空白字符(空格、换行等)
+                await asyncio.sleep(random.uniform(0.05, 0.1))
+            elif any(punct in word for punct in '。!?'):
+                # 包含句末标点的单词
+                await asyncio.sleep(random.uniform(0.5, 0.8))
+            elif any(punct in word for punct in ',;:'):
+                # 包含句中标点的单词
+                await asyncio.sleep(random.uniform(0.3, 0.5))
+            elif len(word.strip()) > 5:
+                # 长单词
+                await asyncio.sleep(random.uniform(0.2, 0.4))
+            else:
+                # 普通单词
+                await asyncio.sleep(random.uniform(0.1, 0.3))
+        
+        # 发送结束消息
+        end_message = {
+            "type": "bot_message_end",
+            "complete_message": bot_reply,
             "timestamp": datetime.now().isoformat()
         }
+        yield f"data: {json.dumps(end_message, ensure_ascii=False)}\n\n"
+        
+        print(f"✅ 打字机效果发送完成")
+        
+        # 发送完成状态
+        yield f"data: {json.dumps({'type': 'complete', 'message': '回复完成'}, ensure_ascii=False)}\n\n"
         
     except Exception as e:
         print(f"处理消息错误: {e}")
-        return {"success": False, "error": f"处理错误: {str(e)}"}
+        error_message = {
+            "type": "error", 
+            "message": f"处理错误: {str(e)}"
+        }
+        yield f"data: {json.dumps(error_message, ensure_ascii=False)}\n\n"
+
+@app.get("/api/chat")
+async def chat_api_sse(message: str = ""):
+    """SSE聊天接口"""
+    if not message.strip():
+        return {"error": "消息不能为空"}
+    
+    return StreamingResponse(
+        generate_chat_response(message.strip()),
+        media_type="text/event-stream",
+        headers={
+            "Cache-Control": "no-cache",
+            "Connection": "keep-alive",
+            "Access-Control-Allow-Origin": "*",
+        }
+    )
 
 @app.get("/api/status")
 async def get_status():
     """获取系统状态"""
     return {
         "status": "running",
-        "messages": len(message_queue),
+        "agent_available": global_agent is not None,
         "timestamp": datetime.now().isoformat()
     }
 
 @app.post("/api/clear")
 async def clear_messages():
-    """清空消息历史"""
-    global message_queue
-    message_queue.clear()
-    
-    # 添加清空提示消息
-    clear_msg = {
-        "type": "system_message",
-        "message": "💬 消息历史已清空",
-        "timestamp": datetime.now().isoformat()
-    }
-    message_queue.append(clear_msg)
+    """清空消息历史(现在使用直接SSE,无需清空队列)"""
+    # 由于现在使用直接SSE,不再需要清空消息队列
+    # 这个接口保留用于兼容性,但实际不执行任何操作
     
-    return {"success": True, "message": "消息历史已清空"}
+    return {"success": True, "message": "消息历史已清空(使用SSE直接模式)"}
 
 @app.get("/api/memory")
 async def get_memory_status():