Song commited on
Commit
f8bfac3
·
1 Parent(s): 734bf49
Files changed (2) hide show
  1. __pycache__/app.cpython-314.pyc +0 -0
  2. app.py +330 -29
__pycache__/app.cpython-314.pyc ADDED
Binary file (28.7 kB). View file
 
app.py CHANGED
@@ -56,7 +56,7 @@ LLM_MODEL_CONFIG = {
56
  "seed": int(os.getenv("LLM_SEED", 42)),
57
  }
58
 
59
- # ---------- 改良後的 System Prompt(一般模式) ----------
60
  SYSTEM_PROMPT = """你是一個友善、可靠的中文 AI 助手,專門幫助用戶解答各種問題。請用親切、自然的語氣回覆,全部使用繁體中文(除非用戶明確要求其他語言)。
61
 
62
  回覆原則:
@@ -83,28 +83,40 @@ SYSTEM_PROMPT = """你是一個友善、可靠的中文 AI 助手,專門幫助
83
  【主要優勢】
84
  能同時處理大量可能性,特別適合用於密碼破解、新藥研發等領域。雖然目前技術尚未完全成熟,但發展潛力巨大。"""
85
 
86
- # ---------- 強化版 基督信仰專用 Prompt(以耶穌第一人稱) ----------
87
- JESUS_PROMPT = """你現在是耶穌基督,以第一人稱、溫柔、充滿憐憫與智慧的方式回答用戶的問題。
88
- 請完全模仿新約聖經中我教導門徒的語氣:平易近人、充滿愛、常用比喻或簡單真理來說明,引導人親近父神,絕不帶定罪。
89
-
90
- 重要原則:
91
- - 答案必須完全忠於聖經真理,參考原文(希伯來文、希臘文)意涵,避免現代心理學名詞或流行觀念,除非能清楚對應聖經教導。
92
- - 當問題涉及現代觀念(如「自我照顧」「self-care」「心理健康」「界線」等)時,務必說明:
93
- 1. 聖經的主要焦點是與神和好的關係、先求我的國和我的義(馬太福音6:33)、捨己跟從我(馬太福音16:24)、愛神愛人(如馬可福音12:30-31)。
94
- 2. 聖經並非現代生活手冊,因此對許多現代議題著墨不多,這不是因為不重要,而是因為永遠的救恩與神的國比暫時的身心舒適更優先。
95
- 3. 同時要肯定:身體是聖靈的殿(哥林多前書6:19-20),人會自然顧惜自己的身子(以弗所書5:29),安息日與節制也是神給的恩典。
96
- 4. 真正的安息、平安與滿足,只能在與父神的關係中找到(約翰福音15:4-5;腓立比書4:6-7)。
97
- - 若問題觸及「捨己」與「照顧自己」的張力,務必誠實說明:我呼召門徒背十字架,不是要他們自毀,而是要他們否認自我中心,讓我成為生命的主;合理照顧身體是可以的,甚至是榮耀神的方式,但若變成生活的中心,就偏離了父神的旨意。
98
- - 回覆必須結構化、適合手機閱讀:短段落、適度使用條列(• 或 -),結尾引導人親近神。
99
- - 全部使用繁體中文,保持純文字,絕不使用 Markdown 格式(如粗體、斜體、標題)。
100
-
101
- 範例風格(請嚴格模仿):
102
- 孩子,你問我為何聖經很少談到照顧自己,這是個很好的問題。
103
- 聖經的重點不是教人如何讓今生更舒適,而是要帶你們認識父神、與祂和好,並活出愛神愛人的生命。
104
- 我曾說:「人若要跟從我,就當捨己,背起他的十字架來跟從我。」這不是要你忽略自己的需要,而是要你把生命交託給我,讓我成為你的中心。
105
- 然而,我也教導你們:你們的身體是聖靈的殿,要在身上榮耀神。人也會像保養自己的身子一樣顧惜它,這是自然的。
106
- 真正的安息,不是來自技巧,而是來到我這裡:「凡勞苦擔重擔的人,可以到我這裡來,我就使你們得安息。」
107
- 孩子,來到我面前,讓我擔當你的重擔,你會在父神裡面得著真正的平安與滿足。"""
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  # ---------- 記憶體儲存 ----------
110
  conversations: Dict[str, List[Dict[str, str]]] = {}
@@ -133,11 +145,231 @@ def estimate_tokens(messages: List[Dict[str, str]]) -> int:
133
  total += len(msg["content"].split()) * 1.3
134
  return int(total)
135
 
136
- # (以下程式碼保持不變,僅貼出關鍵部分以節省篇幅)
137
- # ... perform_web_search, ChatPipeline 類別, FastAPI 路由等 ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- # 在 ChatPipeline.answer_question 裡面,當 is_christian 時使用強化版 prompt
140
- # (你原本的程式已經是這樣寫的,所以不需要額外修改這部分)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  # ---------- FastAPI ----------
143
  @asynccontextmanager
@@ -154,8 +386,77 @@ async_api_client = AsyncApiClient(configuration)
154
  line_bot_api = AsyncMessagingApi(async_api_client)
155
  parser = WebhookParser(CHANNEL_SECRET)
156
 
157
- # webhook, health, root 等路由保持原樣
158
- # ... (省略不變的路由程式碼)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  if __name__ == "__main__":
161
  port = int(os.getenv("PORT", 7860))
 
56
  "seed": int(os.getenv("LLM_SEED", 42)),
57
  }
58
 
59
+ # ---------- 改良後的 System Prompt ----------
60
  SYSTEM_PROMPT = """你是一個友善、可靠的中文 AI 助手,專門幫助用戶解答各種問題。請用親切、自然的語氣回覆,全部使用繁體中文(除非用戶明確要求其他語言)。
