AlanRex commited on
Commit
e9e4b9f
·
verified ·
1 Parent(s): b4108eb

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +208 -169
main.py CHANGED
@@ -1,177 +1,216 @@
1
- import json, os, glob, time
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException
4
- from google import genai
5
- from linebot import LineBotApi, WebhookHandler
6
- from linebot.exceptions import InvalidSignatureError
7
- from linebot.models import MessageEvent, TextMessage, TextSendMessage
8
- import PyPDF2
9
- import logging
10
 
11
- logging.basicConfig(level=logging.INFO)
12
- logger = logging.getLogger(__name__)
13
 
14
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
15
- if not GOOGLE_API_KEY:
16
- raise ValueError("請設定 GOOGLE_API_KEY")
17
-
18
- client = genai.Client(api_key=GOOGLE_API_KEY)
19
-
20
- # 讀取 PDF
21
- files = glob.glob('docs/*.pdf')
22
- pdf_content = ''
23
-
24
- if files:
25
- logger.info(f"找到 {len(files)} 個 PDF")
26
- for filename in files:
27
- try:
28
- with open(filename, 'rb') as pdf_file:
29
- pdf_reader = PyPDF2.PdfReader(pdf_file)
30
- for page in pdf_reader.pages:
31
- pdf_content += page.extract_text()
32
- except Exception as e:
33
- logger.error(f"讀取失敗: {e}")
34
-
35
- logger.info(f"PDF 長度: {len(pdf_content)} 字元")
36
-
37
- # 限制長度
38
- if len(pdf_content) > 50000:
39
- pdf_content = pdf_content[:50000]
40
-
41
- # 使用 Context Caching 儲存 PDF 內容
42
- cache_name = None
43
- try:
44
- # 建立 cached content
45
- cached_content = client.caches.create(
46
- model="gemini-1.5-flash-001",
47
- contents=[
48
- genai.types.Content(
49
- role="user",
50
- parts=[genai.types.Part(text=f"""以下是參考資料,請記住這些內容:
51
-
52
- {pdf_content}
53
-
54
- 你是問答助手,使用以上資料回答問題。規則:
55
- 1. 只用提供的資料回答
56
- 2. 不知道就說「請聯繫服務專員」
57
- 3. 你是租借音樂空間的回覆助手,請你用音樂人員或產業人員來回覆""")]
58
- )
59
- ],
60
- ttl="300s", # 快取 5 分鐘
61
- )
62
- cache_name = cached_content.name
63
- logger.info(f"成功建立 Cache: {cache_name}")
64
- except Exception as e:
65
- logger.error(f"無法建立 Cache: {e}")
66
- logger.info("將使用一般模式(不使用 Cache)")
67
-
68
- # LINE Bot 設定
69
- line_bot_api = LineBotApi(os.getenv("CHANNEL_ACCESS_TOKEN"))
70
- line_handler = WebhookHandler(os.getenv("CHANNEL_SECRET"))
71
- working_status = True
72
-
73
- last_request_time = {}
74
- REQUEST_COOLDOWN = 3
75
-
76
- app = FastAPI()
77
- app.add_middleware(
78
- CORSMiddleware,
79
- allow_origins=["*"],
80
- allow_credentials=True,
81
- allow_methods=["*"],
82
- allow_headers=["*"],
83
- )
 
 
 
 
84
 
