Spaces:
Sleeping
Sleeping
Commit ·
df89a78
1
Parent(s): 9746ac7
feat: 修復emotion flow即時響應,添加emotion_callback機制改善前端UI更新
Browse files- app.py +26 -7
- core/pipeline.py +21 -0
- tests/test_heartbeat.py +4 -0
- tests/test_websocket_manager.py +4 -0
app.py
CHANGED
|
@@ -587,7 +587,16 @@ async def websocket_endpoint_with_jwt(
|
|
| 587 |
async def _do_process_and_send():
|
| 588 |
try:
|
| 589 |
logger.info(f"🚀 開始處理訊息: user_id={user_id}, chat_id={chat_id}")
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
logger.info(f"📥 handle_message 返回: type={type(response)}, response={response}")
|
| 592 |
|
| 593 |
# 【優化】處理空回應:轉換為帶情緒的 dict 格式
|
|
@@ -1086,6 +1095,9 @@ async def websocket_endpoint_with_jwt(
|
|
| 1086 |
# 如果有轉錄文字,送給 AI Agent 處理
|
| 1087 |
if transcription:
|
| 1088 |
logger.info(f"🤖 處理即時轉錄結果: {transcription}")
|
|
|
|
|
|
|
|
|
|
| 1089 |
|
| 1090 |
# === 方案 B:語音情緒辨識(情緒分佈驗證 + 智能回退)===
|
| 1091 |
audio_emotion = None
|
|
@@ -1139,9 +1151,6 @@ async def websocket_endpoint_with_jwt(
|
|
| 1139 |
client_info.pop("audio_buffer", None)
|
| 1140 |
manager.set_client_info(user_id, client_info)
|
| 1141 |
|
| 1142 |
-
# 通知前端開始思考
|
| 1143 |
-
await websocket.send_json({"type": "typing", "message": "thinking"})
|
| 1144 |
-
|
| 1145 |
# 異步處理對話邏輯
|
| 1146 |
async def _process_realtime_chat():
|
| 1147 |
chat_id = message_data.get("chat_id")
|
|
@@ -1169,6 +1178,15 @@ async def websocket_endpoint_with_jwt(
|
|
| 1169 |
# 取得語言設定
|
| 1170 |
language = client_info.get("language", "auto")
|
| 1171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
# 處理對話(透過 handle_message,自動處理 pipeline)
|
| 1173 |
response = await handle_message(
|
| 1174 |
transcription,
|
|
@@ -1176,7 +1194,8 @@ async def websocket_endpoint_with_jwt(
|
|
| 1176 |
chat_id,
|
| 1177 |
[], # messages 參數(會自動從數據庫載入)
|
| 1178 |
audio_emotion=audio_emotion, # 傳遞音頻情緒
|
| 1179 |
-
language=language # 傳遞語言設定(新增)
|
|
|
|
| 1180 |
)
|
| 1181 |
|
| 1182 |
# 發送回應
|
|
@@ -1250,7 +1269,7 @@ async def websocket_endpoint_with_jwt(
|
|
| 1250 |
# -----------------------------
|
| 1251 |
# 消息處理與AI
|
| 1252 |
# -----------------------------
|
| 1253 |
-
async def handle_message(user_message, user_id, chat_id, messages, request_id: str = None, audio_emotion: dict = None, language: str = None):
|
| 1254 |
logger.info(f"📥 handle_message: 收到訊息='{user_message}', user_id={user_id}, audio_emotion={audio_emotion}, language={language}")
|
| 1255 |
|
| 1256 |
# 指令優先,避免進入管線造成不必要延遲
|
|
@@ -1346,7 +1365,7 @@ async def handle_message(user_message, user_id, chat_id, messages, request_id: s
|
|
| 1346 |
ai_timeout=20.0, # AI回應超時 (30 → 20)
|
| 1347 |
)
|
| 1348 |
logger.info(f"⚙️ 準備調用 ChatPipeline.process,user_message='{user_message}', audio_emotion={audio_emotion}, language={language}")
|
| 1349 |
-
res: PipelineResult = await pipeline.process(user_message, user_id=user_id, chat_id=chat_id, request_id=request_id, audio_emotion=audio_emotion, language=language)
|
| 1350 |
logger.info(f"⚙️ ChatPipeline.process 完成,結果='{res.text}', is_fallback={res.is_fallback}, reason={res.reason}")
|
| 1351 |
|
| 1352 |
# 檢查是否有工具元數據
|
|
|
|
| 587 |
async def _do_process_and_send():
|
| 588 |
try:
|
| 589 |
logger.info(f"🚀 開始處理訊息: user_id={user_id}, chat_id={chat_id}")
|
| 590 |
+
|
| 591 |
+
async def _on_text_emotion(em: str, cm: bool):
|
| 592 |
+
logger.info(f"📤 [即時回調] 發送 text emotion_detected: {em}, care_mode={cm}")
|
| 593 |
+
await websocket.send_json({
|
| 594 |
+
"type": "emotion_detected",
|
| 595 |
+
"emotion": em,
|
| 596 |
+
"care_mode": cm
|
| 597 |
+
})
|
| 598 |
+
|
| 599 |
+
response = await handle_message(user_message, user_id, chat_id, messages_for_handler, request_id=request_id, emotion_callback=_on_text_emotion)
|
| 600 |
logger.info(f"📥 handle_message 返回: type={type(response)}, response={response}")
|
| 601 |
|
| 602 |
# 【優化】處理空回應:轉換為帶情緒的 dict 格式
|
|
|
|
| 1095 |
# 如果有轉錄文字,送給 AI Agent 處理
|
| 1096 |
if transcription:
|
| 1097 |
logger.info(f"🤖 處理即時轉錄結果: {transcription}")
|
| 1098 |
+
|
| 1099 |
+
# 立即通知前端開始思考,提升即時響應感
|
| 1100 |
+
await websocket.send_json({"type": "typing", "message": "thinking"})
|
| 1101 |
|
| 1102 |
# === 方案 B:語音情緒辨識(情緒分佈驗證 + 智能回退)===
|
| 1103 |
audio_emotion = None
|
|
|
|
| 1151 |
client_info.pop("audio_buffer", None)
|
| 1152 |
manager.set_client_info(user_id, client_info)
|
| 1153 |
|
|
|
|
|
|
|
|
|
|
| 1154 |
# 異步處理對話邏輯
|
| 1155 |
async def _process_realtime_chat():
|
| 1156 |
chat_id = message_data.get("chat_id")
|
|
|
|
| 1178 |
# 取得語言設定
|
| 1179 |
language = client_info.get("language", "auto")
|
| 1180 |
|
| 1181 |
+
# 發送即時情緒的回調函數
|
| 1182 |
+
async def _on_emotion_detected(em: str, cm: bool):
|
| 1183 |
+
logger.info(f"📤 [即時回調] 發送 emotion_detected: {em}, care_mode={cm}")
|
| 1184 |
+
await websocket.send_json({
|
| 1185 |
+
"type": "emotion_detected",
|
| 1186 |
+
"emotion": em,
|
| 1187 |
+
"care_mode": cm
|
| 1188 |
+
})
|
| 1189 |
+
|
| 1190 |
# 處理對話(透過 handle_message,自動處理 pipeline)
|
| 1191 |
response = await handle_message(
|
| 1192 |
transcription,
|
|
|
|
| 1194 |
chat_id,
|
| 1195 |
[], # messages 參數(會自動從數據庫載入)
|
| 1196 |
audio_emotion=audio_emotion, # 傳遞音頻情緒
|
| 1197 |
+
language=language, # 傳遞語言設定(新增)
|
| 1198 |
+
emotion_callback=_on_emotion_detected
|
| 1199 |
)
|
| 1200 |
|
| 1201 |
# 發送回應
|
|
|
|
| 1269 |
# -----------------------------
|
| 1270 |
# 消息處理與AI
|
| 1271 |
# -----------------------------
|
| 1272 |
+
async def handle_message(user_message, user_id, chat_id, messages, request_id: str = None, audio_emotion: dict = None, language: str = None, emotion_callback=None):
|
| 1273 |
logger.info(f"📥 handle_message: 收到訊息='{user_message}', user_id={user_id}, audio_emotion={audio_emotion}, language={language}")
|
| 1274 |
|
| 1275 |
# 指令優先,避免進入管線造成不必要延遲
|
|
|
|
| 1365 |
ai_timeout=20.0, # AI回應超時 (30 → 20)
|
| 1366 |
)
|
| 1367 |
logger.info(f"⚙️ 準備調用 ChatPipeline.process,user_message='{user_message}', audio_emotion={audio_emotion}, language={language}")
|
| 1368 |
+
res: PipelineResult = await pipeline.process(user_message, user_id=user_id, chat_id=chat_id, request_id=request_id, audio_emotion=audio_emotion, language=language, emotion_callback=emotion_callback)
|
| 1369 |
logger.info(f"⚙️ ChatPipeline.process 完成,結果='{res.text}', is_fallback={res.is_fallback}, reason={res.reason}")
|
| 1370 |
|
| 1371 |
# 檢查是否有工具元數據
|
core/pipeline.py
CHANGED
|
@@ -209,6 +209,7 @@ class ChatPipeline:
|
|
| 209 |
request_id: Optional[str] = None,
|
| 210 |
audio_emotion: Optional[Dict[str, Any]] = None,
|
| 211 |
language: Optional[str] = None,
|
|
|
|
| 212 |
) -> PipelineResult:
|
| 213 |
if not user_message or not user_message.strip():
|
| 214 |
return PipelineResult(
|
|
@@ -269,6 +270,13 @@ class ChatPipeline:
|
|
| 269 |
logger.info(f"💙 用戶 {user_id} 在關懷模式中,跳過工具調用,使用關懷 AI")
|
| 270 |
# 直接用關懷模式 AI 回應(不檢測意圖,不調用工具)
|
| 271 |
care_emotion = EmotionCareManager.get_care_emotion(user_id, chat_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
ai_res = await self._with_timeout(
|
| 273 |
self._ai_generator(
|
| 274 |
user_message,
|
|
@@ -301,6 +309,13 @@ class ChatPipeline:
|
|
| 301 |
):
|
| 302 |
logger.warning(f"⚠️ 偵測到極端情緒 [{emotion_value}](置信度: {emotion_confidence:.2f}),進入關懷模式")
|
| 303 |
# 立即使用關懷模式 AI 回應
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
ai_res = await self._with_timeout(
|
| 305 |
self._ai_generator(
|
| 306 |
user_message,
|
|
@@ -325,6 +340,12 @@ class ChatPipeline:
|
|
| 325 |
exit_hint = "\n\n💙 關懷模式已啟動。說「我沒事了」可以退出。"
|
| 326 |
return PipelineResult(text=text + exit_hint, is_fallback=False, meta={"care_mode": True, "emotion": emotion_value})
|
| 327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
# 3) 有功能 → 功能處理(限時)
|
| 329 |
if has_feature and intent_data:
|
| 330 |
feat_res = await self._with_timeout(
|
|
|
|
| 209 |
request_id: Optional[str] = None,
|
| 210 |
audio_emotion: Optional[Dict[str, Any]] = None,
|
| 211 |
language: Optional[str] = None,
|
| 212 |
+
emotion_callback = None,
|
| 213 |
) -> PipelineResult:
|
| 214 |
if not user_message or not user_message.strip():
|
| 215 |
return PipelineResult(
|
|
|
|
| 270 |
logger.info(f"💙 用戶 {user_id} 在關懷模式中,跳過工具調用,使用關懷 AI")
|
| 271 |
# 直接用關懷模式 AI 回應(不檢測意圖,不調用工具)
|
| 272 |
care_emotion = EmotionCareManager.get_care_emotion(user_id, chat_id)
|
| 273 |
+
final_emotion = care_emotion or emotion_value
|
| 274 |
+
if emotion_callback:
|
| 275 |
+
try:
|
| 276 |
+
await emotion_callback(final_emotion, True)
|
| 277 |
+
except Exception as e:
|
| 278 |
+
logger.warning(f"emotion_callback 錯誤: {e}")
|
| 279 |
+
|
| 280 |
ai_res = await self._with_timeout(
|
| 281 |
self._ai_generator(
|
| 282 |
user_message,
|
|
|
|
| 309 |
):
|
| 310 |
logger.warning(f"⚠️ 偵測到極端情緒 [{emotion_value}](置信度: {emotion_confidence:.2f}),進入關懷模式")
|
| 311 |
# 立即使用關懷模式 AI 回應
|
| 312 |
+
|
| 313 |
+
if emotion_callback:
|
| 314 |
+
try:
|
| 315 |
+
await emotion_callback(emotion_value, True)
|
| 316 |
+
except Exception as e:
|
| 317 |
+
logger.warning(f"emotion_callback 錯誤: {e}")
|
| 318 |
+
|
| 319 |
ai_res = await self._with_timeout(
|
| 320 |
self._ai_generator(
|
| 321 |
user_message,
|
|
|
|
| 340 |
exit_hint = "\n\n💙 關懷模式已啟動。說「我沒事了」可以退出。"
|
| 341 |
return PipelineResult(text=text + exit_hint, is_fallback=False, meta={"care_mode": True, "emotion": emotion_value})
|
| 342 |
|
| 343 |
+
if emotion_callback:
|
| 344 |
+
try:
|
| 345 |
+
await emotion_callback(emotion_value, False)
|
| 346 |
+
except Exception as e:
|
| 347 |
+
logger.warning(f"emotion_callback 錯誤: {e}")
|
| 348 |
+
|
| 349 |
# 3) 有功能 → 功能處理(限時)
|
| 350 |
if has_feature and intent_data:
|
| 351 |
feat_res = await self._with_timeout(
|
tests/test_heartbeat.py
CHANGED
|
@@ -2,6 +2,10 @@
|
|
| 2 |
測試 websocket/heartbeat.py 心跳機制
|
| 3 |
"""
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import pytest
|
| 6 |
import time
|
| 7 |
from websocket.heartbeat import HeartbeatManager, HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT
|
|
|
|
| 2 |
測試 websocket/heartbeat.py 心跳機制
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 8 |
+
|
| 9 |
import pytest
|
| 10 |
import time
|
| 11 |
from websocket.heartbeat import HeartbeatManager, HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT
|
tests/test_websocket_manager.py
CHANGED
|
@@ -2,6 +2,10 @@
|
|
| 2 |
測試 websocket/manager.py 連線管理器
|
| 3 |
"""
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import pytest
|
| 6 |
from datetime import datetime, timedelta
|
| 7 |
from websocket.manager import ConnectionManager
|
|
|
|
| 2 |
測試 websocket/manager.py 連線管理器
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 8 |
+
|
| 9 |
import pytest
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
from websocket.manager import ConnectionManager
|