XiaoBai1221 commited on
Commit
f998449
·
1 Parent(s): 3c14891

修復 WebSocket env_snapshot 處理與 Firestore 查詢語法

Browse files

- 修復 WebSocket 中 env_snapshot 消息處理邏輯的結構問題
- 更新所有 Firestore 查詢使用新的 filter=FieldFilter() 語法
- 添加必要的 FieldFilter import
- 確保 env_snapshot 消息在 chat 模式下正確處理

app.py CHANGED
@@ -1322,7 +1322,7 @@ async def handle_message(user_message, user_id, chat_id, messages, request_id: s
1322
  logger.info(f"🔧 Pipeline: 功能處理結果='{result}'")
1323
  return result
1324
 
1325
- async def _ai(messages_in, cid, model, rid, chat_id, use_care_mode=False, care_emotion=None):
1326
  # 取得用戶名稱(優先順序:Google 名稱 > 語音 label > "用戶")
1327
  user_name = "用戶"
1328
  try:
@@ -1342,7 +1342,8 @@ async def handle_message(user_message, user_id, chat_id, messages, request_id: s
1342
  chat_id=chat_id,
1343
  use_care_mode=use_care_mode,
1344
  care_emotion=care_emotion,
1345
- user_name=user_name
 
1346
  )
1347
  else:
1348
  return await ai_service.generate_response_for_user(
@@ -1353,7 +1354,8 @@ async def handle_message(user_message, user_id, chat_id, messages, request_id: s
1353
  chat_id=chat_id,
1354
  use_care_mode=use_care_mode,
1355
  care_emotion=care_emotion,
1356
- user_name=user_name
 
1357
  )
1358
 
1359
  model = settings.OPENAI_MODEL
 
1322
  logger.info(f"🔧 Pipeline: 功能處理結果='{result}'")
1323
  return result
1324
 
1325
+ async def _ai(messages_in, cid, model, rid, chat_id, use_care_mode=False, care_emotion=None, emotion_label=None):
1326
  # 取得用戶名稱(優先順序:Google 名稱 > 語音 label > "用戶")
1327
  user_name = "用戶"
1328
  try:
 
1342
  chat_id=chat_id,
1343
  use_care_mode=use_care_mode,
1344
  care_emotion=care_emotion,
1345
+ user_name=user_name,
1346
+ emotion_label=emotion_label,
1347
  )
1348
  else:
1349
  return await ai_service.generate_response_for_user(
 
1354
  chat_id=chat_id,
1355
  use_care_mode=use_care_mode,
1356
  care_emotion=care_emotion,
1357
+ user_name=user_name,
1358
+ emotion_label=emotion_label,
1359
  )
1360
 
1361
  model = settings.OPENAI_MODEL
core/database/base.py CHANGED
@@ -45,6 +45,12 @@ def _get_user_doc_ref(user_id: str) -> DocumentReference:
45
  def _get_user_memories_collection(user_id: str) -> CollectionReference:
46
  return _get_user_doc_ref(user_id).collection("memories")
47
 
 
 
 
 
 
 
48
  def connect_to_firestore():
49
  """初始化 Firebase Firestore 連接"""
50
  global firestore_db, messages_collection, users_collection, chats_collection, memories_collection, health_data_collection, device_bindings_collection
@@ -415,26 +421,23 @@ async def get_chat(chat_id):
415
  chat = doc.to_dict() or {}
416
  chat["chat_id"] = doc.id
417
 
418
- # 從 messages 集合讀取完整對話(按時間升序)
419
  try:
420
- from google.cloud import firestore as _fs
421
-
422
  def _fetch_msgs():
423
- q = (
424
- messages_collection
425
- .where(filter=FieldFilter("chat_id", "==", chat_id))
426
- .order_by("timestamp", direction=_fs.Query.ASCENDING)
427
- )
428
- return [d.to_dict() for d in q.stream()]
429
 
430
  msgs = await _asyncio.to_thread(_fetch_msgs)
431
  chat["messages"] = msgs
432
- logger.info(f"獲取到對話 {chat_id},包含 {len(msgs)} 條消息(messages 集合)")
433
  except Exception as _e:
434
  # 向後相容:若讀取失敗,退回文件內嵌 messages(若存在)
435
  msgs_fallback = chat.get('messages', []) or []
436
  chat["messages"] = msgs_fallback
437
- logger.warning(f"讀取 messages 集合失敗,使用內嵌 messages。原因: {_e}")
438
 
439
  return {"success": True, "chat": chat}
440
  except Exception as e:
@@ -442,8 +445,8 @@ async def get_chat(chat_id):
442
  return {"success": False, "error": str(e)}
443
 
444
  async def save_chat_message(chat_id, sender, content):
445
- """保存對話消息(使用 messages 集合作為單一事實來源)"""
446
- if messages_collection is None or chats_collection is None:
447
  logger.error("Firestore尚未連接,無法保存消息")
448
  return {"success": False, "error": "數據庫未連接"}
449
  try:
@@ -458,7 +461,16 @@ async def save_chat_message(chat_id, sender, content):
458
  }
