File size: 12,785 Bytes
dd70e33
 
 
 
 
 
b82a60c
dd70e33
 
c278602
 
dddc990
c278602
 
 
dd70e33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82a60c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd70e33
 
 
 
 
 
 
 
 
 
 
 
 
f4d5e8a
dd70e33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4d5e8a
 
 
 
 
dd70e33
f4d5e8a
 
 
 
dd70e33
f4d5e8a
 
 
 
dd70e33
 
 
 
 
 
 
 
 
 
 
f4d5e8a
dd70e33
 
 
b82a60c
 
 
 
dd70e33
 
b82a60c
 
dd70e33
 
 
 
 
 
 
 
 
f4d5e8a
 
 
 
 
 
dd70e33
 
 
f4d5e8a
 
 
 
dd70e33
 
 
 
 
 
 
 
1fa1fdf
 
 
 
 
dd70e33
1fa1fdf
 
 
 
 
 
 
 
 
 
 
 
dd70e33
 
 
 
 
 
b82a60c
dd70e33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2188936
 
 
 
 
 
 
 
6a8fc80
 
2188936
6a8fc80
 
 
 
 
 
 
 
 
 
 
2188936
 
 
dd70e33
 
 
 
 
 
c278602
2188936
 
 
 
c278602
dd70e33
 
c278602
 
 
 
 
dd70e33
c278602
81736e4
6a8fc80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd70e33
c278602
 
 
 
 
81736e4
c278602
 
dddc990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31481c9
 
 
 
dddc990
 
31481c9
 
 
 
 
 
 
 
 
dddc990
 
 
c278602
 
832a92e
e9b2abf
81736e4
e9b2abf
c820ac8
604ac28
 
 
31481c9
604ac28
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
import os  # 匯入 os 模組以處理環境變數和檔案路徑
import io  # 匯入 io 模組以處理二進位資料流
import PIL.Image  # 匯入 PIL 的 Image 模組以處理圖片
import requests  # 匯入 requests 模組以進行 HTTP 請求
from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案
import json # 匯入 json 庫用於序列化
from urllib.parse import urlparse

# LangChain 相關匯入
from langchain.agents import create_agent
from langchain.tools import tool
from langchain.messages import AIMessage, HumanMessage, ToolMessage

from langchain_google_genai import ChatGoogleGenerativeAI

from google import genai  # 匯入 Google GenAI 函式庫
from google.genai import types  # 匯入 GenAI 的類型定義
from services.deblur import deblur_image_tiled  # 從本地服務匯入去模糊函式

# ==========================
#  環境設定與工具函式
# ==========================
load_dotenv()

# 設置 Google AI API 金鑰 (從環境變數讀取)
google_api = os.environ["GOOGLE_API_KEY"]

# 初始化 Google GenAI 客戶端
genai_client = genai.Client(api_key=google_api)

# ==========================
#  some 工具定義
# ==========================
def load_image(file_url: str) ->  PIL.Image.Image:
    """
    支援本地檔案或 HTTP(S) URL 讀取圖片
    """
    parsed = urlparse(file_url)
    
    if parsed.scheme in ("http", "https"):
        # 網路圖片
        try:
            print(f"Agent 正在下載圖片: {file_url}")
            resp = requests.get(file_url, timeout=15)
            resp.raise_for_status()
            img =  PIL.Image.open(io.BytesIO(resp.content)).convert("RGB")
            return img
        except Exception as e:
            raise ValueError(f"下載圖片失敗: {e}")
    else:
        # 本地檔案
        if not os.path.exists(file_url):
            raise ValueError("圖片路徑無效,無法進行分析。")
        try:
            img =  PIL.Image.open(file_url).convert("RGB")
            return img
        except Exception as e:
            raise ValueError(f"開啟本地圖片失敗: {e}")


# ==========================
#  LangChain 工具定義
# ==========================

