iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0

昨天串接了 LINE 通知與強化整體系統架構,今天的目標是建置 Gradio 前端,讓使用者能透過視覺化介面上傳音訊並下達指令,串接 MCP Agent,並測試完整的端到端流程。

今天的目標與挑戰

  • 專案結構標準化,使用 pyproject.toml
  • 安裝並設定 Gradio 前端框架
  • 建立音訊上傳和指令輸入的互動式網頁介面
  • 完成 MCP Agent 後端與 Gradio 之間的串接
  • 執行全部流程的測試

為什麼選擇 Gradio?

在規劃 MCPAgent 的使用者介面時,我有兩個想法,要使用 Flask 框架從頭打造,或者是採用專為 AI 應用設計的工具。
既然我的目的不是要花心思在網站的架設上面,所以我決定使用專為 AI 應用設計的工具──Gradio ,它以簡潔、高效的特性脫穎而出,成為此次前端開發的最佳選擇,其主要理由有

  1. 較低的開發門檻:Gradio 讓開發者能用 Python 程式碼快速生成互動式網頁介面,完全無需撰寫 HTML、CSS 或 JavaScript,這能大幅縮短開發時間。
  2. 專為 AI 應用而生的元件:Gradio 內建了豐富的元件,像是我們會使用到的音訊上傳(gr.Audio)、文字輸入(gr.Textbox) 等,能完美契合這次接收會議音訊和處理指令的需求,且這些現成元件可以為我節省大量的前端工作。
  3. 與 Python 函式無縫整合:Gradio 的核心設計是將一個 Python 函式直接轉換為可互動的應用程式,這意味著我可以將 MCP Agent 的核心處理邏輯封裝成一個函式,Gradio 會幫忙處理所有資料輸入和輸出的網頁互動。
  4. 快速原型製作與分享:只需在啟動指令中加入 share=True,Gradio 就能產生一個公開的臨時網址,讓我能容易地向其他人展示原型,或在不同裝置上進行測試,不需複雜的部署設定。
  5. 靈活的介面佈局:透過 gr.Blocks 的功能,Gradio 提供了比基本介面更高的客製化彈性,讓我可以自由組合、排列各個元件,打造出更符合使用者操作邏輯的介面佈局。
  6. 專注於 Agent 核心功能:使用 Gradio,我可以將精力集中在 MCP Agent 的後端邏輯開發與 n8n 工作流的最佳化上,不需要分心處理繁瑣的前端事務,而且 Gradio 也承包了所有 UI 的工作,讓我能專注於打造更強大的 Agent。

使用 Gradio 能讓我不用專注在前端技術上,能讓我把所有精力投入到 MCP Agent 的核心功能開發與最佳化上。


Step 1:專案結構升級 pyproject.toml

在今天開始今天的內容前,我想先優化一下我們的專案結構。我採用 Python 社群更推薦的標準作法,就是使用 pyproject.toml 設定檔搭配 「可編輯模式安裝 (Editable Install)」

1-1 pyproject.toml 是什麼?

pyproject.toml 是現代 Python 專案的 「身分證」「說明書」 的集合體。它的誕生是為了解決過去 Python 專案設定檔混亂的局面,目標是成為一個統一的、標準化的專案設定中心。

我們可以將它解剖成以下幾個部分

  • [build-system]建造說明書
    • 告訴 pip 等工具要用什麼來「蓋房子」(建置專案)。這部分通常是固定的。
  • [project]專案的身分證
    • 記錄專案名稱、版本、描述、依賴套件等核心資訊。
  • [tool.*]工具的專屬設定區
    • 像是我們可以告訴 setuptools 這個工具,我們專案的原始碼都放在 src 資料夾裡。

1-2 建立設定檔與安裝

  1. 建立 pyproject.toml
    在專案的根目錄 M2A Agent/ 下,建立 pyproject.toml 檔案,並在其中寫入以下內容
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "m2a-agent"
version = "0.1.0"
description = "A meeting processing agent"
requires-python = ">=3.8"