459
 
460
  def _write_message():
461
- messages_collection.add(message)
 
 
 
 
 
 
 
 
 
462
 
463
  def _touch_chat():
464
  doc_ref = chats_collection.document(chat_id)
@@ -469,11 +481,13 @@ async def save_chat_message(chat_id, sender, content):
469
  return True
470
 
471
  await _asyncio.to_thread(_write_message)
 
 
472
  touched = await _asyncio.to_thread(_touch_chat)
473
  if not touched:
474
- logger.warning(f"對話 {chat_id} 不存在,但消息已寫入 messages 集合")
475
 
476
- logger.info(f"消息已保存到 messages 集合(chat_id={chat_id})")
477
  return {"success": True, "message": message}
478
  except Exception as e:
479
  logger.error(f"保存消息時發生錯誤: {e}")
@@ -481,8 +495,8 @@ async def save_chat_message(chat_id, sender, content):
481
 
482
 
483
  async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: bool = True):
484
- """讀取指定對話的消息(來自 messages 集合)"""
485
- if messages_collection is None:
486
  logger.error("Firestore尚未連接,無法讀取消息")
487
  return []
488
  try:
@@ -490,19 +504,57 @@ async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: b
490
  from google.cloud import firestore as _fs
491
 
492
  def _query():
493
- q = messages_collection.where(filter=FieldFilter("chat_id", "==", chat_id))
494
  direction = _fs.Query.ASCENDING if ascending else _fs.Query.DESCENDING
495
- q = q.order_by("timestamp", direction=direction)
496
  if limit and limit > 0:
497
  q = q.limit(limit)
498
  docs = q.stream()
499
- res = [d.to_dict() for d in docs]
 
 
 
 
500
  if not ascending:
501
- res.reverse() # 若取降序+limit,回傳前再反轉為時間正序
502
- return res
503
 
504
  messages = await _asyncio.to_thread(_query)
505
- return messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  except Exception as e:
507
  logger.error(f"讀取對話消息失敗: {e}")
508
  return []
@@ -548,6 +600,13 @@ async def delete_chat(chat_id):
548
  doc = doc_ref.get()
549
  if not doc.exists:
550
  return False
 
 
 
 
 
 
 
551
  doc_ref.delete()
552
  return True
553
 
 
45
  def _get_user_memories_collection(user_id: str) -> CollectionReference:
46
  return _get_user_doc_ref(user_id).collection("memories")
47
 
48
+
49
+ def _get_chat_messages_collection(chat_id: str) -> CollectionReference:
50
+ if chats_collection is None:
51
+ raise RuntimeError("Firestore尚未連接,無法取得對話消息集合")
52
+ return chats_collection.document(chat_id).collection("messages")
53
+
54
  def connect_to_firestore():
55
  """初始化 Firebase Firestore 連接"""
56
  global firestore_db, messages_collection, users_collection, chats_collection, memories_collection, health_data_collection, device_bindings_collection
 
421
  chat = doc.to_dict() or {}
422
  chat["chat_id"] = doc.id
423
 
424
+ # 從 chat 集合讀取完整對話(按時間升序)
425
  try:
 
 
426
  def _fetch_msgs():
427
+ ref = _get_chat_messages_collection(chat_id)
428
+ return [
429
+ {**doc.to_dict(), "id": doc.id}
430
+ for doc in ref.order_by("timestamp").stream()
431
+ ]
 
432
 
433
  msgs = await _asyncio.to_thread(_fetch_msgs)
434
  chat["messages"] = msgs
435
+ logger.info(f"獲取到對話 {chat_id},包含 {len(msgs)} 條消息(chat 集合)")
436
  except Exception as _e:
437
  # 向後相容:若讀取失敗,退回文件內嵌 messages(若存在)
438
  msgs_fallback = chat.get('messages', []) or []
439
  chat["messages"] = msgs_fallback
440
+ logger.warning(f"讀取 chat 集合失敗,使用內嵌 messages。原因: {_e}")
441
 
442
  return {"success": True, "chat": chat}
443
  except Exception as e:
 
445
  return {"success": False, "error": str(e)}
446
 
447
  async def save_chat_message(chat_id, sender, content):
448
+ """保存對話消息(chat/{chat_id}/messages 集合作為主要儲存)"""
449
+ if chats_collection is None:
450
  logger.error("Firestore尚未連接,無法保存消息")
451
  return {"success": False, "error": "數據庫未連接"}
452
  try:
 
461
  }
462
 