@tool
def generate_and_upload_image(prompt: str) -> str:
    """
    這個工具可以根據文字提示生成圖片,並將其上傳到伺服器。
    
    Args:
        prompt: 用於生成圖片的文字提示。
        
    Returns:
        一個 JSON 格式的字串,包含圖片 URL 和描述,或錯誤訊息。
    """
    try:
        # 呼叫 Google GenAI 模型生成內容
        response = genai_client.models.generate_content(
            model="gemini-2.0-flash-preview-image-generation",#"gemini-2.5-flash-image", # 指定圖片生成模型
            contents=prompt, # 傳入文字提示
            config=types.GenerateContentConfig(response_modalities=['Text', 'Image']) # 指定回應類型
        )
        
        image_binary = None
        # 遍歷回應的 parts,找到圖片的二進位數據
        for part in response.candidates[0].content.parts:
            if part.inline_data is not None:
                image_binary = part.inline_data.data
                break
        
        if image_binary:
            # 使用 PIL 將二進位數據轉換為圖片物件
            image = PIL.Image.open(io.BytesIO(image_binary))
            # 隨機生成一個檔案名以避免衝突,並儲存在 static 資料夾
            file_name = f"static/{os.urandom(16).hex()}.jpg"
            image.save(file_name, format="JPEG")
            
            # 從環境變數獲取 Hugging Face Space 的 URL (或你的伺服器 URL)
            # 並組合完整的圖片 URL
            image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space
            # 統一回傳 JSON 成功格式
            return json.dumps({
                "image_url": image_url,
                "text_result": f"圖片已成功生成並上傳。這是根據提示 '{prompt[:50]}...' 生成的圖片。"
            })
            
        # 處理圖片生成失敗但 API 未報錯的情況
        return json.dumps({
            "error": "圖片生成失敗。API 回應中未包含圖片數據,請嘗試修改提示詞。"
        })
    except Exception as e:
        error_msg = f"圖片生成與上傳失敗: {e}"
        return json.dumps({
            "error": error_msg
        })

@tool
def analyze_image_with_text(image_path: str, user_text: str) -> str:
    """
    這個工具可以根據圖片和文字提示來回答問題 (多模態分析)。
    
    Args:
        image_path: 圖片在本地端儲存的路徑。
        user_text: 針對圖片提出的文字問題。
        
    Returns:
        一個 JSON 格式的字串,包含模型回應或錯誤訊息。
    """
    try:
        # 檢查圖片路徑是否存在
        #if not os.path.exists(image_path):
        #    return json.dumps({
        #        "error": "圖片路徑無效,無法進行分析。"
        #    })
            
        # 使用 PIL 開啟圖片
        #img_user = PIL.Image.open(image_path)
        img_user = load_image(image_path)
        # 呼叫 Google GenAI 模型 (gemini-2.5-flash) 進行多模態分析
        response = genai_client.models.generate_content(
                    model="gemini-2.5-flash",
                    contents=[img_user, user_text] # 同時傳入圖片物件和文字
        )
        if (response.text != None):
            out = response.text
        else:
            out = "Gemini沒答案!請換個說法!"

        # 統一回傳 JSON 成功格式 (只有文字結果)
        return json.dumps({
            "text_result": out
        })
    
    except Exception as e:
        # 處理錯誤
        out = f"Gemini執行出錯: {e}"
        # 統一回傳 JSON 錯誤格式
        return json.dumps({
            "error": out
        })
        

@tool
def deblur_image_from_url(
    file_url: str,
    user_text: str
) -> str:
    """
    這個工具可以從提供的圖片來源載入影像(支援 HTTP/HTTPS 網址與本地檔案路徑),
    並使用分塊處理(Tiled Processing)進行去模糊(deblur)。處理完成後,
    會將結果儲存於伺服器的 static/ 目錄,並回傳去模糊後圖片的 **絕對 URL 路徑**
    以及根據 user_text 生成的額外文字結果。

    Args:
        file_url:  
            圖片來源,可為:
            - HTTP/HTTPS 網路圖片網址(例如:https://example.com/img.png)  
            - 本地檔案路徑(例如:/tmp/xxx.png)

        user_text:  
            使用者針對圖片提出的處理需求或描述文字。

    Returns: 
            JSON 格式的字串,包含:
            - "image_url": 去模糊後圖片的絕對 URL 路徑  
            - "text_result": 根據 user_text 產生的額外文字說明
    """
    try:
        tile_size = 512
        overlap = 32

        # 內容轉換為 PIL Image
        img_input = load_image(file_url)

        # 2. 執行去模糊處理
        img_deblurred = deblur_image_tiled(
            img_input, 
            tile_size=tile_size, 
            overlap=overlap
        )
        
        # 建立一個唯一的檔案名
        ext = img_input.format if img_input.format else 'JPEG'
        file_name = f"static/{os.urandom(16).hex()}.jpg"

        img_deblurred.save(file_name, format=ext)

        # 4. 建構絕對 URL 路徑 (供客戶端存取)
        # 這裡假設 BASE_URL 已經設定好,並與 FastAPI 的 static mount 匹配
        image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space

        analysis_result = f"圖片已成功去模糊。用戶請求的描述為:'{user_text}'。模型已根據此要求調整參數進行處理。"

        # 5. 返回 JSON 字串
        return json.dumps({
            "image_url": image_url,
            "text_result": analysis_result
        })

    except requests.exceptions.RequestException as e:
        return json.dumps({
            "error": f"下載圖片失敗或 URL 無效: {e}"
        })
    except Exception as e:
        return json.dumps({
            "error": f"圖片處理失敗。錯誤訊息: {e}"
        })
    
