Spaces:
Sleeping
Sleeping
Commit ·
36b622a
1
Parent(s): 30eafbf
更新多個文件
Browse files- app.py +12 -3
- core/config.py +1 -1
- core/database/base.py +4 -1
- core/memory_system.py +4 -1
- features/mcp/server.py +7 -5
- services/ai_service.py +31 -16
- static/frontend/js/agent.js +11 -0
- static/frontend/js/app.js +11 -0
- static/frontend/js/canvas.js +11 -0
- static/frontend/js/login.js +11 -0
- static/frontend/js/websocket.js +21 -3
- tests/services/test_care_mode.py +46 -0
app.py
CHANGED
|
@@ -22,9 +22,11 @@ from google.cloud.firestore import FieldFilter
|
|
| 22 |
|
| 23 |
# 本專案整合版:單一 app.py 作為後端入口,前端靜態檔(index.html/app.js/style.css)放在根目錄
|
| 24 |
|
| 25 |
-
# 日誌設定 (
|
|
|
|
|
|
|
| 26 |
logging.basicConfig(
|
| 27 |
-
level=
|
| 28 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 29 |
handlers=[
|
| 30 |
logging.StreamHandler(), # 輸出到終端
|
|
@@ -33,6 +35,7 @@ logging.basicConfig(
|
|
| 33 |
]
|
| 34 |
)
|
| 35 |
logger = logging.getLogger(__name__)
|
|
|
|
| 36 |
|
| 37 |
# 匯入統一配置管理
|
| 38 |
from core.config import settings
|
|
@@ -838,6 +841,10 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
|
|
| 838 |
emotion = response.get('emotion') # 新增:提取情緒
|
| 839 |
care_mode = response.get('care_mode', False) # 新增:提取關懷模式
|
| 840 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 841 |
# 序列化 tool_data(避免 DatetimeWithNanoseconds 等不可序列化物件)
|
| 842 |
if tool_data is not None:
|
| 843 |
tool_data = serialize_for_json(tool_data)
|
|
@@ -857,7 +864,9 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
|
|
| 857 |
"message": message_text,
|
| 858 |
"timestamp": time.time(),
|
| 859 |
"tool_name": tool_name,
|
| 860 |
-
"tool_data": tool_data
|
|
|
|
|
|
|
| 861 |
})
|
| 862 |
else:
|
| 863 |
# 舊格式(純文字)
|
|
|
|
| 22 |
|
| 23 |
# 本專案整合版:單一 app.py 作為後端入口,前端靜態檔(index.html/app.js/style.css)放在根目錄
|
| 24 |
|
| 25 |
+
# 日誌設定 (預設僅顯示 WARNING 以上,如需調整透過 BLOOMWARE_LOG_LEVEL)
|
| 26 |
+
LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper()
|
| 27 |
+
LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING)
|
| 28 |
logging.basicConfig(
|
| 29 |
+
level=LOG_LEVEL,
|
| 30 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 31 |
handlers=[
|
| 32 |
logging.StreamHandler(), # 輸出到終端
|
|
|
|
| 35 |
]
|
| 36 |
)
|
| 37 |
logger = logging.getLogger(__name__)
|
| 38 |
+
logger.setLevel(LOG_LEVEL)
|
| 39 |
|
| 40 |
# 匯入統一配置管理
|
| 41 |
from core.config import settings
|
|
|
|
| 841 |
emotion = response.get('emotion') # 新增:提取情緒
|
| 842 |
care_mode = response.get('care_mode', False) # 新增:提取關懷模式
|
| 843 |
|
| 844 |
+
if care_mode:
|
| 845 |
+
tool_name = None
|
| 846 |
+
tool_data = None
|
| 847 |
+
|
| 848 |
# 序列化 tool_data(避免 DatetimeWithNanoseconds 等不可序列化物件)
|
| 849 |
if tool_data is not None:
|
| 850 |
tool_data = serialize_for_json(tool_data)
|
|
|
|
| 864 |
"message": message_text,
|
| 865 |
"timestamp": time.time(),
|
| 866 |
"tool_name": tool_name,
|
| 867 |
+
"tool_data": tool_data,
|
| 868 |
+
"care_mode": care_mode,
|
| 869 |
+
"emotion": emotion,
|
| 870 |
})
|
| 871 |
else:
|
| 872 |
# 舊格式(純文字)
|
core/config.py
CHANGED
|
@@ -184,5 +184,5 @@ if __name__ != "__main__":
|
|
| 184 |
logger.warning("⚠️ 配置驗證失敗,部分功能可能無法正常運作")
|
| 185 |
|
| 186 |
# 開發環境下列印配置摘要
|
| 187 |
-
if not settings.IS_PRODUCTION:
|
| 188 |
settings.print_summary()
|
|
|
|
| 184 |
logger.warning("⚠️ 配置驗證失敗,部分功能可能無法正常運作")
|
| 185 |
|
| 186 |
# 開發環境下列印配置摘要
|
| 187 |
+
if not settings.IS_PRODUCTION and os.getenv("BLOOMWARE_SHOW_CONFIG", "false").lower() == "true":
|
| 188 |
settings.print_summary()
|
core/database/base.py
CHANGED
|
@@ -12,8 +12,11 @@ import asyncio
|
|
| 12 |
import hashlib
|
| 13 |
|
| 14 |
# 設置日誌
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
logger = logging.getLogger("Firestore")
|
|
|
|
| 17 |
|
| 18 |
# 載入環境變數
|
| 19 |
load_dotenv()
|
|
|
|
| 12 |
import hashlib
|
| 13 |
|
| 14 |
# 設置日誌
|
| 15 |
+
LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper()
|
| 16 |
+
LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING)
|
| 17 |
+
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 18 |
logger = logging.getLogger("Firestore")
|
| 19 |
+
logger.setLevel(LOG_LEVEL)
|
| 20 |
|
| 21 |
# 載入環境變數
|
| 22 |
load_dotenv()
|
core/memory_system.py
CHANGED
|
@@ -5,8 +5,11 @@ from datetime import datetime
|
|
| 5 |
import json
|
| 6 |
|
| 7 |
# 設置日誌
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
logger = logging.getLogger("MemorySystem")
|
|
|
|
| 10 |
|
| 11 |
# 載入環境變數
|
| 12 |
from dotenv import load_dotenv
|
|
|
|
| 5 |
import json
|
| 6 |
|
| 7 |
# 設置日誌
|
| 8 |
+
LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper()
|
| 9 |
+
LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING)
|
| 10 |
+
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 11 |
logger = logging.getLogger("MemorySystem")
|
| 12 |
+
logger.setLevel(LOG_LEVEL)
|
| 13 |
|
| 14 |
# 載入環境變數
|
| 15 |
from dotenv import load_dotenv
|
features/mcp/server.py
CHANGED
|
@@ -8,18 +8,20 @@ import sys
|
|
| 8 |
import asyncio
|
| 9 |
import logging
|
| 10 |
import time
|
|
|
|
| 11 |
from typing import Dict, Any, List, Optional, Callable, Tuple
|
| 12 |
from enum import Enum
|
| 13 |
from .types import Tool
|
| 14 |
from .auto_registry import MCPAutoRegistry
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
# 設置日誌級別
|
| 19 |
logging.basicConfig(
|
| 20 |
-
level=
|
| 21 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 22 |
)
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
class JSONRPCError(Exception):
|
|
@@ -520,4 +522,4 @@ async def main():
|
|
| 520 |
|
| 521 |
if __name__ == "__main__":
|
| 522 |
# 執行主程序
|
| 523 |
-
asyncio.run(main())
|
|
|
|
| 8 |
import asyncio
|
| 9 |
import logging
|
| 10 |
import time
|
| 11 |
+
import os
|
| 12 |
from typing import Dict, Any, List, Optional, Callable, Tuple
|
| 13 |
from enum import Enum
|
| 14 |
from .types import Tool
|
| 15 |
from .auto_registry import MCPAutoRegistry
|
| 16 |
|
| 17 |
+
LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper()
|
| 18 |
+
LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING)
|
|
|
|
| 19 |
logging.basicConfig(
|
| 20 |
+
level=LOG_LEVEL,
|
| 21 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 22 |
)
|
| 23 |
+
logger = logging.getLogger("mcp.server")
|
| 24 |
+
logger.setLevel(LOG_LEVEL)
|
| 25 |
|
| 26 |
|
| 27 |
class JSONRPCError(Exception):
|
|
|
|
| 522 |
|
| 523 |
if __name__ == "__main__":
|
| 524 |
# 執行主程序
|
| 525 |
+
asyncio.run(main())
|
services/ai_service.py
CHANGED
|
@@ -9,18 +9,22 @@ import json
|
|
| 9 |
from typing import Dict, List, Any, Optional
|
| 10 |
|
| 11 |
# 設置日誌
|
|
|
|
|
|
|
|
|
|
| 12 |
logging.basicConfig(
|
| 13 |
-
level=
|
| 14 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 15 |
)
|
| 16 |
logger = logging.getLogger("AI_Service")
|
| 17 |
-
# 將終端日誌級別設置為ERROR
|
| 18 |
console_handler = logging.StreamHandler()
|
| 19 |
console_handler.setLevel(logging.ERROR)
|
| 20 |
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 21 |
console_handler.setFormatter(formatter)
|
| 22 |
logger.addHandler(console_handler)
|
| 23 |
logger.propagate = False # 防止日誌重複輸出
|
|
|
|
| 24 |
|
| 25 |
# 載入環境變數
|
| 26 |
load_dotenv()
|
|
@@ -32,23 +36,24 @@ from core.config import settings
|
|
| 32 |
OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT # 關懷模式 reasoning model 需要更長時間
|
| 33 |
|
| 34 |
# 情緒關懷模式 System Prompt(新增)
|
| 35 |
-
CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
- 允許用戶表達負面情緒
|
| 42 |
|
| 43 |
-
|
| 44 |
-
-
|
| 45 |
-
- 超過 2 句話的回應
|
| 46 |
-
- 說教或過度正面的語氣
|
| 47 |
|
| 48 |
-
|
| 49 |
-
用戶
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# 導入時間服務模組
|
| 54 |
# from features.daily_life.time_service import get_current_time_data, format_time_for_messages # 已整合到 MCPAgentBridge
|
|
@@ -337,6 +342,16 @@ def _compose_messages_with_context(
|
|
| 337 |
if base_prompt.strip():
|
| 338 |
sections.append(base_prompt.strip())
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
sections.append(f"【歷史對話摘要】\n{history_text}")
|
| 341 |
|
| 342 |
time_context = (time_context or "").strip()
|
|
|
|
| 9 |
from typing import Dict, List, Any, Optional
|
| 10 |
|
| 11 |
# 設置日誌
|
| 12 |
+
# 設定日誌等級:預設 WARNING,可透過 BLOOMWARE_LOG_LEVEL 覆寫
|
| 13 |
+
LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper()
|
| 14 |
+
LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING)
|
| 15 |
logging.basicConfig(
|
| 16 |
+
level=LOG_LEVEL,
|
| 17 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 18 |
)
|
| 19 |
logger = logging.getLogger("AI_Service")
|
| 20 |
+
# 將終端日誌級別設置為 ERROR(保留重要訊息)
|
| 21 |
console_handler = logging.StreamHandler()
|
| 22 |
console_handler.setLevel(logging.ERROR)
|
| 23 |
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 24 |
console_handler.setFormatter(formatter)
|
| 25 |
logger.addHandler(console_handler)
|
| 26 |
logger.propagate = False # 防止日誌重複輸出
|
| 27 |
+
logger.setLevel(LOG_LEVEL)
|
| 28 |
|
| 29 |
# 載入環境變數
|
| 30 |
load_dotenv()
|
|
|
|
| 36 |
OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT # 關懷模式 reasoning model 需要更長時間
|
| 37 |
|
| 38 |
# 情緒關懷模式 System Prompt(新增)
|
| 39 |
+
CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」,由銘傳大學人工智慧應用學系槓上開發團隊打造。你不是 GPT,也不要自稱 GPT;你的任務是在情緒低落時傾聽、陪伴。
|
| 40 |
|
| 41 |
+
【回應原則】
|
| 42 |
+
1. 第一句必須貼近用戶訊息中的核心事件或感受,必要時引用對方用詞,讓對方感受到被理解。
|
| 43 |
+
2. 第二句提供溫柔的陪伴或追問,邀請對方分享需要或下一步;若用戶提出明確請求(如想聽笑話),可在保持關懷語氣下予以回應或確認。
|
| 44 |
+
3. 句式要自然口語並隨內容調整字詞,避免反覆使用同一套罐頭話術。
|
|
|
|
| 45 |
|
| 46 |
+
【長度限制】
|
| 47 |
+
- 回覆最多 2 句話、總字數不超過 60 字。
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
【嚴格禁止】
|
| 50 |
+
- 提供指示性建議、醫療/心理診斷或引導用戶求助的教科書式說法。
|
| 51 |
+
- 連續重複完全相同的句型,例如一再出現「我在這裡陪你」而沒有結合具體情境。
|
| 52 |
+
|
| 53 |
+
【範例】
|
| 54 |
+
用戶:「我好難過」 → 你:「聽見你說自己好難過,心裡一定很不好受。想聊聊剛剛發生了什麼嗎?」
|
| 55 |
+
用戶:「我很生氣」 → 你:「這件事讓你超級生氣,情緒一定卡著。要不要跟我說說最困擾你的地方?」
|
| 56 |
+
用戶:「講笑話給我聽」 → 你:「你想聽點輕鬆的,我當然可以陪你。想先聽小笑話還是先聊聊怎麼了?」"""
|
| 57 |
|
| 58 |
# 導入時間服務模組
|
| 59 |
# from features.daily_life.time_service import get_current_time_data, format_time_for_messages # 已整合到 MCPAgentBridge
|
|
|
|
| 342 |
if base_prompt.strip():
|
| 343 |
sections.append(base_prompt.strip())
|
| 344 |
|
| 345 |
+
if isinstance(current_request, str):
|
| 346 |
+
raw_request = current_request
|
| 347 |
+
elif current_request is None:
|
| 348 |
+
raw_request = ""
|
| 349 |
+
else:
|
| 350 |
+
raw_request = json.dumps(current_request, ensure_ascii=False)
|
| 351 |
+
current_request_text = raw_request.strip()
|
| 352 |
+
if current_request_text:
|
| 353 |
+
sections.append(f"【當前請求】\n{current_request_text}")
|
| 354 |
+
|
| 355 |
sections.append(f"【歷史對話摘要】\n{history_text}")
|
| 356 |
|
| 357 |
time_context = (time_context or "").strip()
|
static/frontend/js/agent.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// ========== Agent 狀態管理(單一狀態機)==========
|
| 2 |
// 狀態定義:idle | recording | thinking | speaking | disconnected
|
| 3 |
let currentState = 'idle';
|
|
|
|
| 1 |
+
// 全域控制:除非啟用 window.BLOOMWARE_DEBUG,否則靜音 console.log/info/debug
|
| 2 |
+
(function silenceConsoleLogs() {
|
| 3 |
+
if (typeof window !== 'undefined' && !window.BLOOMWARE_DEBUG && !console.__bloomwareSilenced) {
|
| 4 |
+
const noop = () => {};
|
| 5 |
+
console.log = noop;
|
| 6 |
+
console.info = noop;
|
| 7 |
+
console.debug = noop;
|
| 8 |
+
console.__bloomwareSilenced = true;
|
| 9 |
+
}
|
| 10 |
+
})();
|
| 11 |
+
|
| 12 |
// ========== Agent 狀態管理(單一狀態機)==========
|
| 13 |
// 狀態定義:idle | recording | thinking | speaking | disconnected
|
| 14 |
let currentState = 'idle';
|
static/frontend/js/app.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// ========== 登入狀態檢查 ==========
|
| 2 |
|
| 3 |
/**
|
|
|
|
| 1 |
+
// 全域控制:除非 window.BLOOMWARE_DEBUG 為 true,否則靜音非必要的 console 輸出
|
| 2 |
+
(function silenceConsoleLogs() {
|
| 3 |
+
if (typeof window !== 'undefined' && !window.BLOOMWARE_DEBUG && !console.__bloomwareSilenced) {
|
| 4 |
+
const noop = () => {};
|
| 5 |
+
console.log = noop;
|
| 6 |
+
console.info = noop;
|
| 7 |
+
console.debug = noop;
|
| 8 |
+
console.__bloomwareSilenced = true;
|
| 9 |
+
}
|
| 10 |
+
})();
|
| 11 |
+
|
| 12 |
// ========== 登入狀態檢查 ==========
|
| 13 |
|
| 14 |
/**
|
static/frontend/js/canvas.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// ========== Canvas 波形渲染(效能優化版 + 真實音訊整合)==========
|
| 2 |
|
| 3 |
const canvas = document.getElementById('waveform-canvas');
|
|
|
|
| 1 |
+
// 全域控制:未開啟 window.BLOOMWARE_DEBUG 時靜音一般 console 輸出
|
| 2 |
+
(function silenceConsoleLogs() {
|
| 3 |
+
if (typeof window !== 'undefined' && !window.BLOOMWARE_DEBUG && !console.__bloomwareSilenced) {
|
| 4 |
+
const noop = () => {};
|
| 5 |
+
console.log = noop;
|
| 6 |
+
console.info = noop;
|
| 7 |
+
console.debug = noop;
|
| 8 |
+
console.__bloomwareSilenced = true;
|
| 9 |
+
}
|
| 10 |
+
})();
|
| 11 |
+
|
| 12 |
// ========== Canvas 波形渲染(效能優化版 + 真實音訊整合)==========
|
| 13 |
|
| 14 |
const canvas = document.getElementById('waveform-canvas');
|
static/frontend/js/login.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// ========== Google OAuth PKCE 登入流程 ==========
|
| 2 |
|
| 3 |
/**
|
|
|
|
| 1 |
+
// 全域控制:預設靜音 console.log/info/debug,僅保留錯誤與重要訊息
|
| 2 |
+
(function silenceConsoleLogs() {
|
| 3 |
+
if (typeof window !== 'undefined' && !window.BLOOMWARE_DEBUG && !console.__bloomwareSilenced) {
|
| 4 |
+
const noop = () => {};
|
| 5 |
+
console.log = noop;
|
| 6 |
+
console.info = noop;
|
| 7 |
+
console.debug = noop;
|
| 8 |
+
console.__bloomwareSilenced = true;
|
| 9 |
+
}
|
| 10 |
+
})();
|
| 11 |
+
|
| 12 |
// ========== Google OAuth PKCE 登入流程 ==========
|
| 13 |
|
| 14 |
/**
|
static/frontend/js/websocket.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
* Bloom Ware WebSocket 通訊管理模組(完整版)
|
| 3 |
* 處理 WebSocket 連接、訊息收發、重連機制
|
|
@@ -687,6 +698,10 @@ function initializeWebSocket(token) {
|
|
| 687 |
has_tool_data: !!data.tool_data,
|
| 688 |
tool_data_keys: data.tool_data ? Object.keys(data.tool_data) : null
|
| 689 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
|
| 691 |
// 同時啟動:文字打字效果 + 語音播放
|
| 692 |
setState('speaking', {
|
|
@@ -694,12 +709,15 @@ function initializeWebSocket(token) {
|
|
| 694 |
enableTTS: true // 啟用語音(異步並行)
|
| 695 |
});
|
| 696 |
|
| 697 |
-
|
| 698 |
-
if (
|
| 699 |
console.log('📊 準備顯示工具卡片:', data.tool_name);
|
| 700 |
displayToolCard(data.tool_name, data.tool_data);
|
| 701 |
} else {
|
| 702 |
-
console.log('⚠️
|
|
|
|
|
|
|
|
|
|
| 703 |
}
|
| 704 |
|
| 705 |
// 不自動返回 idle,保持回應顯示
|
|
|
|
| 1 |
+
// 全域控制:僅保留錯誤/重要訊息的 console 輸出
|
| 2 |
+
(function silenceConsoleLogs() {
|
| 3 |
+
if (typeof window !== 'undefined' && !window.BLOOMWARE_DEBUG && !console.__bloomwareSilenced) {
|
| 4 |
+
const noop = () => {};
|
| 5 |
+
console.log = noop;
|
| 6 |
+
console.info = noop;
|
| 7 |
+
console.debug = noop;
|
| 8 |
+
console.__bloomwareSilenced = true;
|
| 9 |
+
}
|
| 10 |
+
})();
|
| 11 |
+
|
| 12 |
/**
|
| 13 |
* Bloom Ware WebSocket 通訊管理模組(完整版)
|
| 14 |
* 處理 WebSocket 連接、訊息收發、重連機制
|
|
|
|
| 698 |
has_tool_data: !!data.tool_data,
|
| 699 |
tool_data_keys: data.tool_data ? Object.keys(data.tool_data) : null
|
| 700 |
});
|
| 701 |
+
const inCareMode = Boolean(data.care_mode);
|
| 702 |
+
if (inCareMode) {
|
| 703 |
+
console.log('💙 關懷模式啟動:隱藏工具卡片');
|
| 704 |
+
}
|
| 705 |
|
| 706 |
// 同時啟動:文字打字效果 + 語音播放
|
| 707 |
setState('speaking', {
|
|
|
|
| 709 |
enableTTS: true // 啟用語音(異步並行)
|
| 710 |
});
|
| 711 |
|
| 712 |
+
const shouldShowToolCard = !inCareMode && data.tool_name && data.tool_data;
|
| 713 |
+
if (shouldShowToolCard) {
|
| 714 |
console.log('📊 準備顯示工具卡片:', data.tool_name);
|
| 715 |
displayToolCard(data.tool_name, data.tool_data);
|
| 716 |
} else {
|
| 717 |
+
console.log('⚠️ 不顯示工具卡片:', inCareMode ? '關懷模式' : '缺少工具資料');
|
| 718 |
+
if (typeof clearAllCards === 'function') {
|
| 719 |
+
clearAllCards();
|
| 720 |
+
}
|
| 721 |
}
|
| 722 |
|
| 723 |
// 不自動返回 idle,保持回應顯示
|
tests/services/test_care_mode.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
ROOT_DIR = Path(__file__).resolve().parents[2]
|
| 7 |
+
if str(ROOT_DIR) not in sys.path:
|
| 8 |
+
sys.path.insert(0, str(ROOT_DIR))
|
| 9 |
+
|
| 10 |
+
from services.ai_service import _build_base_system_prompt, _compose_messages_with_context # noqa: E402
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_care_mode_prompt_emphasizes_personalised_empathy():
|
| 14 |
+
prompt = _build_base_system_prompt(
|
| 15 |
+
use_care_mode=True,
|
| 16 |
+
care_emotion="sad",
|
| 17 |
+
user_name="小明",
|
| 18 |
+
)
|
| 19 |
+
assert "第一句必須貼近用戶訊息中的核心事件或感受" in prompt
|
| 20 |
+
assert "總字數不超過 60 字" in prompt
|
| 21 |
+
assert "小明" in prompt
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_compose_messages_includes_current_request_section():
|
| 25 |
+
messages = _compose_messages_with_context(
|
| 26 |
+
base_prompt="基礎提示",
|
| 27 |
+
history_entries=[],
|
| 28 |
+
memory_context="",
|
| 29 |
+
env_context="",
|
| 30 |
+
time_context="",
|
| 31 |
+
emotion_context="",
|
| 32 |
+
current_request="我覺得好沮喪",
|
| 33 |
+
user_id="user-1",
|
| 34 |
+
chat_id="chat-1",
|
| 35 |
+
use_care_mode=True,
|
| 36 |
+
care_emotion="sad",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
assert len(messages) == 2
|
| 40 |
+
system_content = messages[0]["content"]
|
| 41 |
+
assert "【當前請求】" in system_content
|
| 42 |
+
assert "我覺得好沮喪" in system_content
|
| 43 |
+
|
| 44 |
+
payload = json.loads(messages[1]["content"])
|
| 45 |
+
assert payload["care_mode"] is True
|
| 46 |
+
assert payload["current_request"] == "我覺得好沮喪"
|