Spaces:
Running
Running
File size: 10,305 Bytes
6d820cb 2be2326 6d820cb e060de2 6d820cb f4d5e8a c278602 e060de2 2be2326 6d820cb e060de2 6d820cb e060de2 6d820cb e060de2 6d820cb e060de2 832a92e e060de2 832a92e e060de2 832a92e e060de2 1eb4ba5 e060de2 6ff45ca f4d5e8a e9b2abf c820ac8 dddc990 c820ac8 dddc990 c820ac8 f4d5e8a dddc990 f4d5e8a dddc990 f4d5e8a dddc990 f4d5e8a dddc990 e060de2 f4d5e8a e060de2 f4d5e8a e060de2 f4d5e8a e060de2 f4d5e8a dddc990 f4d5e8a dddc990 f4d5e8a dddc990 e060de2 f4d5e8a f46e2fc f4d5e8a e060de2 6d820cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | import os
from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案
from typing import Optional
from collections import defaultdict # 匯入 defaultdict,用於建立預設值的字典
from linebot import LineBotApi, WebhookHandler # 匯入 Line Bot SDK
from linebot.exceptions import InvalidSignatureError # 匯入 Line 簽章無效的例外
from linebot.models import ( # 匯入 Line Bot 的各種訊息模型
MessageEvent,
TextMessage,
TextSendMessage,
ImageSendMessage,
ImageMessage,
FollowEvent
)
import json
from services.agents import run_agent
load_dotenv()
# --- 設定和初始化 ---
# 設置 Line Bot 的 API 金鑰和秘密金鑰 (從環境變數讀取)
# 初始化 Line Bot API 客戶端和 Webhook 處理器
try:
line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
line_handler = WebhookHandler(os.environ["CHANNEL_SECRET"])
print("Line Bot API 客戶端和 Handler 初始化成功。")
except Exception as e:
print(f"初始化 Line Bot 失敗: {e}")
# 在實際應用中,這裡可能需要更嚴格的錯誤處理
line_bot_api = None
line_handler = None
# 使用 defaultdict 模擬用戶訊息歷史存儲
# 鍵(key)為 user_id,值(value)為一個儲存訊息的列表(list)
user_message_history = defaultdict(list)
def get_image_url_from_line(message_id):
"""
從 Line 訊息 ID 獲取圖片內容並儲存到暫存檔案。
Args:
message_id: Line 訊息的 ID。
Returns:
成功時回傳圖片儲存的本地路徑,失敗時回傳 None。
"""
try:
# 透過 Line Bot API 獲取訊息內容
message_content = line_bot_api.get_message_content(message_id)
# 定義暫存檔案路徑
file_path = f"/tmp/{message_id}.png"
# 將圖片內容以二進位寫入模式寫入檔案
with open(file_path, "wb") as f:
for chunk in message_content.iter_content():
f.write(chunk)
print(f"✅ 圖片成功儲存到:{file_path}")
return file_path
except Exception as e:
print(f"❌ 圖片取得失敗:{e}")
return None
def store_user_message(user_id, message_type, message_content):
"""
儲存用戶的訊息到 user_message_history 字典中。
Args:
user_id: 用戶的 ID。
message_type: 訊息類型 (例如 "image" 或 "text")。
message_content: 訊息內容 (例如圖片路徑或文字)。
"""
user_message_history[user_id].append(
{"type": message_type, "content": message_content}
)
def get_previous_message(user_id):
"""
獲取用戶的上一則訊息。
Args:
user_id: 用戶的 ID。
Returns:
如果歷史紀錄存在,回傳上一則訊息的字典;否則回傳預設的文字訊息。
"""
if user_id in user_message_history and len(user_message_history[user_id]) > 0:
# 回傳最後一則訊息
return user_message_history[user_id][-1]
# 如果沒有歷史紀錄,回傳一個預設值
return {"type": "text", "content": "No message!"}
# --- 事件處理器定義 ---
# 1. 處理所有接收到的文字訊息事件
@line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
def handle_message(event):
"""
處理接收到的文字訊息,並進行回覆。
"""
# 獲取用戶 ID
user_id = event.source.user_id
# 情況一:處理圖片上傳
if event.message.type == "image":
# 獲取 Line 傳來的圖片,並儲存到本地
image_path = get_image_url_from_line(event.message.id)
if image_path:
# 將圖片路徑儲存到用戶的訊息歷史中
store_user_message(user_id, "image", image_path)
# 回覆用戶,告知圖片已收到,並請他輸入問題
line_bot_api.reply_message(
event.reply_token, TextSendMessage(text="圖片已接收成功囉,幫我輸入你想詢問的問題喔~")
)
else:
line_bot_api.reply_message(
event.reply_token, TextSendMessage(text="沒有接收到圖片~")
)
elif event.message.type == "text":
user_text = event.message.text # 獲取用戶傳來的文字
agent_input = ""
# 獲取該用戶的「上一則」訊息
previous_message = get_previous_message(user_id)
print(f"上一則訊息: {previous_message}") # 在後台印出除錯訊息
# 根據上一則訊息類型,動態組合給代理人的輸入
if previous_message["type"] == "image":
# 如果上一則是圖片,代表用戶現在的文字是「針對圖片的提問」
image_path = previous_message["content"]
agent_input = f"請根據這張圖片回答問題。圖片的路徑是 {image_path},我的問題是:{user_text}"
# 清除上一則圖片訊息,避免下一次文字訊息還被當作是圖片問答
user_message_history[user_id].pop()
else:
# 如果上一則不是圖片 (或沒有上一則),代表這是一般的文字提問 (可能是要求生成圖片)
agent_input = user_text
out_str = ""
try:
# 運行 LangChain 代理人
response = run_agent(agent_input)
# Agent 的輸出是 JSON 字串 (無論成功或失敗)
out_str = response["output"]
#print(f"out_str:{out_str}")
tool_result = out_str.get("tool_result", {})
#print(f"tool_result:{tool_result}")
final_response = out_str.get("final_response")
#print(f"final_response:{final_response}")
if "error" in tool_result:
# 3A. 處理錯誤情況 (例如:圖片下載失敗、API 錯誤)
error_msg = tool_result["error"]
reply_text = f"🚫 圖片工具執行失敗:\n{error_msg}"
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text))
elif "image_url" in tool_result:
# 3B. 處理成功情況 (帶有圖片 URL,例如:圖片生成或去模糊工具)
image_url = tool_result["image_url"]
text_result = tool_result.get("text_result", "圖片處理完成。")
# Line 要求圖片 URL 必須是 HTTPS
# 由於 HF_SPACE 預設是 https,這裡做一個保險轉換
if image_url.startswith("http://"):
image_url = image_url.replace("http://", "https://")
# 使用 reply_message (或 push_message) 同時推送文字和圖片
# 註:reply_message 只能回覆一次。若要一次發送多個訊息,需要使用 list
# 這裡假設 text_result 是主要回覆,圖片作為輔助。
line_bot_api.reply_message(
event.reply_token,
[
TextSendMessage(text=f"✨ {text_result}"),
ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
]
)
elif "text_result" in tool_result:
# 3C. 處理純文字結果 (例如:多模態分析工具)
text_result = tool_result["text_result"]
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=text_result))
elif final_response:
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=final_response))
else:
# 3D. 處理意外的 JSON 結構
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="代理人回覆格式無法識別,請聯繫管理員。")
)
except json.JSONDecodeError:
# 4. 處理 Agent 返回了無法解析的純文字 (非 JSON)
# 這通常發生在 Agent 決定不使用工具,直接回覆純文字,或者推理過程出錯。
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out_str))
except Exception as e:
# 處理代理人執行時的錯誤
print(f"代理人執行出錯: {e}")
out = f"代理人執行出錯!錯誤訊息:{e}"
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
# 2. 處理關注 (Follow) 事件
@line_handler.add(FollowEvent)
def handle_follow(event: FollowEvent):
"""
處理 Line Bot 被用戶關注的事件。
"""
if event.source.user_id:
user_id = event.source.user_id
print(f"用戶 {user_id} 關注了 Bot。")
welcome_message = "感謝您的關注!我是您的圖像助理,可以幫您生成圖片或對圖片進行去模糊處理。請直接輸入您的請求。"
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=welcome_message)
)
# --- 核心 Webhook 處理函數 ---
# 這個函數將在 app.py 的 BackgroundTasks 中被調用
def line_handle(body: str, signature: Optional[str]):
"""
Line Bot Webhook 的主要處理入口。
Args:
body: Webhook 請求的原始 body 內容 (字串)。
signature: 來自 Line 伺服器的 X-Line-Signature 標頭值。
"""
if not line_handler:
print("錯誤:Line Bot Handler 未成功初始化。")
return
try:
# line_handler 會自動驗證簽章,並根據 body 內容分派事件到上面定義的處理器
line_handler.handle(body, signature)
except InvalidSignatureError:
# 如果簽章無效,會在這裡捕獲 (雖然 app.py 已經有捕獲,但這裡也做一次防禦性處理)
print("Invalid signature. 無效的簽章,可能請求不是來自 Line 官方伺服器。")
# 這裡不應 raise,因為它是在背景任務中
except Exception as e:
print(f"處理 Webhook 時發生未知錯誤: {e}")
print("Webhook 事件處理流程完成。") |