# ------------------------------
# 1️⃣ 意圖分類工具
# ------------------------------
@tool
def classify_intent(user_input: str) -> str:
    """
    判斷使用者輸入意圖:
    - "deblur" -> 去模糊 / 修復 / 影像清晰化
    - "qa" -> 一般問題或圖片分析
    """
    deblur_keywords = [
        # 中文
        "去模糊", "清晰", "清楚", "修復", "模糊", "變清楚", "提高清晰度", 
        "還原", "去噪", "降噪", "去霧", "增強", "超解析", "超分辨", 
        # 英文/拼音
        "deblur", "restore", "restoration", "denoise", "noise", "enhance", 
        "enhancement", "super resolution", "sr", "defog", "dehaze", 
        "sharpen", "blurry", "blurred", "fix blur"
    ]
    text = user_input.lower()
    if any(k in text for k in deblur_keywords):
        return "deblur"
    else:
        return "qa"

# ==========================
#  LangChain 代理人設定
# ==========================

# 結合所有定義的工具
tools = [
        classify_intent,              # 意圖分類
        generate_and_upload_image,    # 生成圖片
        analyze_image_with_text,      # 分析圖片
        deblur_image_from_url         # 去模糊圖片
    ]

# 建立 LLM 模型實例 (使用 LangChain 的 ChatGoogleGenerativeAI)
llm = ChatGoogleGenerativeAI(
        google_api_key=google_api,
        model="gemini-2.5-flash", 
        temperature=0.2
    )

# ✅ 建立 Prompt (新版語法)
sys_prompt = """
你是一個圖像生成、去模糊與圖片問答助理,請依流程使用工具。

【可用工具】
1. classify_intent(user_input) → 回傳 "deblur" 或 "qa"
2. deblur_image_from_url(file_url, user_text) → 圖片去模糊/修復
3. analyze_image_with_text(image_path, user_text) → 圖片理解與問答
4. generate_and_upload_image(prompt) → 生成圖像

【流程】
- 先呼叫 classify_intent 判斷意圖
- 若為 "deblur" → 呼叫 deblur_image_from_url
- 若為 "qa":
    - 若與圖片內容有關 → analyze_image_with_text
    - 若需生成新圖 → generate_and_upload_image

【回覆規則】
- 若工具成功輸出圖片 → 回覆必須包含:
    - 圖片完整 URL
    - 簡要說明(如:已完成去模糊/生成圖片)
- 若工具失敗 → 用自然語言說明錯誤,不輸出技術錯誤碼或 traceback

【判斷原則】
- 有「去模糊、清晰、修復」等語意 → deblur
- 有提問或描述圖片 → qa
- 有「生成、畫、幫我做一張圖」→ generate_and_upload_image

請嚴格遵循流程,不要跳步。
"""

# --- 4. 建立代理人與執行器 ---
# 建立工具調用代理人 (Tool Calling Agent)
agent = create_agent(
        model=llm,
        tools=tools,
        system_prompt=sys_prompt
    )

def format_agent_result(result):
    output = {
        "user": None,
        "tool_call": None,
        "tool_result": None,
        "final_response": None
    }

    for msg in result["messages"]:
        if isinstance(msg, HumanMessage):
            output["user"] = msg.content

        elif isinstance(msg, AIMessage) and msg.additional_kwargs.get("function_call"):
            fn = msg.additional_kwargs["function_call"]
            output["tool_call"] = {
                "name": fn["name"],
                "arguments": fn["arguments"]
            }

        elif isinstance(msg, ToolMessage):
            try:
                output["tool_result"] = json.loads(msg.content)
            except Exception:
                output["tool_result"] = msg.content  # 若非 JSON

        elif isinstance(msg, AIMessage) and not msg.additional_kwargs.get("function_call"):
            # 如果是 list of dict(如 [{'type': 'text','text':...}])
            if isinstance(msg.content, list):
                # 只取第一個 text
                if len(msg.content) > 0 and "text" in msg.content[0]:
                    output["final_response"] = msg.content[0]["text"]
                else:
                    output["final_response"] = str(msg.content)
            else:
                output["final_response"] = msg.content

    return output

def run_agent(user_input: str):
    """呼叫此函式來執行 Agent"""
    print(f"UserInput:{user_input}")
    result = agent.invoke({
        "messages": [{"role": "user", "content": user_input }]
    })
    #print(f"result:{result}")

    output_format = format_agent_result( result )
    print(f"output_format:{output_format}")

    return { "output": output_format }