iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
自我挑戰組

從零打造客製化 AI 聊天機器人系列 第 29

[從零打造客製化 AI 聊天機器人] 建立旅遊即時聊天機器人4 (多人聊天)

  • 分享至 

  • xImage
  •  

上一篇已經把旅遊聊天機器人建置好,今天要來做支援多用戶同時使用 WebSocket 聊天服務,需要處理多用戶的連接管理。
以下是修改的重點:
**管理多用戶連接:**使用 websockets 來儲存每個用戶的 WebSocket 連接,以便能夠識別不同用戶的訊息。
**用戶唯一識別:**可以透過傳遞用戶識別符來區分不同用戶,例如:在 websockets 連接時透過 URL 參數user_id或訊息傳遞識別用戶。

1.修改前端 ChatBot.tsx 文件

確保每個使用者都有唯一的 user_id。你可以從登入狀態中提取,或者隨機生成一個。

import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useWebSocket from '../hooks/useWebSocket';

const ChatLoading = () => {
    return (
        <div className='flex space-x-2 justify-center items-center dark:invert h-full'>
            <div className='h-2 w-2 bg-gray-500 rounded-full animate-bounce [animation-delay:-0.3s]'></div>
            <div className='h-2 w-2 bg-gray-500 rounded-full animate-bounce [animation-delay:-0.15s]'></div>
            <div className='h-2 w-2 bg-gray-500 rounded-full animate-bounce'></div>
        </div>
    );
};

const ChatBot: React.FC = () => {
  const [userId, setUserId] = useState<string | null>(null);
  const { messages, sendMessage, loading } = useWebSocket(`ws://localhost:8000/tourism/chatbot/${userId}`);
  const [input, setInput] = useState<string>('');

  useEffect(() => {
    // 模擬獲取 user_id,實際應該從登入系統或後端 API 獲取
    const storedUserId = localStorage.getItem('user_id');
    if (storedUserId) {
      setUserId(storedUserId);
    } else {
      const newUserId = `user_${Math.floor(Math.random() * 10000)}`;
      localStorage.setItem('user_id', newUserId);
      setUserId(newUserId);
    }
  }, []);

  const handleSend = () => {
    if (input.trim()) {
      sendMessage(input);
      setInput(''); // 清空輸入框
    }
  };

  return (
    <div className='flex flex-col relative w-full'>
      <header className='flex p-2 items-center'><p className='font-bold ml-1 text-xl'>Chatbot</p></header>
      <div style={{ height: 'calc(100vh - 46px)' }} className='overflow-y-auto'>
        {messages.map((msg, index) => (
          <div key={index} className='flex flex-col space-y-4'>
            {msg.role === 'AI' ? (
              <div className='flex space-x-2 justify-start bg-slate-100 p-4 items-center'>
                <div className='bg-sky-600 rounded-lg w-10 h-10 flex items-center justify-center flex-shrink-0'>
                  <Image src='/chatbot.png' alt='robot' width={30} height={30} />
                </div>
                <div>
                  {msg.content === 'loading' ? <ChatLoading /> : msg.content}
                </div>
              </div>
            ) : (
              <div className='flex space-x-2 justify-end p-4 items-center'>
                <div>{msg.content}</div>
                <div className='bg-green-400 rounded-lg w-10 h-10 flex items-center justify-center'>
                  <Image src='/avatar.png' alt='user' width={25} height={25} />
                </div>
              </div>
            )}
          </div>
        ))}
      </div>
      <footer className='p-4 bottom-0 absolute w-full flex flex-col items-center'>
        <div className='flex w-1/2'>
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            className='p-2 rounded-l w-full outline-none bg-gray-100'
            placeholder='請輸入要詢問的內容'
          />
          <button className='rounded-r bg-gray-100' onClick={handleSend}>
            <Image src='/send.png' alt='send' width={30} height={30} className='p-1' />
          </button>
        </div>
      </footer>
    </div>
  );
};

export default ChatBot;

2.修改 WebSocket 鉤子 useWebSocket.ts

確保 useWebSocket 鉤子接收的 url 包含動態的 user_id。

import { useEffect, useState } from 'react';

type Message = {
  role: 'User' | 'AI';
  content: string;
};

type WebSocketHook = {
  messages: Message[];
  sendMessage: (message: string) => void;
  loading: boolean;
};

