alanchen1115 commited on
Commit
de1e300
·
verified ·
1 Parent(s): 1c16ab4

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +86 -209
main.py CHANGED
@@ -1,219 +1,141 @@
1
- import os # 匯入 os 模組,用於讀取環境變數
2
- import io # 匯入 io 模組,用於處理二進位數據流
3
- from collections import defaultdict # 匯入 defaultdict,用於建立預設值的字典
4
- from fastapi.middleware.cors import CORSMiddleware # 匯入 FastAPI 的 CORS 中介軟體
5
- from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException # 匯入 FastAPI 相關元件
6
- from fastapi.staticfiles import StaticFiles # 匯入 StaticFiles,用於提供靜態檔案(如圖片)
7
- from google import genai # 匯入 Google GenAI 函式庫
8
- from google.genai import types # 匯入 GenAI 的類型定義
9
- from linebot import LineBotApi, WebhookHandler # 匯入 Line Bot SDK
10
- from linebot.exceptions import InvalidSignatureError # 匯入 Line 簽章無效的例外
11
- from linebot.models import ( # 匯入 Line Bot 的各種訊息模型
 
 
12
  MessageEvent,
13
  TextMessage,
14
  TextSendMessage,
15
  ImageSendMessage,
16
  ImageMessage,
17
  )
18
- import PIL.Image # 匯入 PIL (Pillow) 函式庫,用於處理圖片
19
- import uvicorn # 匯入 uvicorn,用於運行 FastAPI 應用程式
20
-
21
- # LangChain 相關匯入
22
- from langchain_core.prompts import ChatPromptTemplate # 匯入 LangChain 的聊天提示模板
23
- from langchain_core.tools import tool # 匯入 LangChain 的工具裝飾器
24
- from langchain_google_genai import ChatGoogleGenerativeAI # 匯入 LangChain 的 Google GenAI 聊天模型
25
- from langchain.agents import AgentExecutor, create_tool_calling_agent # 匯入 LangChain 的代理人執行器和建立工具
26
 
 
 
 
 
 
27
 
28
  # ==========================
29
  # 環境設定與工具函式
30
  # ==========================
31
-
32
- # 設置 Google AI API 金鑰 (從環境變數讀取)
33
- google_api = os.environ["GOOGLE_API_KEY"]
34
- # 初始化 Google GenAI 客戶端
35
  genai_client = genai.Client(api_key=google_api)
36
 
37
- # 設置 Line Bot 的 API 金鑰和秘密金鑰 (從環境變數讀取)
38
- line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
39
- line_handler = WebhookHandler(os.environ["CHANNEL_SECRET"])
40
 
41
- # 使用 defaultdict 模擬用戶訊息歷史存儲
42
- # 鍵(key)為 user_id,值(value)為一個儲存訊息的列表(list)
43
  user_message_history = defaultdict(list)
44
 
45
- # 建立 FastAPI 應用程式實例
46
  app = FastAPI()
47
- # 掛載 /static 路徑,使其指向 "static" 資料夾,用於存放和提供生成的圖片
 
 
 
48
  app.mount("/static", StaticFiles(directory="static"), name="static")
49
 
50
- # 設定 CORS (跨來源資源共用)
51
  app.add_middleware(
52
  CORSMiddleware,
53
- allow_origins=["*"], # 允許所有來源
54
- allow_credentials=True, # 允許憑證
55
- allow_methods=["*"], # 允許所有 HTTP 方法
56
- allow_headers=["*"], # 允許所有 HTTP 標頭
57
  )
58
 
59
  def get_image_url_from_line(message_id):
60
- """
61
- 從 Line 訊息 ID 獲取圖片內容並儲存到暫存檔案。
62
-
63
- Args:
64
- message_id: Line 訊息的 ID。
65
-
66
- Returns:
67
- 成功時回傳圖片儲存的本地路徑,失敗時回傳 None。
68
- """
69
  try:
70
- # 透過 Line Bot API 獲取訊息內容
71
  message_content = line_bot_api.get_message_content(message_id)
72
- # 定義暫檔案路徑
73
- file_path = f"/tmp/{message_id}.png"
74
- # 將圖片內容以二進位寫入模式寫入檔案
75
  with open(file_path, "wb") as f:
76
  for chunk in message_content.iter_content():
77
  f.write(chunk)
78
- print(f"✅ 圖片成功儲存到:{file_path}")
79
  return file_path
80
  except Exception as e:
81
  print(f"❌ 圖片取得失敗:{e}")
82
  return None
83
 
84
  def store_user_message(user_id, message_type, message_content):
