2025年2月25日 星期二

Python 學習筆記 : 簡單好用的 Web app 套件 gradio (九)

最近因為測試 DuckDuckGo 搜尋引擎的 API, 回頭找 Gradio 教學文件才發現它還有一個專門用在開發聊天機器人的好物 : Chatbot 類別, 今天就來測試看看吧! 

本系列文章索引參考 : 



十二. 使用 Chatbot 元件製作聊天機器人 :    

Chatbot 類別是 Gradio 專為對話式應用設計的 UI 元件, 它會以類似聊天泡泡的形式呈現使用者與聊天機器人之間的互動紀錄 (支援 Markdown 語法), 但它只是呈現聊天輸出, 仍需搭配 Textbox 與 Button 等輸入 UI 元件才能實現完整的聊天介面. 


1. 建立 Chatbot 物件 : 

呼叫 Chatbot() 建構式並傳入參數即可建立 Chatbot 物件 : 

chatbot=gr.Chatbot(type='messages', height=400, placeholder='我們的對話')

Chatbot() 建構式常用參數如下表 : 


Chatbot() 常用參數 說明
type value 的類型 : "tuples" (舊版串列對格式, 預設) 或 "messages" (新版)
value 初始對話歷史, 舊版格式為二維串列對, 新版為 role 與 content 鍵之字典串列 
height 聊天視窗高度, 可以是整數 (單位 px) 或字串 (如 "400px")
placeholder 對話歷史為 None 時顯示的文字
label 聊天視窗上方的標籤文字
bubble_full_width 聊天泡泡是否佔據整個寬度, 預設 True
avatar_images 使用者與機器人的頭像 (tuple), 格式 (user_avatar, bot_avatar)
render_markdown 是否將對話內容渲染為 Markdown 格式, 預設 True
show_copy_button 是否在聊天泡泡旁顯示「複製」按鈕, 預設 False
visible 元件是否可見, 預設 True
scale 在佈局中相對於其他元件的縮放比例, 類型為 int, 預設 0
min_width 元件的最小寬度 (單位 px), 類型為 int


注意, type 參數預設值為 "tuples", 表示聊天歷史 value 參數值的格式是二維串列對 : 

[[user_message, bot_response], ...]  

例如 : 

 [["嗨", "你好"], ["你是誰", "我是 AI 助理"], ...]

但新版 Gradio 已經改用 type="messages", 表示 value 參數要使用如下字典串列格式 : 

[{"role": "user", "content": "提示詞1"}, {"role": "assistant", "content": "AI 回覆1"}, ... ]

但為了向下支援, type 預設值仍然是 "tuples", 因此使用新版的 Chatbot 時 type 參數務必設定為 "messages", 例如 : 

[{"role": "user", "content": "嗨"}, {"role": "assistant", "content": "你好"}]

下面是一個鸚鵡聊天機器人的範例, 它會將收到的訊息直接回應給詢問者 : 


import gradio as gr

def handler(in1):
    global chat_history
    chat_history.append({'role': 'user', 'content': in1})
    bot_response=in1  # 鸚鵡回應
    chat_history.append({'role': 'assistant', 'content': bot_response})
    return chat_history

chat_history=[]  # 儲存對話歷史
in1=gr.Textbox(placeholder='輸入你的訊息 ...')
out1=gr.Chatbot(type='messages', height=400, placeholder='我們的對話')
iface=gr.Interface(
    fn=handler,
    inputs=in1,
    outputs=out1,
    title='鸚鵡聊天機器人',
    flagging_mode='never'  
    )
iface.launch()

此例使用一個串列 chat_history 來儲存對話歷史紀錄, 其元素格式為以 role 與 content 為鍵的字典序列 (新版 Gradio Chatbot 格式), 處理函式 handler 會將詢問者的輸入訊息直接放入 content 鍵中存入歷史對話串列中後傳回給輸出元件 Chatbot 物件, 結果如下 : 




此例只是用來說明 Chatbot 用法的偽聊天機器人, 它只會鸚鵡學語而已. 參考下面範例改成串接 OpenAI API 與大語言模型聊天, 但用的是 Textbox 元件 :


下面範例則是改用 Chatbot : 

import gradio as gr
from openai import OpenAI