463
  def _write_message():
464
+ ref = _get_chat_messages_collection(chat_id)
465
+ ref.add(message)
466
+
467
+ def _write_legacy_copy():
468
+ if messages_collection is None:
469
+ return
470
+ try:
471
+ messages_collection.add(message)
472
+ except Exception as legacy_err: # pragma: no cover
473
+ logger.debug(f"寫入頂層 messages 集合失敗(兼容用途,可忽略): {legacy_err}")
474
 
475
  def _touch_chat():
476
  doc_ref = chats_collection.document(chat_id)
 
481
  return True
482
 
483
  await _asyncio.to_thread(_write_message)
484
+ # 兼容舊資料模型:非阻塞地寫入頂層 messages 集合,供舊功能查詢使用
485
+ await _asyncio.to_thread(_write_legacy_copy)
486
  touched = await _asyncio.to_thread(_touch_chat)
487
  if not touched:
488
+ logger.warning(f"對話 {chat_id} 不存在,但消息已寫入 chat 集合")
489
 
490
+ logger.info(f"消息已保存到 chat 集合(chat_id={chat_id})")
491
  return {"success": True, "message": message}
492
  except Exception as e:
493
  logger.error(f"保存消息時發生錯誤: {e}")
 
495
 
496
 
497
  async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: bool = True):
498
+ """讀取指定對話的消息(優先使用 chat 集合)"""
499
+ if chats_collection is None:
500
  logger.error("Firestore尚未連接,無法讀取消息")
501
  return []
502
  try:
 
504
  from google.cloud import firestore as _fs
505
 
506
  def _query():
507
+ ref = _get_chat_messages_collection(chat_id)
508
  direction = _fs.Query.ASCENDING if ascending else _fs.Query.DESCENDING
509
+ q = ref.order_by("timestamp", direction=direction)
510
  if limit and limit > 0:
511
  q = q.limit(limit)
512
  docs = q.stream()
513
+ records = []
514
+ for doc in docs:
515
+ data = doc.to_dict()
516
+ data["id"] = doc.id
517
+ records.append(data)
518
  if not ascending:
519
+ records = list(reversed(records))
520
+ return records
521
 
522
  messages = await _asyncio.to_thread(_query)
523
+ if messages:
524
+ return messages
525
+
526
+ # 向後相容:若子集合無資料,嘗試讀取舊頂層 messages 集合
527
+ if messages_collection is None:
528
+ return []
529
+
530
+ def _legacy_query():
531
+ docs = messages_collection.where(filter=FieldFilter("chat_id", "==", chat_id)).stream()
532
+ legacy = [d.to_dict() for d in docs]
533
+ legacy.sort(key=lambda item: item.get("timestamp"))
534
+ if limit and limit > 0:
535
+ legacy = legacy[:limit]
536
+ return legacy
537
+
538
+ legacy_sorted = await _asyncio.to_thread(_legacy_query)
539
+ view_messages = list(legacy_sorted)
540
+ if not ascending:
541
+ view_messages.reverse()
542
+ if legacy_sorted:
543
+ def _backfill():
544
+ try:
545
+ ref = _get_chat_messages_collection(chat_id)
546
+ # 若子集合仍為空,將舊資料搬遷過去
547
+ has_existing = any(True for _ in ref.limit(1).stream())
548
+ if has_existing:
549
+ return
550
+ for legacy_msg in legacy_sorted:
551
+ ref.add(legacy_msg)
552
+ logger.info(f"已將 legacy messages 回填至 chat 子集合(chat_id={chat_id})")
553
+ except Exception as backfill_err:
554
+ logger.warning(f"回填 legacy messages 失敗(可忽略): {backfill_err}")
555
+
556
+ await _asyncio.to_thread(_backfill)
557
+ return view_messages
558
  except Exception as e:
559
  logger.error(f"讀取對話消息失敗: {e}")
560
  return []
 
600
  doc = doc_ref.get()
601
  if not doc.exists:
602
  return False
603
+ # 先刪除子集合中的消息,避免孤兒資料
604
+ try:
605
+ messages_ref = _get_chat_messages_collection(chat_id)
606
+ for msg_snapshot in messages_ref.stream():
607
+ msg_snapshot.reference.delete()
608
+ except Exception as msg_err:
609
+ logger.warning(f"刪除對話 {chat_id} 的子消息時發生錯誤:{msg_err}")
610
  doc_ref.delete()
611
  return True
612
 
core/pipeline.py CHANGED
@@ -34,7 +34,7 @@ class ChatPipeline:
34
  self,
35
  intent_detector: Callable[[str], Awaitable[Tuple[bool, dict]]],
36
  feature_processor: Callable[[dict, str, str, Optional[str]], Awaitable[Any]],