85
- @app.get("/")
86
- def root():
87
- return {
88
- "status": "running",
89
- "cache_enabled": cache_name is not None,
90
- "pdf_length": len(pdf_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
 
 
 
 
 
 
 
 
92
 
93
- @app.post("/webhook")
94
- async def webhook(request: Request, background_tasks: BackgroundTasks, x_line_signature=Header(None)):
95
- body = await request.body()
96
- try:
97
- background_tasks.add_task(line_handler.handle, body.decode("utf-8"), x_line_signature)
98
- except InvalidSignatureError:
99
- raise HTTPException(status_code=400, detail="Invalid signature")
100
- return "ok"
101
-
102
- @line_handler.add(MessageEvent, message=TextMessage)
103
- def handle_message(event):
104
- if event.type != "message" or event.message.type != "text":
105
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- if event.message.text == "再見":
108
- line_bot_api.reply_message(event.reply_token, TextSendMessage(text="Bye!"))
109
- return
110
-
111
- # 速率限制
112
- user_id = event.source.user_id
113
- current_time = time.time()
114
-
115
- if user_id in last_request_time:
116
- elapsed = current_time - last_request_time[user_id]
117
- if elapsed < REQUEST_COOLDOWN:
118
- line_bot_api.reply_message(
119
- event.reply_token,
120
- TextSendMessage(text=f"請稍等 {REQUEST_COOLDOWN - int(elapsed)} 秒")
121
- )
122
- return
123
-
124
- last_request_time[user_id] = current_time
125
-
126
- if working_status:
127
- try:
128
- prompt = event.message.text
129
- logger.info(f"提問: {prompt[:50]}")
130
-
131
- # 如果有 Cache,使用 Cache
132
- if cache_name:
133
- response = client.models.generate_content(
134
- model="gemini-1.5-flash-001",
135
- contents=prompt,
136
- config=genai.types.GenerateContentConfig(
137
- max_output_tokens=1000,
138
- temperature=0.1,
139
- cached_content=cache_name
140
- )
141
- )
142
- else:
143
- # 沒有 Cache,使用一般模式
144
- response = client.models.generate_content(
145
- model="gemini-2.5-flash",
146
- contents=prompt,
147
- config=genai.types.GenerateContentConfig(
148
- max_output_tokens=1000,
149
- temperature=0.1,
150
- system_instruction=f"""你是問答助手。參考資料:
151
- {pdf_content[:10000]}
152
- 規則:只用資料回答,不知道就說聯繫專員。"""
153
- )
154
- )
155
 
156
- if response and response.text:
157
- out = response.text
158
- logger.info("回應成功")
159
- else:
160
- out = "無法回答,請聯繫服務專員。"
161
-
162
- except Exception as e:
163
- error_msg = str(e)
164
- logger.error(f"錯誤: {error_msg}")
 
165
 
166
- if "quota" in error_msg.lower() or "resource_exhausted" in error_msg.lower():
167
- out = "系統忙碌,請稍後再試。"
168
- elif "429" in error_msg:
169
- out = "請求過於頻繁,請稍候。"
170
- else:
171
- out = "系統錯誤,請聯繫服務專員。"
172
-
173
- line_bot_api.reply_message(event.reply_token, TextSendMessage(text=out))
174
-
175
- if __name__ == "__main__":
176
- import uvicorn
177
- uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
 
1
+ # 在主程式中加入以下功能
 
 
 
 
 
 
 
 
2
 
3
+ import re
4
+ from datetime import datetime, timedelta
5
 
6
+ # ========== 日期解析與驗證 ==========
7
+ def parse_date(date_str):
8
+ """解析並驗證日期"""
9
+ today = datetime.now()
10
+
11
+ # 處理相對日期
12
+ if "今天" in date_str or "今日" in date_str:
13
+ return today.strftime("%Y-%m-%d"), None
14
+ elif "明天" in date_str or "明日" in date_str:
15
+ return (today + timedelta(days=1)).strftime("%Y-%m-%d"), None
16
+ elif "後天" in date_str:
17
+ return (today + timedelta(days=2)).strftime("%Y-%m-%d"), None
18
+
19
+ # 處理絕對日期格式
20
+ patterns = [
21
+ (r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})', '%Y-%m-%d'), # 2025-12-26
22
+ (r'(\d{1,2})[/-](\d{1,2})', '%m-%d'), # 12/26
23
+ ]
24
+
25
+ for pattern, fmt in patterns:
26
+ match = re.search(pattern, date_str)
27
+ if match:
28
+ try:
29
+ if fmt == '%m-%d':
30
+ date = datetime.strptime(f"{today.year}-{match.group(0)}", f'%Y-{fmt}')
31
+ else:
32
+ date = datetime.strptime(match.group(0), fmt)
33
+
34
+ # 檢查是否為過去的日期
35
+ if date.date() < today.date():
36
+ return None, "日期不能是過去的時間,請重新輸入。"
37
+
38
+ return date.strftime("%Y-%m-%d"), None
39
+ except ValueError:
40
+ pass
41
+
42
+ return None, "日期格式不正確,請使用如:明天、12/26、2025-12-30"
43
+
44
+ # ========== 時間驗證 ==========
45
+ def validate_time(time_str):
46
+ """驗證時段格式"""
47
+ # 簡單驗證是否包含時間相關字詞
48
+ time_keywords = ["點", "時", ":", "-", "到", "至"]
49
+ if any(kw in time_str for kw in time_keywords):
50
+ return True, None
51
+ return False, "時段格式不正確,請使用如:09:00-11:00 或 下午2點到4點"
52
+
53
+ # ========== 人數驗證 ==========
54
+ def parse_people(people_str):
55
+ """解析並驗證人數"""
56
+ # 提取數字
57
+ numbers = re.findall(r'\d+', people_str)
58
+ if numbers:
59
+ count = int(numbers[0])
60
+ if 1 <= count <= 10: # 假設最多10人
61
+ return count, None
62
+ else:
63
+ return None, "人數必須在 1-10 人之間,請重新輸入。"
64
+ return None, "請輸入有效的人數,例如:2人、3"
65
+
66
+ # ========== 琴房驗證 ==========
67
+ AVAILABLE_ROOMS = ["A琴房", "B琴房", "C琴房", "D琴房", "任意"]
68
+
69
+ def validate_room(room_str):
70
+ """驗證琴房選擇"""
71
+ # 標準化輸入
72
+ room_normalized = room_str.strip().upper()
73
+
74
+ # 檢查是否在可用琴房列表中
75
+ for room in AVAILABLE_ROOMS:
76
+ if room.upper() in room_normalized or room_normalized in room.upper():
77
+ return room, None
78
+
79
+ return None, f"請選擇有效的琴房:{', '.join(AVAILABLE_ROOMS)}"
80
 
81
+ # ========== 將預約資料存入檔案 ==========
82
+ def save_booking_to_file(user_id, booking_data):
83
+ """將預約資料儲存到 JSON 檔案"""
84
+ import json
85
+ from pathlib import Path
86
+
87
+ # 建立 bookings 資料夾
88
+ bookings_dir = Path("bookings")
89
+ bookings_dir.mkdir(exist_ok=True)
90
+
91
+ # 預約檔案路徑
92
+ booking_file = bookings_dir / "bookings.json"
93
+
94
+ # 讀取現有預約
95
+ if booking_file.exists():
96
+ with open(booking_file, 'r', encoding='utf-8') as f:
97
+ bookings = json.load(f)
98
+ else:
99
+ bookings = []
100
+
101
+ # 新增預約
102
+ booking_record = {
103
+ "booking_id": f"BK{int(time.time())}",
104
+ "user_id": user_id,
105
+ "date": booking_data["date"],
106
+ "time": booking_data["time"],
107
+ "people": booking_data["people"],
108
+ "room": booking_data["room"],
109
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
110
+ "status": "pending" # pending, confirmed, cancelled
111
  }
112
+
113
+ bookings.append(booking_record)
114
+
115
+ # 寫入檔案
116
+ with open(booking_file, 'w', encoding='utf-8') as f:
117
+ json.dump(bookings, f, ensure_ascii=False, indent=2)
118
+
119
+ return booking_record["booking_id"]
120
 
121
+ # ========== 修改後的 handle_booking_flow 函數 ==========
122
+ def handle_booking_flow_enhanced(user_id, user_message):
123
+ """處理預約流程(加強版)"""
124
+ booking = get_user_booking(user_id)
125
+ current_state = booking["state"]
126
+
127
+ # 超時重置
128
+ if time.time() - booking["last_update"] > 600:
129
+ reset_booking(user_id)
130
+ booking = get_user_booking(user_id)
131
+ current_state = booking["state"]
132
+
133
+ booking["last_update"] = time.time()
134
+
135
+ # 取消預約
136
+ if user_message in ["取消", "取消預約", "重來"]:
137
+ reset_booking(user_id)
138
+ return "已取消預約流程。如需重新預約,請輸入「預約」。"
139
+
140
+ # 開始預約
141
+ if current_state == BookingState.IDLE:
142
+ if is_booking_keyword(user_message):
143
+ booking["state"] = BookingState.ASKING_DATE
144
+ return "好的!請問您想要預約哪一天呢?\n例如:明天、12/26、2025/12/30\n\n(輸入「取消」可取消預約)"
145
+ else:
146
+ return None
147
+
148
+ # 詢問日期(加入驗證)
149
+ elif current_state == BookingState.ASKING_DATE:
150
+ parsed_date, error = parse_date(user_message)
151
+ if error:
152
+ return error
153
+ booking["date"] = parsed_date
154
+ booking["state"] = BookingState.ASKING_TIME
155
+ return f"好的,預約日期:{parsed_date}\n\n請問您想要預約哪個時段呢?\n例如:09:00-11:00、下午2點到4點"
156
+
157
+ # 詢問時段(加入驗證)
158
+ elif current_state == BookingState.ASKING_TIME:
159
+ is_valid, error = validate_time(user_message)
160
+ if not is_valid:
161
+ return error
162
+ booking["time"] = user_message
163
+ booking["state"] = BookingState.ASKING_PEOPLE
164
+ return f"好的,時段:{user_message}\n\n請問有幾位使用呢?\n例如:1人、2人"
165
+
166
+ # 詢問人數(加入驗證)
167
+ elif current_state == BookingState.ASKING_PEOPLE:
168
+ people_count, error = parse_people(user_message)
169
+ if error:
170
+ return error
171
+ booking["people"] = people_count
172
+ booking["state"] = BookingState.ASKING_ROOM
173
+ return f"好的,人數:{people_count}人\n\n請問您想使用哪個琴房呢?\n可選擇:{', '.join(AVAILABLE_ROOMS)}"
174
+
175
+ # 詢問琴房(加入驗證)
176
+ elif current_state == BookingState.ASKING_ROOM:
177
+ room, error = validate_room(user_message)
178
+ if error:
179
+ return error
180
+ booking["room"] = room
181
+ booking["state"] = BookingState.CONFIRMING
182
 
183
+ summary = (
184
+ f"📋 請確認您的預約資訊:\n\n"
185
+ f"📅 日期:{booking['date']}\n"
186
+ f"⏰ 時段:{booking['time']}\n"
187
+ f"👥 人數:{booking['people']}人\n"
188
+ f"🎹 琴房:{booking['room']}\n\n"
189
+ f"請輸入「確認」送出預約,或輸入「取消」重新填寫。"
190
+ )
191
+ return summary
192
+
193
+ # 確認預約(儲存資料)
194
+ elif current_state == BookingState.CONFIRMING:
195
+ if user_message in ["確認", "確定", "送出", "ok", "OK"]:
196
+ # 儲存預約資料
197
+ booking_id = save_booking_to_file(user_id, booking)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ summary = (
200
+ f"✅ 預約成功!\n\n"
201
+ f"📅 日期:{booking['date']}\n"
202
+ f"⏰ 時段:{booking['time']}\n"
203
+ f"👥 人數:{booking['people']}人\n"
204
+ f"🎹 琴房:{booking['room']}\n"
205
+ f"📝 預約編號:{booking_id}\n\n"
206
+ f"我們已收到您的預約,服務專員將盡快與您聯繫確認。\n"
207
+ f"如有任何問題,歡迎隨時詢問!"
208
+ )
209
 
210
+ logger.info(f"新預約已儲存:{booking_id}")
211
+ reset_booking(user_id)
212
+ return summary
213
+ else:
214
+ return "請輸入「確認」來完成預約,或輸入「取消」重新開始。"
215
+
216
+ return None