const useWebSocket = (url: string): WebSocketHook => {
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [messages, setMessages] = useState<Message[]>([
    { role: 'AI', content: '您好,有什麼可以幫你的嗎?' }
  ]);
  const [loading, setLoading] = useState<boolean>(false);

  useEffect(() => {
    if (!url.includes('null')) { // 確保只有當 userId 有值時才建立 WebSocket 連接
      const ws = new WebSocket(url);
      setSocket(ws);

      ws.onopen = () => {
        setLoading(false);
        console.log('已連接到 WebSocket');
      };

      ws.onmessage = (event: MessageEvent) => {
        setLoading(false);
        setMessages((prevMessages) => {
          const updatedMessages = [...prevMessages];
          updatedMessages.splice(-1, 1); // 移除最後的 "loading" 訊息
          return [...updatedMessages, { role: 'AI', content: event.data }];
        });
        console.log('收到伺服器的回應:', event.data);
      };

      ws.onclose = () => {
        console.log('WebSocket 連接已關閉');
      };

      ws.onerror = (error: Event) => {
        console.error('WebSocket 發生錯誤:', error);
      };

      // 在組件卸載時關閉 WebSocket
      return () => {
        ws.close();
      };
    }
  }, [url]);

  // 發送消息的函數
  const sendMessage = (message: string) => {
    if (socket && socket.readyState === WebSocket.OPEN) {
      setLoading(true);
      socket.send(message);
      setMessages((prevMessages) => [
        ...prevMessages,
        { role: 'User', content: message },
        { role: 'AI', content: 'loading' } // 添加一個 "loading" 消息表示 AI 正在回應
      ]);
    }
  };

  return { messages, sendMessage, loading };
};

export default useWebSocket;

3.修改後端 FastAPI WebSocket 連接

用戶通過前端傳遞 user_id 時,後端能夠識別並正確管理每個 WebSocket 連接。

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from chatbot_intents_function import get_attraction_info, get_embedding, provide_info
from chromadb import PersistentClient
from openai import AzureOpenAI
from openai_config import *

# 初始化 FastAPI
app = FastAPI()

# 初始化 ChromaDB 客戶端和 collection
chroma_client = PersistentClient(path="./data/cut")
collection_name_intents = "taipei_tourist_intents"
intents_collection = chroma_client.get_collection(name=collection_name_intents)

collection_name_tourism = "taipei_tourism"
tourism_collection = chroma_client.get_collection(name=collection_name_tourism)

# 初始化 Azure OpenAI 客戶端
client = AzureOpenAI(
    azure_endpoint=azure_endpoint,
    api_key=api_key,
    api_version=api_version
)

# 管理 WebSocket 連接的管理器
class ConnectionManager:
    def __init__(self):
        self.active_connections: dict = {}

    async def connect(self, websocket: WebSocket, user_id: str):
        await websocket.accept()
        self.active_connections[user_id] = websocket

    def disconnect(self, user_id: str):
        if user_id in self.active_connections:
            del self.active_connections[user_id]

    async def send_message(self, message: str, user_id: str):
        websocket = self.active_connections.get(user_id)
        if websocket:
            await websocket.send_text(message)

    async def broadcast(self, message: str):
        for connection in self.active_connections.values():
            await connection.send_text(message)


# 建立 WebSocket 連接管理器實例
manager = ConnectionManager()

# 意圖識別函數
def detect_intent(user_input):
    user_input_embedding = get_embedding(user_input)
    result = intents_collection.query(
        query_embeddings=[user_input_embedding], n_results=1
    )
    if not result["documents"]:
        return None, "未能識別意圖。"

    intents = result["documents"][0][0]
    action = result["metadatas"][0][0]["function"]
    return intents, action

# 處理使用者輸入
def process_user_input(user_input):
    intents, action = detect_intent(user_input)

    if not intents:
        return action  # 返回錯誤信息

    if action == "provide_info":
        response = provide_info(user_input, tourism_collection)
        return response
    
    return "請輸入其他詳細資訊"


@app.websocket("/tourism/chatbot/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str):
    await manager.connect(websocket, user_id)
    try:
        while True:
            # 接收用戶的訊息
            user_input = await websocket.receive_text()

            # 處理使用者輸入
            response = process_user_input(user_input)
            
            # 回傳訊息給對應的用戶
            await manager.send_message(response, user_id)

    except WebSocketDisconnect:
        manager.disconnect(user_id)
        print(f"User {user_id} disconnected.")
    except Exception as e:
        print(f"WebSocket connection closed with exception: {e}")

透過這些修改,就能支援多用戶同時使用聊天機器人。如果每個用戶有自己的 user_id,系統就能夠正確區分來自不同用戶的訊息並作出回應。

https://ithelp.ithome.com.tw/upload/images/20241012/2016941585Ukwo7SQr.png


上一篇
[從零打造客製化 AI 聊天機器人] 建立旅遊即時聊天機器人3 (意圖執行功能)
下一篇
[從零打造客製化 AI 聊天機器人] 總結
系列文
從零打造客製化 AI 聊天機器人30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言