[tool.setuptools.packages.find]
where = ["src"]
  1. 執行可編輯模式安裝
    在專案根目錄的終端機中,執行以下指令:
pip install -e .
  • -e:代表 editable(可編輯)
    這個指令不會真的複製檔案,而是在 Python 環境中建立一個指向專案原始碼的「捷徑」。這代表我們之後對 src 資料夾內任何 .py 檔案的修改,都會立刻生效,完全無需重新安裝,大大地提升了開發的效率。

Step 2:安裝與設定 Gradio 前端框架

2-1 Gradio 套件安裝

在專案的虛擬環境中打開終端機,並執行以下指令來安裝 Gradio。使用 -U 是用來確保安裝的是最新版本,以獲得最完整的功能與最好的穩定性

pip install gradio -U

裝好後可以利用 import gradio as gr 來匯入模組。

2-2 驗證 Gradio 安裝狀態

進入 Python 交互介面或 Jupyter/Colab,執行以下程式能檢查當前版本:

import gradio as gr
print(gr.__version__)

會看到 5.x.x 的版本資訊。若遇到安裝錯誤建議先升級 pip,再重新安裝一次 Gradio。

2-3 建立簡單的輸入輸出頁面

import gradio as gr


# 1. 定義一個函式 greet,它接收了一個輸入user,並回傳一個輸出
def greet(user):
    return "Hello, " + user + "~"


# 2. 建立 Gradio 介面
# fn: 指定要執行的函式
# inputs: 定義輸入元件的類型
# outputs: 定義輸出元件的類型
demo = gr.Interface(fn=greet, inputs="text", outputs="text")

# 3. 啟動 Gradio 服務
# share=True 會產生一個公開的臨時網址,方便分享給他人測試
demo.launch(share=True)

看到以下的輸出,代表 Gradio 服務已經成功啟動了!

Running on local URL:  http://127.0.0.1:7860
Running on public URL: https://gradio.live

This share link expires in 1 week. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)

接著用瀏覽器打開 http://127.0.0.1:7860 這個網址,會看到一個簡單的輸入框和輸出框,在輸入框輸入名字後,按下 Submit 就會在輸出框看到結果了。
First Test


Step 3:建立音訊上傳與指令輸入介面

3-1 基本元件

Gradio 支援了豐富的互動元件,常用的基本元件有

  • Textbox: 用來輸入文字指令
  • Audio: 上傳或錄製音訊檔案
  • Button: 觸發送出事件

3-2 元件測試

以下為簡易的「音訊上傳」與「指令輸入」前端程式,先用來測試確認「音訊上傳」與「指令輸入」是否可以正常上傳與輸入,並且使用 Blocks 排版,讓 UI 元件自動對齊並維持整體美觀。

import gradio as gr
import time


# 模擬的後端處理函式
def process_audio_and_command(audio_filepath, command_text):
    if audio_filepath is None:
        return "您尚未上傳音訊檔案,請先上傳一個音訊檔案!"
    if not command_text.strip():
        return "指令為空,請輸入指令!"

    # 模擬後端處理時間
    print(f"音訊檔案路徑:{audio_filepath}")
    print(f"指令:{command_text}")
    print("正在處理中,請稍候...")
    time.sleep(3)  # 模擬 AI 處理延遲

    # 模擬回傳結果
    summary = "AI 生成的會議摘要內容測試"
    action_items = "行動內容提取結果測試"

    result = f"""
    ## 會議摘要
    {summary}

    ## 行動項目
    {action_items}
    """
    return result


