昨天我們成功整合了 Gradio 前端與 MCP Agent ,今天的目標是讓它變得更加智慧,能夠自動從會議內容中提取「會議類型」和「專案名稱」,並在 Notion 中建立正確的關聯,同時為每個會議生成唯一的 Session ID 以便日後追蹤。
雖然我們已經可以成功處理會議音訊並且生成摘要,但系統還缺乏「脈絡記憶」,每次處理都是獨立的。
Session ID 的存在就像是每次會議的「身分證」,它能夠
在 Python 裡,可以使用 uuid
模組來生成全域唯一識別碼,針對 Session ID 選擇 uuid4()
是因為它具有以下優勢
在 src/mcp_agent.py
中新增 Session ID 生成功能
import requests
import uuid
from src.whisper_service import WhisperService
from datetime import datetime
class MCPAgent:
def __init__(
self, model="medium", webhook_url="http://localhost:5678/webhook/m2a-test"
):
self.whisper = WhisperService(model)
self.webhook_url = webhook_url
def session_id(self):
# 生成唯一的 Session ID,結合時間戳和 UUID。
# 格式: session_YYYYMMDD_HHMMSS_UUID8
session_uuid = uuid.uuid4()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 回傳結合時間戳與部分 UUID 的字串
return f"session_{timestamp}_{str(session_uuid)[:8]}"
def process_audio(self, audio_path, instruction):
# 0. 生成唯一的 Session ID
session_id = self.generate_session_id()
print(f"本次處理的 Session ID:{session_id}")
# 1. 呼叫 WhisperService 轉錄
result = self.whisper.transcribe(audio_path)
if result.get("error"):
# 轉錄失敗,回傳錯誤
return {"error": f"轉錄失敗:{result['error']}"}
# 2. 準備要送給 Webhook 的資料
payload = {
"session_id": session_id,
"text": result["text"],
"instruction": instruction,
}
# 3. 呼叫 n8n Webhook
try:
resp = requests.post(self.webhook_url, json=payload)
resp.raise_for_status()
return resp.json()
except Exception as e:
return {"error": f"Webhook 呼叫失敗:{e}"}
我們設計的 Session ID 格式為 session_YYYYMMDD_HHMMSS_UUID8位
例如:session_20250923_143056_a7b8c9d2
這種格式具有以下優點
進入「會議記錄」資料庫,新增或確保有以下欄位
Session ID
會議類型
關聯專案
建立時間
要讓 AI 知道我們有哪些專案,首先就是要讓 n8n 把專案清單從 Notion 讀取出來。
在「Webhook」節點後面,新增 Notion 節點 Actios 底下的「Get many database pages」。
GIGI工作室專案庫
ON
再來我們需要將上一步輸出的結果,轉換為 AI 最容易理解的純文字格式。
在「取得專案清單」節點後面,新增 Code 節點。
// 取得上一個節點的所有輸出項目
const projects = $input.all();
// 從每個專案物件中,只取出「專案名稱」
// 從 simplified 格式的 json.property_ 屬性中取得名稱
const projectNames = projects.map(item => item.json.property_);
// 過濾掉空的專案名稱
// Notion 資料庫可能會有空白標題的頁面
const filteredProjectNames = projectNames.filter(name => name && name.trim() !== '');
// 將有效的專案名稱陣列轉換成一個字串
const projectListString = filteredProjectNames.join(', ');
// 回傳一個包含這個清單的物件
return [{
json: {
project_list: projectListString
}
}];
我們要來賦予它從會議內容中辨識「會議類型」和「專案名稱」的智慧。
在 n8n 的「AI 摘要與任務提取」節點中修改 Prompt,讓 AI 不只能生成摘要和提取任務,還能辨識「會議類型」和「專案名稱」。
以下是修改節點後的內容
// 從 Webhook 拿原始資料 (明確指定拿第一筆)
const webhookData = $('Webhook').first().json.body;
// 從「格式化專案列表」拿專案列表 (明確指定拿第一筆)
const projectListData = $('格式化專案列表').first().json;
const sessionId = webhookData.session_id;
const text = webhookData.text;
const instruction = webhookData.instruction;
const projectList = projectListData.project_list;
// ===== AI 提取函式 =====
async function extractMeetingInfo(text, instruction, projectList) {
const payload = {
model: "qwen2.5-taiwan-7b-instruct-i1",
messages: [
{
role: "system",
content: `你是一個頂尖的會議分析師。你的任務是根據「會議逐字稿」和一份「已知的專案清單」來提取資訊。
[已知的專案清單]
${projectList}
[你的任務]
1. 仔細閱讀「會議逐字稿」。
2. 判斷逐字稿的內容主要是在討論「已知的專案清單」中的哪一個專案。
3. 如果找到完全匹配的專案,請在 project_name 欄位回傳該專案的確切名稱。
4. 如果逐字稿內容與任何已知專案都無關,或者不明確,請務必在 project_name 欄位回傳 null。
5. 完成會議類型、摘要和行動任務的提取。
[回傳格式]
請務必嚴格按照以下 JSON 格式回傳,僅包含指定的四個鍵:
{"project_name":"從清單中匹配到的專案名稱或 null","meeting_type":"會議類型(如專案討論、技術討論、需求分析、進度報告、創意發想、決策會議、一般會議)","summary":"會議摘要 (Markdown 格式)","tasks":"行動任務 (Markdown 格式)"}`
},
{
role: "user",
content: `[會議逐字稿]\n${text}\n\n[使用者指令]\n${instruction}`
}
],
temperature: 0.3,
max_tokens: 2048
};
try {
const response = await this.helpers.httpRequest({
method: 'POST',
url: 'http://host.docker.internal:1234/v1/chat/completions',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const responseData = typeof response === 'string' ? JSON.parse(response) : response;
if (responseData.choices && responseData.choices[0] && responseData.choices[0].message) {
const content = responseData.choices[0].message.content;
const match = content.match(/\{[\s\S]*\}/);
if (match && match[0]) {
return JSON.parse(match[0]);
}
}
throw new Error('AI 回應格式異常,找不到有效的 JSON 內容');
} catch (error) {
console.error(`[${sessionId}] AI 資訊提取失敗:`, error);
return {
project_name: null,
meeting_type: "錯誤",
summary: `AI 分析失敗: ${error.message}`,
tasks: "無法提取行動項目"
};
}
}
// ===== 主流程 =====
const meetingInfo = await extractMeetingInfo(text, instruction, projectList);
return [{ json: { session_id: sessionId, meeting_info: meetingInfo } }];
/\{[\s\S]*\}/
抓取 JSON,避免任何多餘文字干擾解析。temperature: 0.3
保持回應穩定、一致,避免過度創意導致格式錯亂。project_name
、meeting_type
、summary
、tasks
,保持前後端欄位名稱一致。AI 給出答案後,我們需要一個 「If」 節點根據是否找到專案,來執行後續流程走向不同的路徑。
在「AI 摘要與任務提取」節點後面,新增 「If」 節點。
{{ $('AI 摘要與任務提取').item.json.meeting_info.project_name }}
is not empty
當成功匹配到專案後,要在建立「會議記錄」時寫入關聯,因此要
GIGI工作室專案庫
{{ $('AI 摘要與任務提取').item.json.meeting_info.project_name }}
{{ $('AI 摘要與任務提取').item.json.meeting_info.meeting_type }}
{{ $('專案查詢').first().json.id }}
{{ $('AI 摘要與任務提取').item.json.session_id }}
當未匹配到專案時,就不建立專案關聯了。
最後無論流程走了哪條分支,都需要一個統一的「打包」的節點,將結果整理成前端介面與 LINE 通知所需的最終格式。
以下為修改後的節點內容
const aiNodeData = $('AI 摘要與任務提取').first();
const summary = aiNodeData.json.meeting_info.summary;
const tasks = aiNodeData.json.meeting_info.tasks;
const notionUrl = $input.item.json.url;
function markdownToPlainText(markdown) {
if (!markdown || typeof markdown !== 'string') {
return '';
}
let text = markdown;
text = text.replace(/^#{1,6}\s+/gm, '');
text = text.replace(/(**|__)(.*?)\1/g, '$2');
text = text.replace(/(*|_)(.*?)\1/g, '$2');
text = text.replace(/~~(.*?)~~/g, '$1');
text = text.replace(/!\[.*?\]\(.*?\)/g, '');
text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
text = text.replace(/^\s*[*\-\+]\s+/gm, '• ');
text = text.replace(/^\s*\d+\.\s+/gm, '');
text = text.replace(/^(---|___|***)\s*$/gm, '');
text = text.replace(/``````/g, '');
text = text.replace(/`([^`]+)`/g, '$1');
text = text.replace(/\n{3,}/g, '\n\n');
return text.trim();
}
const plainSummary = markdownToPlainText(summary);
const plainTasks = markdownToPlainText(tasks);
const lineMessageText = `🎯 會議處理完成通知
- 專案:GIGI 工作室內部專案
- 日期:${new Date().toLocaleDateString('zh-TW')}
- Notion 頁面:${notionUrl}
--- 會議摘要 ---
${plainSummary}
--- 行動任務 ---
${plainTasks}`;
const lineApiPayload = {
"messages": [
{
"type": "text",
"text": lineMessageText
}
]
};
const finalOutput = {
summary: summary,
tasks: tasks,
url: notionUrl,
lineApiPayload: lineApiPayload
};
return [finalOutput];
先確認所有服務都已處於「待命」狀態
M2A Agent
工作流處於 Active 的狀態。python app.py
,並確認沒有任何錯誤訊息,且程式正在運作中。http://127.0.0.1:7860
)。Gradio 前端頁面
LINE 通知
Notion
終端機
n8n workflow
確認過以上的結果後,代表我們成功了!!
✅ 完成項目
Session ID
,提升了可追蹤性null
情境的能力If
節點成功實現了工作流的條件分支,讓 Agent 能根據不同情況執行不同路徑Relation
功能,實現了會議記錄與專案的自動化關聯今天的 M2A Agent 開始邁向「智慧化」了,我成功地使用 RAG (Retrieval-Augmented Generation),也就是「檢索增強生成」提供「已知專案清單」給 AI,讓我的 Agent 擁有了「理解上下文」,還有透過 If
節點根據輸出結果進行條件分支擁有了「決策」的能力,讓原本線性的流程,進化成了具備初步判斷能力的智慧系統。
這樣實作的成功讓我感到我的 Agent 更厲害了,我也更開心了!
🎯 明天計劃
導入錯誤處理與狀態回報機制,並在失敗時透過 LINE 主動通知,提升系統穩定性。