37
- ai_generator: Callable[[List[dict], str, str, Optional[str], Optional[str]], Awaitable[str]],
38
  model: str = "gpt-5-nano",
39
  detect_timeout: float = 5.0, # 2025 最佳實踐:Structured Outputs 通常 2-3秒
40
  feature_timeout: float = 10.0, # MCP 工具已有內部超時(30秒)
@@ -87,7 +87,16 @@ class ChatPipeline:
87
  # 直接用關懷模式 AI 回應(不檢測意圖,不調用工具)
88
  care_emotion = EmotionCareManager.get_care_emotion(user_id, chat_id)
89
  ai_res = await self._with_timeout(
90
- self._ai_generator(user_message, user_id, self._model, request_id, chat_id, use_care_mode=True, care_emotion=care_emotion),
 
 
 
 
 
 
 
 
 
91
  self._ai_timeout,
92
  reason="ai-care",
93
  )
@@ -116,7 +125,16 @@ class ChatPipeline:
116
  logger.warning(f"⚠️ 偵測到極端情緒 [{emotion}],進入關懷模式")
117
  # 立即使用關懷模式 AI 回應
118
  ai_res = await self._with_timeout(
119
- self._ai_generator(user_message, user_id, self._model, request_id, chat_id, use_care_mode=True, care_emotion=emotion),
 
 
 
 
 
 
 
 
 
120
  self._ai_timeout,
121
  reason="ai-care",
122
  )
@@ -178,7 +196,14 @@ class ChatPipeline:
178
  # 3) 無功能 → 一般聊天(限時)
179
  # 注意:不傳 messages,改傳 user_message,讓 ai_generator 自動載入歷史對話和記憶
180
  ai_res = await self._with_timeout(
181
- self._ai_generator(user_message, user_id or "default", self._model, request_id, chat_id),
 
 
 
 
 
 
 
182
  self._ai_timeout,
183
  reason="ai",
184
  )
 
34
  self,
35
  intent_detector: Callable[[str], Awaitable[Tuple[bool, dict]]],
36
  feature_processor: Callable[[dict, str, str, Optional[str]], Awaitable[Any]],
37
+ ai_generator: Callable[..., Awaitable[str]],
38
  model: str = "gpt-5-nano",
39
  detect_timeout: float = 5.0, # 2025 最佳實踐:Structured Outputs 通常 2-3秒
40
  feature_timeout: float = 10.0, # MCP 工具已有內部超時(30秒)
 
87
  # 直接用關懷模式 AI 回應(不檢測意圖,不調用工具)
88
  care_emotion = EmotionCareManager.get_care_emotion(user_id, chat_id)
89
  ai_res = await self._with_timeout(
90
+ self._ai_generator(
91
+ user_message,
92
+ user_id,
93
+ self._model,
94
+ request_id,
95
+ chat_id,
96
+ use_care_mode=True,
97
+ care_emotion=care_emotion,
98
+ emotion_label=care_emotion,
99
+ ),
100
  self._ai_timeout,
101
  reason="ai-care",
102
  )
 
125
  logger.warning(f"⚠️ 偵測到極端情緒 [{emotion}],進入關懷模式")
126
  # 立即使用關懷模式 AI 回應
127
  ai_res = await self._with_timeout(
128
+ self._ai_generator(
129
+ user_message,
130
+ user_id,
131
+ self._model,
132
+ request_id,
133
+ chat_id,
134
+ use_care_mode=True,
135
+ care_emotion=emotion,
136
+ emotion_label=emotion,
137
+ ),
138
  self._ai_timeout,
139
  reason="ai-care",
140
  )
 
196
  # 3) 無功能 → 一般聊天(限時)
197
  # 注意:不傳 messages,改傳 user_message,讓 ai_generator 自動載入歷史對話和記憶
198
  ai_res = await self._with_timeout(
199
+ self._ai_generator(
200
+ user_message,
201
+ user_id or "default",
202
+ self._model,
203
+ request_id,
204
+ chat_id,
205
+ emotion_label=emotion_value,
206
+ ),
207
  self._ai_timeout,
208
  reason="ai",
209
  )
features/mcp/agent_bridge.py CHANGED
@@ -12,6 +12,7 @@ from .server import FeaturesMCPServer
12
  import services.ai_service as ai_service
13
  from services.ai_service import StrictResponseError
14
  from core.reasoning_strategy import get_optimal_reasoning_effort
 
15
 
16
  logger = logging.getLogger("mcp.agent_bridge")
17
  logger.setLevel(logging.DEBUG) # 強制設置為 DEBUG 級別
@@ -84,6 +85,60 @@ class MCPAgentBridge:
84
  return registered_name
85
 
