hazelhh commited on
Commit
6578989
·
verified ·
1 Parent(s): 6c8ae75

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +114 -94
main.py CHANGED
@@ -1,11 +1,19 @@
1
  import os
2
  import io
 
3
  from collections import defaultdict
4
- from fastapi.middleware.cors import CORSMiddleware
 
 
 
 
5
  from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
 
6
  from fastapi.staticfiles import StaticFiles
 
7
  from google import genai
8
  from google.genai import types
 
9
  from linebot import LineBotApi, WebhookHandler
10
  from linebot.exceptions import InvalidSignatureError
11
  from linebot.models import (
@@ -15,8 +23,6 @@ from linebot.models import (
15
  ImageSendMessage,
16
  ImageMessage,
17
  )
18
- import PIL.Image
19
- import uvicorn
20
 
21
  # LangChain 相關匯入
22
  from langchain_core.prompts import ChatPromptTemplate
@@ -26,22 +32,38 @@ from langchain.agents import AgentExecutor, create_tool_calling_agent
26
 
27
 
28
  # ==========================
29
- # 環境設定與工具函式
30
  # ==========================
31
 
32
- # 設置 Google AI API 金鑰
33
- google_api = os.environ["GOOGLE_API_KEY"]
34
- genai_client = genai.Client(api_key=google_api)
 
 
 
35
 
36
- # 設置 Line Bot 的 API 金鑰和秘密金鑰
37
- line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
38
- line_handler = WebhookHandler(os.environ["CHANNEL_SECRET"])
39
 
40
- # 使用字典模擬用戶訊息歷史存儲
41
- user_message_history = defaultdict(list)
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  # 建立 FastAPI 應用程式
44
  app = FastAPI()
 
 
 
45
  app.mount("/static", StaticFiles(directory="static"), name="static")
46
 
47
  # 設定 CORS
@@ -53,12 +75,13 @@ app.add_middleware(
53
  allow_headers=["*"],
54
  )
55
 
56
- def get_image_url_from_line(message_id):
57
  """
58
  從 Line 訊息 ID 獲取圖片內容並儲存到暫存檔案。
59
  """
60
  try:
61
  message_content = line_bot_api.get_message_content(message_id)
 
62
  file_path = f"/tmp/{message_id}.png"
63
  with open(file_path, "wb") as f:
64
  for chunk in message_content.iter_content():
@@ -66,25 +89,9 @@ def get_image_url_from_line(message_id):
66
  print(f"✅ 圖片成功儲存到:{file_path}")
67
  return file_path
68
  except Exception as e:
69
- print(f"❌ 圖片取得失敗:{e}")
70
  return None
71
 
72
- def store_user_message(user_id, message_type, message_content):
73
- """
74
- 儲存用戶的訊息。
75
- """
76
- user_message_history[user_id].append(
77
- {"type": message_type, "content": message_content}
78
- )
79
-
80
- def get_previous_message(user_id):
81
- """
82
- 獲取用戶的上一則訊息。
83
- """
84
- if user_id in user_message_history and len(user_message_history[user_id]) > 0:
85
- return user_message_history[user_id][-1]
86
- return {"type": "text", "content": "No message!"}
87
-
88
 
89
  # ==========================
90
  # LangChain 工具定義
@@ -99,33 +106,37 @@ def generate_and_upload_image(prompt: str) -> str:
99
  prompt: 用於生成圖片的文字提示。
100
 
101
  Returns:
102
- 回傳生成圖片的 URL。
103
  """
104
  try:
105
- response = genai_client.models.generate_content(
106
- model="gemini-2.0-flash-preview-image-generation",
 
107
  contents=prompt,
108
- config=types.GenerateContentConfig(response_modalities=['Text', 'Image'])
109
  )
110
 
111
  image_binary = None
112
  for part in response.candidates[0].content.parts:
113
- if part.inline_data is not None:
114
  image_binary = part.inline_data.data
115
  break
116
 
117
  if image_binary:
118
  image = PIL.Image.open(io.BytesIO(image_binary))
119
  # 隨機生成一個檔案名以避免衝突
120
- file_name = f"static/{os.urandom(16).hex()}.png"
121
- image.save(file_name, format="PNG")
122
-
123
- image_url = os.path.join(os.getenv("HF_SPACE"), file_name)
124
- return image_url
125
 
126
- return "圖片生成失敗。"
 
 
 
 
 
127
  except Exception as e:
128
- return f"圖片生成與上傳失敗: {e}"
129
 
130
  @tool
131
  def analyze_image_with_text(image_path: str, user_text: str) -> str:
@@ -144,19 +155,15 @@ def analyze_image_with_text(image_path: str, user_text: str) -> str:
144
  return "圖片路徑無效,無法進行分析。"
145
 
146
  img_user = PIL.Image.open(image_path)
147
- response = genai_client.models.generate_content(
148
- model="gemini-2.5-flash",
149
- contents=[img_user, user_text]
150
- )
151
- if (response.text != None):
152
- out = response.text
153
  else:
154
- out = "Gemini沒答案!請換個說法!"
155
  except Exception as e:
156
- # 處理錯誤
157
- out = f"Gemini執行出錯: {e}"
158
-
159
- return out
160
 
161
 
162
  # ==========================
@@ -167,18 +174,22 @@ def analyze_image_with_text(image_path: str, user_text: str) -> str:
167
  tools = [generate_and_upload_image, analyze_image_with_text]
168
 
169
  # 建立 LLM 模型實例
170
- llm = ChatGoogleGenerativeAI(google_api_key=google_api, model="gemini-2.5-flash", temperature=0.2)
171
 
172
  # 建立提示模板
173
- prompt_template = ChatPromptTemplate([
174
- ("system", "你是一個強大的圖像生成與問答助理,可以根據用戶的請求使用提供的工具。當你執行 generate_and_upload_image 工具\
175
- 成功後會獲得一個 URL,然後你回答的 output 要包含有這個 URL 的完整資訊。如果工具有產生錯誤訊息請解讀並回應。"),
 
 
 
176
  ("user", "{input}"),
177
  ("placeholder", "{agent_scratchpad}"),
178
  ])
179
 
 
180
  # 建立代理人
181
- agent = create_tool_calling_agent(llm, tools, prompt_template)
182
  agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
183
 
184
  # ==========================
@@ -187,16 +198,17 @@ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
187
 
188
  @app.get("/")
189
  def root():
190
- return {"title": "Line Bot"}
191
 
192
  @app.post("/webhook")
193
  async def webhook(
194
  request: Request,
195
  background_tasks: BackgroundTasks,
196
- x_line_signature=Header(None),
197
  ):
198
  body = await request.body()
199
  try:
 
200
  background_tasks.add_task(
201
  line_handler.handle, body.decode("utf-8"), x_line_signature
202
  )
@@ -207,56 +219,64 @@ async def webhook(
207
  @line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
208
  def handle_message(event):
209
  user_id = event.source.user_id
210
-
211
- # 處理圖片上傳
212
- if event.message.type == "image":
213
- image_path = get_image_url_from_line(event.message.id)
214
  if image_path:
215
- store_user_message(user_id, "image", image_path)
216
- line_bot_api.reply_message(
217
- event.reply_token, TextSendMessage(text="圖片已接收成功囉,幫我輸入你想詢問的問題喔~")
218
- )
 
 
 
 
 
 
 
 
 
 
 
 
219
  else:
220
  line_bot_api.reply_message(
221
- event.reply_token, TextSendMessage(text="沒有接收到圖片~")
222
  )
223
-
224
- # 處理文字訊息
225
- elif event.message.type == "text":
226
  user_text = event.message.text
227
- previous_message = get_previous_message(user_id)
228
- print(previous_message)
229
-
230
- # 根據上一則訊息類型,動態傳遞給代理人
231
- if previous_message["type"] == "image":
232
- image_path = previous_message["content"]
233
- agent_input = {
234
- "input": f"請根據這張圖片回答問題。圖片的路徑是 {image_path},我的問題是:{user_text}"
235
- }
236
- # 清除上一則圖片訊息,避免重複觸發
237
- user_message_history[user_id].pop()
238
- else:
239
- agent_input = {"input": user_text}
240
  try:
241
  # 運行代理人
242
  response = agent_executor.invoke(agent_input)
243
- out = response["output"]
244
- if 'https' in out:
245
- img_tmp = 'https'+out.split('https')[1]
246
- image_url = img_tmp.split('png')[0]+'png'
 
 
 
 
247
  line_bot_api.push_message(
248
  event.source.user_id,
249
  [
250
- TextSendMessage(text="✨ 這是我為你生成的圖片喔~"),
251
  ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
252
  ]
253
  )
254
  else:
255
- line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
 
256
  except Exception as e:
257
  print(f"代理人執行出錯: {e}")
258
- out = f"代理人執行出錯!錯誤訊息:{e}"
259
- line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
260
 
261
  if __name__ == "__main__":
262
- uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
 
 
 
1
  import os
2
  import io
3
+ import re
4
  from collections import defaultdict
5
+ import PIL.Image
6
+ import uvicorn
7
+ import requests
8
+ from pydantic_settings import BaseSettings
9
+
10
  from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.staticfiles import StaticFiles
13
+
14
  from google import genai
15
  from google.genai import types
16
+
17
  from linebot import LineBotApi, WebhookHandler
18
  from linebot.exceptions import InvalidSignatureError
19
  from linebot.models import (
 
23
  ImageSendMessage,
24
  ImageMessage,
25
  )
 
 
26
 
27
  # LangChain 相關匯入
28
  from langchain_core.prompts import ChatPromptTemplate
 
32
 
33
 
34
  # ==========================
35
+ # 環境變數與設定管理
36
  # ==========================
37
 
38
+ class Settings(BaseSettings):
39
+ """使用 Pydantic 管理環境變數"""
40
+ google_api_key: str
41
+ channel_access_token: str
42
+ channel_secret: str
43
+ base_url: str # 應用程式的公開網址,例如 ngrok 或 Hugging Face Space 的 URL
44
 
45
+ class Config:
46
+ env_file = ".env"
 
47
 
48
+ # 載入設定
49
+ settings = Settings()
50
+
51
+ # ==========================
52
+ # API 客戶端與工具函式初始化
53
+ # ==========================
54
+
55
+ # 設置 Google AI API 金鑰
56
+ genai.configure(api_key=settings.google_api_key)
57
+
58
+ # 設置 Line Bot API
59
+ line_bot_api = LineBotApi(settings.channel_access_token)
60
+ line_handler = WebhookHandler(settings.channel_secret)
61
 
62
  # 建立 FastAPI 應用程式
63
  app = FastAPI()
64
+
65
+ # 確保靜態檔案目錄存在
66
+ os.makedirs("static", exist_ok=True)
67
  app.mount("/static", StaticFiles(directory="static"), name="static")
68
 
69
  # 設定 CORS
 
75
  allow_headers=["*"],
76
  )
77
 
78
+ def get_image_from_line(message_id: str) -> str | None:
79
  """
80
  從 Line 訊息 ID 獲取圖片內容並儲存到暫存檔案。
81
  """
82
  try:
83
  message_content = line_bot_api.get_message_content(message_id)
84
+ # 使用 /tmp 目錄儲存暫存檔案,適合多數雲端環境
85
  file_path = f"/tmp/{message_id}.png"
86
  with open(file_path, "wb") as f:
87
  for chunk in message_content.iter_content():
 
89
  print(f"✅ 圖片成功儲存到:{file_path}")
90
  return file_path
91
  except Exception as e:
92
+ print(f"❌ 從 Line 取得圖片失敗:{e}")
93
  return None
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  # ==========================
97
  # LangChain 工具定義
 
106
  prompt: 用於生成圖片的文字提示。
107
 
108
  Returns:
109
+ 回傳生成圖片的公開 URL。
110
  """
111
  try:
112
+ # 使用 gemini-2.5-flash-image-preview 模型進行圖片生成
113
+ model = genai.GenerativeModel('gemini-2.5-flash-image-preview')
114
+ response = model.generate_content(
115
  contents=prompt,
116
+ generation_config=types.GenerationConfig(response_modalities=['IMAGE'])
117
  )
118
 
119
  image_binary = None
120
  for part in response.candidates[0].content.parts:
121
+ if part.inline_data and part.inline_data.data:
122
  image_binary = part.inline_data.data
123
  break
124
 
125
  if image_binary:
126
  image = PIL.Image.open(io.BytesIO(image_binary))
127
  # 隨機生成一個檔案名以避免衝突
128
+ file_name = f"{os.urandom(16).hex()}.png"
129
+ file_path = os.path.join("static", file_name)
130
+ image.save(file_path, format="PNG")
 
 
131
 
132
+ # 使用環境變數中的 BASE_URL 來建立完整的公開網址
133
+ image_url = f"{settings.base_url}/{file_path}"
134
+ print(f"✅ 圖片生成成功,URL: {image_url}")
135
+ return f"圖片生成成功,請查看此 URL: {image_url}"
136
+
137
+ return "圖片生成失敗,未收到有效的圖片資料。"
138
  except Exception as e:
139
+ return f"圖片生成與上傳過程中發生錯誤: {e}"
140
 
141
  @tool
142
  def analyze_image_with_text(image_path: str, user_text: str) -> str:
 
155
  return "圖片路徑無效,無法進行分析。"
156
 
157
  img_user = PIL.Image.open(image_path)
158
+ model = genai.GenerativeModel("gemini-1.5-flash") # 使用 gemini 1.5 flash 模型
159
+ response = model.generate_content([img_user, user_text])
160
+
161
+ if response.text:
162
+ return response.text
 
163
  else:
164
+ return "Gemini 沒有給出答案,請嘗試換個方式提問!"
165
  except Exception as e:
166
+ return f"圖片分析過程中發生錯誤: {e}"
 
 
 
167
 
168
 
169
  # ==========================
 
174
  tools = [generate_and_upload_image, analyze_image_with_text]
175
 
176
  # 建立 LLM 模型實例
177
+ llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.3)
178
 