85
- """
86
- 儲存用戶的訊息到 user_message_history 字典中。
87
-
88
- Args:
89
- user_id: 用戶的 ID。
90
- message_type: 訊息類型 (例如 "image" 或 "text")。
91
- message_content: 訊息內容 (例如圖片路徑或文字)。
92
- """
93
- user_message_history[user_id].append(
94
- {"type": message_type, "content": message_content}
95
- )
96
 
97
  def get_previous_message(user_id):
98
- """
99
- 獲取用戶的上一則訊息。
100
-
101
- Args:
102
- user_id: 用戶的 ID。
103
-
104
- Returns:
105
- 如果歷史紀錄存在,回傳上一則訊息的字典;否則回傳預設的文字訊息。
106
- """
107
  if user_id in user_message_history and len(user_message_history[user_id]) > 0:
108
- # 回傳最後一則訊息
109
  return user_message_history[user_id][-1]
110
- # 如果沒有歷史紀錄,回傳一個預設值
111
  return {"type": "text", "content": "No message!"}
112
 
113
-
114
  # ==========================
115
  # LangChain 工具定義
116
  # ==========================
117
 
118
  @tool
119
  def generate_and_upload_image(prompt: str) -> str:
120
- """
121
- 這個工具可以根據文字提示生成圖片,並將其上傳到伺服器。
122
-
123
- Args:
124
- prompt: 用於生成圖片的文字提示。
125
-
126
- Returns:
127
- 回傳生成圖片的 URL。
128
- """
129
  try:
130
- # 呼叫 Google GenAI 模型生成內容
131
  response = genai_client.models.generate_content(
132
- model="imagen-4.0-generate-001", # 指定圖片生成模型
133
- contents=prompt, # 傳入文字提示
134
- config=types.GenerateContentConfig(response_modalities=['Text', 'Image']) # 指定回應類型
135
  )
136
 
137
  image_binary = None
138
- # 遍歷回應的 parts,找到圖片的二進位數據
139
  for part in response.candidates[0].content.parts:
140
- if part.inline_data is not None:
141
  image_binary = part.inline_data.data
142
  break
143
 
144
  if image_binary:
145
- # 使用 PIL 將二進位數據轉換為圖片物件
146
  image = PIL.Image.open(io.BytesIO(image_binary))
147
- # 隨機生成一個檔案名以避免衝突,並儲存在 static 資料夾
148
- file_name = f"static/{os.urandom(16).hex()}.png"
149
  image.save(file_name, format="PNG")
150
 
151
- # 從環境變數獲取 Hugging Face Space 的 URL (或你的伺服器 URL)
152
- # 並組合完整的圖片 URL
153
- image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space
154
  return image_url
155
-
156
- return "圖片生成失敗。"
157
  except Exception as e:
158
- return f"圖片生成與上傳失敗: {e}"
159
 
160
  @tool
161
  def analyze_image_with_text(image_path: str, user_text: str) -> str:
162
- """
163
- 這個工具可以根據圖片和文字提示來回答問題 (多模態分析)。
164
-
165
- Args:
166
- image_path: 圖片在本地端儲存的路徑。
167
- user_text: 針對圖片提出的文字問題。
168
-
169
- Returns:
170
- 模型針對圖片和文字提示給出的回應。
171
- """
172
  try:
173
- # 檢查圖片路徑是否存在
174
  if not os.path.exists(image_path):
175
- return "圖片路徑無效,無法進行分析。"
176
 
177
- # 使用 PIL 開啟圖片
178
  img_user = PIL.Image.open(image_path)
179
- # 呼叫 Google GenAI 模型 (gemini-2.5-flash) 進行多模態分析
180
  response = genai_client.models.generate_content(
181
- model="gemini-2.5-flash",
182
- contents=[img_user, user_text] # 同時傳入圖片物件和文字
183
  )
184
- if (response.text != None):
185
- out = response.text
186
- else:
187
- out = "Gemini沒答案!請換個說法!"
188
  except Exception as e:
189
- # 處理
190
- out = f"Gemini執行出錯: {e}"
191
-
192
- return out
193
-
194
 
195
  # ==========================
196
  # LangChain 代理人設定
197
  # ==========================
198
-
199
- # 結合所有定義的工具
200
  tools = [generate_and_upload_image, analyze_image_with_text]
 
 
201
 
