Spaces:
Sleeping
Sleeping
Commit ·
0267618
1
Parent(s): 4c9d246
language_upgrade
Browse files- core/pipeline.py +59 -162
- services/ai_service.py +25 -86
core/pipeline.py
CHANGED
|
@@ -47,158 +47,110 @@ class ChatPipeline:
|
|
| 47 |
self._feature_timeout = feature_timeout
|
| 48 |
self._ai_timeout = ai_timeout
|
| 49 |
self._model = model
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
self._language_names = {
|
| 53 |
-
"zh": "繁體中文",
|
| 54 |
-
"en": "English",
|
| 55 |
-
"ja": "日本語",
|
| 56 |
-
"ko": "한국어",
|
| 57 |
-
"id": "Bahasa Indonesia",
|
| 58 |
-
"vi": "Tiếng Việt",
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
def _detect_language(self, text: str) -> str:
|
| 62 |
"""
|
| 63 |
-
簡
|
| 64 |
-
|
| 65 |
Args:
|
| 66 |
-
text:
|
| 67 |
-
|
| 68 |
Returns:
|
| 69 |
-
|
| 70 |
"""
|
| 71 |
if not text:
|
| 72 |
-
return
|
| 73 |
-
|
| 74 |
-
#
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
vietnamese_chars = set("àáảãạăằắẳẵặâầấẩẫậèéẻẽẹêềếểễệìíỉĩịòóỏõọôồốổỗộơờớởỡợùúủũụưừứửữựỳýỷỹỵđ")
|
| 82 |
-
|
| 83 |
-
for char in text:
|
| 84 |
-
code = ord(char)
|
| 85 |
-
# 韓文
|
| 86 |
-
if 0xAC00 <= code <= 0xD7AF or 0x1100 <= code <= 0x11FF:
|
| 87 |
-
korean_count += 1
|
| 88 |
-
# 日文假名
|
| 89 |
-
elif 0x3040 <= code <= 0x309F or 0x30A0 <= code <= 0x30FF:
|
| 90 |
-
japanese_count += 1
|
| 91 |
-
# 中文
|
| 92 |
-
elif 0x4E00 <= code <= 0x9FFF:
|
| 93 |
-
chinese_count += 1
|
| 94 |
-
# 拉丁字母
|
| 95 |
-
elif 0x0041 <= code <= 0x007A:
|
| 96 |
-
latin_count += 1
|
| 97 |
-
# 越南文特殊字符
|
| 98 |
-
if char.lower() in vietnamese_chars:
|
| 99 |
-
vietnamese_count += 1
|
| 100 |
-
|
| 101 |
-
# 判斷主要語言
|
| 102 |
-
if korean_count > 0:
|
| 103 |
-
return "ko"
|
| 104 |
-
if japanese_count > chinese_count and japanese_count > 0:
|
| 105 |
-
return "ja"
|
| 106 |
-
if vietnamese_count > 0:
|
| 107 |
-
return "vi"
|
| 108 |
-
if chinese_count > latin_count and chinese_count > 0:
|
| 109 |
-
return "zh"
|
| 110 |
-
if latin_count > 0:
|
| 111 |
-
# 可能是英文或印尼文,預設英文
|
| 112 |
-
return "en"
|
| 113 |
-
|
| 114 |
-
return "zh"
|
| 115 |
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
| 117 |
"""
|
| 118 |
-
|
| 119 |
-
|
| 120 |
Args:
|
| 121 |
tool_data: 工具資料字典
|
| 122 |
-
|
| 123 |
-
|
| 124 |
Returns:
|
| 125 |
翻譯後的工具資料
|
| 126 |
"""
|
| 127 |
-
if not tool_data
|
| 128 |
return tool_data
|
| 129 |
-
|
| 130 |
try:
|
| 131 |
import copy
|
| 132 |
translated_data = copy.deepcopy(tool_data)
|
| 133 |
-
|
| 134 |
-
# 需要翻譯的欄位
|
| 135 |
translatable_keys = {
|
| 136 |
-
"description", "main", "name", "title", "summary",
|
| 137 |
"content", "message", "text", "label", "status"
|
| 138 |
}
|
| 139 |
-
|
| 140 |
-
# 收集需要翻譯的文字
|
| 141 |
texts_to_translate = []
|
| 142 |
-
text_paths = []
|
| 143 |
-
|
| 144 |
def collect_texts(obj, path="", parent_key=""):
|
| 145 |
"""遞迴收集需要翻譯的文字"""
|
| 146 |
if isinstance(obj, dict):
|
| 147 |
for key, value in obj.items():
|
| 148 |
new_path = f"{path}.{key}" if path else key
|
| 149 |
-
# 跳過
|
| 150 |
-
if key in ("id", "url", "link", "lat", "lon", "timestamp", "code", "icon"
|
| 151 |
continue
|
| 152 |
collect_texts(value, new_path, key)
|
| 153 |
elif isinstance(obj, list):
|
| 154 |
for i, item in enumerate(obj):
|
| 155 |
collect_texts(item, f"{path}[{i}]", parent_key)
|
| 156 |
elif isinstance(obj, str) and len(obj) > 1:
|
| 157 |
-
# 翻譯條件
|
| 158 |
-
# 1. 欄位名稱在可翻譯列表中
|
| 159 |
-
# 2. 或字串包含中文
|
| 160 |
-
# 3. 或字串是純英文描述(非數字、非代碼)
|
| 161 |
should_translate = (
|
| 162 |
parent_key.lower() in translatable_keys or
|
| 163 |
-
any('\u4e00' <= c <= '\u9fff' for c in obj)
|
| 164 |
-
(obj.isalpha() or ' ' in obj) and len(obj) > 2
|
| 165 |
)
|
| 166 |
if should_translate:
|
| 167 |
texts_to_translate.append(obj)
|
| 168 |
text_paths.append(path)
|
| 169 |
-
|
| 170 |
collect_texts(translated_data)
|
| 171 |
-
|
| 172 |
if not texts_to_translate:
|
| 173 |
return tool_data
|
| 174 |
-
|
| 175 |
-
# 批量翻譯
|
| 176 |
import services.ai_service as ai_service
|
| 177 |
-
|
| 178 |
-
|
| 179 |
combined_text = "\n---\n".join(texts_to_translate)
|
| 180 |
messages = [
|
| 181 |
{
|
| 182 |
"role": "system",
|
| 183 |
-
"content": f"將以下內容翻譯成
|
| 184 |
},
|
| 185 |
{"role": "user", "content": combined_text}
|
| 186 |
]
|
| 187 |
-
|
| 188 |
translated = await ai_service.generate_response_async(
|
| 189 |
messages=messages,
|
| 190 |
model="gpt-5-nano",
|
| 191 |
reasoning_effort="minimal",
|
| 192 |
-
max_tokens=800,
|
| 193 |
)
|
| 194 |
-
|
| 195 |
if translated:
|
| 196 |
translated_parts = translated.strip().split("---")
|
| 197 |
translated_parts = [p.strip() for p in translated_parts if p.strip()]
|
| 198 |
-
|
| 199 |
# 回填翻譯結果
|
| 200 |
def set_value(obj, path, value):
|
| 201 |
-
"""根據路徑設置值"""
|
| 202 |
parts = path.replace("]", "").replace("[", ".").split(".")
|
| 203 |
for part in parts[:-1]:
|
| 204 |
if part.isdigit():
|
|
@@ -210,67 +162,20 @@ class ChatPipeline:
|
|
| 210 |
obj[int(last)] = value
|
| 211 |
else:
|
| 212 |
obj[last] = value
|
| 213 |
-
|
| 214 |
for i, path in enumerate(text_paths):
|
| 215 |
if i < len(translated_parts):
|
| 216 |
try:
|
| 217 |
set_value(translated_data, path, translated_parts[i])
|
| 218 |
except Exception:
|
| 219 |
pass
|
| 220 |
-
|
| 221 |
logger.info(f"🌐 工具卡片已翻譯: {len(texts_to_translate)} 個欄位")
|
| 222 |
return translated_data
|
| 223 |
-
|
| 224 |
-
except Exception as e:
|
| 225 |
-
logger.warning(f"⚠️ 工具卡片翻譯失敗: {e}")
|
| 226 |
-
return tool_data
|
| 227 |
|
| 228 |
-
async def _translate_tool_response(self, text: str, target_language: str) -> str:
|
| 229 |
-
"""
|
| 230 |
-
翻譯工具回應到目標語言
|
| 231 |
-
|
| 232 |
-
Args:
|
| 233 |
-
text: 原始文字(中文)
|
| 234 |
-
target_language: 目標語言代碼(en, ja, ko, id, vi)
|
| 235 |
-
|
| 236 |
-
Returns:
|
| 237 |
-
翻譯後的文字
|
| 238 |
-
"""
|
| 239 |
-
if not text or target_language == "zh":
|
| 240 |
-
return text
|
| 241 |
-
|
| 242 |
-
try:
|
| 243 |
-
import services.ai_service as ai_service
|
| 244 |
-
|
| 245 |
-
lang_name = self._language_names.get(target_language, target_language)
|
| 246 |
-
|
| 247 |
-
messages = [
|
| 248 |
-
{
|
| 249 |
-
"role": "system",
|
| 250 |
-
"content": f"你是一個翻譯助手。將以下內容翻譯成 {lang_name},保持格式、表情符號和數字不變。只輸出翻譯結果,不要加任何解釋。"
|
| 251 |
-
},
|
| 252 |
-
{
|
| 253 |
-
"role": "user",
|
| 254 |
-
"content": text
|
| 255 |
-
}
|
| 256 |
-
]
|
| 257 |
-
|
| 258 |
-
translated = await ai_service.generate_response_async(
|
| 259 |
-
messages=messages,
|
| 260 |
-
model="gpt-5-nano",
|
| 261 |
-
reasoning_effort="minimal",
|
| 262 |
-
max_tokens=500, # 工具回應翻譯:實際輸出限制 500 tokens
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
if translated and translated.strip():
|
| 266 |
-
logger.info(f"🌐 工具回應已翻譯: {target_language}")
|
| 267 |
-
return translated.strip()
|
| 268 |
-
|
| 269 |
-
return text
|
| 270 |
-
|
| 271 |
except Exception as e:
|
| 272 |
-
logger.warning(f"⚠️ 翻譯失敗,使用原
|
| 273 |
-
return
|
| 274 |
|
| 275 |
async def _with_timeout(self, coro: Awaitable[Any], timeout: float, reason: str) -> Any:
|
| 276 |
try:
|
|
@@ -302,10 +207,7 @@ class ChatPipeline:
|
|
| 302 |
if not user_message or not user_message.strip():
|
| 303 |
return PipelineResult(text="我沒有收到您的消息,請重新輸入。", is_fallback=True, reason="empty")
|
| 304 |
|
| 305 |
-
# 自動
|
| 306 |
-
if not language:
|
| 307 |
-
language = self._detect_language(user_message)
|
| 308 |
-
logger.info(f"🌐 自動檢測語言: {language}")
|
| 309 |
|
| 310 |
# 0) 先進行意圖偵測以提取情緒(需要在關懷模式檢查前執行)
|
| 311 |
detect_res = await self._with_timeout(
|
|
@@ -415,14 +317,11 @@ class ChatPipeline:
|
|
| 415 |
tool_data = feat_res.get('tool_data')
|
| 416 |
if not text:
|
| 417 |
return PipelineResult(text="抱歉,功能處理沒有產出結果。", is_fallback=True, reason="feature-empty")
|
| 418 |
-
|
| 419 |
-
#
|
| 420 |
-
if
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
if tool_data:
|
| 424 |
-
tool_data = await self._translate_tool_data(tool_data, language)
|
| 425 |
-
|
| 426 |
# 返回帶有工具元數據的結果(包含情緒)
|
| 427 |
meta_dict = {}
|
| 428 |
if tool_name:
|
|
@@ -441,11 +340,9 @@ class ChatPipeline:
|
|
| 441 |
text = str(feat_res or "").strip()
|
| 442 |
if not text:
|
| 443 |
return PipelineResult(text="抱歉,功能處理沒有產出結果。", is_fallback=True, reason="feature-empty")
|
| 444 |
-
|
| 445 |
-
#
|
| 446 |
-
|
| 447 |
-
text = await self._translate_tool_response(text, language)
|
| 448 |
-
|
| 449 |
return PipelineResult(
|
| 450 |
text=text,
|
| 451 |
is_fallback=False,
|
|
|
|
| 47 |
self._feature_timeout = feature_timeout
|
| 48 |
self._ai_timeout = ai_timeout
|
| 49 |
self._model = model
|
| 50 |
+
|
| 51 |
+
def _is_chinese_message(self, text: str) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
"""
|
| 53 |
+
簡化語言判斷:檢測訊息是否為中文
|
| 54 |
+
|
| 55 |
Args:
|
| 56 |
+
text: 用戶訊息
|
| 57 |
+
|
| 58 |
Returns:
|
| 59 |
+
True 如果訊息主要是中文,False 如果是其他語言
|
| 60 |
"""
|
| 61 |
if not text:
|
| 62 |
+
return True # 預設為中文
|
| 63 |
+
|
| 64 |
+
# 計算中文字符比例
|
| 65 |
+
chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
| 66 |
+
total_chars = len(text.replace(' ', '').replace('\n', ''))
|
| 67 |
+
|
| 68 |
+
if total_chars == 0:
|
| 69 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
# 如果中文字符超過 30%,視為中文訊息
|
| 72 |
+
return chinese_chars > total_chars * 0.3
|
| 73 |
+
|
| 74 |
+
async def _translate_tool_data(self, tool_data: Dict[str, Any], user_message: str) -> Dict[str, Any]:
|
| 75 |
"""
|
| 76 |
+
簡化版工具卡片翻譯:讓 GPT 自動判斷目標語言
|
| 77 |
+
|
| 78 |
Args:
|
| 79 |
tool_data: 工具資料字典
|
| 80 |
+
user_message: 用戶原始訊息(用於推斷目標語言)
|
| 81 |
+
|
| 82 |
Returns:
|
| 83 |
翻譯後的工具資料
|
| 84 |
"""
|
| 85 |
+
if not tool_data:
|
| 86 |
return tool_data
|
| 87 |
+
|
| 88 |
try:
|
| 89 |
import copy
|
| 90 |
translated_data = copy.deepcopy(tool_data)
|
| 91 |
+
|
| 92 |
+
# 需要翻譯的欄位(天氣、新聞等工具的顯示欄位)
|
| 93 |
translatable_keys = {
|
| 94 |
+
"description", "main", "name", "title", "summary",
|
| 95 |
"content", "message", "text", "label", "status"
|
| 96 |
}
|
| 97 |
+
|
| 98 |
+
# 收集需要翻譯的文字
|
| 99 |
texts_to_translate = []
|
| 100 |
+
text_paths = []
|
| 101 |
+
|
| 102 |
def collect_texts(obj, path="", parent_key=""):
|
| 103 |
"""遞迴收集需要翻譯的文字"""
|
| 104 |
if isinstance(obj, dict):
|
| 105 |
for key, value in obj.items():
|
| 106 |
new_path = f"{path}.{key}" if path else key
|
| 107 |
+
# 跳過技術欄位
|
| 108 |
+
if key in ("id", "url", "link", "lat", "lon", "timestamp", "code", "icon"):
|
| 109 |
continue
|
| 110 |
collect_texts(value, new_path, key)
|
| 111 |
elif isinstance(obj, list):
|
| 112 |
for i, item in enumerate(obj):
|
| 113 |
collect_texts(item, f"{path}[{i}]", parent_key)
|
| 114 |
elif isinstance(obj, str) and len(obj) > 1:
|
| 115 |
+
# 需要翻譯的條件
|
|
|
|
|
|
|
|
|
|
| 116 |
should_translate = (
|
| 117 |
parent_key.lower() in translatable_keys or
|
| 118 |
+
any('\u4e00' <= c <= '\u9fff' for c in obj) # 包含中文
|
|
|
|
| 119 |
)
|
| 120 |
if should_translate:
|
| 121 |
texts_to_translate.append(obj)
|
| 122 |
text_paths.append(path)
|
| 123 |
+
|
| 124 |
collect_texts(translated_data)
|
| 125 |
+
|
| 126 |
if not texts_to_translate:
|
| 127 |
return tool_data
|
| 128 |
+
|
| 129 |
+
# 批量翻譯(讓 GPT 自動判斷目標語言)
|
| 130 |
import services.ai_service as ai_service
|
| 131 |
+
|
|
|
|
| 132 |
combined_text = "\n---\n".join(texts_to_translate)
|
| 133 |
messages = [
|
| 134 |
{
|
| 135 |
"role": "system",
|
| 136 |
+
"content": f"將以下內容翻譯成與用戶訊息「{user_message}」相同的語言。保持格式和表情符號。每段用 '---' 分隔,輸出也用 '---' 分隔。只輸出翻譯結果,不要加解釋。"
|
| 137 |
},
|
| 138 |
{"role": "user", "content": combined_text}
|
| 139 |
]
|
| 140 |
+
|
| 141 |
translated = await ai_service.generate_response_async(
|
| 142 |
messages=messages,
|
| 143 |
model="gpt-5-nano",
|
| 144 |
reasoning_effort="minimal",
|
| 145 |
+
max_tokens=800,
|
| 146 |
)
|
| 147 |
+
|
| 148 |
if translated:
|
| 149 |
translated_parts = translated.strip().split("---")
|
| 150 |
translated_parts = [p.strip() for p in translated_parts if p.strip()]
|
| 151 |
+
|
| 152 |
# 回填翻譯結果
|
| 153 |
def set_value(obj, path, value):
|
|
|
|
| 154 |
parts = path.replace("]", "").replace("[", ".").split(".")
|
| 155 |
for part in parts[:-1]:
|
| 156 |
if part.isdigit():
|
|
|
|
| 162 |
obj[int(last)] = value
|
| 163 |
else:
|
| 164 |
obj[last] = value
|
| 165 |
+
|
| 166 |
for i, path in enumerate(text_paths):
|
| 167 |
if i < len(translated_parts):
|
| 168 |
try:
|
| 169 |
set_value(translated_data, path, translated_parts[i])
|
| 170 |
except Exception:
|
| 171 |
pass
|
| 172 |
+
|
| 173 |
logger.info(f"🌐 工具卡片已翻譯: {len(texts_to_translate)} 個欄位")
|
| 174 |
return translated_data
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
except Exception as e:
|
| 177 |
+
logger.warning(f"⚠️ 工具卡片翻譯失敗,使用原始數據: {e}")
|
| 178 |
+
return tool_data
|
| 179 |
|
| 180 |
async def _with_timeout(self, coro: Awaitable[Any], timeout: float, reason: str) -> Any:
|
| 181 |
try:
|
|
|
|
| 207 |
if not user_message or not user_message.strip():
|
| 208 |
return PipelineResult(text="我沒有收到您的消息,請重新輸入。", is_fallback=True, reason="empty")
|
| 209 |
|
| 210 |
+
# language 參數保留以向後兼容,但不使用(GPT 自動判斷語言)
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
# 0) 先進行意圖偵測以提取情緒(需要在關懷模式檢查前執行)
|
| 213 |
detect_res = await self._with_timeout(
|
|
|
|
| 317 |
tool_data = feat_res.get('tool_data')
|
| 318 |
if not text:
|
| 319 |
return PipelineResult(text="抱歉,功能處理沒有產出結果。", is_fallback=True, reason="feature-empty")
|
| 320 |
+
|
| 321 |
+
# 簡化翻譯:非中文用戶 → 翻譯工具卡片
|
| 322 |
+
if not self._is_chinese_message(user_message) and tool_data:
|
| 323 |
+
tool_data = await self._translate_tool_data(tool_data, user_message)
|
| 324 |
+
|
|
|
|
|
|
|
|
|
|
| 325 |
# 返回帶有工具元數據的結果(包含情緒)
|
| 326 |
meta_dict = {}
|
| 327 |
if tool_name:
|
|
|
|
| 340 |
text = str(feat_res or "").strip()
|
| 341 |
if not text:
|
| 342 |
return PipelineResult(text="抱歉,功能處理沒有產出結果。", is_fallback=True, reason="feature-empty")
|
| 343 |
+
|
| 344 |
+
# 不再翻譯工具回應,讓 GPT 自己處理並用對應語言描述
|
| 345 |
+
|
|
|
|
|
|
|
| 346 |
return PipelineResult(
|
| 347 |
text=text,
|
| 348 |
is_fallback=False,
|
services/ai_service.py
CHANGED
|
@@ -17,35 +17,22 @@ from core.ai_client import get_openai_client
|
|
| 17 |
# 超時設定(秒)
|
| 18 |
OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
LANGUAGE_INSTRUCTIONS = {
|
| 22 |
-
"zh": "請使用繁體中文回覆",
|
| 23 |
-
"en": "Please respond in English",
|
| 24 |
-
"id": "Silakan balas dalam Bahasa Indonesia",
|
| 25 |
-
"ja": "日本語で返信してください",
|
| 26 |
-
"vi": "Vui lòng trả lời bằng tiếng Việt",
|
| 27 |
-
"auto": "請使用與用戶相同的語言回覆"
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
# 情緒關懷模式 System Prompt(新增)
|
| 31 |
CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」,由銘傳大學人工智慧應用學系槓上開發團隊打造。你不是 GPT,也不要自稱 GPT;你的任務是在情緒低落時傾聽、陪伴。
|
| 32 |
|
| 33 |
【回應原則】
|
| 34 |
-
1. 第一句必須貼近用戶訊息中的核心事件或感受,必要時引用對方用詞,讓對方感受到被理解
|
| 35 |
-
2. 第二句提供溫柔的陪伴或追問,邀請對方分享需要或下一步;若用戶提出明確請求(如想聽笑話),可在保持關懷語氣下予以回應或確認
|
| 36 |
-
3. 句式要自然口語並隨內容調整字詞,避免反覆使用同一套罐頭話術
|
| 37 |
|
| 38 |
【長度限制】
|
| 39 |
-
- 回覆最多 2 句話、總字數不超過 60 字
|
| 40 |
|
| 41 |
【嚴格禁止】
|
| 42 |
-
- 提供指示性建議、醫療/心理診斷或引導用戶求助的教科書式說法
|
| 43 |
-
- 連續重複完全相同的句型,例如一再出現「我在這裡陪你」而沒有結合具體情境
|
| 44 |
|
| 45 |
-
【
|
| 46 |
-
用戶:「我好難過」 → 你:「聽見你說自己好難過,心裡一定很不好受。想聊聊剛剛發生了什麼嗎?」
|
| 47 |
-
用戶:「我很生氣」 → 你:「這件事讓你超級生氣,情緒一定卡著。要不要跟我說說最困擾你的地方?」
|
| 48 |
-
用戶:「講笑話給我聽」 → 你:「你想聽點輕鬆的,我當然可以陪你。想先聽小笑話還是先聊聊怎麼了?」"""
|
| 49 |
|
| 50 |
# 取得 OpenAI 客戶端(使用統一管理)
|
| 51 |
def _get_client():
|
|
@@ -81,7 +68,7 @@ def _build_base_system_prompt(
|
|
| 81 |
use_care_mode: bool,
|
| 82 |
care_emotion: Optional[str],
|
| 83 |
user_name: Optional[str],
|
| 84 |
-
language: Optional[str] = None,
|
| 85 |
) -> str:
|
| 86 |
if use_care_mode:
|
| 87 |
base_prompt = CARE_MODE_SYSTEM_PROMPT.strip()
|
|
@@ -93,10 +80,8 @@ def _build_base_system_prompt(
|
|
| 93 |
"你不是 GPT,也不要自稱 GPT。"
|
| 94 |
"你是一個友善、有禮、幽默且能夠提供幫助的AI助手。"
|
| 95 |
)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
language_instruction = LANGUAGE_INSTRUCTIONS.get(language or "auto", LANGUAGE_INSTRUCTIONS["auto"])
|
| 99 |
-
base_prompt = f"{base_prompt}\n\n【重要】{language_instruction},保持簡潔清晰的表達。"
|
| 100 |
|
| 101 |
if user_name:
|
| 102 |
base_prompt = f"用戶名稱:{user_name}\n\n{base_prompt}"
|
|
@@ -750,36 +735,13 @@ async def _generate_response_with_chat_db(
|
|
| 750 |
try:
|
| 751 |
if messages:
|
| 752 |
if not any(msg.get("role") == "system" for msg in messages):
|
| 753 |
-
#
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
language_instruction = {
|
| 761 |
-
"zh": "繁體中文",
|
| 762 |
-
"en": "English",
|
| 763 |
-
"ko": "한국어 (Korean)",
|
| 764 |
-
"ja": "日本語 (Japanese)",
|
| 765 |
-
"id": "Bahasa Indonesia",
|
| 766 |
-
"vi": "Tiếng Việt (Vietnamese)"
|
| 767 |
-
}.get(language, "繁體中文")
|
| 768 |
-
|
| 769 |
-
system_prompt = (
|
| 770 |
-
"你是 BloomWare 的個人化助理 小花,由銘傳大學人工智慧應用學系 槓上開發 團隊開發。"
|
| 771 |
-
"你不是 GPT,也不要自稱 GPT。"
|
| 772 |
-
"你是一個友善、有禮、幽默且能夠提供幫助的AI助手。\n\n"
|
| 773 |
-
f"【重要】語言使用規範:\n"
|
| 774 |
-
f"- 回覆用戶時:必須使用 {language_instruction},保持簡潔清晰的表達\n"
|
| 775 |
-
"- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等)\n"
|
| 776 |
-
"- 範例:用戶問「台北天氣」→ 調用工具時參數用 {\"city\": \"Taipei\"},回覆時用對應語言描述"
|
| 777 |
-
)
|
| 778 |
-
|
| 779 |
-
# 在系統提示前加上用戶名稱
|
| 780 |
-
if user_name:
|
| 781 |
-
system_prompt = f"用戶名稱:{user_name}\n\n{system_prompt}"
|
| 782 |
-
|
| 783 |
messages.insert(0, {"role": "system", "content": system_prompt})
|
| 784 |
ai_response = await generate_response_async(
|
| 785 |
messages,
|
|
@@ -970,36 +932,13 @@ async def _generate_response_with_global_history(
|
|
| 970 |
try:
|
| 971 |
if messages:
|
| 972 |
if not any(msg.get("role") == "system" for msg in messages):
|
| 973 |
-
#
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
language_instruction = {
|
| 981 |
-
"zh": "繁體中文",
|
| 982 |
-
"en": "English",
|
| 983 |
-
"ko": "한국어 (Korean)",
|
| 984 |
-
"ja": "日本語 (Japanese)",
|
| 985 |
-
"id": "Bahasa Indonesia",
|
| 986 |
-
"vi": "Tiếng Việt (Vietnamese)"
|
| 987 |
-
}.get(language, "繁體中文")
|
| 988 |
-
|
| 989 |
-
system_prompt = (
|
| 990 |
-
"你是 BloomWare 的個人化助理 小花,由銘傳大學人工智慧應用學系 槓上開發 團隊開發。"
|
| 991 |
-
"你不是 GPT,也不要自稱 GPT。"
|
| 992 |
-
"你是一個友善、有禮、幽默且能夠提供幫助的AI助手。\n\n"
|
| 993 |
-
f"【重要】語言使用規範:\n"
|
| 994 |
-
f"- 回覆用戶時:必須使用 {language_instruction},保持簡潔清晰的表達\n"
|
| 995 |
-
"- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等)\n"
|
| 996 |
-
"- 範例:用戶問「台北天氣」→ 調用工具時參數用 {\"city\": \"Taipei\"},回覆時用對應語言描述"
|
| 997 |
-
)
|
| 998 |
-
|
| 999 |
-
# 在系統提示前加上用戶名稱
|
| 1000 |
-
if user_name:
|
| 1001 |
-
system_prompt = f"用戶名稱:{user_name}\n\n{system_prompt}"
|
| 1002 |
-
|
| 1003 |
messages.insert(0, {"role": "system", "content": system_prompt})
|
| 1004 |
user_messages = [msg for msg in messages if msg.get("role") == "user"]
|
| 1005 |
if user_messages and user_id not in conversation_history:
|
|
|
|
| 17 |
# 超時設定(秒)
|
| 18 |
OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT
|
| 19 |
|
| 20 |
+
# 情緒關懷模式 System Prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」,由銘傳大學人工智慧應用學系槓上開發團隊打造。你不是 GPT,也不要自稱 GPT;你的任務是在情緒低落時傾聽、陪伴。
|
| 22 |
|
| 23 |
【回應原則】
|
| 24 |
+
1. 第一句必須貼近用戶訊息中的核心事件或感受,必要時引用對方用詞,讓對方感受到被理解
|
| 25 |
+
2. 第二句提供溫柔的陪伴或追問,邀請對方分享需要或下一步;若用戶提出明確請求(如想聽笑話),可在保持關懷語氣下予以回應或確認
|
| 26 |
+
3. 句式要自然口語並隨內容調整字詞,避免反覆使用同一套罐頭話術
|
| 27 |
|
| 28 |
【長度限制】
|
| 29 |
+
- 回覆最多 2 句話、總字數不超過 60 字
|
| 30 |
|
| 31 |
【嚴格禁止】
|
| 32 |
+
- 提供指示性建議、醫療/心理診斷或引導用戶求助的教科書式說法
|
| 33 |
+
- 連續重複完全相同的句型,例如一再出現「我在這裡陪你」而沒有結合具體情境
|
| 34 |
|
| 35 |
+
【重要】請用與用戶相同的語言回應,匹配他們的語言風格和情感語調。"""
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# 取得 OpenAI 客戶端(使用統一管理)
|
| 38 |
def _get_client():
|
|
|
|
| 68 |
use_care_mode: bool,
|
| 69 |
care_emotion: Optional[str],
|
| 70 |
user_name: Optional[str],
|
| 71 |
+
language: Optional[str] = None, # 保留參數以兼容現有調用,但不使用
|
| 72 |
) -> str:
|
| 73 |
if use_care_mode:
|
| 74 |
base_prompt = CARE_MODE_SYSTEM_PROMPT.strip()
|
|
|
|
| 80 |
"你不是 GPT,也不要自稱 GPT。"
|
| 81 |
"你是一個友善、有禮、幽默且能夠提供幫助的AI助手。"
|
| 82 |
)
|
| 83 |
+
# 簡化語言指令 - 讓 GPT 自動判斷用戶語言
|
| 84 |
+
base_prompt = f"{base_prompt}\n\n【重要】請用與用戶相同的語言回應,保持簡潔清晰的表達。"
|
|
|
|
|
|
|
| 85 |
|
| 86 |
if user_name:
|
| 87 |
base_prompt = f"用戶名稱:{user_name}\n\n{base_prompt}"
|
|
|
|
| 735 |
try:
|
| 736 |
if messages:
|
| 737 |
if not any(msg.get("role") == "system" for msg in messages):
|
| 738 |
+
# 使用統一的 System Prompt 構建函數
|
| 739 |
+
system_prompt = _build_base_system_prompt(
|
| 740 |
+
use_care_mode=use_care_mode,
|
| 741 |
+
care_emotion=care_emotion,
|
| 742 |
+
user_name=user_name,
|
| 743 |
+
language=language # 參數保留但不使用,GPT 自動判斷語言
|
| 744 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
messages.insert(0, {"role": "system", "content": system_prompt})
|
| 746 |
ai_response = await generate_response_async(
|
| 747 |
messages,
|
|
|
|
| 932 |
try:
|
| 933 |
if messages:
|
| 934 |
if not any(msg.get("role") == "system" for msg in messages):
|
| 935 |
+
# 使用統一的 System Prompt 構建函數
|
| 936 |
+
system_prompt = _build_base_system_prompt(
|
| 937 |
+
use_care_mode=use_care_mode,
|
| 938 |
+
care_emotion=care_emotion,
|
| 939 |
+
user_name=user_name,
|
| 940 |
+
language=language # 參數保留但不使用,GPT 自動判斷語言
|
| 941 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
messages.insert(0, {"role": "system", "content": system_prompt})
|
| 943 |
user_messages = [msg for msg in messages if msg.get("role") == "user"]
|
| 944 |
if user_messages and user_id not in conversation_history:
|