adjust system_prompt

#2
by Rene0119 - opened
Files changed (1) hide show
  1. app.py +267 -549
app.py CHANGED
@@ -12,138 +12,112 @@ from linebot.v3.webhook import WebhookParser, WebhookHandler
12
  from linebot.v3.webhooks import MessageEvent, TextMessageContent, ImageMessageContent
13
  from linebot.v3.messaging import MessagingApi, Configuration, ApiClient, MessagingApiBlob
14
  from linebot.v3.messaging.models import (
15
- TextMessage, ReplyMessageRequest, PushMessageRequest,
16
  FlexMessage, FlexBubble, FlexBox, FlexText, FlexButton, URIAction,
17
- QuickReply, QuickReplyItem, LocationAction, ImageMessage, DatetimePickerAction
18
  )
19
  from linebot.v3.exceptions import InvalidSignatureError
20
 
21
  import google.generativeai as genai
 
22
  import json
23
- import datetime
24
- from apscheduler.schedulers.background import BackgroundScheduler
25
- import pytz
26
 
27
- CHANNEL_SECRET = os.environ.get("YOUR_CHANNEL_SECRET")
28
- CHANNEL_ACCESS_TOKEN = os.environ.get("YOUR_CHANNEL_ACCESS_TOKEN")
29
- GOOGLE_MAP_API_KEY = os.environ.get("GOOGLE_MAP_API_KEY")
30
- GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
31
- base_url = os.environ.get("HF_SPACE_URL", "localhost")
32
 
33
- if not CHANNEL_SECRET or not CHANNEL_ACCESS_TOKEN or not GOOGLE_API_KEY:
34
- raise RuntimeError("Missing essential environment variables")
35
 
36
  app = Flask(__name__)
37
 
38
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
39
- DB_PATH = os.path.join(BASE_DIR, "linebot.db")
 
40
 
41
- print("目前資料庫路徑:", DB_PATH)
42
- print("資料庫檔案是否存在:", os.path.exists(DB_PATH))
43
- try:
44
- with open(DB_PATH, "ab") as f:
45
- f.write(b"")
46
- print("✅ 資料庫有寫入權限")
47
- except Exception as e:
48
- print("❌ 資料庫無法寫入:", e)
49
 
50
- static_tmp_path = "/tmp"
51
- os.makedirs(static_tmp_path, exist_ok=True)
 
52
 
53
- configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
54
- parser = WebhookParser(CHANNEL_SECRET)
55
- handler = WebhookHandler(CHANNEL_SECRET)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  genai.configure(api_key=GOOGLE_API_KEY)
 
58
  chat = genai.GenerativeModel(model_name="gemini-1.5-flash")
59
 
60
  logging.basicConfig(level=logging.INFO)
61
  app.logger.setLevel(logging.INFO)
62
 
63
- user_states = {}
64
-
65
- def init_reminders_table():
66
- conn = sqlite3.connect(DB_PATH)
67
- cursor = conn.cursor()
68
- cursor.execute("""
69
- CREATE TABLE IF NOT EXISTS reminders (
70
- id INTEGER PRIMARY KEY AUTOINCREMENT,
71
- user_id TEXT NOT NULL,
72
- medicine TEXT NOT NULL,
73
- start_date TEXT NOT NULL,
74
- end_date TEXT NOT NULL,
75
- times TEXT NOT NULL,
76
- sent INTEGER DEFAULT 0
77
- );
78
- """)
79
- cursor.execute("""
80
- CREATE TABLE IF NOT EXISTS reminders_log (
81
- id INTEGER PRIMARY KEY AUTOINCREMENT,
82
- reminder_id INTEGER,
83
- date TEXT,
84
- time TEXT
85
- );
86
- """)
87
- cursor.execute("""
88
- CREATE TABLE IF NOT EXISTS drugs (
89
- 中文品名 TEXT,
90
- 英文品名 TEXT,
91
- 適應症 TEXT
92
- );
93
- """)
94
- conn.commit()
95
- conn.close()
96
- init_reminders_table()
97
-
98
- def add_reminder(user_id, medicine, start_date, end_date, times):
99
- print("[DEBUG] add_reminder 被呼叫")
100
- print(f"[DEBUG] 嘗試寫入提醒:{user_id}, {medicine}, {start_date}, {end_date}, {times}")
101
- conn = sqlite3.connect(DB_PATH)
102
- cursor = conn.cursor()
103
- cursor.execute(
104
- "INSERT INTO reminders (user_id, medicine, start_date, end_date, times, sent) VALUES (?, ?, ?, ?, ?, 0)",
105
- (user_id, medicine, start_date, end_date, json.dumps(times))
106
- )
107
- conn.commit()
108
- cursor.execute("SELECT * FROM reminders")
109
- print("[DEBUG] reminders 資料表內容:", cursor.fetchall())
110
- conn.close()
111
- print("[DEBUG] ✅ 寫入 reminders 成功")
112
-
113
- def check_and_send_reminders():
114
- tz = pytz.timezone('Asia/Taipei')
115
- now = datetime.datetime.now(tz)
116
- today = now.strftime("%Y-%m-%d")
117
- now_time = now.strftime("%H:%M")
118
- conn = sqlite3.connect(DB_PATH)
119
- cursor = conn.cursor()
120
- cursor.execute("SELECT id, user_id, medicine, start_date, end_date, times FROM reminders")
121
- rows = cursor.fetchall()
122
- for rid, user_id, medicine, start_date, end_date, times_json in rows:
123
- if start_date <= today <= end_date:
124
- times = json.loads(times_json)
125
- for t in times:
126
- cursor.execute("SELECT COUNT(*) FROM reminders_log WHERE reminder_id=? AND date=? AND time=?", (rid, today, t))
127
- if now_time == t and cursor.fetchone()[0] == 0:
128
- print(f"[DEBUG] 發送提醒給 {user_id}:{medicine} @ {t}")
129
- with ApiClient(configuration) as api_client:
130
- messaging_api = MessagingApi(api_client)
131
- messaging_api.push_message(
132
- push_message_request=PushMessageRequest(
133
- to=user_id,
134
- messages=[TextMessage(text=f"⏰ 用藥提醒:該服用「{medicine}」囉!")]
135
- )
136
- )
137
- cursor.execute("INSERT INTO reminders_log (reminder_id, date, time) VALUES (?, ?, ?)", (rid, today, t))
138
- conn.commit()
139
- conn.close()
140
-
141
- if not hasattr(app, "reminder_scheduler_started"):
142
- scheduler = BackgroundScheduler()
143
- scheduler.add_job(check_and_send_reminders, 'interval', seconds=20)
144
- scheduler.start()
145
- app.reminder_scheduler_started = True
146
-
147
  @app.route("/images/<filename>")