202
- # 建立 LLM 模型實例 (使用 LangChain 的 ChatGoogleGenerativeAI)
203
- llm = ChatGoogleGenerativeAI(google_api_key=google_api, model="gemini-2.5-flash", temperature=0.2)
204
-
205
- # 建立提示模板
206
- prompt_template = ChatPromptTemplate([
207
- ("system", "你是一個強大的圖像生成與問答助理,可以根據用戶的請求使用提供的工具。當你執行 generate_and_upload_image 工具\
208
- 成功後會獲得一個 URL,然後你回答的 output 要包含有這個 URL 的完整資訊。如果工具有產生錯誤訊息請解讀並回應。"), # 系統提示 (System Prompt)
209
- ("user", "{input}"), # 用戶輸入的佔位符
210
- ("placeholder", "{agent_scratchpad}"), # 代理人思考過程的佔位符
211
  ])
212
 
213
- # 建立工具調用代理人 (Tool Calling Agent)
214
  agent = create_tool_calling_agent(llm, tools, prompt_template)
215
- # 建立代理人執行器 (Agent Executor)
216
- agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) # verbose=True 會在終端印出代理人的思考過程
217
 
218
  # ==========================
219
  # FastAPI 路由
@@ -221,106 +143,61 @@ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) # verbose
221
 
222
  @app.get("/")
223
  def root():
224
- """
225
- 根路徑,用於基本測試。
226
- """
227
- return {"title": "Line Bot"}
228
 
229
  @app.post("/webhook")
230
- async def webhook(
231
- request: Request,
232
- background_tasks: BackgroundTasks,
233
- x_line_signature=Header(None), # 從標頭獲取 Line 的簽章
234
- ):
235
- """
236
- Line Bot 的 Webhook 路由。
237
- """
238
- # 獲取請求的原始內容 (body)
239
  body = await request.body()
240
  try:
241
- # 使用背景任務來處理 Webhook,這樣可以立即回傳 200 OK 給 Line 伺服器
242
- background_tasks.add_task(
243
- line_handler.handle, body.decode("utf-8"), x_line_signature
244
- )
245
  except InvalidSignatureError:
246
- # 如果簽章無效,拋出 400 錯誤
247
- raise HTTPException(status_code=400, detail="Invalid signature")
248
  return "ok"
249
 
250
- # 註冊訊息處理器,處理「圖片訊息」和「文字訊息」
251
  @line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
252
  def handle_message(event):
253
- """
254
- 主要的訊息處理邏輯。
255
- """
256
- # 獲取用戶 ID
257
  user_id = event.source.user_id
258
 
259
- # 情況一:處理圖片上傳
260
  if event.message.type == "image":
261
- # 獲取 Line 傳來的圖片,並儲存到本地
262
  image_path = get_image_url_from_line(event.message.id)
263
  if image_path:
264
- # 將圖片路徑儲存到用戶的訊息歷史中
265
  store_user_message(user_id, "image", image_path)
266
- # 回覆用戶,告知圖片已收到,並他輸入
267
- line_bot_api.reply_message(
268
- event.reply_token, TextSendMessage(text="圖片已接收成功囉,幫我輸入你想詢問的問題喔~")
269
- )
270
- else:
271
- line_bot_api.reply_message(
272
- event.reply_token, TextSendMessage(text="沒有接收到圖片~")
273
- )
274
 
275
- # 情況二:處理文字訊息
276
  elif event.message.type == "text":
277
- user_text = event.message.text # 獲取用戶傳來的文字
278
- # 獲取該用戶的「上一則」訊息
279
  previous_message = get_previous_message(user_id)
280
- print(f"上一則訊息: {previous_message}") # 在後台印出除錯訊息
281
-
282
- # 根據上一則訊息類型,動態組合給代理人的輸入
283
  if previous_message["type"] == "image":
284
- # 如果上一則是圖片,代表用戶現在的文字是「針對圖片的提問」
285
  image_path = previous_message["content"]
286
- agent_input = {
287
- "input": f"請根據這張圖片回答問題。圖片的路徑是 {image_path},我的問題是:{user_text}"
288
- }
289
- # 清除上一則圖片訊息,避免下一次文字訊息還被當作是圖片問答
290
- user_message_history[user_id].pop()
291
  else:
292
- # 如果上一則不是圖片 (或沒有上一���),代表這是一般的文字提問 (可能是要求生成圖片)
293
  agent_input = {"input": user_text}
294
-
295
  try:
296
- # 運行 LangChain 代理人
297
  response = agent_executor.invoke(agent_input)
298
- # 獲取代理人最終的輸出
299
  out = response["output"]
300
 
