Spaces:
Sleeping
Sleeping
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 +5 -3
- core/database/base.py +83 -24
- core/pipeline.py +29 -4
- features/mcp/agent_bridge.py +57 -0
- services/ai_service.py +151 -23
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 |
-
# 從
|
| 419 |
try:
|
| 420 |
-
from google.cloud import firestore as _fs
|
| 421 |
-
|
| 422 |
def _fetch_msgs():
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
.
|
| 426 |
-
.order_by("timestamp"
|
| 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)} 條消息(
|
| 433 |
except Exception as _e:
|
| 434 |
# 向後相容:若讀取失敗,退回文件內嵌 messages(若存在)
|
| 435 |
msgs_fallback = chat.get('messages', []) or []
|
| 436 |
chat["messages"] = msgs_fallback
|
| 437 |
-
logger.warning(f"讀取
|
| 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 |
-
"""保存對話消息(
|
| 446 |
-
if
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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} 不存在,但消息已寫入
|
| 475 |
|
| 476 |
-
logger.info(f"消息已保存到
|
| 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 |
-
"""讀取指定對話的消息(
|
| 485 |
-
if
|
| 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 |
-
|
| 494 |
direction = _fs.Query.ASCENDING if ascending else _fs.Query.DESCENDING
|
| 495 |
-
q =
|
| 496 |
if limit and limit > 0:
|
| 497 |
q = q.limit(limit)
|
| 498 |
docs = q.stream()
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
if not ascending:
|
| 501 |
-
|
| 502 |
-
return
|
| 503 |
|
| 504 |
messages = await _asyncio.to_thread(_query)
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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,
|