148
  def serve_image(filename):
149
  return send_from_directory(static_tmp_path, filename)
@@ -152,29 +126,23 @@ def serve_image(filename):
152
  def home():
153
  return {"message": "Line Webhook Server"}
154
 
155
- @app.route("/show_reminders")
156
- def show_reminders():
157
- conn = sqlite3.connect(DB_PATH)
158
- cursor = conn.cursor()
159
- cursor.execute("SELECT * FROM reminders")
160
- rows = cursor.fetchall()
161
- conn.close()
162
- print("[DEBUG] /show_reminders 查詢結果:", rows)
163
- return {"reminders": rows}
164
-
165
  @app.route("/callback", methods=["POST"])
166
  def callback():
 
 
 
 
 
 
167
  signature = request.headers.get("X-Line-Signature", "")
168
  body = request.get_data(as_text=True)
169
- print(f"[DEBUG] 收到 callback 請求,body={body}")
170
 
171
  try:
172
  events = parser.parse(body, signature)
173
  except InvalidSignatureError:
174
- print("[DEBUG] InvalidSignatureError")
175
  abort(400)
176
  except Exception as e:
177
- print("[DEBUG] Webhook parse error:", e)
178
  abort(400)
179
 
180
  with ApiClient(configuration) as api_client:
@@ -182,470 +150,220 @@ def callback():
182
  blob_api = MessagingApiBlob(api_client)
183
 
184
  for event in events:
185
- print(f"[DEBUG] event.type={event.type}, event={event}")
186
- # ====== 用藥提醒對話流程 ======
187
- if event.type == "message" and event.message.type == "text":
188
- user_id = event.source.user_id
189
- user_input = event.message.text.strip()
190
- print(f"[DEBUG] user_input: {user_input}, user_states: {user_states.get(user_id)}")
191
- # 新增常用藥物查詢與修改
192
- if user_input == "常用藥物":
193
- conn = sqlite3.connect(DB_PATH)
194
- cursor = conn.cursor()
195
- cursor.execute("SELECT DISTINCT medicine FROM reminders WHERE user_id=?", (user_id,))
196
- medicines = [row[0] for row in cursor.fetchall()]
197
- conn.close()
198
- if not medicines:
199
- reply_text = "你還沒有設定過任何藥物提醒。"
200
- reply_request = ReplyMessageRequest(
201
- reply_token=event.reply_token,
202
- messages=[TextMessage(text=reply_text)]
203
- )
204
- messaging_api.reply_message(reply_message_request=reply_request)
205
- return "OK"
206
- quick_reply = QuickReply(
207
- items=[QuickReplyItem(action=TextMessage(text=med, label=med)) for med in medicines]
208
- )
209
- reply_text = "請選擇你要修改的藥品:"
210
- reply_request = ReplyMessageRequest(
211
- reply_token=event.reply_token,
212
- messages=[TextMessage(text=reply_text, quick_reply=quick_reply)]
213
- )
214
- messaging_api.reply_message(reply_message_request=reply_request)
215
- user_states[user_id] = {'step': 'edit_medicine'}
216
- return "OK"
217
- elif user_id in user_states:
218
- state = user_states[user_id]
219
- print(f"[DEBUG] user_states[{user_id}] = {state}")
220
- if state.get('step') == 'ask_medicine':
221
- state['medicine'] = user_input
222
- state['step'] = 'ask_start'
223
- print(f"[DEBUG] 進入 ask_start, user_id={user_id}, medicine={user_input}")
224
- quick_reply = QuickReply(
225
- items=[
226
- QuickReplyItem(
227
- action=DatetimePickerAction(
228
- label="選擇開始日期",
229
- data="start_date",
230
- mode="date"
231
- )
232
- )
233
- ]
234
- )
235
- reply_text = "請選擇提醒開始日期:"
236
- reply_request = ReplyMessageRequest(
237
- reply_token=event.reply_token,
238
- messages=[TextMessage(text=reply_text, quick_reply=quick_reply)]
239
- )
240
- messaging_api.reply_message(reply_message_request=reply_request)
241
- return "OK"
242
- elif state.get('step') == 'ask_times':
243
- print(f"[DEBUG] 進入 ask_times, user_id={user_id}, state={state}")
244
- times = [t.strip() for t in user_input.split(",") if t.strip()]
245
- # 檢查每個時間格式是否為 HH:MM
246
- import re
247
- valid = True
248
- for t in times:
249
- if not re.match(r"^(?:[01]\d|2[0-3]):[0-5]\d$", t):
250
- valid = False
251
- break
252
- if not times or not valid:
253
- reply_text = "時間格式錯誤,請重新輸入(24小時制,如 08:00,12:00,18:00):"
254
- reply_request = ReplyMessageRequest(
255
- reply_token=event.reply_token,
256
- messages=[TextMessage(text=reply_text)]
257
- )
258
- messaging_api.reply_message(reply_message_request=reply_request)
259
- return "OK"
260
- # 時間格式正確才繼續
261
- add_reminder(user_id, state['medicine'], state['start_date'], state['end_date'], times)
262
- reply_text = f"已設定提醒:{state['medicine']}\n從 {state['start_date']} 到 {state['end_date']}\n每天:{', '.join(times)}"
263
  reply_request = ReplyMessageRequest(
264
  reply_token=event.reply_token,
265
  messages=[TextMessage(text=reply_text)]
266
  )