301
- # 檢查輸出中是否包含 'https' (判斷是否為生成的圖片 URL)
302
- if 'https' in out:
303
- # 解析 URL (這裡的解析方式比較簡易,可能需要更穩健的正規表達式)
304
- img_tmp = 'https'+out.split('https')[1]
305
- image_url = img_tmp.split('png')[0]+'png'
306
-
307
- # 使用 push_message 同時推送文字和圖片
308
- line_bot_api.push_message(
309
- event.source.user_id,
310
- [
311
- TextSendMessage(text="✨ 這是我為你生成的圖片喔~"),
312
- ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
313
- ]
314
- )
315
- else:
316
- # 如果輸出不是 URL,則直接回覆文字
317
- line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
318
- except Exception as e:
319
- # 處理代理人執行時的錯誤
320
- print(f"代理人執行出錯: {e}")
321
- out = f"代理人執行出錯!錯誤訊息:{e}"
322
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
 
 
 
323
 
324
  if __name__ == "__main__":
325
- # 程式執行的進入點,使用 uvicorn 啟動 FastAPI 伺服器
326
- uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
 
1
+ import os
2
+ import io
3
+ import PIL.Image
4
+ import uvicorn
5
+ from collections import defaultdict
6
+ from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+ from google import genai
10
+ from google.genai import types
11
+ from linebot import LineBotApi, WebhookHandler
12
+ from linebot.exceptions import InvalidSignatureError
13
+ from linebot.models import (
14
  MessageEvent,
15
  TextMessage,
16
  TextSendMessage,
17
  ImageSendMessage,
18
  ImageMessage,
19
  )
 
 
 
 
 
 
 
 
20
 
21
+ # --- 修正後的 LangChain 匯入路徑 ---
22
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
23
+ from langchain_core.tools import tool
24
+ from langchain_google_genai import ChatGoogleGenerativeAI
25
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
26
 
27
  # ==========================
28
  # 環境設定與工具函式
29
  # ==========================
30
+ google_api = os.environ.get("GOOGLE_API_KEY")
 
 
 
31
  genai_client = genai.Client(api_key=google_api)
32
 
33
+ line_bot_api = LineBotApi(os.environ.get("CHANNEL_ACCESS_TOKEN"))
34
+ line_handler = WebhookHandler(os.environ.get("CHANNEL_SECRET"))
 
35
 
 
 
36
  user_message_history = defaultdict(list)
37
 
 
38
  app = FastAPI()
39
+
40
+ # 確保 static 資料夾存在
41
+ if not os.path.exists("static"):
42
+ os.makedirs("static")
43
  app.mount("/static", StaticFiles(directory="static"), name="static")
44
 
 
45
  app.add_middleware(
46
  CORSMiddleware,
47
+ allow_origins=["*"],
48
+ allow_credentials=True,
49
+ allow_methods=["*"],
50
+ allow_headers=["*"],
51
  )
52
 
53
  def get_image_url_from_line(message_id):
 
 
 
 
 
 
 
 
 
54
  try:
 
55
  message_content = line_bot_api.get_message_content(message_id)
56
+ file_path = f"static/{message_id}.png" # 到 static 方便讀取
 
 
57
  with open(file_path, "wb") as f:
58
  for chunk in message_content.iter_content():
59
  f.write(chunk)
 
60
  return file_path
61
  except Exception as e:
62
  print(f"❌ 圖片取得失敗:{e}")
63
  return None
64
 
65
  def store_user_message(user_id, message_type, message_content):
66
+ user_message_history[user_id].append({"type": message_type, "content": message_content})
 
 
 
 
 
 
 
 
 
 
67
 
68
  def get_previous_message(user_id):
 
 
 
 
 
 
 
 
 
69
  if user_id in user_message_history and len(user_message_history[user_id]) > 0:
 
70
  return user_message_history[user_id][-1]
 
71
  return {"type": "text", "content": "No message!"}
72
 
 
73
  # ==========================
74
  # LangChain 工具定義
75
  # ==========================
76
 
77
  @tool
78
  def generate_and_upload_image(prompt: str) -> str:
79
+ """根據文字提示生成圖片。"""
 
 
 
 
 
 
 
 
80
  try:
81
+ # 修正模型名稱:目前最新穩定版為 imagen-3
82
  response = genai_client.models.generate_content(
83
+ model="imagen-3.0-generate-001",
84
+ contents=prompt,
85
+ config=types.GenerateContentConfig(response_modalities=['IMAGE'])
86
  )
87
 
88
  image_binary = None
 
89
  for part in response.candidates[0].content.parts:
90
+ if part.inline_data:
91
  image_binary = part.inline_data.data
92
  break
93
 