# 使用 gr.Blocks() 來建立更美觀的版面
with gr.Blocks(theme=gr.themes.Soft()) as demo:
    # 標題
    gr.Markdown("# M2A Agent 會議處理平台")
    gr.Markdown("請上傳會議音訊檔案,並輸入執行的指令。")

    # 使用 gr.Row() 來將元件水平排列
    with gr.Row():
        # 左側:輸入元件
        with gr.Column(scale=1):
            audio_input = gr.Audio(type="filepath", label="會議音訊")
            command_input = gr.Textbox(
                lines=3,
                label="處理指令",
                placeholder="範例:請幫我摘要重點並列出行動項目",
            )
            submit_button = gr.Button("開始處理", variant="primary")

        # 右側:輸出元件
        with gr.Column(scale=2):
            output_display = gr.Markdown(label="處理結果")

    # 設定按鈕的點擊事件
    # 當 submit_button 被點擊時,會執行 process_audio_and_command 函式
    # inputs: 將 audio_input 和 command_input 的值作為函式參數傳入
    # outputs: 將函式的回傳結果顯示在 output_display 元件中
    submit_button.click(
        fn=process_audio_and_command,
        inputs=[audio_input, command_input],
        outputs=output_display,
    )

# 啟動 Gradio 服務
print("Gradio 即將啟動")
demo.launch(share=True)

執行結果

Home Page

現在確認了這兩個功能可以正常運作了!
Result

錯誤測試:無上傳音訊
Error:No audio

錯誤測試:無輸入指令
Error:No command


Step 4:整合 MCP Agent 與 Gradio 前端

我們確認了「音訊上傳」與「指令輸入」可以正常運作後,接下來就是將 Gradio 與我們的 MCP Agent 結合起來。

4-1 匯入 MCPAgent 並建立實例

app.py 中直接匯入並使用在 src/mcp_agent.py 中定義好的 MCPAgent 類別。

import gradio as gr
import json
from mcp_agent import MCPAgent

# 在應用程式啟動時建立一個全域的 Agent 實例,可避免每次點擊都重新載入模型,提升效能。
print("正在初始化 MCP Agent")
agent = MCPAgent(model="medium")
print("✅ MCPAgent 初始化完成")

4-2 修改處理函式以呼叫 Agent

接收 Gradio 介面的輸入,呼叫 MCPAgent 進行處理,並回傳格式化的 Markdown 結果。

def process_and_run_agent(audio_filepath, command_text):
    # 驗證輸入
    if audio_filepath is None:
        return "## 錯誤\n您尚未上傳音訊檔案,請先**上傳**一個音訊檔案!"
    if not command_text.strip():
        return "## 錯誤\n指令為空,請**輸入**您希望執行的指令!"

    print(f"接收到音訊:{audio_filepath}")
    print(f"執行指令:{command_text}")

    # 呼叫核心 Agent 邏輯
    result = agent.process_audio(audio_filepath, command_text)

    # 根據處理結果回傳格式化的 Markdown 字串
    try:
        data = None
        # 判斷收到的 result 是列表還是字典
        if isinstance(result, list) and len(result) > 0:
            # 如果是列表,取出第一個元素
            data = result[0]
        elif isinstance(result, dict):
            # 如果直接就是字典,直接使用
            data = result

        if data:
            # 從物件中取出資料
            summary = data.get("summary", "摘要生成失敗")
            tasks = data.get("tasks", "任務提取失敗")
            notion_url = data.get("url", "")

            # 組裝在 Gradio 上顯示的 Markdown
            output_markdown = (
                f"## ✅ 處理完成\n\n"
                f"**會議記錄已成功建立至 Notion ,並且已發送 Line 通知!**\n\n"
                f"🔗 點此查看[ Notion 頁面]({notion_url})\n"
                f"---\n"
                f"### 會議摘要\n\n"
                f"{summary}\n\n"
                f"### 行動項目\n\n"
                f"{tasks}"
            )
            return output_markdown
        else:
            # 如果回傳的格式不是預期的,則顯示原始資料以便除錯
            formatted_result = json.dumps(result, indent=2, ensure_ascii=False)
            return (
                "### ⚠️ 處理異常\n\n"
                "n8n 工作流已執行,但回傳格式非預期(不是列表或字典)。\n\n"
            )
    except Exception as e:
        return f"## 前端解析失敗\n\n解析 n8n 回傳結果時發生錯誤\n"

