JasonFinley0821 commited on
Commit
dd70e33
·
1 Parent(s): 5e0a683

feat : add test line and ai agent

Browse files
Files changed (3) hide show
  1. app.py +42 -12
  2. requirements.txt +6 -1
  3. services/agents.py +201 -0
app.py CHANGED
@@ -1,9 +1,19 @@
1
- from fastapi import FastAPI, Request, Response, Form
2
  from fastapi.responses import JSONResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware # 匯入 FastAPI 的 CORS 中介軟體
5
  import requests
 
6
  from typing import Annotated # 推薦用於 Pydantic v2+
 
 
 
 
 
 
 
 
 
7
 
8
  from services.deblur import deblur_image_tiled
9
 
@@ -12,6 +22,7 @@ import io
12
  import os
13
  from datetime import datetime
14
  import uvicorn
 
15
 
16
  STATIC_DIR = "static"
17
 
@@ -21,6 +32,16 @@ os.environ["TRANSFORMERS_CACHE"] = "./.cache"
21
  os.makedirs("./.cache", exist_ok=True)
22
  os.makedirs(STATIC_DIR, exist_ok=True)
23
 
 
 
 
 
 
 
 
 
 
 
24
  # =====================
25
  # 初始化 FastAPI
26
  # =====================
@@ -43,17 +64,26 @@ app.add_middleware(
43
  def root():
44
  return {"message": "DeblurGANv2 API ready!"}
45
 
46
- @app.get("/greetjson")
47
- def greet_json(request: Request, response: Response):
48
- # 可以使用 request 讀取資訊
49
- client_host = request.client.host
50
-
51
- # 設定 response 狀態碼或 headers
52
- response.status_code = 200
53
- response.headers["X-Custom-Header"] = "HelloHeader"
54
-
55
- # 回傳 JSON
56
- return JSONResponse(content={"message": "Hello World", "client": client_host})
 
 
 
 
 
 
 
 
 
57
 
58
  @app.post("/predict")
59
  async def predict(
 
1
+ from fastapi import FastAPI, Request, Response, Form, Header, HTTPException, BackgroundTasks
2
  from fastapi.responses import JSONResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware # 匯入 FastAPI 的 CORS 中介軟體
5
  import requests
6
+ from collections import defaultdict # 匯入 defaultdict,用於建立預設值的字典
7
  from typing import Annotated # 推薦用於 Pydantic v2+
8
+ from linebot import LineBotApi, WebhookHandler # 匯入 Line Bot SDK
9
+ from linebot.exceptions import InvalidSignatureError # 匯入 Line 簽章無效的例外
10
+ from linebot.models import ( # 匯入 Line Bot 的各種訊息模型
11
+ MessageEvent,
12
+ TextMessage,
13
+ TextSendMessage,
14
+ ImageSendMessage,
15
+ ImageMessage,
16
+ )
17
 
18
  from services.deblur import deblur_image_tiled
19
 
 
22
  import os
23
  from datetime import datetime
24
  import uvicorn
25
+ from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案
26
 
27
  STATIC_DIR = "static"
28
 
 
32
  os.makedirs("./.cache", exist_ok=True)
33
  os.makedirs(STATIC_DIR, exist_ok=True)
34
 
35
+ load_dotenv()
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
  # =====================
46
  # 初始化 FastAPI
47
  # =====================
 
64
  def root():
65
  return {"message": "DeblurGANv2 API ready!"}
66
 
67
+ @app.post("/webhook")
68
+ async def webhook(
69
+ request: Request,
70
+ background_tasks: BackgroundTasks,
71
+ x_line_signature=Header(None), # 從標頭獲取 Line 的簽章
72
+ ):
73
+ """
74
+ Line Bot 的 Webhook 路由。
75
+ """
76
+ # 獲取請求的原始內容 (body)
77
+ body = await request.body()
78
+ try:
79
+ # 使用背景任務來處理 Webhook,這樣可以立即回傳 200 OK 給 Line 伺服器
80
+ background_tasks.add_task(
81
+ line_handler.handle, body.decode("utf-8"), x_line_signature
82
+ )
83
+ except InvalidSignatureError:
84
+ # 如果簽章無效,拋出 400 錯誤
85
+ raise HTTPException(status_code=400, detail="Invalid signature")
86
+ return "ok"
87
 
88
  @app.post("/predict")
89
  async def predict(
requirements.txt CHANGED
@@ -10,4 +10,9 @@ pytorch-msssim
10
  opencv-python
11
  tqdm
12
  torchsummary
13
- requests
 
 
 
 
 
 
10
  opencv-python
11
  tqdm
12
  torchsummary
13
+ requests
14
+ google-genai
15
+ langchain
16
+ langchain-google-genai
17
+ python-dotenv
18
+ line-bot-sdk
services/agents.py CHANGED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os # 匯入 os 模組以處理環境變數和檔案路徑
2
+ import io # 匯入 io 模組以處理二進位資料流
3
+ import PIL.Image # 匯入 PIL 的 Image 模組以處理圖片
4
+ import requests # 匯入 requests 模組以進行 HTTP 請求
5
+ from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案
6
+ import json # 匯入 json 庫用於序列化
7
+
8
+ # LangChain 相關匯入
9
+ from langchain_core.prompts import ChatPromptTemplate # 匯入 LangChain 的聊天提示模板
10
+ from langchain_core.tools import tool # 匯入 LangChain 的工具裝飾器
11
+ from langchain_google_genai import ChatGoogleGenerativeAI # 匯入 LangChain 的 Google GenAI 聊天模型
12
+ from langchain.agents import AgentExecutor, create_tool_calling_agent # 匯入 LangChain 的代理人執行器和建立工具
13
+ from google import genai # 匯入 Google GenAI 函式庫
14
+ from google.genai import types # 匯入 GenAI 的類型定義
15
+ from services.deblur import deblur_image_tiled # 從本地服務匯入去模糊函式
16
+
17
+ # ==========================
18
+ # 環境設定與工具函式
19
+ # ==========================
20
+ load_dotenv()
21
+
22
+ # 設置 Google AI API 金鑰 (從環境變數讀取)
23
+ google_api = os.environ["GOOGLE_API_KEY"]
24
+
25
+ # 初始化 Google GenAI 客戶端
26
+ genai_client = genai.Client(api_key=google_api)
27
+
28
+ # ==========================
29
+ # LangChain 工具定義
30
+ # ==========================
31
+
32
+ @tool
33
+ def generate_and_upload_image(prompt: str) -> str:
34
+ """
35
+ 這個工具可以根據文字提示生成圖片,並將其上傳到伺服器。
36
+
37
+ Args:
38
+ prompt: 用於生成圖片的文字提示。
39
+
40
+ Returns:
41
+ 回傳生成圖片的 URL。
42
+ """
43
+ try:
44
+ # 呼叫 Google GenAI 模型生成內容
45
+ response = genai_client.models.generate_content(
46
+ model="gemini-2.0-flash-preview-image-generation",#"gemini-2.5-flash-image", # 指定圖片生成模型
47
+ contents=prompt, # 傳入文字提示
48
+ config=types.GenerateContentConfig(response_modalities=['Text', 'Image']) # 指定回應類型
49
+ )
50
+
51
+ image_binary = None
52
+ # 遍歷回應的 parts,找到圖片的二進位數據
53
+ for part in response.candidates[0].content.parts:
54
+ if part.inline_data is not None:
55
+ image_binary = part.inline_data.data
56
+ break
57
+
58
+ if image_binary:
59
+ # 使用 PIL 將二進位數據轉換為圖片物件
60
+ image = PIL.Image.open(io.BytesIO(image_binary))
61
+ # 隨機生成一個檔案名以避免衝突,並儲存在 static 資料夾
62
+ file_name = f"static/{os.urandom(16).hex()}.jpg"
63
+ image.save(file_name, format="JPEG")
64
+
65
+ # 從環境變數獲取 Hugging Face Space 的 URL (或你的伺服器 URL)
66
+ # 並組合完整的圖片 URL
67
+ image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space
68
+ return image_url
69
+
70
+ return "圖片生成失敗。"
71
+ except Exception as e:
72
+ return f"圖片生成與上傳失敗: {e}"
73
+
74
+ @tool
75
+ def analyze_image_with_text(image_path: str, user_text: str) -> str:
76
+ """
77
+ 這個工具可以根據圖片和文字提示來回答問題 (多模態分析)。
78
+
79
+ Args:
80
+ image_path: 圖片在本地端儲存的路徑。
81
+ user_text: 針對圖片提出的文字問題。
82
+
83
+ Returns:
84
+ 模型針對圖片和文字提示給出的回應。
85
+ """
86
+ try:
87
+ # 檢查圖片路徑是否存在
88
+ if not os.path.exists(image_path):
89
+ return "圖片路徑無效,無法進行分析。"
90
+
91
+ # 使用 PIL 開啟圖片
92
+ img_user = PIL.Image.open(image_path)
93
+ # 呼叫 Google GenAI 模型 (gemini-2.5-flash) 進行多模態分析
94
+ response = genai_client.models.generate_content(
95
+ model="gemini-2.5-flash",
96
+ contents=[img_user, user_text] # 同時傳入圖片物件和文字
97
+ )
98
+ if (response.text != None):
99
+ out = response.text
100
+ else:
101
+ out = "Gemini沒答案!請換個說法!"
102
+ except Exception as e:
103
+ # 處理錯誤
104
+ out = f"Gemini執行出錯: {e}"
105
+
106
+ return out
107
+
108
+ @tool
109
+ def deblur_image_from_url(
110
+ file_url: str,
111
+ user_text: str
112
+ ) -> str:
113
+ """
114
+ 這個工具可以從提供的 URL 下載圖片,使用分塊處理 (Tiled Processing)
115
+ 對其進行去模糊 (deblur) 處理,將結果儲存在伺服器的 static/ 目錄,
116
+ 並回傳該去模糊後圖片的 **絕對 URL 路徑**和 基於 user_text 的額外文字結果。
117
+
118
+ Args:
119
+ file_url: 圖片在網路上可存取的 HTTP(s) 下載網址。
120
+ user_text: 針對圖片提出的處理方式或需求的文字描述。
121
+
122
+ Returns:
123
+ 一個 JSON 格式的字串,包含去模糊後的圖片 URL (image_url)
124
+ 和根據 user_text 提供的處理結��描述 (text_result)。
125
+ """
126
+ try:
127
+ tile_size = 512
128
+ overlap = 32
129
+ # 1. 下載圖片
130
+ print(f"Agent 正在下載圖片: {file_url}")
131
+ resp = requests.get(file_url, timeout=15)
132
+ resp.raise_for_status()
133
+
134
+ # 內容轉換為 PIL Image
135
+ img_input = PIL.Image.open(io.BytesIO(resp.content)).convert("RGB")
136
+
137
+ # 2. 執行去模糊處理
138
+ img_deblurred = deblur_image_tiled(
139
+ img_input,
140
+ tile_size=tile_size,
141
+ overlap=overlap
142
+ )
143
+
144
+ # 建立一個唯一的檔案名
145
+ ext = img_input.format if img_input.format else 'JPEG'
146
+ file_name = f"static/{os.urandom(16).hex()}.jpg"
147
+
148
+ img_deblurred.save(file_name, format=ext)
149
+
150
+ # 4. 建構絕對 URL 路徑 (供客戶端存取)
151
+ # 這裡假設 BASE_URL 已經設定好,並與 FastAPI 的 static mount 匹配
152
+ image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space
153
+
154
+ analysis_result = f"圖片已成功去模糊。用戶請求的描述為:'{user_text}'。模型已根據此要求調整參數進行處理。"
155
+
156
+ # 5. 返回 JSON 字串
157
+ return json.dumps({
158
+ "image_url": image_url,
159
+ "text_result": analysis_result
160
+ })
161
+
162
+ except requests.exceptions.RequestException as e:
163
+ return json.dumps({
164
+ "error": f"下載圖片失敗或 URL 無效: {e}"
165
+ })
166
+ except Exception as e:
167
+ return json.dumps({
168
+ "error": f"圖片處理失敗。錯誤訊息: {e}"
169
+ })
170
+
171
+ # ==========================
172
+ # LangChain 代理人設定
173
+ # ==========================
174
+
175
+ # 結合所有定義的工具
176
+ tools = [generate_and_upload_image, analyze_image_with_text, deblur_image_from_url]
177
+
178
+ # 建立 LLM 模型實例 (使用 LangChain 的 ChatGoogleGenerativeAI)
179
+ llm = ChatGoogleGenerativeAI(google_api_key=google_api, model="gemini-2.5-flash", temperature=0.2)
180
+
181
+ # 建立提示模板
182
+ prompt_template = ChatPromptTemplate([
183
+ ("system",
184
+ """
185
+ 你是一個強大的圖像生成、圖像去模糊與問答助理,可以根據用戶的請求使用提供的工具。
186
+ ### 核心輸出規範
187
+ * **結果呈現**:當你執行以下任一圖像處理工具成功後,你最終的回答 output **必須包含該 URL 的完整資訊**:
188
+ * `generate_and_upload_image`
189
+ * `deblur_image_from_url` (或其他任何產生圖像輸出的工具)
190
+
191
+ * **錯誤處理**:如果工具有產生錯誤訊息,請解讀錯誤並以自然語言回應給用戶。
192
+ """
193
+ ), # 系統提示 (System Prompt)
194
+ ("user", "{input}"), # 用戶輸入的佔位符
195
+ ("placeholder", "{agent_scratchpad}"), # 代理人思考過程的佔位符
196
+ ])
197
+
198
+ # 建立工具調用代理人 (Tool Calling Agent)
199
+ agent = create_tool_calling_agent(llm, tools, prompt_template)
200
+ # 建立代理人執行器 (Agent Executor)
201
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) # verbose=True 會在終端印出代理人的思考過程