179
  # 建立提示模板
180
+ prompt = ChatPromptTemplate.from_messages([
181
+ ("system", """你是一個強大的圖像生成與問答助理。
182
+ - 當用戶的指令明顯是要生成圖片時 (例如:'畫一張...'、'生成...'、'幫我做一張圖...'),請使用 `generate_and_upload_image` 工具。
183
+ - 當用戶的指令包含圖片路徑 (image_path) 和問題時,請使用 `analyze_image_with_text` 工具。
184
+ - 成功執行 `generate_and_upload_image` 工具後,你會獲得一個 URL,你的最終回答必須包含這個 URL。
185
+ - 如果工具執行過程中產生任何錯誤訊息,請以友善的方式解讀並回應給用戶。"""),
186
  ("user", "{input}"),
187
  ("placeholder", "{agent_scratchpad}"),
188
  ])
189
 
190
+
191
  # 建立代理人
192
+ agent = create_tool_calling_agent(llm, tools, prompt)
193
  agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
194
 
195
  # ==========================
 
198
 
199
  @app.get("/")
200
  def root():
201
+ return {"message": "Line Bot is running!"}
202
 
203
  @app.post("/webhook")
204
  async def webhook(
205
  request: Request,
206
  background_tasks: BackgroundTasks,
207
+ x_line_signature: str = Header(None),
208
  ):