267
  messaging_api.reply_message(reply_message_request=reply_request)
268
- user_states.pop(user_id, None)
269
- print(f"[DEBUG] 完成提醒流程,user_states 移除 {user_id}")
270
- return "OK"
271
- # ====== 常用物修改流程 ======
272
- elif state.get('step') == 'edit_medicine':
273
- selected_medicine = user_input
274
- conn = sqlite3.connect(DB_PATH)
275
- cursor = conn.cursor()
276
- cursor.execute(
277
- "SELECT id, start_date, end_date, times FROM reminders WHERE user_id=? AND medicine=? ORDER BY id DESC LIMIT 1",
278
- (user_id, selected_medicine)
279
- )
280
- row = cursor.fetchone()
281
- conn.close()
282
- if not row:
283
- reply_text = "查無此藥品提醒資料。"
284
- reply_request = ReplyMessageRequest(
285
- reply_token=event.reply_token,
286
- messages=[TextMessage(text=reply_text)]
287
- )
288
- messaging_api.reply_message(reply_message_request=reply_request)
289
- user_states.pop(user_id, None)
290
- return "OK"
291
- reminder_id, start_date, end_date, times_json = row
292
- times = ','.join(json.loads(times_json))
293
- reply_text = (
294
- f"你目前的提醒設定:\n"
295
- f"藥品:{selected_medicine}\n"
296
- f"開始:{start_date}\n"
297
- f"結束:{end_date}\n"
298
- f"時間:{times}\n"
299
- "請輸入你要修改的欄位(開始日期/結束日期/時間),或輸入 skip 跳過:"
300
- )
301
- state['step'] = 'edit_field'
302
- state['reminder_id'] = reminder_id
303
- state['medicine'] = selected_medicine
304
  reply_request = ReplyMessageRequest(
305
  reply_token=event.reply_token,
306
  messages=[TextMessage(text=reply_text)]
307
  )
308
  messaging_api.reply_message(reply_message_request=reply_request)
309
- return "OK"
310
- elif state.get('step') == 'edit_field':
311
- field = user_input.strip()
312
- if field == "開始日期":
313
- state['step'] = 'edit_start_date'
314
- reply_text = "請輸入新的開始日期(YYYY-MM-DD):"
315
- elif field == "結束日期":
316
- state['step'] = 'edit_end_date'
317
- reply_text = "請輸入新的結束日期(YYYY-MM-DD):"
318
- elif field == "時間":
319
- state['step'] = 'edit_times'
320
- reply_text = "請輸入新的提醒時間(24小時制,用逗號分隔):"
321
- elif field.lower() == "skip":
322
- reply_text = "已取消修改。"
323
- user_states.pop(user_id, None)
324
- else:
325
- reply_text = "請輸入「開始日期」、「結束日期」或「時間」,或輸入 skip 跳過:"
326
- reply_request = ReplyMessageRequest(
327
- reply_token=event.reply_token,
328
- messages=[TextMessage(text=reply_text)]
329
  )
330
- messaging_api.reply_message(reply_message_request=reply_request)
331
- if field.lower() == "skip":
332
- return "OK"
333
- else:
334
- return "OK"
335
- elif state.get('step') == 'edit_start_date':
336
- new_start = user_input.strip()
337
- conn = sqlite3.connect(DB_PATH)
338
- cursor = conn.cursor()
339
- cursor.execute("UPDATE reminders SET start_date=? WHERE id=?", (new_start, state['reminder_id']))
340
- conn.commit()
341
- conn.close()
342
- reply_text = "開始日期已更新!"
343
  reply_request = ReplyMessageRequest(
344
  reply_token=event.reply_token,
345
- messages=[TextMessage(text=reply_text)]
346
  )
347
  messaging_api.reply_message(reply_message_request=reply_request)
348
- user_states.pop(user_id, None)
349
- return "OK"
350
- elif state.get('step') == 'edit_end_date':
351
- new_end = user_input.strip()
352
- conn = sqlite3.connect(DB_PATH)
353
- cursor = conn.cursor()
354
- cursor.execute("UPDATE reminders SET end_date=? WHERE id=?", (new_end, state['reminder_id']))
355
- conn.commit()
356
- conn.close()
357
- reply_text = "結束日期已更新!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  reply_request = ReplyMessageRequest(
359
  reply_token=event.reply_token,
360
- messages=[TextMessage(text=reply_text)]
361
  )
362
  messaging_api.reply_message(reply_message_request=reply_request)
363
- user_states.pop(user_id, None)
364
- return "OK"
365
- elif state.get('step') == 'edit_times':
366
- import re
367
- times = [t.strip() for t in user_input.split(",") if t.strip()]
368
- valid = all(re.match(r"^(?:[01]\d|2[0-3]):[0-5]\d$", t) for t in times)
369
- if not times or not valid:
370
- reply_text = "時間格式錯誤,請重新輸入(24小時制,如 08:00,12:00,18:00):"
371
- reply_request = ReplyMessageRequest(
372
- reply_token=event.reply_token,
373
- messages=[TextMessage(text=reply_text)]
374
- )
375
- messaging_api.reply_message(reply_message_request=reply_request)
376
- return "OK"
377
- conn = sqlite3.connect(DB_PATH)
378
- cursor = conn.cursor()
379
- cursor.execute("UPDATE reminders SET times=? WHERE id=?", (json.dumps(times), state['reminder_id']))
380
- conn.commit()
381
- conn.close()
382
- reply_text = "提醒時間已更新!"
383
  reply_request = ReplyMessageRequest(
384
  reply_token=event.reply_token,
385
- messages=[TextMessage(text=reply_text)]
386
  )
