從今天開始就要再一步步建立旅遊即時聊天機器人給大家看啦~
細節的部份可以看之前的文章,從今天開始主要是要把之前教的全部融合再一起。
今天要實作 OpenAI 初步的聊天室功能啦!
FastAPI 與 Azure OpenAI 串接建立 WebSocket 後端,並在 Next.js 中使用 WebSocket 與後端通信的前端頁面。
基於 FastAPI 的 WebSocket 服務端,該服務會與 Azure OpenAI 的 chat
API 進行交互,並將結果發送回客戶端。
openai_config 注意是 AzureOpenAI 的金鑰
from fastapi import FastAPI
from openai import AzureOpenAI
from fastapi import FastAPI, WebSocket
from openai_config import *
app = FastAPI() # 產生 FastAPI物件
client = AzureOpenAI(
azure_endpoint = azure_endpoint,
api_key=api_key,
api_version=api_version
)
@app.websocket("/chatbot")
async def websocket_endpoint(websocket: WebSocket):
# 接受前端的 websocket 連接請求
await websocket.accept()
try:
while True:
# 等待前端訊息
data = await websocket.receive_text()
prompt = data.strip()
message_text =[
{"role": "system", "content": "你是一個AI助理, 幫人類回答問題"},
{"role": "user", "content": prompt}
]
# 生成式 ai 回覆
response = client.chat.completions.create(
model=model_name,
messages = message_text,
temperature=0,
max_tokens=150,
top_p=0.95,
frequency_penalty=0,
presence_penalty=0,
stop=None
)
# 發送給前端
await websocket.send_text(response.choices[0].message.content)
except Exception as e:
print(f"WebSocket connection closed with exception: {e}")
頁面會使用這個 useWebSocket
Hook 來管理 WebSocket 連接並與 FastAPI 進行通信。
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: '您好,我是AideAI,有什麼可以幫你的嗎?' }
]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
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;
在 Next.js 頁面中使用 WebSocket Hook,並建立聊天介面。
import React, { useState } 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 { messages, sendMessage, loading } = useWebSocket('ws://localhost:8000/chatbot');
const [input, setInput] = useState<string>('');
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'>Taipei Tourism 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;