209
  body = await request.body()
210
  try:
211
+ # 使用背景任務處理 Webhook,避免 Line Server 超時
212
  background_tasks.add_task(
213
  line_handler.handle, body.decode("utf-8"), x_line_signature
214
  )
 
219
  @line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
220
  def handle_message(event):
221
  user_id = event.source.user_id
222
+
223
+ # 處理圖片上傳:使用者上傳圖片後直接進行分析
224
+ if isinstance(event.message, ImageMessage):
225
+ image_path = get_image_from_line(event.message.id)
226
  if image_path:
227
+ try:
228
+ # 組合給代理人的輸入,使用一個通用的問題來分析圖片
229
+ agent_input = {
230
+ "input": f"這是一張使用者上傳的圖片,請詳細描述你看到了什麼。圖片的路徑是 '{image_path}'。"
231
+ }
232
+ # 運行代理人
233
+ response = agent_executor.invoke(agent_input)
234
+ output_text = response["output"]
235
+
236
+ # 回覆分析結果
237
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=output_text))
238
+
239
+ except Exception as e:
240
+ print(f"代理人執行出錯: {e}")
241
+ error_message = f"圖片分析時發生錯誤,請稍後再試。\n錯誤訊息:{e}"
242
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=error_message))
243
  else:
244
  line_bot_api.reply_message(
245
+ event.reply_token, TextSendMessage(text="❌ 圖片接收失敗,請再試一次。")
246
  )