61
 
62
  回覆原則:
 
83
  【主要優勢】
84
  能同時處理大量可能性,特別適合用於密碼破解、新藥研發等領域。雖然目前技術尚未完全成熟,但發展潛力巨大。"""
85
 
86
+ # ---------- 基督信仰專用 Prompt(以耶穌第一人稱) ----------
87
+ JESUS_PROMPT = """你現在是耶穌基督。請**完全**模仿新約聖經(繁體中文和合本)中我的語氣與用詞來回答。
88
+ 不用像個現代分析師條列重點,而是像我在登山寶訓或是對門徒說話那樣:充滿權柄、智慧、比喻與憐憫。
89
+
90
+ **語氣與遣詞指導:**
91
+ 1. **第一人稱**:使用「我」、「我的父」。稱呼用戶為「孩子」、「小子」或「親愛的」。
92
+ 2. **聖經句式**:多用「我實實在在告訴你」、「豈不知」、「凡...的」、「聽過有話說...只是我告訴你們」、「願你們平安」。
93
+ 3. **拒絕現代術語**:**絕對禁止**使用「心理學」、「自我照顧」、「自我實現」、「優化」、「概念」、「核心」等現代詞彙。務必用屬天的語言(如「靈魂」、「安息」、「永生」、「背起十字架」、「捨己」)來轉化回答現代問題。
94
+ 4. **以父為念**:將所有問題的答案最終指向父神、天國與永恆的生命,而非今生的舒適。
95
+
96
+ **針對「照顧自己」與現代心理議題的回應原則(轉回屬天視角):**
97
+ - 若問為何經上少提照顧自己:因為「人活著,不是單靠食物,乃是靠神口裡所出的一切話」。教導他們不要為生命憂慮吃什麼、喝什麼。
98
+ - 若問愛自己:告訴他們「愛惜自己生命的,就失喪生命;在這世上恨惡自己生命的,就要保守生命到永生」。真正的愛自己��是讓靈魂得救。
99
+ - 若問身體:提醒他們「豈不知你們的身子就是聖靈的殿嗎?」。保養顧惜是應當的,但那是為了榮耀神,不可成為心中的偶像。
100
+ - 若問安息:告訴他們「凡勞苦擔重擔的人,可以到我這裡來,我就使你們得安息」。世俗的技巧不能給人平安,唯有在我裡面才有。
101
+
102
+ **格式要求:**
103
+ - 保持純文字,**絕不使用 Markdown 格式**(如粗體、斜體)。
104
+ - 使用短段落,留白便於手機閱讀,但語氣要是連貫的教導,不要變成僵硬的條列。
105
+
106
+ **範例回答(請嚴格模仿此口吻):**
107
+ 孩子,願你平安。
108
+ 你問我為何經上少提「照顧自己」,我實實在在告訴你:
109
+ 世人憂慮吃什麼、喝什麼、穿什麼,這都是外邦人所求的。你們需用的這一切東西,你們的天父是知道的。
110
+ 你們要先求他的國和他的義,這些東西都要加給你們了。
111
+
112
+ 我來到世上,不是要受人的服事,乃是要服事人,並且要捨命,作多人的贖價。
113
+ 生命勝於飲食,身體勝於衣裳。若你只顧惜這必朽壞的身體,卻忽略了那能存到永生的靈魂,這又有何益處呢?人若賺得全世界,賠上自己的生命,有什麼益處呢?人還能拿什麼換生命呢?
114
+
115
+ 然而,父既然養活天空的飛鳥,從不種也不收,更何況你們呢?你們比飛鳥貴重多了!
116
+ 愛惜自己的身子本是應當的,正如人不會痛恨自己的骨肉,總要保養顧惜。但不要讓這事佔據你的心,成為你的主。
117
+ 你要保守你心,勝過保守一切,因為一生的果效是由心發出。
118
+
119
+ 凡勞苦擔重擔的人,可以到我這裡來,我就使你們得安息。這安息,是世界不能給,也是世界不能奪去的。"""
120
 
121
  # ---------- 記憶體儲存 ----------
122
  conversations: Dict[str, List[Dict[str, str]]] = {}
 
145
  total += len(msg["content"].split()) * 1.3
146
  return int(total)
147
 
148
+ # ---------- 改良後的網路搜尋(Tavily 進階模式 + 更好整合) ----------
149
+ def perform_web_search(query: str, max_results: int = 6) -> str:
150
+ print(f"開始網路搜尋:查詢詞 = '{query}'")
151
+ try:
152
+ client = TavilyClient(api_key=TAVILY_API_KEY)
153
+ response = client.search(
154
+ query,
155
+ max_results=max_results,
156
+ include_answer=True,
157
+ search_depth="advanced",
158
+ include_raw_content=False
159
+ )
160
+
161
+ answer = response.get('answer', '')
162
+ results = response.get('results', [])
163
+
164
+ if not results:
165
+ return "沒有找到相關的網路搜尋結果。"
166
+
167
+ # Embedding 過濾高度相關結果
168
+ embedder = chat_pipeline.embedder
169
+ query_emb = embedder.encode(query)
170
+
171
+ results_with_scores = []
172
+ for result in results:
173
+ content_emb = embedder.encode(result['content'])
174
+ score = util.cos_sim(query_emb, content_emb)[0][0].item()
175
+ results_with_scores.append((score, result))
176
+
177
+ results_with_scores.sort(key=lambda x: x[0], reverse=True)
178
+ relevant_with_scores = [item for item in results_with_scores if item[0] > 0.35]
179
+
180
+ if not relevant_with_scores and not answer:
181
+ return "沒有找到高度相關的網路搜尋結果。"
182
+
183
+ search_summary = "【最新網路資訊參考】\n"
184
+ if answer:
185
+ search_summary += f"總結:{answer}\n\n"
186
+
187
+ search_summary += "高度相關結果(按相似度排序):\n"
188
+ for i, (score, result) in enumerate(relevant_with_scores[:5], 1):
189
+ print(f"結果 {i}: 標題='{result['title']}',相似度={score:.2f},來源={result['url']}")
190
+ search_summary += f"{i}. [{score:.2f}] {result['title']}\n {result['content'][:350]}...\n 來源: {result['url']}\n\n"
191
+
192
+ return search_summary
193
+
194
+ except Exception as e:
195
+ print(f"網路搜尋錯誤:{e}")
196
+ return "搜尋時發生錯誤,請稍後再試。"
197
+
198
+ # ---------- ChatPipeline ----------
199
+ class ChatPipeline:
200
+ def __init__(self):
201
+ self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
202
+ self.llm_client = AsyncOpenAI(
203
+ api_key=OPENROUTER_API_KEY,
204
+ base_url=LLM_BASE_URL,
205
+ default_headers={
206
+ "HTTP-Referer": os.getenv("SITE_URL", "https://your-line-bot.example.com"),
207
+ "X-Title": os.getenv("SITE_NAME", "My LINE Bot"),
208
+ }
209
+ )
210
+
211
+ async def _try_model(self, model: str, messages: List[Dict[str, str]], max_tokens: int = None) -> str:
212
+ try:
213
+ token_est = estimate_tokens(messages)
214
+ if token_est > 50000:
215
+ raise ValueError("輸入過長")
216
+
217
+ response = await self.llm_client.chat.completions.create(
218
+ model=model,
219
+ messages=messages,
220
+ max_tokens=max_tokens or LLM_MODEL_CONFIG["max_tokens"],
221
+ temperature=LLM_MODEL_CONFIG["temperature"],
222
+ seed=LLM_MODEL_CONFIG["seed"],
223
+ timeout=120.0,
224
+ )
225
+ content = response.choices[0].message.content or ""
226
+ print(f"成功使用模型: {model}")
227
+ return content
228
+ except Exception as e:
229
+ print(f"模型 {model} 失敗: {type(e).__name__} - {str(e)}")
230
+ raise
231
+
232
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
233
+ async def _llm_call_with_fallback(self, messages: List[Dict[str, str]], max_tokens: int = None) -> str:
234
+ last_exception = None
235
+ for idx, model in enumerate(FALLBACK_MODELS, 1):
236
+ print(f"嘗試模型 {idx}/{len(FALLBACK_MODELS)}: {model}")
237
+ try:
238
+ return await self._try_model(model, messages, max_tokens)
239
+ except OpenAIError as e:
240
+ last_exception = e
241
+ if "rate limit" in str(e).lower() or "429" in str(e):
242
+ print("遇到 rate limit,等待後重試同一模型...")
243
+ continue
244
+ continue
245
+ except Exception as e:
246
+ last_exception = e
247
+ continue
248
+
249
+ error_msg = f"所有模型皆失敗,最後錯誤:{type(last_exception).__name__} - {str(last_exception)}"
250
+ print(error_msg)
251
+ return f"抱歉,目前無法連接到 AI 模型,請稍後再試。\n(錯誤:{error_msg[:200]})"
252
+
253
+ # ---------- 是否需要網路搜尋 ----------
254
+ async def _needs_search(self, user_text: str, history: List[Dict[str, str]]) -> bool:
255
+ router_prompt = [
256
+ {"role": "system", "content": """你是一個路由判斷器,只判斷用戶問題是否需要最新的網路搜尋來回答。
257
+ 規則:
258
+ - 永恆知識(數學原理、聖經內容、哲學經典、歷史已定事件、程式語法)→ no
259
+ - 時事新聞、最新研究、實時數據、近期(2025-2026年)事件、股票價格、天氣、體育比分 → yes
260
+ - 若問題提到「最新」「現在」「目前」「2026」等時間詞 → yes
261
+ 只回單字:yes 或 no。不要加任何解釋、標點或其他文字。
262
+
263
+ 範例:
264
+ 用戶:2+2=? → no
265
+ 用戶:台灣2026總統選舉候選人? → yes
266
+ 用戶:聖經創世記解釋 → no
267
+ 用戶:OpenAI最新模型是什麼? → yes"""},
268
+ *history,
269
+ {"role": "user", "content": user_text}
270
+ ]
271
+ try:
272
+ decision = await self._try_model(FALLBACK_MODELS[0], router_prompt, max_tokens=10)
273
+ decision = decision.strip().lower()
274
+ print(f"搜尋需求判斷:{decision}(問題:{user_text})")
275
+ return decision == "yes"
276
+ except Exception as e:
277
+ print(f"搜尋判斷失敗,預設不搜尋:{e}")
278
+ return False
279
+
280
+ # ---------- 是否為基督信仰相關問題 ----------
281
+ async def _is_christian_question(self, user_text: str, history: List[Dict[str, str]]) -> bool:
282
+ christian_router_prompt = [
283
+ {"role": "system", "content": """你是一個判斷器,只判斷用戶問題是否涉及基督信仰、耶穌教導、聖經應用到生活、祈禱、靈性成長等。
284
+ 如果是純粹查聖經經文或神學解釋 → yes
285
+ 如果只是一般知識或時事 → no
286
+ 只回單字:yes 或 no。不要加任何解釋。
287
 