387
  messaging_api.reply_message(reply_message_request=reply_request)
388
- user_states.pop(user_id, None)
389
- return "OK"
390
-
391
- # ====== 原有功能區塊(查詢藥品、AI、藥局) ======
392
- user_input = event.message.text.strip()
393
- print("[DEBUG] 進入原有功能區塊,收到訊息:", user_input)
394
 
395
- # AI 問答
396
- if user_input.startswith("AI "):
397
- prompt = "你是一個中文的AI助手,請用繁體中文回答。\n" + user_input[3:].strip()
398
- try:
399
- response = chat.generate_content(prompt)
400
- reply_text = response.text
401
- except Exception as e:
402
- reply_text = f"AI 回答失敗:{e}"
403
 
404
- reply_request = ReplyMessageRequest(
405
- reply_token=event.reply_token,
406
- messages=[TextMessage(text=reply_text)]
407
  )
408
- messaging_api.reply_message(reply_message_request=reply_request)
 
409
 
410
- # 查詢藥品
411
- elif user_input == "查詢藥品":
412
- reply_text = "請輸入藥品名稱"
413
- reply_request = ReplyMessageRequest(
414
- reply_token=event.reply_token,
415
- messages=[TextMessage(text=reply_text)]
416
  )
417
- messaging_api.reply_message(reply_message_request=reply_request)
418
- #圖片查詢
419
- elif user_input == "圖片查詢":
420
- reply_text = "請上傳藥品圖片"
421
- reply_request = ReplyMessageRequest(
422
- reply_token=event.reply_token,
423
- messages=[TextMessage(text=reply_text)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  )
425
- messaging_api.reply_message(reply_message_request=reply_request)
426
-
427
- # 查詢藥局
428
- elif "查詢藥局" in user_input:
429
- quick_reply = QuickReply(
430
- items=[QuickReplyItem(action=LocationAction(label="傳送我的位置"))]
431
  )
 
432
  reply_request = ReplyMessageRequest(
433
  reply_token=event.reply_token,
434
- messages=[TextMessage(text="請點選下方按鈕傳送你的位置,我才能幫你找附近藥局喔~", quick_reply=quick_reply)]
435
  )
436
  messaging_api.reply_message(reply_message_request=reply_request)
437
 
438
- # 查詢藥品資料庫/AI
439
- else:
440
- medicine_name = user_input
441
  try:
442
- conn = sqlite3.connect(DB_PATH)
443
- cursor = conn.cursor()
444
- query = """
445
- SELECT DISTINCT 中文品名, 英文品名, 適應症
446
- FROM drugs
447
- WHERE 中文品名 LIKE ? OR 英文品名 LIKE ?
448
- LIMIT 1
449
- """
450
- like_param = f'%{medicine_name}%'
451
- cursor.execute(query, (like_param, like_param))
452
- row = cursor.fetchone()
453
- conn.close()
454
- print(f"[DEBUG] 查詢 drugs 結果{row}")
455
-
456
- if row:
457
- zh_name, en_name, indication = row
458
- # 副作用由 AI 產生
459
- prompt = (
460
- f"請只用簡短條列式(每點用-開頭,不要用*),僅列出副作用,"
461
- f"針對藥品「{zh_name}」(英文名:{en_name}),"
462
- "請用繁體中文回答,不要加任何說明、警語或強調語句。"
463
- "請用繁體中文回答,不要使用 Markdown 格式或 `*` 符號,不要提到你是 AI。"
464
- )
465
- try:
466
- ai_resp = chat.generate_content(prompt)
467
- side_effects = ai_resp.text.strip()
468
- except Exception as e:
469
- side_effects = f"AI 回答失敗:{e}"
470
- reply_text = (
471
- f"🔹 中文品名:{zh_name}\n"
472
- f"📌 英文品名:{en_name}\n"
473
- f"📄 適應症:{indication}\n"
474
- f"⚠️ 副作用:{side_effects}"
475
- )
476
- else:
477
- prompt = (
478
- f"請用以下格式,幫我介紹藥品「{medicine_name}」,"
479
- "請用繁體中文回答,不要使用 Markdown 格式或 `*` 符號,不要提到你是 AI。"
480
- "只要條列資料本身,不要加任何說明、警語或強調語句:\n"
481
- "🔹 中文品名:\n"
482
- "📌 英文品名:\n"
483
- "📄 適應症:\n"
484
- "⚠️ 副作用:(請用-開頭條列,不要用*)"
485
- )
486
- try:
487
- ai_resp = chat.generate_content(prompt)
488
- reply_text = ai_resp.text
489
- except Exception as e:
490
- reply_text = f"AI 回答失敗:{e}"
491
 
 
 
 
 
 
 
 
 
 
 
 
492
  except Exception as e:
493
- reply_text = f"⚠️ 查詢資料時發生錯誤:{str(e)}"
494
-
495
- reply_request = ReplyMessageRequest(
496
- reply_token=event.reply_token,
497
- messages=[TextMessage(text=reply_text.strip())]
498
- )
499
- messaging_api.reply_message(reply_message_request=reply_request)
500
-
501
- elif event.type == "message" and event.message.type == "location":
502
- print("[DEBUG] 收到位置訊息")
503
- user_lat = event.message.latitude
504
- user_lng = event.message.longitude
505
-
506
- nearby_url = (
507
- f"https://maps.googleapis.com/maps/api/place/nearbysearch/json?"
508
- f"location={user_lat},{user_lng}&radius=1000&type=pharmacy&key={GOOGLE_MAP_API_KEY}"
509
- )
510
- nearby_res = requests.get(nearby_url).json()
511
- print(f"[DEBUG] nearby_res: {nearby_res}")
512
-
513
- if not nearby_res.get('results'):
514
- reply_request = ReplyMessageRequest(
515
- reply_token=event.reply_token,
516
- messages=[TextMessage(text="附近找不到藥局")]
517
- )
518
- messaging_api.reply_message(reply_message_request=reply_request)
519
- continue
520
-
521
- place = nearby_res['results'][0]
522
- place_id = place['place_id']
523
- name = place.get('name', '藥局名稱未知')
524
- location = place['geometry']['location']
525
- dest_lat, dest_lng = location['lat'], location['lng']
526
-
527
- details_url = (
528
- f"https://maps.googleapis.com/maps/api/place/details/json?"
529
- f"place_id={place_id}&fields=name,formatted_phone_number&key={GOOGLE_MAP_API_KEY}"
530
- )
531
- details_res = requests.get(details_url).json()
532
- phone = details_res.get('result', {}).get('formatted_phone_number', '電話不詳')
533
-
534
- dist_url = (
535
- f"https://maps.googleapis.com/maps/api/distancematrix/json?"
536
- f"origins={user_lat},{user_lng}&destinations={dest_lat},{dest_lng}&key={GOOGLE_MAP_API_KEY}"
537
- )
538
- dist_res = requests.get(dist_url).json()
539
- distance = dist_res['rows'][0]['elements'][0]['distance']['text']
540
-
541
- map_url = f"https://www.google.com/maps/search/?api=1&query={dest_lat},{dest_lng}"
542
-
543
- bubble = FlexBubble(
544
- body=FlexBox(
545
- layout="vertical",
546
- contents=[
547
- FlexText(text=name, weight="bold", size="lg"),
548
- FlexText(text=f"電話:{phone}", size="sm", color="#555555"),
549
- FlexText(text=f"距離:{distance}", size="sm", color="#777777"),
550
- ],
551
- ),
552
- footer=FlexBox(
553
- layout="vertical",
554
- contents=[
555
- FlexButton(
556
- style="link",
557
- height="sm",
558
- action=URIAction(label="地圖導航", uri=map_url),
559
- )
560
- ],
561
- ),
562
- )
563
-
564
- flex_message = FlexMessage(
565
- alt_text="附近藥局推薦",
566
- contents=bubble
567
- )
568
-
569
- reply_request = ReplyMessageRequest(
570
- reply_token=event.reply_token,
571
- messages=[flex_message]
572
- )
573
- messaging_api.reply_message(reply_message_request=reply_request)
574
- elif event.type == "message" and event.message.type == "image":
575
- print("[DEBUG] 收到圖片訊息")
576
- try:
577
- content = blob_api.get_message_content(message_id=event.message.id)
578
- with tempfile.NamedTemporaryFile(dir=static_tmp_path, suffix=".jpg", delete=False) as tf:
579
- tf.write(content)
580
- filename = os.path.basename(tf.name)
581
- image = Image.open(tf.name)
582
-
583
- prompt = (
584
- "請根據這張圖片判斷藥品資訊,若圖片無法判斷適應症或副作用,請根據藥品名稱推測並補充,"
585
- "請用繁體中文回答,不要使用 Markdown 格式或 `*` 符號,不要提到你是 AI。"
586
- "只要條列資料本身,不要加任何說明、警語或強調語句,也不要加**:\n"
587
- "🔹 中文品名:\n"
588
- "📌 英文品名:\n"
589
- "📄 適應症:\n"
590
- "⚠️ 副作用:(請用-開頭條列,不要用*)"
591
- )
592
- response = chat.generate_content([image, prompt])
593
- description = response.text
594
-
595
- reply_request = ReplyMessageRequest(
596
- reply_token=event.reply_token,
597
- messages=[TextMessage(text=description.strip())]
598
- )
599
- messaging_api.reply_message(reply_message_request=reply_request)
600
- except Exception as e:
601
- print(f"[DEBUG] 圖片處理失敗:{e}")
602
- reply_request = ReplyMessageRequest(
603
- reply_token=event.reply_token,
604
- messages=[TextMessage(text=f"圖片處理失敗:{e}")]
605
- )
606
- messaging_api.reply_message(reply_message_request=reply_request)
607
-
608
-
609
- elif event.type == "postback":
610
- user_id = event.source.user_id
611
- data = event.postback.data
612
- print(f"[DEBUG] postback data: {data}, user_states: {user_states.get(user_id)}")
613
- if data == "start_date":
614
- user_states[user_id]['start_date'] = event.postback.params['date']
615
- user_states[user_id]['step'] = 'ask_end'
616
- print(f"[DEBUG] 已設定 start_date={event.postback.params['date']}")
617
- quick_reply = QuickReply(
618
- items=[
619
- QuickReplyItem(
620
- action=DatetimePickerAction(
621
- label="選擇結束日期",
622
- data="end_date",
623
- mode="date"
624
- )
625
- )
626
- ]
627
- )
628
- reply_text = "請選擇提醒結束日期:"
629
- reply_request = ReplyMessageRequest(
630
- reply_token=event.reply_token,
631
- messages=[TextMessage(text=reply_text, quick_reply=quick_reply)]
632
- )
633
- messaging_api.reply_message(reply_message_request=reply_request)
634
- return "OK"
635
- elif data == "end_date":
636
- user_states[user_id]['end_date'] = event.postback.params['date']
637
- user_states[user_id]['step'] = 'ask_times'
638
- print(f"[DEBUG] 已設定 end_date={event.postback.params['date']}")
639
- reply_text = "請輸入每天要提醒的時間(24小時制,可多個,用逗號分隔,如 08:00,12:00,18:00):"
640
- reply_request = ReplyMessageRequest(
641
- reply_token=event.reply_token,
642
- messages=[TextMessage(text=reply_text)]
643
- )
644
- messaging_api.reply_message(reply_message_request=reply_request)
645
- return "OK"
646
 
647
- print("[DEBUG] callback 執行結束")
648
  return "OK"
649
 
650
  if __name__ == "__main__":
651
- app.run(host="0.0.0.0", port=7860)
 
 
12
  from linebot.v3.webhooks import MessageEvent, TextMessageContent, ImageMessageContent
13
  from linebot.v3.messaging import MessagingApi, Configuration, ApiClient, MessagingApiBlob
14
  from linebot.v3.messaging.models import (
15
+ TextMessage, ReplyMessageRequest,
16
  FlexMessage, FlexBubble, FlexBox, FlexText, FlexButton, URIAction,
17
+ QuickReply, QuickReplyItem, LocationAction, ImageMessage
18
  )
19
  from linebot.v3.exceptions import InvalidSignatureError
20
 
21
  import google.generativeai as genai
22
+ import io
23
  import json
24
+ from dotenv import load_dotenv
 
 
25
 
26
+ # --- Google Drive API 相關導入 ---
27
+ from google.oauth2 import service_account # 修改這裡
28
+ from googleapiclient.discovery import build
29
+ from googleapiclient.http import MediaIoBaseDownload
30
+ # --- END Google Drive API 相關導入 ---
31
 
32
+ load_dotenv()
 
33
 
34
  app = Flask(__name__)
35
 
36
+ # 從環境變數讀取 LINE Bot 設定
37
+ LINE_CHANNEL_SECRET = os.environ.get("LINE_CHANNEL_SECRET")
38
+ LINE_CHANNEL_ACCESS_TOKEN = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN")
39
 
40
+ if not LINE_CHANNEL_SECRET or not LINE_CHANNEL_ACCESS_TOKEN:
41
+ raise RuntimeError("Missing essential environment variables")
 
 
 
 
 
 
42
 
43
+ print(f"LINE_CHANNEL_SECRET: {LINE_CHANNEL_SECRET}")
44
+ print(f"LINE_CHANNEL_ACCESS_TOKEN: {LINE_CHANNEL_ACCESS_TOKEN}")
45
+ #print(f"GOOGLE_MAP_API_KEY: {GOOGLE_MAP_API_KEY}")
46
 
47
+ # --- 資料庫路徑設定 ---
48
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
49
+ DB_FILENAME = "medicine.db"
50
+ DB_PATH = os.path.join(BASE_DIR, DB_FILENAME) # 確保 DB_PATH 指向容器內的預期路徑
51
+ # --- END 資料庫路徑設定 ---
52
+
53
+ # --- Google Drive 下載函式 ---
54
+ def download_db_from_google_drive():
55
+ print("Attempting to download database from Google Drive...")
56
+ google_creds_json_str = os.getenv("GOOGLE_CREDENTIALS_JSON")
57
+ file_id = os.getenv("GOOGLE_DRIVE_FILE_ID")
58
+
59
+ if not google_creds_json_str:
60
+ print("Error: GOOGLE_CREDENTIALS_JSON secret not found.")
61
+ return False
62
+ if not file_id:
63
+ print("Error: GOOGLE_DRIVE_FILE_ID secret not found.")
64
+ return False
65
+
66
+ try:
67
+ # 將 JSON 字串轉換為字典
68
+ creds_info = json.loads(google_creds_json_str)
69
+ # 從服務帳戶資訊建立憑證
70
+ creds = service_account.Credentials.from_service_account_info(
71
+ creds_info,
72
+ scopes=['https://www.googleapis.com/auth/drive.readonly'] # 只需要唯讀權限
73
+ )
74
+ drive_service = build('drive', 'v3', credentials=creds)
75
+
76
+ request_dl = drive_service.files().get_media(fileId=file_id)
77
+ # fh = io.BytesIO() # 記憶體中處理
78
+ # downloader = MediaIoBaseDownload(fh, request_dl)
79
+
80
+ # 直接寫入檔案
81
+ with open(DB_PATH, 'wb') as fh:
82
+ downloader = MediaIoBaseDownload(fh, request_dl)
83
+ done = False
84
+ while done is False:
85
+ status, done = downloader.next_chunk()
86
+ print(f"Download {int(status.progress() * 100)}%.")
87
+ print(f"Database '{DB_FILENAME}' downloaded successfully to '{DB_PATH}'.")
88
+ return True
89
+ except Exception as e:
90
+ print(f"Error downloading database from Google Drive: {e}")
91
+ return False
92
+ # --- END Google Drive 下載函式 ---
93
+
94
+ # --- 在應用程式啟動時執行資料庫下載 ---
95
+ DOWNLOAD_SUCCESS = False # Initialize
96
+ # Check if running in Hugging Face Space or if secrets are generally available
97
+ # In HF Spaces, secrets are environment variables. Locally, .env would be used.
98
+ if os.getenv("GOOGLE_CREDENTIALS_JSON") and os.getenv("GOOGLE_DRIVE_FILE_ID"):
99
+ print("Found Google Drive credentials, attempting download...")
100
+ DOWNLOAD_SUCCESS = download_db_from_google_drive()
101
+ else:
102
+ print("Warning: GOOGLE_CREDENTIALS_JSON or GOOGLE_DRIVE_FILE_ID not found in environment. Skipping DB download.")
103
+ print("If running locally, ensure they are in your .env file or environment.")
104
+ print("If on Hugging Face, ensure secrets are set in Space settings.")
105
+
106
+ if not DOWNLOAD_SUCCESS:
107
+ print("CRITICAL: Database download failed or was skipped. The application might not function as expected if the database is required at startup.")
108
+ # --- END 應用程式啟動時執行資料庫下載 ---
109
+
110
+ configuration = Configuration(access_token=LINE_CHANNEL_ACCESS_TOKEN)
111
+ parser = WebhookParser(LINE_CHANNEL_SECRET)
112
+ handler = WebhookHandler(LINE_CHANNEL_SECRET)
113
 
114
  genai.configure(api_key=GOOGLE_API_KEY)
115
+ text_system_prompt = "你是一個專業的中文藥物安全衛教AI,運行於Linebot平台,負責為台灣用戶提供用藥查詢、衛教提醒、藥品辨識與互動諮詢。所有回應必須以繁體中文呈現,語氣需保持專業、中立、清晰,嚴禁使用非正式語彙或網路用語。你的回答僅限於台灣現行合法藥品、常見用藥安全及一般衛教知識,絕不涉及診斷、處方或違法用途。遇重要藥品資訊或警語時,務必標示資料來源(如衛福部、健保署或官方藥物資料庫);無法查證時,需說明資訊有限並提醒用戶諮詢藥師。遇到模糊、非藥物相關、或疑似緊急情境(如中毒、嚴重過敏),請直接回覆:「請儘速就醫或聯絡藥師,Linebot無法提供緊急醫療協助。」回答時,優先給出簡明結論,再補充必要說明,遇複雜內容可分點陳述,藥品名稱、注意事項及用法用量需明顯標註。若用戶詢問非本功能範圍問題,請回覆:「本Linebot僅提供藥物安全與衛生教育資訊。」並簡要列舉可查詢主題(如用藥禁忌、藥物交互作用、藥品保存方式等)。所有資訊僅反映截至2025年6月之官方資料,若遇新藥、召回或重大警訊,應提醒用戶查閱衛福部或官方藥事機構。"
116
  chat = genai.GenerativeModel(model_name="gemini-1.5-flash")
117
 
118
  logging.basicConfig(level=logging.INFO)
119
  app.logger.setLevel(logging.INFO)
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  @app.route("/images/<filename>")
122
  def serve_image(filename):
123
  return send_from_directory(static_tmp_path, filename)
 
126
  def home():
127
  return {"message": "Line Webhook Server"}
128
 
 
 
 
 
 
 
 
 
 
 
129
  @app.route("/callback", methods=["POST"])
130
  def callback():
131
+
132
+ # 檢查資料庫是否已成功下載 (可選,但建議)
133
+ if not DOWNLOAD_SUCCESS and not os.path.exists(DB_PATH):
134
+ print("Database not available, aborting callback.")
135
+ abort(500) # 或返回一個提示用戶稍後再試的訊息
136
+
137
  signature = request.headers.get("X-Line-Signature", "")
138
  body = request.get_data(as_text=True)
 
139
 
140
  try:
141
  events = parser.parse(body, signature)
142
  except InvalidSignatureError:
 
143
  abort(400)
144
  except Exception as e:
145
+ print("Webhook parse error:", e)
146
  abort(400)
147
 
148
  with ApiClient(configuration) as api_client:
 
150
  blob_api = MessagingApiBlob(api_client)
151
 
152
  for event in events:
153
+ if event.type == "message":
154
+ # 文字訊息
155
+ if event.message.type == "text":
156
+ user_input = event.message.text.strip()
157
+ print("📨 收到訊息:", user_input)
158
+
159
+ # AI 問答
160
+ if user_input.startswith("AI "):
161
+ prompt = text_system_prompt + "\n" + user_input[3:].strip()
162
+ try:
163
+ response = chat.generate_content(prompt)
164
+ reply_text = response.text
165
+ except Exception as e:
166
+ reply_text = f"AI 回答失敗:{e}"
167
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  reply_request = ReplyMessageRequest(
169
  reply_token=event.reply_token,
170
  messages=[TextMessage(text=reply_text)]
171
  )
172
  messaging_api.reply_message(reply_message_request=reply_request)
173
+
174
+ # 查詢藥品
175
+ elif user_input == "查詢藥品":
176
+ reply_text = "請輸入品名稱,例如:口服感冒藥"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  reply_request = ReplyMessageRequest(
178
  reply_token=event.reply_token,
179
  messages=[TextMessage(text=reply_text)]
180
  )
181
  messaging_api.reply_message(reply_message_request=reply_request)
182
+
183
+ # 查詢藥局
184
+ elif "查詢藥局" in user_input:
185
+ quick_reply = QuickReply(
186
+ items=[QuickReplyItem(action=LocationAction(label="傳送我的位置"))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  reply_request = ReplyMessageRequest(
189
  reply_token=event.reply_token,
190
+ messages=[TextMessage(text="請點選下方按鈕傳送你的位置,我才能幫你找附近藥局喔~", quick_reply=quick_reply)]
191
  )
192
  messaging_api.reply_message(reply_message_request=reply_request)
193
+
194
+ # 其他:查詢藥品資料庫,副作用永遠由AI產生
195
+ else:
196
+ medicine_name = user_input.lower()
197
+ try:
198
+ conn = sqlite3.connect(DB_PATH)
199
+ cursor = conn.cursor()
200
+ query = """
201
+ SELECT DISTINCT 中文品名, 英文品名, 適應症
202
+ FROM drugs
203
+ WHERE LOWER(中文品名) LIKE ?
204
+ LIMIT 3
205
+ """
206
+ like_param = f'%{medicine_name}%'
207
+ cursor.execute(query, (like_param,))
208
+ rows = cursor.fetchall()
209
+ conn.close()
210
+
211
+ if rows:
212
+ zh_name, en_name, indication = rows[0]
213
+ # 副作用由 AI 產生
214
+ prompt = text_system_prompt + "\n" + (
215
+ f"請用簡短條列式,僅列出副作用,針對藥品「{zh_name}」(英文名:{en_name}),"
216
+ "請用繁體中文回答,若無法判斷請推測。"
217
+ )
218
+ try:
219
+ ai_resp = chat.generate_content(prompt)
220
+ side_effects = ai_resp.text.strip()
221
+ except Exception as e:
222
+ side_effects = f"系統繁忙,請稍後再試:{e}"
223
+ reply_text = (
224
+ f"🔹 中文品名:{zh_name}\n"
225
+ f"📌 英文品名:{en_name}\n"
226
+ f"📄 適應症:{indication}\n"
227
+ f"⚠️ 副作用:{side_effects}"
228
+ )
229
+ else:
230
+ # 全部請AI生成
231
+ prompt = text_system_prompt + "\n" + (
232
+ f"請用以下格式,幫我介紹藥品「{medicine_name}」,若無法查到請盡量推測:\n"
233
+ "🔹 中文品名:\n"
234
+ "📌 英文品名:\n"
235
+ "📄 適應症:\n"
236
+ "⚠️ 副作用:"
237
+ )
238
+ try:
239
+ ai_resp = chat.generate_content(prompt)
240
+ reply_text = ai_resp.text
241
+ except Exception as e:
242
+ reply_text = f"系統繁忙,請稍後再試:{e}"
243
+
244
+ except Exception as e:
245
+ reply_text = f"⚠️ 查詢資料時發生錯誤:{str(e)}"
246
+
247
  reply_request = ReplyMessageRequest(
248
  reply_token=event.reply_token,
249
+ messages=[TextMessage(text=reply_text.strip())]
250
  )
251
  messaging_api.reply_message(reply_message_request=reply_request)
252
+
253
+ # 處理位置訊息(查詢附近藥局)
254
+ elif event.message.type == "location":
255
+ user_lat = event.message.latitude
256
+ user_lng = event.message.longitude
257
+
258
+ nearby_url = (
259
+ f"https://maps.googleapis.com/maps/api/place/nearbysearch/json?"
260
+ f"location={user_lat},{user_lng}&radius=1000&type=pharmacy&key={GOOGLE_MAP_API_KEY}"
261
+ )
262
+ nearby_res = requests.get(nearby_url).json()
263
+
264
+ if not nearby_res.get('results'):
 
 
 
 
 
 
 
265
  reply_request = ReplyMessageRequest(
266
  reply_token=event.reply_token,
267
+ messages=[TextMessage(text="附近找不到藥局")]
268
  )
269
  messaging_api.reply_message(reply_message_request=reply_request)
270
+ continue
 
 
 
 
 
271
 
272
+ place = nearby_res['results'][0]
273
+ place_id = place['place_id']
274
+ name = place.get('name', '藥局名稱未知')
275
+ location = place['geometry']['location']
276
+ dest_lat, dest_lng = location['lat'], location['lng']
 
 
 
277
 
278
+ details_url = (
279
+ f"https://maps.googleapis.com/maps/api/place/details/json?"
280
+ f"place_id={place_id}&fields=name,formatted_phone_number&key={GOOGLE_MAP_API_KEY}"
281
  )
282
+ details_res = requests.get(details_url).json()
283
+ phone = details_res.get('result', {}).get('formatted_phone_number', '電話不詳')
284
 
285
+ dist_url = (
286
+ f"https://maps.googleapis.com/maps/api/distancematrix/json?"
287
+ f"origins={user_lat},{user_lng}&destinations={dest_lat},{dest_lng}&key={GOOGLE_MAP_API_KEY}"
 
 
 
288
  )
289
+ dist_res = requests.get(dist_url).json()
290
+ distance = dist_res['rows'][0]['elements'][0]['distance']['text']
291
+
292
+ map_url = f"https://www.google.com/maps/search/?api=1&query={dest_lat},{dest_lng}"
293
+
294
+ bubble = FlexBubble(
295
+ body=FlexBox(
296
+ layout="vertical",
297
+ contents=[
298
+ FlexText(text=name, weight="bold", size="lg"),
299
+ FlexText(text=f"電話:{phone}", size="sm", color="#555555"),
300
+ FlexText(text=f"距離:{distance}", size="sm", color="#777777"),
301
+ ],
302
+ ),
303
+ footer=FlexBox(
304
+ layout="vertical",
305
+ contents=[
306
+ FlexButton(
307
+ style="link",
308
+ height="sm",
309
+ action=URIAction(label="地圖導航", uri=map_url),
310
+ )
311
+ ],
312
+ ),
313
  )
314
+
315
+ flex_message = FlexMessage(
316
+ alt_text="附近藥局推薦",
317
+ contents=bubble
 
 
318
  )
319
+
320
  reply_request = ReplyMessageRequest(
321
  reply_token=event.reply_token,
322
+ messages=[flex_message]
323
  )
324
  messaging_api.reply_message(reply_message_request=reply_request)
325
 
326
+ # 圖片訊息:用 Gemini AI 以藥品格式解釋圖片
327
+ elif event.message.type == "image":
 
328
  try:
329
+ content = blob_api.get_message_content(message_id=event.message.id)
330
+ with tempfile.NamedTemporaryFile(dir=static_tmp_path, suffix=".jpg", delete=False) as tf:
331
+ tf.write(content)
332
+ filename = os.path.basename(tf.name)
333
+ image_url = f"https://{base_url}/images/{filename}"
334
+ image = Image.open(tf.name)
335
+
336
+ # Gemini 圖片說明(指定格式,四欄都AI產生)
337
+ prompt = text_system_prompt + "\n" + (
338
+ "請根據這張圖片判斷藥品資訊,並用以下格式回答,若無法判斷請盡量推測:\n"
339
+ "🔹 中文品名:\n"
340
+ "📌 英文品名:\n"
341
+ "📄 適應症\n"
342
+ "⚠️ 副作用:"
343
+ )
344
+ response = chat.generate_content([image, prompt])
345
+ description = response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
+ reply_request = ReplyMessageRequest(
348
+ reply_token=event.reply_token,
349
+ messages=[
350
+ ImageMessage(
351
+ original_content_url=image_url,
352
+ preview_image_url=image_url
353
+ ),
354
+ TextMessage(text=description)
355
+ ]
356
+ )
357
+ messaging_api.reply_message(reply_message_request=reply_request)
358
  except Exception as e:
359
+ reply_request = ReplyMessageRequest(
360
+ reply_token=event.reply_token,
361
+ messages=[TextMessage(text=f"圖片處理失敗:{e}")]
362
+ )
363
+ messaging_api.reply_message(reply_message_request=reply_request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
 
365
  return "OK"
366
 
367
  if __name__ == "__main__":
368
+ port = int(os.environ.get("PORT", 7860)) # 讀取環境變數 PORT,預設為 7860
369
+ app.run(host="0.0.0.0", port=port, debug=False)