94
  if image_binary:
 
95
  image = PIL.Image.open(io.BytesIO(image_binary))
96
+ file_name = f"static/{os.urandom(8).hex()}.png"
 
97
  image.save(file_name, format="PNG")
98
 
99
+ # 修正 URL 拼接邏輯
100
+ base_url = os.getenv("HF_SPACE", "http://localhost:7860").rstrip("/")
101
+ image_url = f"{base_url}/{file_name}"
102
  return image_url
103
+ return "圖片生成失敗:模型未回傳數據。"
 
104
  except Exception as e:
105
+ return f"圖片生成失敗: {e}"
106
 
107
  @tool
108
  def analyze_image_with_text(image_path: str, user_text: str) -> str:
109
+ """根據圖片路徑和文字提問進行分析。"""
 
 
 
 
 
 
 
 
 
110
  try:
 
111
  if not os.path.exists(image_path):
112
+ return "錯誤:找不到該圖片檔案。"
113
 
 
114
  img_user = PIL.Image.open(image_path)
115
+ # 修正模型名稱:目前為 gemini-1.5-flash 或 gemini-2.0-flash-exp
116
  response = genai_client.models.generate_content(
117
+ model="gemini-3-flash-preview",
118
+ contents=[img_user, user_text]
119
  )
120
+ return response.text if response.text else "Gemini 沒答案!"
 
 
 
121
  except Exception as e:
122
+ return f"分析出: {e}"
 
 
 
 
123
 
124
  # ==========================
125
  # LangChain 代理人設定
126
  # ==========================
 
 
127
  tools = [generate_and_upload_image, analyze_image_with_text]
128
+ # 修正模型名稱
129
+ llm = ChatGoogleGenerativeAI(model="gemini-3-flash-preview", temperature=0.2)
130
 
131
+ prompt_template = ChatPromptTemplate.from_messages([
132
+ ("system", "你是一個強大的助理。如果生成了圖片,請直接給出 URL。"),
133
+ ("user", "{input}"),
134
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
 
 
 
 
 
135
  ])
136
 
 
137
  agent = create_tool_calling_agent(llm, tools, prompt_template)
138
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
 
139
 
140
  # ==========================
141
  # FastAPI 路由
 
143
 
144
  @app.get("/")
145
  def root():
146
+ return {"status": "running"}
 
 
 
147
 
148
  @app.post("/webhook")
149
+ async def webhook(request: Request, background_tasks: BackgroundTasks, x_line_signature=Header(None)):
 
 
 
 
 
 
 
 
150
  body = await request.body()
151
  try:
152
+ background_tasks.add_task(line_handler.handle, body.decode("utf-8"), x_line_signature)
 
 
 
153
  except InvalidSignatureError:
154
+ raise HTTPException(status_code=400)
 
155
  return "ok"
156
 
 
157
  @line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
158
  def handle_message(event):
 
 
 
 
159
  user_id = event.source.user_id
160
 
 
161
  if event.message.type == "image":
 
162
  image_path = get_image_url_from_line(event.message.id)
163
  if image_path:
 
164
  store_user_message(user_id, "image", image_path)
165
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text="收到圖片了!請問你想對這張圖做什麼分析?"))
 
 
 
 
 
 
 
166
 
 
167
  elif event.message.type == "text":
168
+ user_text = event.message.text
 
169
  previous_message = get_previous_message(user_id)
170
+
 
 
171
  if previous_message["type"] == "image":
 
172
  image_path = previous_message["content"]
173
+ agent_input = {"input": f"請分析這張圖片 {image_path},問題是:{user_text}"}
174
+ user_message_history[user_id].pop() # 用完即丟
 
 
 
175
  else:
 
176
  agent_input = {"input": user_text}
177
+
178
  try:
 
179
  response = agent_executor.invoke(agent_input)
 
180
  out = response["output"]
181
 
182
+ if "http" in out and (".png" in out or ".jpg" in out):
183
+ # 簡單提取第一個 URL
184
+ import re
185
+ urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', out)
186
+ if urls:
187
+ image_url = urls[0]
188
+ line_bot_api.reply_message(
189
+ event.reply_token,
190
+ [
191
+ TextSendMessage(text="這是為你生成的圖片:"),
192
+ ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
193
+ ]
194
+ )
195
+ return
196
+
 
 
 
 
 
 
197
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
198
+
199
+ except Exception as e:
200
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=f"系統忙碌中,請稍後再試。"))
201
 
202
  if __name__ == "__main__":
203
+ uvicorn.run(app, host="0.0.0.0", port=7860)