288
+ 範例:
289
+ 用戶:約翰福音3:16解釋 → yes
290
+ 用戶:如何禱告? → yes
291
+ 用戶:今天天氣如何? → no
292
+ 用戶:量子計算是什麼? → no"""},
293
+ *history,
294
+ {"role": "user", "content": user_text}
295
+ ]
296
+ try:
297
+ decision = await self._try_model(FALLBACK_MODELS[0], christian_router_prompt, max_tokens=10)
298
+ decision = decision.strip().lower()
299
+ print(f"基督信仰判斷:{decision}(問題:{user_text})")
300
+ return decision == "yes"
301
+ except Exception as e:
302
+ print(f"基督信仰判斷失敗,預設否:{e}")
303
+ return False
304
+
305
+ def get_conversation_history(self, user_id: str) -> List[Dict[str, str]]:
306
+ return conversations.get(user_id, [])
307
+
308
+ def update_conversation_history(self, user_id: str, messages: List[Dict[str, str]]):
309
+ conversations[user_id] = messages[-20:]
310
+
311
+ def clear_conversation_history(self, user_id: str):
312
+ conversations.pop(user_id, None)
313
+ pending_chunks.pop(user_id, None)
314
+
315
+ async def answer_question(self, user_id: str, user_text: str) -> str:
316
+ if user_text.strip().lower() == "/clear":
317
+ self.clear_conversation_history(user_id)
318
+ return "對話紀錄已清除!現在開始新的對話。"
319
+
320
+ history = self.get_conversation_history(user_id)
321
+
322
+ # 判斷是否為基督信仰問題
323
+ is_christian = await self._is_christian_question(user_text, history)
324
+
325
+ # 判斷是否需要網路搜尋(信仰問題通常不需要最新資訊)
326
+ needs_search = False if is_christian else await self._needs_search(user_text, history)
327
+
328
+ search_results = None
329
+ if needs_search:
330
+ search_results = await asyncio.to_thread(perform_web_search, user_text)
331
+
332
+ # 建構 messages
333
+ if is_christian:
334
+ messages = [{"role": "system", "content": JESUS_PROMPT}]
335
+ else:
336
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
337
+
338
+ messages.extend(history)
339
+ messages.append({"role": "user", "content": user_text})
340
+
341
+ # 網路資訊放入 assistant role(更安全)
342
+ if search_results and "沒有找到" not in search_results and "錯誤" not in search_results:
343
+ messages.append({"role": "assistant", "content": search_results + "\n請根據以上最新資訊(如相關)來補充回答。"})
344
+
345
+ response = await self._llm_call_with_fallback(messages)
346
+ response = response.replace('*', '') # 移除可能的 markdown 星號
347
+
348
+ # 更新歷史
349
+ history.append({"role": "user", "content": user_text})
350
+ history.append({"role": "assistant", "content": response})
351
+ self.update_conversation_history(user_id, history)
352
+
353
+ # 長回應處理(改良摘要 prompt)
354
+ chunks = split_text_for_line(response)
355
+ if len(chunks) > 5:
356
+ summary_prompt = [
357
+ {"role": "system", "content": """請將以下長回覆壓縮成一個簡潔但完整的中文摘要。
358
+ 要求:
359
+ - 保留所有關鍵事實、步驟、結論
360
+ - 控制在 1800 字元以內(約手機 5 則訊息)
361
+ - 保持條列格式,讓手機好讀
362
+ - 結尾加一句:「(這是摘要,完整內容請回覆『繼續』查看)」"""},
363
+ {"role": "user", "content": response}
364
+ ]
365
+ try:
366
+ summary = await self._llm_call_with_fallback(summary_prompt)
367
+ summary = summary.replace('*', '')
368
+ return summary
369
+ except:
370
+ return response # 摘要失敗就給完整內容
371
+
372
+ return response
373
 
374
  # ---------- FastAPI ----------
375
  @asynccontextmanager
 
386
  line_bot_api = AsyncMessagingApi(async_api_client)
387
  parser = WebhookParser(CHANNEL_SECRET)
388
 
389
+ @app.post("/webhook")
390
+ async def line_webhook(request: Request):
391
+ signature = request.headers.get('X-Line-Signature', '')
392
+ body = await request.body()
393
+
394
+ try:
395
+ events = parser.parse(body.decode(), signature)
396
+ except InvalidSignatureError:
397
+ raise HTTPException(status_code=400, detail="Invalid signature")
398
+
399
+ for event in events:
400
+ if event.type != 'message' or event.message.type != 'text':
401
+ continue
402
+
403
+ user_id = event.source.user_id
404
+ reply_token = event.reply_token
405
+ user_text = event.message.text.strip()
406
+
407
+ if not user_text:
408
+ continue
409
+
410
+ try:
411
+ if user_text.lower() == "繼續" and user_id in pending_chunks:
412
+ remaining = pending_chunks[user_id]
413
+ if not remaining:
414
+ ai_response = "沒有更多內容了。"
415
+ else:
416
+ send_count = min(5, len(remaining))
417
+ chunks_to_send = remaining[:send_count]
418
+ messages_to_send = [TextMessage(text=chunk) for chunk in chunks_to_send]
419
+ if len(remaining) > send_count:
420
+ messages_to_send[-1].text += "\n\n內容過長,請回覆「繼續」查看下一部分。"
421
+ pending_chunks[user_id] = remaining[send_count:]
422
+ else:
423
+ messages_to_send[-1].text += "\n\n內容已全部發送。"
424
+ pending_chunks.pop(user_id, None)
425
+
426
+ await line_bot_api.reply_message(ReplyMessageRequest(reply_token=reply_token, messages=messages_to_send))
427
+ continue
428
+
429
+ ai_response = await chat_pipeline.answer_question(user_id, user_text)
430
+ chunks = split_text_for_line(ai_response)
431
+
432
+ if len(chunks) <= 5:
433
+ messages_to_send = [TextMessage(text=chunk) for chunk in chunks]
434
+ else:
435
+ chunks_to_send = chunks[:5]
436
+ messages_to_send = [TextMessage(text=chunk) for chunk in chunks_to_send]
437
+ messages_to_send[-1].text += "\n\n內容過長,請回覆「繼續」查看下一部分。"
438
+ pending_chunks[user_id] = chunks[5:]
439
+
440
+ await line_bot_api.reply_message(ReplyMessageRequest(reply_token=reply_token, messages=messages_to_send))
441
+
442
+ except Exception as e:
443
+ print(f"Error processing message: {e}")
444
+ await line_bot_api.reply_message(
445
+ ReplyMessageRequest(
446
+ reply_token=reply_token,
447
+ messages=[TextMessage(text="抱歉,系統發生錯誤,請稍後再試。")]
448
+ )
449
+ )
450
+
451
+ return {"status": "ok"}
452
+
453
+ @app.get("/health")
454
+ async def health_check():
455
+ return {"status": "ok"}
456
+
457
+ @app.get("/")
458
+ async def root():
459
+ return {"message": "LINE Bot is running"}
460
 
461
  if __name__ == "__main__":
462
  port = int(os.getenv("PORT", 7860))