2025年2月27日 星期四

Python 學習筆記 : 使用 DuckDuckGo API 搜尋網路資料 (三)

在前面的測試中, 我們發現 DuckDuckGo 物件的 chat() 背後會串接 OpenAI 的語言模型 (預設使用 gpt-4o-mini 模型). 本篇要使用 Gradio 的 Chatbot 元件作為 UI 介面來實作 AI 聊天機器人. 

本系列之前的文章參考 :


 
9. 使用 Gradio 的 Chatbot 製作 DuckDuckGo 聊天機器人 : 

Gradio 的 Chatbot 元件會用聊天泡泡形式呈現對話過程, 是專為聊天機器人之類的應用而設計的 UI 元件, 作法參考下面這篇串接 OpenAI API 的聊天機器人 :


本測試使用 Chatbot 元件透過 DuckDuckGo 物件的 chat() 方法來與 gpt-3.5-turbo 或 gpt-4-flash 模型聊天, 與上面串接 OpenAI API 不同之處是 chat() 會自動記憶之前的聊天歷史, 因此不需把對話歷史作為 prompt 傳送給模型, 程式碼如下 :

# duckduckgo_chatbot_1.py
import gradio as gr
from duckduckgo_search import DDGS

def ask_duckduckgo(prompt):
    global chat_history
    chat_history.append({'role': 'user', 'content': prompt})
    bot_response=ddgs.chat(prompt)
    chat_history.append({'role': 'assistant', 'content': bot_response})
    return chat_history

ddgs=DDGS()
chat_history=[]
prompt=gr.Textbox(label='您的詢問: ')
chatbot=gr.Chatbot(type='messages',
                   height=400,
                   placeholder='我們的對話',
                   show_copy_button=True)
iface=gr.Interface(
    fn=ask_duckduckgo,
    inputs=prompt,  
    outputs=chatbot,
    title='DuckDuckGo 聊天機器人',
    flagging_mode='never',
    )
iface.launch()

此例設置了一個 Textbox 輸入元件 prompt 來輸入提示詞, 以及一個 Chatbot 元件來輸出對話歷史, 並以一個串列 chat_history 來記錄對話歷史. 當輸入提示詞, 按下 Submit 按鈕時會呼叫 fn 所指的處理函式 ask_duckduckgo(), 將 prompt 傳給 DDGS 物件的 chat() 方法即可取得回應, 結果如下 : 






可見即使沒有將之前的對話歷史連同 prompt 傳給 chat(), 它依然能記得之前說了甚麼. 

但是上面的聊天機器人使用全域變數 chat_history 來儲存對話紀錄並不安全, 較好的做法是使用 Blocks 元件自行排版以建立一個語境, 然後在裡面建立一個 State 物件, 它會自動儲存 Chatbot 元件內的對話歷史. 

修改後的程式如下 :

# duckduckgo_chatbot_2.py
import gradio as gr
from duckduckgo_search import DDGS

def ask_duckduckgo(prompt, history):
    history.append({'role': 'user', 'content': prompt})
    try:
        bot_response=ddgs.chat(prompt) 
        history.append({'role': 'assistant', 'content': bot_response})
    except Exception as e:
        history.append({'role': 'assistant', 'content': f'發生錯誤:{str(e)}'})
    return history, ""

ddgs=DDGS()
with gr.Blocks(title='DuckDuckGo 聊天機器人') as blocks:
    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_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
    prompt.submit(ask_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
blocks.launch()

此例做了如下變動 : 
  • 呼叫 chat() 的程式碼被放入 try except 結構內以避免出現例外時 app 崩潰.
  • ask_duckduckgo() 傳回值增加一個空字串元素用來清空提示詞輸入框以便能直接輸入下一個提示詞, 這對應到 click() 方法 outputs 參數的第二元素 prompt. 
  • 增加 prompt.submit() 以便能在提示詞輸入框按下 Enter 時效果與按下送出鈕一樣. 
結果如下 : 




上面的程式仍有不足之處, 例如無法清除聊天紀錄重新開始, 我們可以在版面中增加一個 "清除歷史" 的按鈕來達成, 程式碼修改如下 :

# duckduckgo_chatbot_3.py
import gradio as gr
from duckduckgo_search import DDGS

def clear_history():
    return [], []  # 同時清除 chatbot 和 state

def ask_duckduckgo(prompt, history):
    history.append({'role': 'user', 'content': prompt})
    try:
        bot_response=ddgs.chat(prompt) 
        history.append({'role': 'assistant', 'content': bot_response})
    except Exception as e:
        history.append({'role': 'assistant', 'content': f'發生錯誤:{str(e)}'})
    return history, ""

ddgs=DDGS()
with gr.Blocks(title='DuckDuckGo 聊天機器人') as blocks:
    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("送出")
        clear_btn=gr.Button("清除對話")
    send_btn.click(ask_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
    prompt.submit(ask_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
    clear_btn.click(clear_history, inputs=None, outputs=[chatbot, state])
blocks.launch()

此例增加了一個 clear_btn 按鈕, 它的輸出有兩個 : chatbot 與 state 物件, 當按下此按鈕時會呼叫 clear_history() 函式, 它會傳回兩個空串列分別清除 chatbot 與 state 物件的內容, 這樣不僅是聊天泡泡內容全部被清除, 連內部儲存的對話歷史狀態也被清除, 結果如下 : 




但是當我按下 "清除歷史" 再次詢問我養的貓咪, 它居然還記得! 這是因為 DuckDuckGo 本身的記憶還在的緣故, 解決之道是要把 DDGS 物件重新起始, 也就是重新建立一個 DDGS 物件, 修改後的程式碼如下 :

# duckduckgo_chatbot_3.py
import gradio as gr
from duckduckgo_search import DDGS

def clear_history():
    global ddgs       # 取用全域變數
    ddgs=DDGS()   # 重新初始化
    return [], []  # 同時清除 chatbot 和 state

def ask_duckduckgo(prompt, history):
    history.append({'role': 'user', 'content': prompt})
    try:
        bot_response=ddgs.chat(prompt) 
        history.append({'role': 'assistant', 'content': bot_response})
    except Exception as e:
        history.append({'role': 'assistant', 'content': f'發生錯誤:{str(e)}'})
    return history, ""

ddgs=DDGS()
with gr.Blocks(title='DuckDuckGo 聊天機器人') as blocks:
    gr.Markdown("# DuckDuckGo 聊天機器人")
    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("送出")
        clear_btn=gr.Button("清除對話")
    send_btn.click(ask_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
    prompt.submit(ask_duckduckgo, inputs=[prompt, state], outputs=[chatbot, prompt])
    clear_btn.click(clear_history, inputs=None, outputs=[chatbot, state])
blocks.launch()

此例主要的變動是在 clear_history() 中利用 global 取用全域變數 ddgs, 然後重新初始化一個 DDGS 物件給它, 這樣就可以重設 DuckDuckGo API 內儲存之歷史對話紀錄了. 另外使用 gr.Markdown() 在 Web app 開頭添加標題. 

首先送出提示詞 '我有養兩隻貓, 名叫小咪與萬萬' 後按下清除歷史鈕, 詢問 '你還記得我養了幾隻貓嗎? 名字是甚麼?' 顯示結果如下 : 




這樣果然就順利清除歷史對話了. 

我把最後一個範例程式佈署在 HuggingFace Space 上 : 


PS : 為了在手機螢幕能看到送出鈕, 我將 Chatbot 元件高度改為 320px.

沒有留言 :