86
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  def get_current_time_data(self) -> Dict[str, Any]:
89
  """
@@ -542,6 +597,8 @@ class MCPAgentBridge:
542
  if not tool.handler:
543
  return f"⚠️ 工具 {tool_name} 尚未實作,請稍後再試"
544
 
 
 
545
  logger.info(f"🔧 調用 MCP 工具: {tool_name}")
546
  logger.debug("📋 調用參數: %s", _safe_json(arguments))
547
 
 
12
  import services.ai_service as ai_service
13
  from services.ai_service import StrictResponseError
14
  from core.reasoning_strategy import get_optimal_reasoning_effort
15
+ from core.database import get_user_env_current
16
 
17
  logger = logging.getLogger("mcp.agent_bridge")
18
  logger.setLevel(logging.DEBUG) # 強制設置為 DEBUG 級別
 
85
  return registered_name
86
 
87
  return None
88
+ async def _fetch_env_context(self, user_id: Optional[str]) -> Dict[str, Any]:
89
+ """讀取使用者最近的環境資訊(Firestore current snapshot)。"""
90
+ if not user_id:
91
+ return {}
92
+ try:
93
+ env_res = await get_user_env_current(user_id)
94
+ if env_res.get("success"):
95
+ ctx = env_res.get("context") or {}
96
+ return ctx
97
+ except Exception as e:
98
+ logger.debug(f"無法取得使用者 {user_id} 環境資訊: {e}")
99
+ return {}
100
+
101
+ async def _enrich_arguments_with_env(self, tool_name: str, arguments: Dict[str, Any], user_id: Optional[str]) -> Dict[str, Any]:
102
+ """自動將環境資訊補入 MCP 工具參數,讓位置相關功能更聰明。"""
103
+ if not user_id:
104
+ return arguments
105
+
106
+ tool_name = (tool_name or "").strip()
107
+ if tool_name not in {"weather_query"}:
108
+ return arguments
109
+
110
+ ctx = await self._fetch_env_context(user_id)
111
+ if not ctx:
112
+ return arguments
113
+
114
+ enriched = dict(arguments or {})
115
+
116
+ def _safe_float(val):
117
+ try:
118
+ if val is None:
119
+ return None
120
+ return float(val)
121
+ except (TypeError, ValueError):
122
+ return None
123
+
124
+ if tool_name == "weather_query":
125
+ if enriched.get("lat") is None:
126
+ lat = _safe_float(ctx.get("lat"))
127
+ if lat is not None:
128
+ enriched["lat"] = lat
129
+ if enriched.get("lon") is None:
130
+ lon = _safe_float(ctx.get("lon"))
131
+ if lon is not None:
132
+ enriched["lon"] = lon
133
+ city_arg = str(enriched.get("city") or "").strip()
134
+ ctx_city = str(ctx.get("city") or "").strip()
135
+ if not city_arg and ctx_city:
136
+ enriched["city"] = ctx_city
137
+
138
+ if enriched != arguments:
139
+ logger.info(f"📍 已自動補齊 {tool_name} 參數: {_safe_json(enriched)}")
140
+
141
+ return enriched
142
 
143
  def get_current_time_data(self) -> Dict[str, Any]:
144
  """
 
597
  if not tool.handler:
598
  return f"⚠️ 工具 {tool_name} 尚未實作,請稍後再試"
599
 
600
+ arguments = await self._enrich_arguments_with_env(tool_name, arguments, user_id)
601
+
602
  logger.info(f"🔧 調用 MCP 工具: {tool_name}")
603
  logger.debug("📋 調用參數: %s", _safe_json(arguments))
604
 
services/ai_service.py CHANGED
@@ -142,12 +142,137 @@ def _format_history_for_prompt(history: List[Dict[str, str]]) -> str:
142
  return "\n".join(lines) if lines else "(無)"