這個函式不僅呼叫了 Agent,還有我在 Debug 的過程中學到了用 isinstance() 來彈性處理 n8n 可能回傳的 listdict 格式。

4-3 定義 Gradio 介面

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown("# MCPAgent 會議處理平台")
    gr.Markdown("請上傳會議音訊檔案,並輸入處理指令(例如:幫我生成會議摘要與行動項目)。")

    with gr.Row():
        with gr.Column(scale=1):
            audio_input = gr.Audio(type="filepath", label="上傳會議音訊")
            command_input = gr.Textbox(lines=3, label="處理指令", placeholder="請幫我摘要重點並列出行動項目")
            submit_button = gr.Button("開始處理", variant="primary")

        with gr.Column(scale=2):
            output_display = gr.Markdown(label="處理結果")

    submit_button.click(
        fn=process_and_run_agent,
        inputs=[audio_input, command_input],
        outputs=output_display
    )

# --- 啟動應用程式 ---
demo.launch(share=True)

4-4 完整的app.py內容

import gradio as gr
import time


# 模擬的後端處理函式
def process_audio_and_command(audio_filepath, command_text):
    if audio_filepath is None:
        return "您尚未上傳音訊檔案,請先上傳一個音訊檔案!"
    if not command_text.strip():
        return "指令為空,請輸入指令!"

    # 模擬後端處理時間
    print(f"音訊檔案路徑:{audio_filepath}")
    print(f"指令:{command_text}")
    print("正在處理中,請稍候...")
    time.sleep(3)  # 模擬 AI 處理延遲

    # 模擬回傳結果
    summary = "AI 生成的會議摘要內容測試"
    action_items = "行動內容提取結果測試"

    result = f"""
    ## 會議摘要
    {summary}

    ## 行動項目
    {action_items}
    """
    return result

def process_and_run_agent(audio_filepath, command_text):
    # 驗證輸入
    if audio_filepath is None:
        return "## 錯誤\n您尚未上傳音訊檔案,請先**上傳**一個音訊檔案!"
    if not command_text.strip():
        return "## 錯誤\n指令為空,請**輸入**您希望執行的指令!"

    print(f"接收到音訊:{audio_filepath}")
    print(f"執行指令:{command_text}")

    # 呼叫核心 Agent 邏輯
    result = agent.process_audio(audio_filepath, command_text)

    # 根據處理結果回傳格式化的 Markdown 字串
    try:
        data = None
        # 判斷收到的 result 是列表還是字典
        if isinstance(result, list) and len(result) > 0:
            # 如果是列表,取出第一個元素
            data = result[0]
        elif isinstance(result, dict):
            # 如果直接就是字典,直接使用
            data = result

        if data:
            # 從物件中取出資料
            summary = data.get("summary", "摘要生成失敗")
            tasks = data.get("tasks", "任務提取失敗")
            notion_url = data.get("url", "")

            # 組裝在 Gradio 上顯示的 Markdown
            output_markdown = (
                f"## ✅ 處理完成\n\n"
                f"**會議記錄已成功建立至 Notion ,並且已發送 Line 通知!**\n\n"
                f"🔗 點此查看[ Notion 頁面]({notion_url})\n"
                f"---\n"
                f"### 會議摘要\n\n"
                f"{summary}\n\n"
                f"### 行動項目\n\n"
                f"{tasks}"
            )
            return output_markdown
        else:
            # 如果回傳的格式不是預期的,則顯示原始資料以便除錯
            formatted_result = json.dumps(result, indent=2, ensure_ascii=False)
            return (
                "### ⚠️ 處理異常\n\n"
                "n8n 工作流已執行,但回傳格式非預期(不是列表或字典)。\n\n"
            )
    except Exception as e:
        return f"## 前端解析失敗\n\n解析 n8n 回傳結果時發生錯誤\n"