def ask_gpt(api_key, prompt):
    global chat_history
    chat_history.append({'role': 'user', 'content': prompt})
    client=OpenAI(api_key=api_key)    
    chat_completion=client.chat.completions.create(
        messages=chat_history,
        model='gpt-3.5-turbo'
        )
    reply=chat_completion.choices[0].message.content
    chat_history.append({'role': 'assistant', 'content': reply})
    return chat_history

chat_history=[]  # 儲存對話歷史
api_key=gr.Textbox(label='輸入 OpenAI 金鑰', type='password') 
prompt=gr.Textbox(label='您的詢問: ')
chatbot=gr.Chatbot(type='messages', height=400, placeholder='我們的對話')
iface=gr.Interface(
    fn=ask_gpt,
    inputs=[api_key, prompt],  
    outputs=chatbot,
    title='OpenAI API 聊天機器人',
    flagging_mode='never',
    )
iface.launch()

此例放置了兩個 Textbox 輸入元件, 一個用來貼上 OpenAI API Key, 另一個用來輸入提示詞, 歷史對話紀錄則是輸出到 Chatbot 元件上, 在處理函式 ask_gpt() 中會先把提示詞以 role=user 角色存入 hat_history 串列中, 然後將此串列傳給 API 的 create() 的 messages 參數, 取得回應後將其以 role=assistant 存入 chat_history 串列中, 結果如下 :




可見使用 Chatbot 元件就可以顯示完整的聊天泡泡了. 


2. 利用 Blocks 與 State 元件管理聊天狀態 : 

上面的範例使用全域變數 chat_history  來管理對話歷史不安全, 且可能被 Gradio 重設, 比較理想的方式是使用 Blocks 元件來建立一個區塊語境, 這樣就可以在裡面使用 gr.State 元件來管理聊天狀態. 

將上面的範例修改為如下 :

# gradio_chatbot_test_3.py
import gradio as gr
from openai import OpenAI

def ask_gpt(api_key, prompt, history):
    history.append({'role': 'user', 'content': prompt})
    client=OpenAI(api_key=api_key)
    try:
        chat_completion=client.chat.completions.create(
            messages=history,
            model='gpt-3.5-turbo'
            )
        reply=chat_completion.choices[0].message.content
        history.append({'role': 'assistant', 'content': reply})        
    except Exception as e:
        history.append({'role': 'assistant', 'content': f'發生錯誤:{str(e)}'})
    return history, ""

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    api_key=gr.Textbox(label='輸入 OpenAI 金鑰', type='password') 
    chatbot=gr.Chatbot(type='messages',
                       height=400,
                       placeholder='我們的對話',
                       show_copy_button=True)
    state=gr.State([])  # 建立狀態物件儲存對話歷史
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")    
    send_btn.click(ask_gpt, inputs=[api_key, prompt, state], outputs=[chatbot, prompt])
    prompt.submit(ask_gpt, inputs=[api_key, prompt, state], outputs=[chatbot, prompt])
blocks.launch()

此例我們改用 Blocks 物件取代 Interface 物件來排版, 這樣就可以在其語境內用 State 物件來儲存對話歷史, 毋須使用不安全之全域變數. 

當按下按鈕時會呼叫回呼函式 ask_gpt(), 並傳入三個參數, 其中第三個就是儲存對話歷史的 State 物件, 傳入 ask_gpt() 後改名為 history, 新版 Chatbot 元件使用 type='message' 指定 history 格式為 OpenAI 的 List[Dict] 格式, 因此要用 role 與 content 為鍵將 prompt 加入 history 中查詢; 回應也是用相同方式加入 history 中. 由於 click() 的 outputs 參數指定了兩個輸出 (chatbot 與 prompt), 所以 ask_gpt() 需傳回一個 tuple, 第二個元素傳回空字串的目的是要自動清空提示詞欄位讓使用者能直接輸入下一個提示詞. 呼叫 prompt 的 submit() 函式則是用在當使用者在 prompt 輸入框按下 Enter 鍵時做出與按下送出按鈕一樣的效果. 結果如下 :




看來 GPT 是有記住對話紀錄 (但 gpt-3.5-turbo 為何要道歉?). 

沒有留言 :