143
 
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  def _compose_messages_with_context(
146
  *,
147
  base_prompt: str,
148
  history_entries: List[Dict[str, str]],
149
  memory_context: str,
150
  env_context: str,
 
 
151
  current_request: str,
152
  user_id: Optional[str],
153
  chat_id: Optional[str],
@@ -162,10 +287,18 @@ def _compose_messages_with_context(
162
 
163
  sections.append(f"【歷史對話摘要】\n{history_text}")
164
 
 
 
 
 
165
  env_context = (env_context or "").strip()
166
  if env_context:
167
  sections.append(f"【環境訊號】\n{env_context}")
168
 
 
 
 
 
169
  memory_context = (memory_context or "").strip()
170
  if memory_context:
171
  sections.append(f"【用戶重要記憶】\n{memory_context}")
@@ -474,6 +607,7 @@ async def generate_response_for_user(
474
  care_emotion: Optional[str] = None,
475
  reasoning_effort: Optional[str] = None,
476
  user_name: Optional[str] = None,
 
477
  ) -> str:
478
  """
479
  為用戶生成AI回應
@@ -503,6 +637,7 @@ async def generate_response_for_user(
503
  care_emotion=care_emotion,
504
  reasoning_effort=reasoning_effort,
505
  user_name=user_name,
 
506
  )
507
  else:
508
  # 回退到原有的全局歷史管理(用於向後兼容)
@@ -519,6 +654,7 @@ async def generate_response_for_user(
519
  care_emotion=care_emotion,
520
  reasoning_effort=reasoning_effort,
521
  user_name=user_name,
 
522
  )
523
 
524
  logger.error("未提供消息列表或用戶消息")
@@ -545,6 +681,7 @@ async def _generate_response_with_chat_db(
545
  care_emotion: Optional[str] = None,
546
  reasoning_effort: Optional[str] = None,
547
  user_name: Optional[str] = None,
 
548
  ):
549
  """使用DB管理對話歷史的實現"""
550
  try:
@@ -654,25 +791,17 @@ async def _generate_response_with_chat_db(
654
  logger.warning(f"載入記憶失敗: {e}")
655
 
656
  # 讀取環境現況(僅組裝,不外呼)
657
- env_context_text = ""
658
  if db_available and user_id:
659
  try:
660
  env_res = await get_user_env_current(user_id)
661
  if env_res.get("success"):
662
  ctx = env_res.get("context") or {}
663
- city = ctx.get("city")
664
- tz = ctx.get("tz")
665
- heading = ctx.get("heading_cardinal") or ctx.get("heading_deg")
666
- acc = ctx.get("accuracy_m")
667
- freshness = "" # updated_at 轉 freshness_sec 可在前端或後端計算
668
- parts = []
669
- if city: parts.append(f"城市: {city}")
670
- if tz: parts.append(f"時區: {tz}")
671
- if heading: parts.append(f"方位: {heading}")
672
- if acc is not None: parts.append(f"定位精度±{int(acc)}m")
673
- env_context_text = "\n".join(parts)
674
  except Exception as e:
675
  logger.debug(f"讀取環境現況失敗: {e}")
 
 
 
676
 
677
  base_prompt = _build_base_system_prompt(
678
  use_care_mode=use_care_mode,
@@ -685,6 +814,8 @@ async def _generate_response_with_chat_db(
685
  history_entries=chat_history,
686
  memory_context=memory_context,
687
  env_context=env_context_text,
 
 
688
  current_request=user_message,
689
  user_id=user_id,
690
  chat_id=chat_id,
@@ -729,6 +860,7 @@ async def _generate_response_with_chat_db(
729
  care_emotion=care_emotion,
730
  reasoning_effort=reasoning_effort,
731
  user_name=user_name,
 
732
  )
733
 
734
 
@@ -746,6 +878,7 @@ async def _generate_response_with_global_history(
746
  care_emotion: Optional[str] = None,
747
  reasoning_effort: Optional[str] = None,
748
  user_name: Optional[str] = None,
 
749
  ):
750
  """使用全局歷史的回退實現(向後兼容)"""
751
  try:
@@ -795,24 +928,17 @@ async def _generate_response_with_global_history(
795
  prior_history = prior_history[-history_limit:]
796
 
797
  # 讀取環境現況
798
- env_context_text = ""
799
  if db_available and user_id:
800
  try:
801
  env_res = await get_user_env_current(user_id)
802
  if env_res.get("success"):
803
  ctx = env_res.get("context") or {}
804
- city = ctx.get("city")
805
- tz = ctx.get("tz")
806
- heading = ctx.get("heading_cardinal") or ctx.get("heading_deg")
807
- acc = ctx.get("accuracy_m")
808
- parts = []
809
- if city: parts.append(f"城市: {city}")
810
- if tz: parts.append(f"時區: {tz}")
811
- if heading: parts.append(f"方位: {heading}")
812
- if acc is not None: parts.append(f"定位精度±{int(acc)}m")
813
- env_context_text = "\n".join(parts)
814
  except Exception as ex:
815
  logger.debug(f"讀取環境現況失敗: {ex}")
 
 
 
816
 
817
  base_prompt = _build_base_system_prompt(
818
  use_care_mode=use_care_mode,
@@ -846,6 +972,8 @@ async def _generate_response_with_global_history(
846
  history_entries=prior_history,
847
  memory_context=memory_context,
848
  env_context=env_context_text,
 
 
849
  current_request=user_message,
850
  user_id=user_id,
851
  chat_id=None,
 
142
  return "\n".join(lines) if lines else "(無)"
143
 
144
 
145
+ def _format_env_context(ctx: Dict[str, Any]) -> str:
146
+ """將環境資訊整理成可讀文字,確保 AI 能掌握使用者所在位置。"""
147
+ if not ctx:
148
+ return ""
149
+
150
+ parts: List[str] = []
151
+
152
+ city = (ctx.get("city") or "").strip()
153
+ admin = (ctx.get("admin") or "").strip()
154
+ if city and admin:
155
+ parts.append(f"城市: {city}({admin})")
156
+ elif city:
157
+ parts.append(f"城市: {city}")
158
+ elif admin:
159
+ parts.append(f"行政區: {admin}")
160
+
161
+ tz = (ctx.get("tz") or "").strip()
162
+ if tz:
163
+ parts.append(f"時區: {tz}")
164
+
165
+ heading = ctx.get("heading_cardinal") or ctx.get("heading_deg")
166
+ if heading is not None:
167
+ parts.append(f"方位: {heading}")
168
+
169
+ acc = ctx.get("accuracy_m")
170
+ try:
171
+ if acc is not None:
172
+ parts.append(f"定位精度: ±{int(round(float(acc)))}m")
173
+ except (ValueError, TypeError):
174
+ pass
175
+
176
+ lat = ctx.get("lat")
177
+ lon = ctx.get("lon")
178
+ try:
179
+ if lat is not None and lon is not None:
180
+ lat_f = float(lat)
181
+ lon_f = float(lon)
182
+ coord_text = f"{lat_f:.5f}, {lon_f:.5f}"
183
+ geohash = (ctx.get("geohash_7") or "").strip()
184
+ if geohash:
185
+ parts.append(f"座標: {coord_text}(Geohash {geohash})")
186
+ else:
187
+ parts.append(f"座標: {coord_text}")
188
+ except (ValueError, TypeError):
189
+ pass
190
+
191
+ locale = (ctx.get("locale") or "").strip()
192
+ if locale:
193
+ parts.append(f"語系: {locale}")
194
+
195
+ device = (ctx.get("device") or "").strip()
196
+ if device:
197
+ parts.append(f"裝置: {device}")
198
+
199
+ return "\n".join(parts)
200
+
201
+
202
+ def _format_time_context(user_tz: Optional[str]) -> str:
203
+ """生成時間相關提示,優先使用使用者所在時區。"""
204
+ try:
205
+ from zoneinfo import ZoneInfo # Python 3.9+
206
+ except Exception: # pragma: no cover - 兼容環境
207
+ ZoneInfo = None # type: ignore
208
+
209
+ tzinfo = None
210
+ if user_tz and ZoneInfo:
211
+ try:
212
+ tzinfo = ZoneInfo(user_tz)
213
+ except Exception:
214
+ tzinfo = None
215
+
216
+ now = datetime.now(tzinfo) if tzinfo else datetime.now()
217
+ hour = now.hour
218
+ if 5 <= hour < 12:
219
+ day_period = "上午"
220
+ elif 12 <= hour < 18:
221
+ day_period = "下午"
222
+ elif 18 <= hour < 22:
223
+ day_period = "晚上"
224
+ else:
225
+ day_period = "深夜" if hour >= 22 else "凌晨"
226
+
227
+ weekday_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
228
+ weekday = weekday_names[now.weekday()]
229
+
230
+ tz_label = user_tz if user_tz else ("系統時區" if tzinfo is None else user_tz)
231
+ return (
232
+ f"當地時間: {now.strftime('%Y-%m-%d %H:%M')}({weekday},{day_period})"
233
+ + (f"\n時區: {tz_label}" if tz_label else "")
234
+ )
235
+
236
+
237
+ def _format_emotion_context(
238
+ emotion_label: Optional[str],
239
+ care_emotion: Optional[str],
240
+ use_care_mode: bool,
241
+ ) -> str:
242
+ """將情緒訊號轉成對話上下文,關懷模式優先描述 care_emotion。"""
243
+ emotion = care_emotion if use_care_mode and care_emotion else (emotion_label or "")
244
+ if not emotion:
245
+ return ""
246
+
247
+ normalized = emotion.lower()
248
+ allowed_labels = {"neutral", "happy", "sad", "angry", "fear", "surprise"}
249
+ display_map = {
250
+ "neutral": "平靜",
251
+ "happy": "開心",
252
+ "sad": "難過",
253
+ "angry": "生氣",
254
+ "fear": "害怕",
255
+ "surprise": "驚訝",
256
+ }
257
+
258
+ if normalized not in allowed_labels:
259
+ logger.debug(f"情緒標籤不在預期集合: {emotion}")
260
+ return f"偵測情緒: {emotion}"
261
+
262
+ translated = display_map.get(normalized, emotion)
263
+ mode_hint = "(關懷模式)" if use_care_mode else ""
264
+ # 顯示原始 label 以保持一致性
265
+ return f"偵測情緒: {emotion}({translated}){mode_hint}"
266
+
267
+
268
  def _compose_messages_with_context(
269
  *,
270
  base_prompt: str,
271
  history_entries: List[Dict[str, str]],
272
  memory_context: str,
273
  env_context: str,
274
+ time_context: str,
275
+ emotion_context: str,
276
  current_request: str,
277
  user_id: Optional[str],
278
  chat_id: Optional[str],
 
287
 
288
  sections.append(f"【歷史對話摘要】\n{history_text}")
289
 
290
+ time_context = (time_context or "").strip()
291
+ if time_context:
292
+ sections.append(f"【時間訊號】\n{time_context}")
293
+
294
  env_context = (env_context or "").strip()
295
  if env_context:
296
  sections.append(f"【環境訊號】\n{env_context}")
297
 
298
+ emotion_context = (emotion_context or "").strip()
299
+ if emotion_context:
300
+ sections.append(f"【情緒訊號】\n{emotion_context}")
301
+
302
  memory_context = (memory_context or "").strip()
303
  if memory_context:
304
  sections.append(f"【用戶重要記憶】\n{memory_context}")
 
607
  care_emotion: Optional[str] = None,
608
  reasoning_effort: Optional[str] = None,
609
  user_name: Optional[str] = None,
610
+ emotion_label: Optional[str] = None,
611
  ) -> str:
612
  """
613
  為用戶生成AI回應
 
637
  care_emotion=care_emotion,
638
  reasoning_effort=reasoning_effort,
639
  user_name=user_name,
640
+ emotion_label=emotion_label,
641
  )
642
  else:
643
  # 回退到原有的全局歷史管理(用於向後兼容)
 
654
  care_emotion=care_emotion,
655
  reasoning_effort=reasoning_effort,
656
  user_name=user_name,
657
+ emotion_label=emotion_label,
658
  )
