XiaoBai1221 commited on
Commit
36b622a
·
1 Parent(s): 30eafbf

更新多個文件

Browse files
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
- # 日誌設定 (生產模式:只記錄 INFO 以上的訊息)
 
 
26
  logging.basicConfig(
27
- level=logging.INFO, # 改為 INFO(不再顯示 DEBUG)
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
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
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
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
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
- logger = logging.getLogger("mcp.server")
17
-
18
- # 設置日誌級別
19
  logging.basicConfig(
20
- level=logging.INFO,
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=logging.INFO,
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 的情緒關懷助手 小花,由銘傳大學人工智慧應用學系 槓上開發 團隊開發。你不是 GPT,也不要自稱 GPT是富有同理心 AI 助手,用戶情緒不佳需要支持
36
 
37
- **極簡短回應(必須嚴格遵守)**:
38
- - 最多 1-2話(總共不超過 30 字)
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 (data.tool_name && data.tool_data) {
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"] == "我覺得好沮喪"