昨天串接了 LINE 通知與強化整體系統架構,今天的目標是建置 Gradio 前端,讓使用者能透過視覺化介面上傳音訊並下達指令,串接 MCP Agent,並測試完整的端到端流程。
在規劃 MCPAgent 的使用者介面時,我有兩個想法,要使用 Flask 框架從頭打造,或者是採用專為 AI 應用設計的工具。
既然我的目的不是要花心思在網站的架設上面,所以我決定使用專為 AI 應用設計的工具──Gradio ,它以簡潔、高效的特性脫穎而出,成為此次前端開發的最佳選擇,其主要理由有
gr.Audio
)、文字輸入(gr.Textbox
) 等,能完美契合這次接收會議音訊和處理指令的需求,且這些現成元件可以為我節省大量的前端工作。share=True
,Gradio 就能產生一個公開的臨時網址,讓我能容易地向其他人展示原型,或在不同裝置上進行測試,不需複雜的部署設定。gr.Blocks
的功能,Gradio 提供了比基本介面更高的客製化彈性,讓我可以自由組合、排列各個元件,打造出更符合使用者操作邏輯的介面佈局。使用 Gradio 能讓我不用專注在前端技術上,能讓我把所有精力投入到 MCP Agent 的核心功能開發與最佳化上。
pyproject.toml
在今天開始今天的內容前,我想先優化一下我們的專案結構。我採用 Python 社群更推薦的標準作法,就是使用 pyproject.toml
設定檔搭配 「可編輯模式安裝 (Editable Install)」。
pyproject.toml
是什麼?pyproject.toml
是現代 Python 專案的 「身分證」 與 「說明書」 的集合體。它的誕生是為了解決過去 Python 專案設定檔混亂的局面,目標是成為一個統一的、標準化的專案設定中心。
我們可以將它解剖成以下幾個部分
[build-system]
:建造說明書
pip
等工具要用什麼來「蓋房子」(建置專案)。這部分通常是固定的。[project]
:專案的身分證
[tool.*]
:工具的專屬設定區
setuptools
這個工具,我們專案的原始碼都放在 src
資料夾裡。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"]
pip install -e .
-e
:代表 editable(可編輯)src
資料夾內任何 .py
檔案的修改,都會立刻生效,完全無需重新安裝,大大地提升了開發的效率。在專案的虛擬環境中打開終端機,並執行以下指令來安裝 Gradio。使用 -U 是用來確保安裝的是最新版本,以獲得最完整的功能與最好的穩定性
pip install gradio -U
裝好後可以利用 import gradio as gr
來匯入模組。
進入 Python 交互介面或 Jupyter/Colab,執行以下程式能檢查當前版本:
import gradio as gr
print(gr.__version__)
會看到 5.x.x
的版本資訊。若遇到安裝錯誤建議先升級 pip,再重新安裝一次 Gradio。
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
就會在輸出框看到結果了。
Gradio 支援了豐富的互動元件,常用的基本元件有
以下為簡易的「音訊上傳」與「指令輸入」前端程式,先用來測試確認「音訊上傳」與「指令輸入」是否可以正常上傳與輸入,並且使用 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)
現在確認了這兩個功能可以正常運作了!
錯誤測試:無上傳音訊
錯誤測試:無輸入指令
我們確認了「音訊上傳」與「指令輸入」可以正常運作後,接下來就是將 Gradio 與我們的 MCP Agent 結合起來。
在 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 初始化完成")
接收 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 可能回傳的 list
或 dict
格式。
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)
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)
雖然已經打通了前後端的完整流程,但在我的測試後,我發現 n8n 的工作流還有一些可以微調的地方,能讓整個系統更符合我的預期。
最佳化過後的工作流如下
這次工作流的改變核心在於並行處理。當 Markdown 格式化處理
節點完成後,工作流會兵分二路。
Respond to Webhook
節點,將包含摘要和任務的 JSON 資料回傳給 Gradio,這確保了使用者介面能收到結果並顯示。發送 LINE 通知
節點,在背景完成通知任務。這樣一來,Gradio 無需等待 LINE 通知發送完成,大幅縮短了前端的等待時間,且提升了使用者體驗。
先確認所有服務都已處於「待命」狀態
M2A Agent
工作流處於 Active 的狀態。python app.py
,並確認沒有任何錯誤訊息,且程式正在運作中。http://127.0.0.1:7860
)。Gradio 前端頁面
LINE 通知
Notion
終端機
n8n workflow
確認過以上的結果後,代表我們成功了!!
✅ 完成項目
pyproject.toml
標準化專案結構今天的實作讓我體會到了 Gradio 能大幅降低前端開發門檻,透過簡單的 Python 程式
就可以快速設計出符合需求的互動式 UI,因此也為我的 Agent 提升了成熟度。
🎯 明天計劃
將升級 Prompt,讓 Agent 能自動從會議中提取「會議類型」和「專案名稱」,並實現智慧關聯。