659
 
660
  logger.error("未提供消息列表或用戶消息")
 
681
  care_emotion: Optional[str] = None,
682
  reasoning_effort: Optional[str] = None,
683
  user_name: Optional[str] = None,
684
+ emotion_label: Optional[str] = None,
685
  ):
686
  """使用DB管理對話歷史的實現"""
687
  try:
 
791
  logger.warning(f"載入記憶失敗: {e}")
792
 
793
  # 讀取環境現況(僅組裝,不外呼)
794
+ ctx: Dict[str, Any] = {}
795
  if db_available and user_id:
796
  try:
797
  env_res = await get_user_env_current(user_id)
798
  if env_res.get("success"):
799
  ctx = env_res.get("context") or {}
 
 
 
 
 
 
 
 
 
 
 
800
  except Exception as e:
801
  logger.debug(f"讀取環境現況失敗: {e}")
802
+ env_context_text = _format_env_context(ctx)
803
+ time_context_text = _format_time_context(ctx.get("tz") if ctx else None)
804
+ emotion_context_text = _format_emotion_context(emotion_label, care_emotion, use_care_mode)
805
 
806
  base_prompt = _build_base_system_prompt(
807
  use_care_mode=use_care_mode,
 
814
  history_entries=chat_history,
815
  memory_context=memory_context,
816
  env_context=env_context_text,
817
+ time_context=time_context_text,
818
+ emotion_context=emotion_context_text,
819
  current_request=user_message,
820
  user_id=user_id,
821
  chat_id=chat_id,
 
860
  care_emotion=care_emotion,
861
  reasoning_effort=reasoning_effort,
862
  user_name=user_name,
863
+ emotion_label=emotion_label,
864
  )
