File size: 7,793 Bytes
d2f3139
 
 
 
 
 
27a1f1f
 
1c3bc77
d2f3139
064ad47
e97e655
d2f3139
 
f91846a
 
26b8cf7
ade6b09
d2f3139
 
ade6b09
d2f3139
 
 
 
 
 
 
 
 
 
b0cc111
 
 
d2f3139
b0cc111
 
d2f3139
 
 
 
 
 
b0cc111
d2f3139
 
 
 
b0cc111
d2f3139
 
 
b0cc111
d2f3139
b0cc111
d2f3139
 
3efacfe
b0cc111
 
 
 
 
 
47d50a2
b0cc111
 
3efacfe
d2f3139
3efacfe
b0cc111
 
d2f3139
b0cc111
d2f3139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0cc111
d2f3139
 
 
 
 
 
 
 
b0cc111
47d50a2
d2f3139
b0cc111
 
3efacfe
b0cc111
3efacfe
d2f3139
b0cc111
 
 
70802c3
 
 
 
d2f3139
 
 
 
 
 
 
 
 
 
b0cc111
 
d2f3139
b0cc111
d2f3139
 
b0cc111
d2f3139
fa26463
d2f3139
b0cc111
 
fa26463
d2f3139
b0cc111
 
d2f3139
 
 
b0cc111
d2f3139
 
 
 
b0cc111
9465e87
b0cc111
 
d2f3139
b0cc111
 
 
3a127fd
d2f3139
 
 
 
 
 
 
b0cc111
 
 
d2f3139
 
b0cc111
d2f3139
b8a4ccf
 
d2f3139
 
 
 
 
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
import os
import io
import traceback
import json

# 解決 Matplotlib/Fontconfig 在無頭環境中的問題
os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
os.environ["XDG_CACHE_HOME"] = "/tmp"
os.environ["USER_AGENT"] = "my-huggingface-space"

import gradio as gr
from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException, status
import requests # 確保導入 requests
import requests_handler # 確保導入 requests_handler
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageSendMessage, ImageMessage, AudioMessage, AudioSendMessage
import google.generativeai as genai 
from chatbot import ChatBot
from line_bot_app import LineBotApp
from system_instruction import system_instruction

# --- 將 app 實例化移到頂層 ---
app = FastAPI()

# --- Gradio 的處理函數 ---
def greet(name):
    """Gradio UI 的簡單測試函數"""
    return "Hello " + name + "!! 橘橘管理員介面測試中..."

# --- FastAPI 路由定義 ---
@app.post("/callback")
async def callback(request: Request, background_tasks: BackgroundTasks, x_line_signature: str = Header(None)):
    """
    Line Webhook 回調路由。
    """
    body = await request.body()
    # 從 app.state 獲取 bot_app 實例
    bot_app = getattr(app.state, 'bot_app', None)
    if not bot_app:
         print("Error: bot_app not initialized in app state.")
         raise HTTPException(status_code=500, detail="Bot application not ready")

    try:
        # 使用背景任務處理 webhook,避免阻塞請求
        background_tasks.add_task(
            bot_app.handle_webhook, body.decode("utf-8"), x_line_signature
        )
        return {"status": "ok"}
    except InvalidSignatureError:
         print("Invalid signature error.")
         raise HTTPException(status_code=400, detail="Invalid signature")
    except Exception as e:
        print(f"處理 webhook 時發生未預期錯誤: {e}")
        print(traceback.format_exc())
        # 即使內部處理出錯,也盡量回覆 Line "ok",避免 Line 不斷重試
        return {"status": "ok"} # 或者返回 500 錯誤,取決於你的策略

@app.get("/")
async def read_root():
    """
    根路由,用於確認服務正常運行。
    """
    return {"status": "ok", "message": "Line Bot Service is running"}