# 使用 gr.Blocks() 來建立更美觀的版面
with gr.Blocks(theme=gr.themes.Soft()) as demo:
    # 標題
    gr.Markdown("# M2A Agent 會議處理平台")
    gr.Markdown("請上傳會議音訊檔案,並輸入執行的指令。")

    # 使用 gr.Row() 來將元件水平排列
    with gr.Row():
        # 左側:輸入元件
        with gr.Column(scale=1):
            audio_input = gr.Audio(type="filepath", label="會議音訊")
            command_input = gr.Textbox(
                lines=3,
                label="處理指令",
                placeholder="範例:請幫我摘要重點並列出行動項目",
            )
            submit_button = gr.Button("開始處理", variant="primary")

        # 右側:輸出元件
        with gr.Column(scale=2):
            output_display = gr.Markdown(label="處理結果")

    # 設定按鈕的點擊事件
    # 當 submit_button 被點擊時,會執行 process_audio_and_command 函式
    # inputs: 將 audio_input 和 command_input 的值作為函式參數傳入
    # outputs: 將函式的回傳結果顯示在 output_display 元件中
    submit_button.click(
        fn=process_audio_and_command,
        inputs=[audio_input, command_input],
        outputs=output_display,
    )

# 啟動 Gradio 服務
print("Gradio 即將啟動")
demo.launch(share=True)

Step 5:n8n 工作流微調與最終測試

雖然已經打通了前後端的完整流程,但在我的測試後,我發現 n8n 的工作流還有一些可以微調的地方,能讓整個系統更符合我的預期。

5-1 工作流最佳化

最佳化過後的工作流如下
Workflow

5-2 修改「Respond to Webhook」節點

Respond to Webhook Settings

5-3 改變說明

這次工作流的改變核心在於並行處理。當 Markdown 格式化處理 節點完成後,工作流會兵分二路。

  1. 第一路:透過 Respond to Webhook 節點,將包含摘要和任務的 JSON 資料回傳給 Gradio,這確保了使用者介面能收到結果並顯示。
  2. 第二路:同時,將資料傳送給 發送 LINE 通知 節點,在背景完成通知任務。

這樣一來,Gradio 無需等待 LINE 通知發送完成,大幅縮短了前端的等待時間,且提升了使用者體驗。


Step 6:端到端流程驗證

6-1 驗證前的準備

先確認所有服務都已處於「待命」狀態

  • n8n 工作流:確認 M2A Agent 工作流處於 Active 的狀態。
  • Gradio 前端:在專案根目錄終端機中,執行 python app.py,並確認沒有任何錯誤訊息,且程式正在運作中。

6-2 最終測試步驟

  1. 打開介面:在瀏覽器中打開 Gradio 提供的本地網址(http://127.0.0.1:7860)。
  2. 上傳音訊:在「會議音訊」區塊,上傳音訊檔案。
  3. 輸入指令:在「處理指令」輸入框中,輸入指令。
  4. 點擊送出:點擊「開始處理」按鈕。

執行結果

Gradio 前端頁面
Successful!!V2
LINE 通知
Line
Notion
Notion
終端機
Terminal
n8n workflow
Successful Workflow

確認過以上的結果後,代表我們成功了!!


今天的成果總結

完成項目

  • 使用 pyproject.toml 標準化專案結構
  • 完成 Gradio 前端框架的安裝與設置
  • 實現 Gradio 前端與 MCPAgent 後端完成串接,可全流程啟動自動化處理
  • 使用 Gradio 打造了「音訊上傳」與「指令輸入」的互動式前端原型介面
  • n8n 工作流微調為並行處理模式,最佳化了前端的回應速度

心得

今天的實作讓我體會到了 Gradio 能大幅降低前端開發門檻,透過簡單的 Python 程式
就可以快速設計出符合需求的互動式 UI,因此也為我的 Agent 提升了成熟度。

🎯 明天計劃
將升級 Prompt,讓 Agent 能自動從會議中提取「會議類型」和「專案名稱」,並實現智慧關聯。


上一篇
Day 9 Line 即時通知與架構升級
下一篇
Day 11 智慧會議辨識類型與專案關聯
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言