- 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>
208 lines
6.5 KiB
Python
208 lines
6.5 KiB
Python
"""Prompt builders 在 language='en' 时返回纯 ASCII;language='zh' 时含中文(防回归)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from app.agents.chat.output_rules import (
|
||
chat_output_rules,
|
||
chat_output_rules_en,
|
||
chat_voice_style,
|
||
chat_voice_style_en,
|
||
)
|
||
from app.agents.chat.personas import AGENT_NAME_EN, AGENT_NAME_ZH, agent_name
|
||
from app.agents.chat.prompts_conversation import (
|
||
get_guided_conversation_prompt,
|
||
get_opening_prompt,
|
||
)
|
||
from app.agents.chat.prompts_profile import (
|
||
get_profile_followup_prompt,
|
||
get_profile_greeting_prompt,
|
||
)
|
||
from app.agents.memoir.prompts import (
|
||
get_creative_title_json_prompt,
|
||
get_memoir_fidelity_facts_only_prompt,
|
||
get_memoir_fidelity_system_prompt,
|
||
get_narrative_json_prompt,
|
||
)
|
||
from app.agents.stage_constants import (
|
||
chapter_category_display,
|
||
stage_display_name,
|
||
)
|
||
|
||
|
||
def _has_cjk(s: str) -> bool:
|
||
return any("\u4e00" <= ch <= "\u9fff" for ch in s)
|
||
|
||
|
||
# ── chat output rules ────────────────────────────────────────────────
|
||
|
||
|
||
def test_chat_output_rules_en_has_no_cjk() -> None:
|
||
txt = chat_output_rules_en()
|
||
assert txt.strip()
|
||
assert not _has_cjk(txt)
|
||
|
||
|
||
def test_chat_voice_style_en_has_no_cjk() -> None:
|
||
txt = chat_voice_style_en()
|
||
assert txt.strip()
|
||
assert not _has_cjk(txt)
|
||
|
||
|
||
def test_chat_output_rules_zh_unchanged_has_cjk() -> None:
|
||
assert _has_cjk(chat_output_rules())
|
||
|
||
|
||
def test_chat_voice_style_zh_unchanged_has_cjk() -> None:
|
||
assert _has_cjk(chat_voice_style())
|
||
|
||
|
||
# ── memoir narrative ────────────────────────────────────────────────
|
||
|
||
|
||
def test_memoir_fidelity_facts_only_en_branch_has_no_cjk() -> None:
|
||
assert not _has_cjk(get_memoir_fidelity_facts_only_prompt(language="en"))
|
||
|
||
|
||
def test_memoir_fidelity_facts_only_zh_default_has_cjk() -> None:
|
||
assert _has_cjk(get_memoir_fidelity_facts_only_prompt())
|
||
|
||
|
||
def test_memoir_fidelity_system_en_branch_has_no_cjk() -> None:
|
||
assert not _has_cjk(get_memoir_fidelity_system_prompt(language="en"))
|
||
|
||
|
||
def test_get_narrative_json_prompt_en_has_no_cjk() -> None:
|
||
out = get_narrative_json_prompt(
|
||
stage="childhood",
|
||
slots={"place": "the village"},
|
||
new_content="[User's oral memory this turn]\nI grew up by the river.",
|
||
language="en",
|
||
)
|
||
assert not _has_cjk(out)
|
||
assert "paragraphs" in out
|
||
|
||
|
||
def test_get_narrative_json_prompt_zh_default_has_cjk() -> None:
|
||
out = get_narrative_json_prompt(
|
||
stage="childhood",
|
||
slots={"place": "村里"},
|
||
new_content="【本段用户口述】\n我小时候住在河边。",
|
||
)
|
||
assert _has_cjk(out)
|
||
|
||
|
||
# ── memoir title ────────────────────────────────────────────────────
|
||
|
||
|
||
def test_creative_title_json_prompt_en_has_no_cjk() -> None:
|
||
out = get_creative_title_json_prompt(
|
||
stage="childhood",
|
||
emotion="warm",
|
||
slots={"place": "the river"},
|
||
language="en",
|
||
)
|
||
assert not _has_cjk(out)
|
||
assert "title" in out
|
||
|
||
|
||
def test_creative_title_json_prompt_zh_default_has_cjk() -> None:
|
||
out = get_creative_title_json_prompt(
|
||
stage="childhood",
|
||
emotion="warm",
|
||
slots={"place": "河边"},
|
||
)
|
||
assert _has_cjk(out)
|
||
|
||
|
||
# ── stage / category display helpers ────────────────────────────────
|
||
|
||
|
||
def test_stage_display_name_branches() -> None:
|
||
assert stage_display_name("childhood", language="en") == "Childhood"
|
||
assert stage_display_name("childhood", language="zh") == "童年时光"
|
||
# unknown stages pass through
|
||
assert stage_display_name("unknown", language="en") == "unknown"
|
||
|
||
|
||
def test_chapter_category_display_branches() -> None:
|
||
assert chapter_category_display("childhood", language="en") == "Childhood & Early Years"
|
||
assert chapter_category_display("childhood", language="zh") == "童年与成长背景"
|
||
|
||
|
||
# ── agent brand name (interviewer identity) ─────────────────────────
|
||
|
||
|
||
def test_agent_name_constants_aligned_with_brand() -> None:
|
||
"""中英品牌名是单一来源,prompt / UI / 兜底标题统一引用此处。"""
|
||
assert AGENT_NAME_ZH == "岁月知己"
|
||
assert AGENT_NAME_EN == "Life Echo"
|
||
|
||
|
||
def test_agent_name_helper_handles_inputs() -> None:
|
||
assert agent_name("zh") == "岁月知己"
|
||
assert agent_name("en") == "Life Echo"
|
||
assert agent_name("EN") == "Life Echo"
|
||
assert agent_name(" en ") == "Life Echo"
|
||
assert agent_name(None) == "岁月知己" # type: ignore[arg-type]
|
||
assert agent_name("ja") == "岁月知己"
|
||
|
||
|
||
def test_profile_greeting_prompt_introduces_life_echo_in_en() -> None:
|
||
en = get_profile_greeting_prompt(
|
||
["birth_year", "occupation"], nickname="Sam", language="en"
|
||
)
|
||
assert "Life Echo" in en
|
||
assert "岁月知己" not in en
|
||
|
||
|
||
def test_profile_greeting_prompt_keeps_chinese_brand_in_zh() -> None:
|
||
zh = get_profile_greeting_prompt(["birth_year"], nickname="老王")
|
||
assert "岁月知己" in zh
|
||
assert "Life Echo" not in zh
|
||
|
||
|
||
def test_profile_followup_prompt_introduces_life_echo_in_en() -> None:
|
||
en = get_profile_followup_prompt(
|
||
missing_fields=["occupation"],
|
||
filled_fields={"birth_year": "1990"},
|
||
language="en",
|
||
)
|
||
assert "Life Echo" in en
|
||
assert "岁月知己" not in en
|
||
|
||
|
||
def test_profile_followup_prompt_full_basics_branch_introduces_life_echo() -> None:
|
||
en = get_profile_followup_prompt(
|
||
missing_fields=[],
|
||
filled_fields={
|
||
"birth_year": "1990",
|
||
"birth_place": "Boston",
|
||
"grew_up_place": "Boston",
|
||
"occupation": "engineer",
|
||
},
|
||
language="en",
|
||
)
|
||
assert "Life Echo" in en
|
||
assert "岁月知己" not in en
|
||
|
||
|
||
def test_opening_prompt_introduces_life_echo_in_en() -> None:
|
||
out = get_opening_prompt(
|
||
current_stage="childhood",
|
||
empty_slots_readable=["place", "people"],
|
||
language="en",
|
||
)
|
||
assert "Life Echo" in out
|
||
assert "岁月知己" not in out
|
||
|
||
|
||
def test_guided_conversation_prompt_introduces_life_echo_in_en() -> None:
|
||
out = get_guided_conversation_prompt(
|
||
current_stage="childhood",
|
||
empty_slots=["place"],
|
||
filled_slots={},
|
||
language="en",
|
||
)
|
||
assert "Life Echo" in out
|
||
assert "岁月知己" not in out
|