@app.get("/healthcheck")
async def health_check():
    """
    健康檢查路由。
    """
    health_status = {
        "status": "ok",
        "details": {}
    }
    # 檢查 ChatBot 是否初始化 (通過 LineBotApp)
    bot_app = getattr(app.state, 'bot_app', None)
    if bot_app and hasattr(bot_app, 'chatbot') and bot_app.chatbot:
        health_status["details"]["chatbot_initialized"] = "ok"
        # 檢查 Gemini 模型是否初始化
        if bot_app.chatbot.model:
             health_status["details"]["gemini_chat_model"] = "ok"
             # 可以加一個簡單的 API 測試,但避免重複配置
             try:
                  # 假設 chatbot 實例已配置好 API Key
                  _ = bot_app.chatbot.model.generate_content("health check", generation_config={"max_output_tokens": 5}) # 限制輸出
             except Exception as e:
                  health_status["details"]["gemini_chat_model"] = f"error: {str(e)}"
                  health_status["status"] = "error"
        else:
             health_status["details"]["gemini_chat_model"] = "error: model not loaded"
             health_status["status"] = "error"
        # 檢查圖片生成模型是否初始化 (如果需要)
        if hasattr(bot_app.chatbot, 'image_generator') and bot_app.chatbot.image_generator and hasattr(bot_app.chatbot.image_generator, 'image_model') and bot_app.chatbot.image_generator.image_model:
             health_status["details"]["gemini_image_model"] = "ok"
        else:
             health_status["details"]["gemini_image_model"] = "warning: model not loaded or check not implemented"
             # 可以考慮不將此設為 error
    else:
        health_status["details"]["chatbot_initialized"] = "error"
        health_status["status"] = "error"

    # 檢查 Line Bot API (簡單檢查實例是否存在)
    if bot_app and hasattr(bot_app, 'line_bot_api'):
        health_status["details"]["line_bot_api"] = "ok (instance exists)"
    else:
        health_status["details"]["line_bot_api"] = "error: instance not found"
        health_status["status"] = "error"

    return health_status

# --- FastAPI 啟動事件 ---
@app.on_event("startup")
async def startup_event():
    """
    FastAPI 啟動事件。初始化 ChatBot、Line Bot 應用和 Gradio UI。
    """
    print("===== Application Startup =====")
    try:
        # 從環境變數獲取配置
        google_api_key = os.getenv("GOOGLE_API_KEY")
        line_channel_access_token = os.getenv("CHANNEL_ACCESS_TOKEN")
        line_channel_secret = os.getenv("CHANNEL_SECRET")
        google_search_api_key = os.getenv("GOOGLE_CUSTOM_SEARCH_API_KEY")
        google_search_cse_id = os.getenv("GOOGLE_CUSTOM_SEARCH_CSE_ID")

        print("Checking required environment variables...")
        if not google_api_key: print("Warning: GOOGLE_API_KEY not set.")
        if not line_channel_access_token: print("Warning: CHANNEL_ACCESS_TOKEN not set.")
        if not line_channel_secret: print("Warning: CHANNEL_SECRET not set.")
        # 根據你的應用是否嚴格需要這些來決定是否 raise ValueError
        # if not all([google_api_key, line_channel_access_token, line_channel_secret]):
        #     raise ValueError("缺少必要的環境變數設定 (Google API Key, Line Channel Access Token, Line Channel Secret)")

        print("Initializing ChatBot...")
        # 初始化 ChatBot
        chatbot = ChatBot(
            google_api_key=google_api_key, # 傳遞 API Key
            system_instruction=system_instruction,
            google_search_api_key=google_search_api_key,
            google_search_cse_id=google_search_cse_id
        )
        print("ChatBot initialized.")

        print("Initializing LineBotApp...")
        # 初始化 Line Bot
        bot_app = LineBotApp(
            channel_access_token=line_channel_access_token,
            channel_secret=line_channel_secret,
            chatbot=chatbot
        )
        print("LineBotApp initialized.")

        # 將 bot_app 保存到應用狀態中 (使用頂層 app)
        app.state.bot_app = bot_app
        print("LineBotApp stored in app state.")

        print("Setting up Gradio UI...")
        # 設置 Gradio UI
        gradio_ui = gr.Interface(
            fn=greet,
            inputs="text",
            outputs="text",
            flagging_options=None, # 使用 flagging_options
            title="Line Bot Admin",
            description="管理員介面 - 用於測試和監控 Line Bot"
        )

        # 使用頂層 app 掛載 Gradio
        # mount_gradio_app 通常會修改傳入的 app 實例
        gr.mount_gradio_app(app, gradio_ui, path="/ui")
        print("Gradio UI mounted at /ui.")

        print("===== Application Startup Complete =====")

    except Exception as e:
        print(f"啟動服務時發生錯誤: {e}")
        print(traceback.format_exc())
        # 啟動失敗時,可以考慮讓應用程式退出或進入錯誤狀態
        # raise e # 重新拋出異常,可能會讓 uvicorn 退出

# --- 主程式入口點 ---
if __name__ == "__main__":
    import uvicorn
    # 從環境變數讀取端口,預設為 7860 (Hugging Face 通常用這個)
    port = int(os.getenv("PORT", 7860))
    print(f"Starting Uvicorn on host 0.0.0.0, port {port}")
    # 使用頂層 app 啟動 uvicorn
    uvicorn.run(app, host="0.0.0.0", port=port)