feat(i18n): persist language preference and thread through chat, memoir, TTS
- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS only; expose on auth and profile APIs - Lite English prompts for chat and memoir; localized stage labels and agent names (Life Echo / 岁月知己) - Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking - WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs for tts_this_turn and TTS decisions; on-demand TTS logging - Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes - Tests for migration, prompts, pipeline, router tts_this_turn, reply segments Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,9 +51,20 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_UNAUTH_TURN = AgentChatTurn(
|
||||
_UNAUTH_TURN_ZH = AgentChatTurn(
|
||||
messages=["暂时没法继续对话,请先登录后再试。"], skip_tts=True
|
||||
)
|
||||
_UNAUTH_TURN_EN = AgentChatTurn(
|
||||
messages=["You'll need to sign in again before we can continue."],
|
||||
skip_tts=True,
|
||||
)
|
||||
|
||||
|
||||
def _user_language(user: Optional["User"]) -> str:
|
||||
if not user:
|
||||
return "zh"
|
||||
lang = getattr(user, "language_preference", None) or "zh"
|
||||
return "en" if str(lang).lower() == "en" else "zh"
|
||||
|
||||
|
||||
async def _fetch_interview_memory_bundle(
|
||||
@@ -145,6 +156,7 @@ class ChatOrchestrator:
|
||||
根据 missing_fields 路由到 ProfileAgent 或 InterviewAgent。
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
language = _user_language(user)
|
||||
|
||||
# --- 资料收集模式 ---
|
||||
if user:
|
||||
@@ -179,7 +191,10 @@ class ChatOrchestrator:
|
||||
# Profile 阶段每轮都抽取:短确认语也可能带可推断资料,跳过抽取会导致槽位长期不更新
|
||||
extracted = (
|
||||
await self.profile_agent.extract_profile_from_message(
|
||||
user_message, missing, conversation_id=conversation_id
|
||||
user_message,
|
||||
missing,
|
||||
conversation_id=conversation_id,
|
||||
language=language,
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
@@ -198,7 +213,7 @@ class ChatOrchestrator:
|
||||
if not remaining:
|
||||
st = await get_or_create_state(user.id, db)
|
||||
interview_stage_hint = life_stage_display_name(
|
||||
st.current_stage
|
||||
st.current_stage, language=language
|
||||
)
|
||||
responses = await self.profile_agent.generate_profile_followup(
|
||||
conversation_id=conversation_id,
|
||||
@@ -207,6 +222,7 @@ class ChatOrchestrator:
|
||||
filled_fields=filled,
|
||||
nickname=user.nickname or "",
|
||||
interview_stage_hint=interview_stage_hint,
|
||||
language=language,
|
||||
)
|
||||
if agent_summary_enabled():
|
||||
logger.info(
|
||||
@@ -223,8 +239,13 @@ class ChatOrchestrator:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("资料收集处理失败: {}", e)
|
||||
fb_msg = (
|
||||
"Sorry, I missed that. Could you say it again?"
|
||||
if language == "en"
|
||||
else "不好意思刚才没接住,你再说一遍好吗?"
|
||||
)
|
||||
return AgentChatTurn(
|
||||
messages=["不好意思刚才没接住,你再说一遍好吗?"],
|
||||
messages=[fb_msg],
|
||||
skip_tts=False,
|
||||
memory_retrieval_trace=None,
|
||||
)
|
||||
@@ -239,7 +260,7 @@ class ChatOrchestrator:
|
||||
(time.perf_counter() - t0) * 1000,
|
||||
conversation_id,
|
||||
)
|
||||
return _UNAUTH_TURN
|
||||
return _UNAUTH_TURN_EN if language == "en" else _UNAUTH_TURN_ZH
|
||||
|
||||
log_agent_detail(
|
||||
logger,
|
||||
@@ -284,6 +305,7 @@ class ChatOrchestrator:
|
||||
birth_place=user.birth_place,
|
||||
grew_up_place=user.grew_up_place,
|
||||
occupation=user.occupation,
|
||||
language=language,
|
||||
)
|
||||
background_voice = infer_background_voice(user.occupation)
|
||||
occupation = user.occupation or ""
|
||||
@@ -331,6 +353,7 @@ class ChatOrchestrator:
|
||||
profile_era_place=profile_era_place,
|
||||
stage_switched_this_turn=stage_switched_this_turn,
|
||||
scene_cues_for_planner=scene_cues_for_planner,
|
||||
language=language,
|
||||
)
|
||||
recent_questions = prompt_state.recent_questions
|
||||
if turn.interview_state_meta and isinstance(turn.interview_state_meta, dict):
|
||||
@@ -387,6 +410,7 @@ class ChatOrchestrator:
|
||||
voice_session_id: str | None = None,
|
||||
user_message_timestamp: datetime | None = None,
|
||||
audio_duration_seconds: int | None = None,
|
||||
language: str = "zh",
|
||||
) -> List[str]:
|
||||
"""委托 ProfileAgent 生成资料追问(持久化由调用方负责)。"""
|
||||
return await self.profile_agent.generate_profile_followup(
|
||||
@@ -395,6 +419,7 @@ class ChatOrchestrator:
|
||||
missing_fields=missing_fields,
|
||||
filled_fields=filled_fields,
|
||||
nickname=nickname,
|
||||
language=language,
|
||||
)
|
||||
|
||||
async def generate_profile_greeting(
|
||||
@@ -402,12 +427,14 @@ class ChatOrchestrator:
|
||||
conversation_id: str,
|
||||
missing_fields: List[str],
|
||||
nickname: str = "",
|
||||
language: str = "zh",
|
||||
) -> List[str]:
|
||||
"""委托 ProfileAgent 生成资料收集开场白(持久化由调用方负责)。"""
|
||||
return await self.profile_agent.generate_profile_greeting(
|
||||
conversation_id=conversation_id,
|
||||
missing_fields=missing_fields,
|
||||
nickname=nickname,
|
||||
language=language,
|
||||
)
|
||||
|
||||
async def generate_response_with_state(
|
||||
@@ -431,6 +458,7 @@ class ChatOrchestrator:
|
||||
profile_era_place: str = "",
|
||||
stage_switched_this_turn: bool = False,
|
||||
scene_cues_for_planner: Optional[list[str]] = None,
|
||||
language: str = "zh",
|
||||
) -> AgentChatTurn:
|
||||
"""委托 InterviewAgent 生成访谈回复(持久化由调用方负责)。"""
|
||||
return await self.interview_agent.generate_response_with_state(
|
||||
@@ -449,6 +477,7 @@ class ChatOrchestrator:
|
||||
profile_era_place=profile_era_place,
|
||||
stage_switched_this_turn=stage_switched_this_turn,
|
||||
scene_cues_for_planner=scene_cues_for_planner,
|
||||
language=language,
|
||||
)
|
||||
|
||||
def detect_user_stage(self, user_message: str) -> str:
|
||||
@@ -464,6 +493,7 @@ class ChatOrchestrator:
|
||||
occupation: str = "",
|
||||
profile_birth_year: Optional[int] = None,
|
||||
profile_era_place: str = "",
|
||||
language: str = "zh",
|
||||
) -> List[str]:
|
||||
"""
|
||||
委托 InterviewAgent 生成访谈开场白(持久化由调用方 ConversationHistoryStore 负责)。
|
||||
@@ -476,4 +506,5 @@ class ChatOrchestrator:
|
||||
occupation=occupation,
|
||||
profile_birth_year=profile_birth_year,
|
||||
profile_era_place=profile_era_place,
|
||||
language=language,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user