247
+
248
+ # 處理文字訊息:主要用於生成圖片或一般問答
249
+ elif isinstance(event.message, TextMessage):
250
  user_text = event.message.text
251
+ agent_input = {"input": user_text}
252
+
 
 
 
 
 
 
 
 
 
 
 
253
  try:
254
  # 運行代理人
255
  response = agent_executor.invoke(agent_input)
256
+ output_text = response["output"]
257
+
258
+ # 使用正規表示法尋找 URL,更穩定
259
+ image_url_match = re.search(r'https?://\S+\.(?:png|jpg|jpeg|gif)', output_text, re.IGNORECASE)
260
+
261
+ if image_url_match:
262
+ image_url = image_url_match.group(0)
263
+ # 推送訊息,包含生成的圖片
264
  line_bot_api.push_message(
265
  event.source.user_id,
266
  [
267
+ TextSendMessage(text="✨ 這是我為您生成的圖片喔~"),
268
  ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
269
  ]
270
  )
271
  else:
272
+ # 若無圖片 URL,則直接回覆文字
273
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=output_text))
274
  except Exception as e:
275
  print(f"代理人執行出錯: {e}")
276
+ error_message = f"代理人執行時發生錯誤,請稍後再試。\n錯誤訊息:{e}"
277
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text=error_message))
278
 
279
  if __name__ == "__main__":
280
+ # 使用 settings 物件中的設定
281
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
282
+