import json, os import logging from fastapi.middleware.cors import CORSMiddleware from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException, status import google.generativeai as genai from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageSendMessage, AudioMessage from dotenv import load_dotenv # 從 .env 檔案載入環境變數 load_dotenv() # 設定日誌記錄 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', encoding='utf-8') # --- 1. 配置設定 --- def configure_gemini(): """ 配置 Google Gemini 模型 設定 API 金鑰、生成參數和模型 """ try: genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) generation_config = genai.types.GenerationConfig( max_output_tokens=2048, temperature=0.2, top_p=0.5, top_k=16 ) model = genai.GenerativeModel( "gemini-2.0-flash-exp", system_instruction="你是咖啡廳員工,請使用親切的招呼語做為開頭,然後以爽朗愉悅的口氣回答問題。", ) logging.info("Gemini 模型配置完成.") return model, generation_config except Exception as e: logging.error(f"Gemini 模型配置錯誤: {e}") raise def configure_line_bot(): """ 配置 Line Bot API 設定 Channel Access Token 和 Channel Secret """ try: line_bot_api = LineBotApi(os.getenv("CHANNEL_ACCESS_TOKEN")) line_handler = WebhookHandler(os.getenv("CHANNEL_SECRET")) logging.info("Line Bot API 配置完成.") return line_bot_api, line_handler except Exception as e: logging.error(f"Line Bot API 配置錯誤: {e}") raise # 初始化配置 try: model, generation_config = configure_gemini() line_bot_api, line_handler = configure_line_bot() except Exception as e: logging.critical(f"初始化配置失敗: {e}") exit(1) # 如果配置失敗,則終止程式 # 從環境變數取得是否啟用對話模式,預設為 true working_status = os.getenv("DEFALUT_TALKING", default="true").lower() == "true" # --- 2. FastAPI 應用程式 --- app = FastAPI() # 設定 CORS,允許跨域請求 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --- 3. 路由設定 --- @app.get("/") def root(): """ 處理根路徑請求,返回 Line Bot 的標題 """ return {"title": "Line Bot"} @app.post("/webhook") async def webhook( request: Request, background_tasks: BackgroundTasks, x_line_signature=Header(None) ): """ 處理 Line Webhook 請求 接收 Line 的訊息並驗證簽章,然後將訊息處理加入背景任務 """ body = await request.body() try: background_tasks.add_task( line_handler.handle, body.decode("utf-8"), x_line_signature ) except InvalidSignatureError: logging.warning("收到無效的簽章.") raise HTTPException(status_code=400, detail="無效的簽章") return "ok" # --- 4. 對話 Session 管理 --- chat_sessions = {} def get_or_create_chat_session(user_id: str): """ 取得或建立特定使用者的對話 Session 如果不存在,則建立一個新的 Session """ if user_id not in chat_sessions: logging.info(f"為使用者 {user_id} 建立新的對話 Session") chat_sessions[user_id] = model.start_chat( history=[ { "role": "user", "parts": ["hi"], }, { "role": "model", "parts": ["Hi there! How can I help you today?\n"], }, ] ) return chat_sessions[user_id] def handle_text_message(event): """ 處理文字訊息,取得使用者輸入,並回覆 Gemini 模型的訊息 """ global working_status user_id = event.source.user_id if event.message.text == "再見": line_bot_api.reply_message(event.reply_token, TextSendMessage(text="Bye!")) if user_id in chat_sessions: del chat_sessions[user_id] logging.info(f"使用者 {user_id} 說再見,已刪除對話 Session") return if not working_status: logging.info("對話模式關閉,忽略訊息.") return try: prompt = event.message.text logging.info(f"收到使用者 {user_id} 的訊息: {prompt}") chat_session = get_or_create_chat_session(user_id) response = chat_session.send_message(prompt) if response.text: out = response.text logging.info(f"Gemini 回覆使用者 {user_id}: {out}") else: out = "Gemini 沒答案!請換個說法!" logging.warning(f"Gemini 沒有回覆文字內容給使用者 {user_id}") except Exception as e: out = "Gemini 執行出錯!請換個說法!" logging.error(f"Gemini 發生錯誤 (使用者: {user_id}): {e}") line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out)) @line_handler.add(MessageEvent, message=TextMessage) def handle_message(event): """ Line Bot 訊息處理的主要函式 驗證訊息類型,然後將文字訊息轉發給處理函數 """ if event.type != "message" or event.message.type != "text": logging.warning("事件類型錯誤:不是文字訊息.") line_bot_api.reply_message(event.reply_token, TextSendMessage(text="事件類型錯誤:不是文字訊息。")) return handle_text_message(event) # --- 5. 啟動應用程式 --- if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)