865
 
866
 
 
878
  care_emotion: Optional[str] = None,
879
  reasoning_effort: Optional[str] = None,
880
  user_name: Optional[str] = None,
881
+ emotion_label: Optional[str] = None,
882
  ):
883
  """使用全局歷史的回退實現(向後兼容)"""
884
  try:
 
928
  prior_history = prior_history[-history_limit:]
929
 
930
  # 讀取環境現況
931
+ ctx: Dict[str, Any] = {}
932
  if db_available and user_id:
933
  try:
934
  env_res = await get_user_env_current(user_id)
935
  if env_res.get("success"):
936
  ctx = env_res.get("context") or {}
 
 
 
 
 
 
 
 
 
 
937
  except Exception as ex:
938
  logger.debug(f"讀取環境現況失敗: {ex}")
939
+ env_context_text = _format_env_context(ctx)
940
+ time_context_text = _format_time_context(ctx.get("tz") if ctx else None)
941
+ emotion_context_text = _format_emotion_context(emotion_label, care_emotion, use_care_mode)
942
 
943
  base_prompt = _build_base_system_prompt(
944
  use_care_mode=use_care_mode,
 
972
  history_entries=prior_history,
973
  memory_context=memory_context,
974
  env_context=env_context_text,
975
+ time_context=time_context_text,
976
+ emotion_context=emotion_context_text,
977
  current_request=user_message,
978
  user_id=user_id,
979
  chat_id=None,