File size: 8,696 Bytes
8e6ac56
 
 
 
55b1be9
8e6ac56
6cbeb06
4ca65f6
8e6ac56
217ceea
f91846a
8e6ac56
 
 
 
 
 
 
6fa6ba2
8e6ac56
 
 
 
 
 
1b42489
d390efa
 
8e6ac56
 
 
98dc21e
8e6ac56
f73bbf5
a412fae
 
8e6ac56
1ce0901
 
98dc21e
8e6ac56
 
4123c19
98dc21e
55b1be9
b59435f
98dc21e
8e6ac56
55b1be9
 
 
df5bf2a
55b1be9
 
 
 
8e6ac56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c47a0e6
8e6ac56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7272a39
8e6ac56
 
 
 
 
 
 
 
 
 
 
 
a6fab3b
251fb1a
a6fab3b
 
 
552dc60
ca680bf
 
 
 
 
 
 
8e6ac56
ca680bf
8e6ac56
 
 
 
 
 
 
 
 
 
d201588
8e6ac56
42558dd
1b42489
42558dd
d201588
 
 
42558dd
d201588
 
8e6ac56
 
 
 
 
 
55b1be9
6482098
a7c9894
2cf7795
6482098
4123c19
 
 
 
 
0a52d39
3a2e91a
34287b9
 
 
3a2e91a
34287b9
0385c62
8ff7523
b92f1fc
8e6ac56
fbc391f
8e6ac56
 
 
 
ebe8304
8e6ac56
 
 
 
 
83355d0
8e6ac56
 
83355d0
8e6ac56
 
1c41fb2
e4477cb
8e6ac56
ebe8304
 
8e6ac56
 
 
 
ec42477
8e6ac56
 
 
e4477cb
8e6ac56
 
 
 
 
8c89980
f374b74
 
bac2825
 
 
 
 
 
 
02acfe6
09a85ee
8e6ac56
 
09a85ee
bac2825
4123c19
3bf6983
8e6ac56
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
import os
import io
import tempfile
from collections import defaultdict
from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
from fastapi.staticfiles import StaticFiles
from google import genai
from google.genai import types
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
    MessageEvent,
    TextMessage,
    TextSendMessage,
    ImageSendMessage,
    ImageMessage,
)
import PIL.Image
import uvicorn

# LangChain 相關匯入
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.agents import AgentExecutor, create_tool_calling_agent


# ==========================
#  環境設定與工具函式
# ==========================

# 設置 Google AI API 金鑰
google_api = os.environ["GOOGLE_API_KEY"]
genai_client = genai.Client(api_key=google_api)

# 設置 Line Bot 的 API 金鑰和秘密金鑰
line_bot_api = LineBotApi(os.environ["CHANNEL_ACCESS_TOKEN"])
line_handler = WebhookHandler(os.environ["CHANNEL_SECRET"])

# 使用字典模擬用戶訊息歷史存儲
user_message_history = defaultdict(list)

# 建立 FastAPI 應用程式
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

# 設定 CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def get_image_url_from_line(message_id):
    """
    從 Line 訊息 ID 獲取圖片內容並儲存到暫存檔案。
    """
    try:
        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[user_id].append(
        {"type": message_type, "content": message_content}
    )

def get_previous_message(user_id):
    """
    獲取用戶的上一則訊息。
    """
    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!"}


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

@tool
def generate_and_upload_image(prompt: str) -> str:
    """
    這個工具可以根據文字提示生成圖片,並將其上傳到伺服器。
    
    Args:
        prompt: 用於生成圖片的文字提示。
        
    Returns:
        生成圖片的 URL。
    """
    try:
        response = genai_client.models.generate_content(
            model="gemini-2.0-flash-preview-image-generation",
            contents=prompt,
            config=types.GenerateContentConfig(response_modalities=['Text', 'Image'])
        )
        
        image_binary = None
        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:
            image = PIL.Image.open(io.BytesIO(image_binary))
            # 隨機生成一個檔案名以避免衝突
            file_name = f"static/{os.urandom(16).hex()}.png"
            image.save(file_name, format="PNG")
            
            image_url = os.path.join(os.getenv("HF_SPACE"), file_name)
            return image_url
            
        return "圖片生成失敗。"
    except Exception as e:
        return f"圖片生成與上傳失敗: {e}"

@tool
def analyze_image_with_text(image_path: str, user_text: str) -> str:
    """
    這個工具可以根據圖片和文字提示來回答問題。
    
    Args:
        image_path: 圖片在本地端儲存的路徑。
        user_text: 針對圖片提出的文字問題。
        
    Returns:
        模型針對圖片和文字提示給出的回應。
    """
    try:
        if not os.path.exists(image_path):
            return "圖片路徑無效,無法進行分析。"
            
        img_user = PIL.Image.open(image_path)
        response = genai_client.models.generate_content(
                    model="gemini-2.5-flash",
                    config=types.GenerateContentConfig(response_mime_type="application/json"),
                    contents=[img_user, user_text]
        )
        if (response.text != None):
            out = response.text
        else:
            out = "Gemini沒答案!請換個說法!"
    except:
        # 處理錯誤
        out = "Gemini執行出錯!請換個說法!"
        
    return out


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

# 結合所有工具
tools = [generate_and_upload_image, analyze_image_with_text]

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

# 建立提示模板
prompt_template = ChatPromptTemplate([
    ("system", "你是一個強大的助理,可以根據用戶的請求使用提供的工具。"),
    ("user", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

# 建立代理人
agent = create_tool_calling_agent(llm, tools, prompt_template)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# ==========================
#  FastAPI 路由
# ==========================

@app.get("/")
def root():
    return {"title": "Line Bot"}

@app.post("/webhook")
async def webhook(
    request: Request,
    background_tasks: BackgroundTasks,
    x_line_signature=Header(None),
):
    body = await request.body()
    try:
        background_tasks.add_task(
            line_handler.handle, body.decode("utf-8"), x_line_signature
        )
    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="Invalid signature")
    return "ok"

@line_handler.add(MessageEvent, message=(ImageMessage, TextMessage))
def handle_message(event):
    user_id = event.source.user_id
    
    # 處理圖片上傳
    if event.message.type == "image":
        image_path = get_image_url_from_line(event.message.id)
        print(image_path)
        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
        previous_message = get_previous_message(user_id)
        print(previous_message)

        # 根據上一則訊息類型,動態傳遞給代理人
        if previous_message["type"] == "image":
            image_path = previous_message["content"]
            agent_input = {
                "input": f"請根據這張圖片回答問題。圖片的路徑是 {image_path},我的問題是:{user_text}"
            }
            # 清除上一則圖片訊息,避免重複觸發
            user_message_history[user_id].pop()
        else:
            agent_input = {"input": user_text}
        try:
            # 運行代理人
            response = agent_executor.invoke(agent_input)
            out = response["output"]
            if 'https' in out:
                img_tmp = 'https'+out.split('https')[1]
                image_url = img_tmp.split('png')[0]+'png'
                line_bot_api.push_message(
                    event.source.user_id,
                    [
                        TextSendMessage(text="✨ 這是我為你生成的圖片喔~"),
                        ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
                    ]
                )
            else:
                line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
        except Exception as e:
            print(f"代理人執行出錯: {e}")
            out = f"代理人執行出錯!錯誤訊息:{e}"
            line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))

if __name__ == "__main__":
    uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)