2026-01-07 11:56:53 +08:00
"""
2026-04-08 21:36:12 +08:00
对话 Agent 提示词模板 ( 场景化承接 + 细节深挖 + 人物串联 ) 。
2026-01-07 11:56:53 +08:00
"""
2026-03-19 14:36:14 +08:00
2026-03-31 23:55:26 +08:00
from typing import Dict , List , Optional
from app . agents . chat . background_voice import (
2026-04-06 22:22:50 +08:00
get_background_voice_tone_hint ,
2026-03-31 23:55:26 +08:00
normalize_background_voice ,
)
2026-04-01 11:49:33 +08:00
from app . agents . chat . occupation_context import get_occupation_chat_hint
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>
2026-05-11 16:16:49 +08:00
from app . agents . chat . output_rules import (
chat_output_rules ,
chat_output_rules_en ,
)
2026-03-31 23:55:26 +08:00
from app . agents . chat . personas import (
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>
2026-05-11 16:16:49 +08:00
AGENT_NAME_EN ,
AGENT_NAME_ZH ,
2026-04-06 22:22:50 +08:00
get_interview_persona_tone_hint ,
2026-03-31 23:55:26 +08:00
normalize_interview_persona ,
)
2026-04-22 16:56:28 +08:00
from app . agents . chat . prompt_layers import (
assemble_guided_prompt ,
build_absolute_donts_block ,
build_behavior_policy_block ,
build_context_block ,
build_question_outline_block ,
build_reply_strategy_block ,
build_style_profile_block ,
)
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>
2026-05-11 16:16:49 +08:00
from app . agents . stage_constants import (
STAGE_ERA_HINTS ,
stage_display_name ,
)
2026-04-10 13:55:08 +08:00
from app . agents . state_schema import KnownFact , PersonaThread
2026-03-26 12:13:36 +08:00
from app . core . config import settings
2026-01-07 11:56:53 +08:00
2026-04-22 16:56:28 +08:00
# 风格示例的单一事实源已迁至 `app.agents.style_profiles.ChatStyleProfile.reply_style_examples`;
# 这里**不再**维护具体字面示例,避免同一模块被当作 few-shot 锚点反复注入,导致风格过拟合。
2026-04-09 15:32:35 +08:00
2026-01-21 22:31:03 +01:00
SLOT_NAME_MAP = {
" place " : " 成长的地方 " ,
" people " : " 重要的人 " ,
" daily_life " : " 日常生活 " ,
" emotion " : " 童年感受 " ,
" turning_event " : " 难忘的事 " ,
" school " : " 学校经历 " ,
" city " : " 求学的城市 " ,
" motivation " : " 学习动力 " ,
" challenge " : " 遇到的挑战 " ,
" change " : " 成长变化 " ,
" job " : " 工作内容 " ,
" environment " : " 工作环境 " ,
" decision " : " 重要决定 " ,
" pressure " : " 压力与困难 " ,
" growth " : " 职业成长 " ,
" relationship " : " 家人关系 " ,
" conflict " : " 矛盾与化解 " ,
" support " : " 相互支持 " ,
" responsibility " : " 家庭责任 " ,
" value " : " 核心价值观 " ,
" regret " : " 遗憾与释怀 " ,
" pride " : " 骄傲的事 " ,
" lesson " : " 人生经验 " ,
}
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>
2026-05-11 16:16:49 +08:00
SLOT_NAME_MAP_EN = {
" place " : " where you grew up " ,
" people " : " important people " ,
" daily_life " : " everyday life " ,
" emotion " : " childhood feelings " ,
" turning_event " : " memorable moments " ,
" school " : " school experiences " ,
" city " : " the city you studied in " ,
" motivation " : " what drove you " ,
" challenge " : " challenges you faced " ,
" change " : " how you changed " ,
" job " : " what you did at work " ,
" environment " : " your work environment " ,
" decision " : " important decisions " ,
" pressure " : " pressure and hardship " ,
" growth " : " career growth " ,
" relationship " : " family relationships " ,
" conflict " : " conflicts and resolutions " ,
" support " : " mutual support " ,
" responsibility " : " family responsibilities " ,
" value " : " core values " ,
" regret " : " regrets and acceptance " ,
" pride " : " moments you ' re proud of " ,
" lesson " : " life lessons " ,
}
def slot_name_map_for ( language : str ) - > Dict [ str , str ] :
return SLOT_NAME_MAP_EN if language == " en " else SLOT_NAME_MAP
2026-01-29 20:09:09 +01:00
2026-04-08 15:37:09 +08:00
def _compact_era_hint (
current_stage : str ,
* ,
birth_year : int | None = None ,
era_place : str = " " ,
) - > str :
2026-04-06 22:22:50 +08:00
if not birth_year :
return " "
2026-04-08 15:37:09 +08:00
birth_place = ( era_place or " " ) . strip ( )
2026-04-06 22:22:50 +08:00
age_range = STAGE_ERA_HINTS . get ( current_stage , ( 0 , 30 ) )
era_start = birth_year + age_range [ 0 ]
era_end = birth_year + age_range [ 1 ]
era_events = [ ]
decade_events = {
1950 : " 新中国成立初期、土地改革、抗美援朝 " ,
1960 : " 大跃进、三年自然灾害、中苏关系变化 " ,
1970 : " 文化大革命、知青上山下乡、中美建交 " ,
1980 : " 改革开放、恢复高考、个体经济兴起、电视普及 " ,
1990 : " 社会主义市场经济、下海潮、香港回归、互联网初期 " ,
2000 : " 加入WTO、房地产兴起、手机普及、北京奥运 " ,
2010 : " 移动互联网爆发、微信时代、共享经济、双创浪潮 " ,
2020 : " 新冠疫情、直播经济、人工智能崛起 " ,
}
for decade , events in decade_events . items ( ) :
if era_start < = decade + 9 and era_end > = decade :
era_events . append ( f " { decade } 年代: { events } " )
2026-01-29 20:09:09 +01:00
2026-04-08 17:10:09 +08:00
parts : List [ str ] = [ ]
if era_events :
place_hint = f " { birth_place } " if birth_place else " "
parts . append (
f " 时代联想(口述里一两句带过即可):约 { era_start } - { era_end } 年 { place_hint } ; "
f " 可提及 { era_events [ 0 ] } "
+ ( f " ; { era_events [ 1 ] } " if len ( era_events ) > 1 else " " )
+ " 。 "
)
parts . append (
" 时代与流行文化(开放式,自然带入): \n "
" - 可从当时的街景、媒介、校园与市井、年节习俗等**泛泛**起头,邀请用户讲自己的版本,勿替用户断言细节。 \n "
" - **优先开放式**问法;少用「你是不是也……」式半封闭逼认。 \n "
" - 与大事记呼应时点到为止,勿展开成长串史实。 "
2026-04-02 12:00:00 +08:00
)
2026-04-08 17:10:09 +08:00
return " \n " . join ( parts ) + " \n "
2026-01-29 20:09:09 +01:00
2026-01-21 22:31:03 +01:00
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>
2026-05-11 16:16:49 +08:00
def _get_opening_prompt_en (
current_stage : str ,
empty_slots_readable : List [ str ] ,
user_profile_context : str = " " ,
profile_birth_year : Optional [ int ] = None ,
profile_era_place : str = " " ,
) - > str :
""" English-lite opening prompt; ignores persona/background-voice nuances. """
stage_name = stage_display_name ( current_stage , language = " en " )
if empty_slots_readable :
topics_str = " , " . join ( empty_slots_readable )
topics_heading = (
f " ## Suggested topics for this stage ( { stage_name } ) \n "
f " Pick one of these to ask about: { topics_str } "
)
task_question = (
" 2. You are a **warm, host-style confidant**: ask one **specific, "
" easy-to-answer, vivid** question that pulls the user into telling a "
" life memory; ideally land on one of the topics above. Avoid vague "
" openers like \" How have you been? \" Open the door with one small "
" anchor (a place, a person, an object, or a tiny scene from a day). "
)
else :
topics_heading = (
f " ## Current stage ( { stage_name } ) \n "
" The main topics for this stage are largely covered. Open with "
" something tied to a previous memory or a fresh small angle of this "
" stage; do not interrogate from the start. "
)
task_question = (
" 2. **Greeting + a memory hook**: after a warm acknowledgement, "
" drop a light, concrete question tied to recollection — never "
" small-talk filler. "
)
profile_section = " "
if user_profile_context . strip ( ) :
profile_section = " ## About the user \n " + user_profile_context . strip ( ) + " \n "
return f """ You are " { AGENT_NAME_EN } " — a warm host-style friend. The user just opened the chat and **has not said anything yet**; you speak first. Tone like an old friend, but your job is to help the user start telling their life story; in two or three short sentences, give a greeting plus **one vivid, recollection-oriented question** tied to the current stage or suggested topics. No flowery prose, no long literary descriptions, no generic small-talk.
{ profile_section } { topics_heading }
## Task
1. Brief greeting .
{ task_question }
3. Sound natural and warm .
## Format
- Use ` [ SPLIT ] ` to break into at most two short bubbles , or keep greeting + question in one short bubble .
- { chat_output_rules_en ( ) } Do not write the user ' s answer for them.
Output ( spoken - style English only , no Markdown ) : """
2026-03-11 14:39:39 +08:00
def get_opening_prompt (
current_stage : str ,
empty_slots_readable : List [ str ] ,
user_profile_context : str = " " ,
2026-03-31 23:55:26 +08:00
persona : str = " default " ,
background_voice : str = " default " ,
2026-04-01 11:49:33 +08:00
occupation : str = " " ,
2026-04-08 17:10:09 +08:00
profile_birth_year : Optional [ int ] = None ,
profile_era_place : str = " " ,
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>
2026-05-11 16:16:49 +08:00
language : str = " zh " ,
2026-03-11 14:39:39 +08:00
) - > str :
2026-03-19 10:54:48 +08:00
""" 空对话时 AI 先开口的提示词 """
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>
2026-05-11 16:16:49 +08:00
if language == " en " :
return _get_opening_prompt_en (
current_stage ,
empty_slots_readable ,
user_profile_context = user_profile_context ,
profile_birth_year = profile_birth_year ,
profile_era_place = profile_era_place ,
)
stage_name = stage_display_name ( current_stage , language = " zh " )
2026-04-08 17:10:09 +08:00
bv_open = normalize_background_voice ( background_voice )
2026-03-20 15:15:35 +08:00
if empty_slots_readable :
topics_str = " 、 " . join ( empty_slots_readable )
topics_heading = (
f " ## 当前建议话题( { stage_name } ) \n 可以从中选一个来问: { topics_str } "
)
task_question = (
2026-04-10 13:55:08 +08:00
" 2. 你是**主持式知己**:接着问一个**具体、好回答、有画面感**的问题,帮用户进入**人生回忆**叙述; "
" 优先落在上述还未聊透的方向上。不要问太宽泛的「有什么想聊的」「最近怎么样」。 "
" 像把门敞开请人讲自己的故事,不要像面试第一题;一句里带一个小锚(地方、人物、物件或一天里的片段即可)。 "
2026-04-09 15:32:35 +08:00
" 不要用「下面我们聊聊…」类未承接的硬切。好问题举例:「说到童年,你脑海里最先蹦出来的是哪个画面?」 "
2026-03-20 15:15:35 +08:00
)
else :
topics_heading = (
f " ## 当前阶段( { stage_name } ) \n "
2026-04-06 22:22:50 +08:00
" 这一阶段的主要话题在素材侧**已有覆盖**。 "
2026-04-22 16:56:28 +08:00
" 开场仍要**回到人生故事线**:优先接续上次聊过的片段、(若有)记忆线索里出现过的事,或当前阶段里**新鲜的一小角**; "
2026-04-10 13:55:08 +08:00
" **禁止**为了凑问题而从「童年在哪长大」等已覆盖模板重头盘问;**也不要**把泛泛近况(「今天忙吗」「最近好吗」)当成默认主线。 "
2026-03-20 15:15:35 +08:00
)
task_question = (
2026-04-10 13:55:08 +08:00
" 2. **问候 + 回忆向勾子**:温暖接话后,带一个与**口述回忆**有关的轻巧引子或具体问题; "
" 若接不上具体事,就用当前阶段的一个**有画面的开放式起头**,仍落在人生经历上,而非纯社交寒暄。 "
2026-03-20 15:15:35 +08:00
)
2026-04-08 17:10:09 +08:00
if bv_open == " cadre " :
opening_style_rules = (
" ## 语境与语气(干部/机关) \n "
" - 问候稳重、敬语适度;避免官样排比与过轻佻的网络撒娇语气。 \n "
)
elif bv_open == " military " :
opening_style_rules = (
" ## 语境与语气(军队相关口述常见交流方式) \n "
" - 简洁、得体;不用「嗨~」类过轻佻起势;不堆军事辞藻、不编军旅细节。 \n "
)
else :
opening_style_rules = (
" ## 风格 \n "
2026-04-10 13:55:08 +08:00
" - 像**温暖的谈话场主持人**:口语、自然、能接住人,但默认把用户带进**人生回忆**叙述; "
" 可轻快,允许一点画面感,不要排比和长段文学描写。 \n "
2026-03-20 15:15:35 +08:00
)
2026-04-06 22:22:50 +08:00
profile_lines : List [ str ] = [ ]
if user_profile_context . strip ( ) :
profile_lines . append ( user_profile_context . strip ( ) )
occ = get_occupation_chat_hint ( occupation , background_voice )
if occ :
profile_lines . append ( occ )
profile_section = " "
if profile_lines :
profile_section = " ## 用户信息 \n " + " \n " . join ( profile_lines ) + " \n "
2026-03-31 23:55:26 +08:00
persona_key = normalize_interview_persona ( persona )
2026-04-06 22:22:50 +08:00
persona_tone = get_interview_persona_tone_hint ( persona_key )
voice_tone = get_background_voice_tone_hint ( background_voice )
tone_bits = [ t for t in ( persona_tone , voice_tone ) if t ]
tone_paragraph = " "
if tone_bits :
tone_paragraph = " " + " " . join ( tone_bits ) + " \n \n "
opening_head = (
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>
2026-05-11 16:16:49 +08:00
f " 你是「 { AGENT_NAME_ZH } 」——主持式知己:用户刚进对话,**还没说话**,请你先开口。 "
2026-04-10 13:55:08 +08:00
" 语气像老朋友,但**职责是帮对方开口讲人生故事**;两三句内问候 + **一个落在当前阶段或建议话题上的、有画面感的问题**; "
" 不要排比、不要长段文学描写,**不要**把泛泛问近况当主菜。 \n \n "
2026-04-06 22:22:50 +08:00
)
2026-04-08 17:10:09 +08:00
if bv_open != " default " :
2026-03-31 23:55:26 +08:00
opening_head = (
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>
2026-05-11 16:16:49 +08:00
f " 你是「 { AGENT_NAME_ZH } 」——主持式知己:用户刚进对话,**还没说话**,请你先开口。 "
2026-04-10 13:55:08 +08:00
" **短**;两三句内问候 + **一个回忆向的具体问题**;不要排比、不要文学描写。 \n \n "
2026-03-31 23:55:26 +08:00
)
2026-04-06 22:22:50 +08:00
2026-04-08 17:10:09 +08:00
era_opening_line = " "
if (
settings . chat_era_context_enabled
and profile_birth_year is not None
and _compact_era_hint (
current_stage ,
birth_year = profile_birth_year ,
era_place = profile_era_place ,
)
) :
era_opening_line = (
" 4. 用户资料里已有出生年份与时代参考时,问候里的具体问题可**轻轻带一点年代氛围**(点到为止), "
" 勿写成长段描写或排比。 \n "
)
2026-04-06 22:22:50 +08:00
return f """ { opening_head } { tone_paragraph } { profile_section } { topics_heading }
2026-03-26 12:13:36 +08:00
## 任务
1. 简短问候 。
2026-03-20 15:15:35 +08:00
{ task_question }
2026-04-08 21:36:12 +08:00
3. 自然 、 温暖 。
2026-04-08 17:10:09 +08:00
{ era_opening_line }
2026-03-26 12:13:36 +08:00
## 格式
- 可用 [ SPLIT ] 分成最多 2 条 ; 或一条里 「 问候 + 问题 」 。
2026-04-03 13:34:27 +08:00
- { chat_output_rules ( ) } 不要替用户编回答 。
2026-03-11 14:39:39 +08:00
2026-04-08 17:10:09 +08:00
{ opening_style_rules }
2026-04-03 14:06:55 +08:00
直接输出 ( 仅自然口语 , 无 Markdown ) : """
2026-03-11 14:39:39 +08:00
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>
2026-05-11 16:16:49 +08:00
def _get_guided_conversation_prompt_en (
current_stage : str ,
empty_slots : List [ str ] ,
filled_slots : Dict [ str , str ] ,
detected_user_stage : str = " " ,
user_profile_context : str = " " ,
memory_evidence_text : str = " " ,
recent_questions : list [ str ] | None = None ,
turn_directive_block : str = " " ,
) - > str :
""" English-lite guided interview prompt (no persona/voice nuances). """
stage_name = stage_display_name ( current_stage , language = " en " )
detected_name = (
stage_display_name ( detected_user_stage , language = " en " )
if detected_user_stage and detected_user_stage != current_stage
else " "
)
empty_readable = [ SLOT_NAME_MAP_EN . get ( s , s ) for s in empty_slots ]
filled_lines = [ ]
for k , v in ( filled_slots or { } ) . items ( ) :
name = SLOT_NAME_MAP_EN . get ( k , k )
if v :
filled_lines . append ( f " - { name } : { v } " )
filled_block = " \n " . join ( filled_lines ) if filled_lines else " (none yet) "
suggested_block = (
" Suggested still-open angles for this stage: " + " , " . join ( empty_readable )
if empty_readable
else " Main angles for this stage are largely covered. "
)
detected_line = (
f " \n The user is currently talking about: ** { detected_name } ** (system was tracking ** { stage_name } **). "
if detected_name
else " "
)
profile_section = " "
if user_profile_context . strip ( ) :
profile_section = " \n ## About the user \n " + user_profile_context . strip ( )
memory_section = " "
if ( memory_evidence_text or " " ) . strip ( ) :
memory_section = (
" \n ## Reference memory snippets (for continuity only — do NOT write them as the user ' s first-person experience this turn) \n "
+ memory_evidence_text . strip ( )
)
recent_q_section = " "
if recent_questions :
last = recent_questions [ - 4 : ]
recent_q_section = (
" \n ## Recently asked questions (do NOT repeat these; offer a new angle) \n "
+ " \n " . join ( f " - { q } " for q in last )
)
directive_block = ( turn_directive_block or " " ) . strip ( )
directive_section = (
f " \n ## This turn ' s plan \n { directive_block } \n " if directive_block else " "
)
return f """ { directive_section } You are " { AGENT_NAME_EN } , " a warm host-style friend helping the user record a memoir. Reply in conversational English.
## Stage context
Currently tracking life stage : * * { stage_name } * * . { detected_line }
{ suggested_block }
## Already gathered for this stage
{ filled_block } { profile_section } { memory_section } { recent_q_section }
## Behaviour
- Pick up the * * specific * * detail the user just said ( one tangible noun or short phrase ) and gently push one step deeper before asking your next question .
- Prefer ONE clear , specific question per reply . Open - ended over forced A / B options .
- If the user is in the middle of a story , follow that thread ; do not switch topics for the sake of coverage .
- If you previously asked about something and the user already answered , do not re - ask .
- Stay short and precise . One acknowledgement sentence + one question is the default shape .
## Strict rules
- { chat_output_rules_en ( ) }
## Format
- Use ` [ SPLIT ] ` to split into at most two short bubbles when natural .
Reply in English only . Do not output Markdown headings . """
2026-01-21 22:31:03 +01:00
def get_guided_conversation_prompt (
current_stage : str ,
empty_slots : List [ str ] ,
filled_slots : Dict [ str , str ] ,
2026-03-31 23:55:26 +08:00
all_stages_coverage : Optional [ Dict [ str , Dict ] ] = None ,
2026-02-13 21:45:56 +01:00
detected_user_stage : str = " " ,
2026-03-01 10:12:23 +01:00
user_profile_context : str = " " ,
2026-03-31 23:55:26 +08:00
persona : str = " default " ,
memory_evidence_text : str = " " ,
background_voice : str = " default " ,
2026-04-01 11:49:33 +08:00
occupation : str = " " ,
2026-04-08 15:37:09 +08:00
profile_birth_year : Optional [ int ] = None ,
profile_era_place : str = " " ,
2026-04-08 21:36:12 +08:00
known_facts : list [ KnownFact ] | None = None ,
persona_threads : list [ PersonaThread ] | None = None ,
recent_questions : list [ str ] | None = None ,
2026-04-10 13:56:44 +08:00
turn_directive_block : str = " " ,
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>
2026-05-11 16:16:49 +08:00
language : str = " zh " ,
2026-01-21 22:31:03 +01:00
) - > str :
2026-04-06 22:22:50 +08:00
""" 生成状态感知的对话提示词;用户原话仅以 HumanMessage 传入,不写入本 system 文本。 """
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>
2026-05-11 16:16:49 +08:00
if language == " en " :
return _get_guided_conversation_prompt_en (
current_stage = current_stage ,
empty_slots = empty_slots ,
filled_slots = filled_slots ,
detected_user_stage = detected_user_stage ,
user_profile_context = user_profile_context ,
memory_evidence_text = memory_evidence_text ,
recent_questions = recent_questions ,
turn_directive_block = turn_directive_block ,
)
2026-03-31 23:55:26 +08:00
persona_key = normalize_interview_persona ( persona )
2026-04-06 22:22:50 +08:00
persona_tone = get_interview_persona_tone_hint ( persona_key )
voice_tone = get_background_voice_tone_hint ( background_voice )
tone_bits = [ t for t in ( persona_tone , voice_tone ) if t ]
tone_line = " "
if tone_bits :
tone_line = " " + " " . join ( tone_bits )
2026-02-13 21:45:56 +01:00
2026-04-02 12:00:00 +08:00
user_jumped = bool ( detected_user_stage and detected_user_stage != current_stage )
2026-04-06 22:22:50 +08:00
active_stage = (
detected_user_stage if user_jumped and detected_user_stage else current_stage
2026-04-02 12:00:00 +08:00
)
2026-04-22 16:56:28 +08:00
2026-04-06 22:22:50 +08:00
era_line = " "
if settings . chat_era_context_enabled :
2026-04-08 15:37:09 +08:00
era_line = _compact_era_hint (
active_stage ,
birth_year = profile_birth_year ,
era_place = profile_era_place ,
)
2026-03-31 23:55:26 +08:00
2026-04-22 16:56:28 +08:00
empty_slots_readable = [ SLOT_NAME_MAP . get ( s , s ) for s in empty_slots ]
2026-04-08 21:36:12 +08:00
2026-04-22 16:56:28 +08:00
# ---- Context 层:纯状态与素材 ----
topic_and_context_block = build_context_block (
current_stage = current_stage ,
detected_user_stage = detected_user_stage ,
empty_slots_readable = empty_slots_readable ,
filled_slots = filled_slots ,
slot_name_map = SLOT_NAME_MAP ,
all_stages_coverage = all_stages_coverage ,
user_profile_context = user_profile_context ,
occupation = occupation ,
background_voice = background_voice ,
known_facts = known_facts ,
persona_threads = persona_threads ,
recent_questions = recent_questions ,
memory_evidence_text = memory_evidence_text ,
era_line = era_line ,
)
2026-01-21 22:31:03 +01:00
2026-04-22 16:56:28 +08:00
question_outline_block = build_question_outline_block ( current_stage , empty_slots )
2026-03-01 10:12:23 +01:00
2026-04-22 16:56:28 +08:00
# ---- BehaviorPolicy 层:通用行为规则(本轮模式由 TurnPlan 单独注入) ----
behavior_policy_block = build_behavior_policy_block ( )
reply_strategy_block = build_reply_strategy_block ( )
absolute_donts_block = build_absolute_donts_block ( chat_output_rules ( ) )
2026-04-08 21:36:12 +08:00
2026-04-22 16:56:28 +08:00
# ---- StyleProfile 层:口吻 + 文采密度 + 成稿质量导向 ----
style_profile_block = build_style_profile_block (
persona = persona , background_voice = background_voice
2026-04-09 15:32:35 +08:00
)
2026-04-22 16:56:28 +08:00
return assemble_guided_prompt (
turn_directive_block = turn_directive_block ,
topic_and_context_block = topic_and_context_block ,
question_outline_block = question_outline_block ,
behavior_policy_block = behavior_policy_block ,
style_profile_block = style_profile_block ,
reply_strategy_block = reply_strategy_block ,
absolute_donts_block = absolute_donts_block ,
intro_tone_line = tone_line ,
2026-04-10 20:35:57 +08:00
)
2026-04-10 13:56:44 +08:00
2026-04-22 16:56:28 +08:00
# 运行时 prompt 生成走 `prompt_layers.assemble_guided_prompt`。
# 旧的超大 system prompt 已拆入 BehaviorPolicy / Context / StyleProfile 三层,此处不再保留快照。
2026-01-21 22:31:03 +01:00
2026-04-06 22:22:50 +08:00
2026-05-12 11:02:58 +08:00
def _get_re_greeting_prompt_en (
current_stage : str ,
empty_slots_readable : List [ str ] ,
user_profile_context : str = " " ,
background_voice : str = " default " ,
occupation : str = " " ,
idle_hours : float = 6.0 ,
) - > str :
""" English re-greeting; mirrors Chinese structure with lighter persona nuance. """
stage_name = stage_display_name ( current_stage , language = " en " )
bv = normalize_background_voice ( background_voice )
if idle_hours > = 168 :
idle_phrase = " it ' s been quite a while "
elif idle_hours > = 48 :
idle_phrase = " it ' s been several days "
elif idle_hours > = 20 :
idle_phrase = " about a day has passed "
else :
idle_phrase = " only a little time has passed "
if empty_slots_readable :
topics_str = " , " . join ( empty_slots_readable [ : 4 ] )
topic_hint = (
f " ## You can still explore ( { stage_name } ) \n "
f " If picking up last time feels hard, gently land on one of these: { topics_str } . "
)
else :
topic_hint = (
f " ## Current focus ( { stage_name } ) \n "
" Most beats here are covered; prefer returning to a concrete person, place, or moment from before. "
)
if bv == " cadre " :
style_note = " ## Tone \n Steady and respectful; no slick slogans or stacked parallelisms. "
elif bv == " military " :
style_note = " ## Tone \n Crisp and appropriate; no dramatic military flourishes. "
else :
style_note = " ## Tone \n Like an old friend you have not seen in a bit: warm, restrained, no preachy lists. "
profile_lines : List [ str ] = [ ]
if user_profile_context . strip ( ) :
profile_lines . append ( user_profile_context . strip ( ) )
occ = get_occupation_chat_hint ( occupation , background_voice )
if occ :
profile_lines . append ( occ )
profile_section = " "
if profile_lines :
profile_section = " ## About the user \n " + " \n " . join ( profile_lines ) + " \n "
head = (
f ' You are " { AGENT_NAME_EN } " — a host-style confidant. The user returns with **existing chat history** and has **not spoken yet** — you speak first. '
f " Context: { idle_phrase } since their last message. \n \n "
" **Job**: offer a warm reopening that shows you remember something specific they shared, then a light, memory-oriented hook you hand back to them. \n \n "
" ## Requirements \n "
" 1. **Must** reference one or two concrete details from the history (a person, place, object, or beat) — do not genericize with \" we had a great chat last time. \" \n "
" 2. **Do not** reuse a brand-new-chat hello; something like \" Last time you mentioned X — want to continue? \" fits better. \n "
" 3. The hook should be **specific, easy to answer, and visual**, tied to life memory — not \" how have you been \" small talk. \n "
" 4. If history offers no usable threads, pick a small landing from the stage hints; still avoid vague interrogation. \n "
" 5. Keep it short: two or three sentences, no long paragraphs. \n "
)
return f """ { head } { profile_section } { topic_hint }
{ style_note }
## Format
- Use ` [ SPLIT ] ` for at most two short bubbles , or one bubble with reopening + hook .
- { chat_output_rules_en ( ) } Do not write the user ' s answer for them.
Output ( spoken English only , no Markdown ) : """
2026-05-07 15:39:33 +00:00
def get_re_greeting_prompt (
current_stage : str ,
empty_slots_readable : List [ str ] ,
user_profile_context : str = " " ,
persona : str = " default " ,
background_voice : str = " default " ,
occupation : str = " " ,
profile_birth_year : Optional [ int ] = None ,
profile_era_place : str = " " ,
idle_hours : float = 6.0 ,
2026-05-12 11:02:58 +08:00
language : str = " zh " ,
2026-05-07 15:39:33 +00:00
) - > str :
""" 老对话回访问候提示词: 用户带着已有历史回到对话, AI 先开口做承接式问候。 """
2026-05-12 11:02:58 +08:00
if language == " en " :
return _get_re_greeting_prompt_en (
current_stage = current_stage ,
empty_slots_readable = empty_slots_readable ,
user_profile_context = user_profile_context ,
background_voice = background_voice ,
occupation = occupation ,
idle_hours = idle_hours ,
)
stage_name = stage_display_name ( current_stage , language = " zh " )
2026-05-07 15:39:33 +00:00
bv = normalize_background_voice ( background_voice )
if idle_hours > = 168 :
idle_phrase = " 好一阵子没聊了 "
elif idle_hours > = 48 :
idle_phrase = " 好几天没聊了 "
elif idle_hours > = 20 :
idle_phrase = " 隔了一天 "
else :
idle_phrase = " 今天又见面 "
if empty_slots_readable :
topics_str = " 、 " . join ( empty_slots_readable [ : 4 ] )
topic_hint = (
f " ## 当前阶段( { stage_name } )还可以聊 \n "
f " 如果上次聊过的事不便直接接续,可从这些方向里挑一个落点: { topics_str } 。 "
)
else :
topic_hint = (
f " ## 当前阶段( { stage_name } ) \n "
" 这一阶段主要话题已有覆盖;优先回到上次聊过的人/事/地方,做温和的承接。 "
)
if bv == " cadre " :
style_note = " ## 语气 \n 稳重、敬语适度;问候不油滑、不堆排比。 "
elif bv == " military " :
style_note = " ## 语气 \n 简洁、得体;不过度起势、不堆军事辞藻。 "
else :
style_note = " ## 语气 \n 像许久未见的老朋友,温暖而克制;不要排比、不要长段文学描写。 "
profile_lines : List [ str ] = [ ]
if user_profile_context . strip ( ) :
profile_lines . append ( user_profile_context . strip ( ) )
occ = get_occupation_chat_hint ( occupation , background_voice )
if occ :
profile_lines . append ( occ )
profile_section = " "
if profile_lines :
profile_section = " ## 用户信息 \n " + " \n " . join ( profile_lines ) + " \n "
persona_key = normalize_interview_persona ( persona )
persona_tone = get_interview_persona_tone_hint ( persona_key )
voice_tone = get_background_voice_tone_hint ( background_voice )
tone_bits = [ t for t in ( persona_tone , voice_tone ) if t ]
tone_paragraph = " "
if tone_bits :
tone_paragraph = " " + " " . join ( tone_bits ) + " \n \n "
head = (
" 你是「岁月知己」——主持式知己。用户带着**已有的对话历史**回到这里,**还没说话**,请你先开口。 "
f " 语境:距上次消息已经 { idle_phrase } 。 "
" **职责**:用一句温暖的承接打招呼,让对方感到「我记得你上次说过的事」,再轻轻递上一个**回忆向**的钩子,把话头交还给他。 \n \n "
" ## 要求 \n "
" 1. **必须**轻轻引用历史里的具体人/事/地方/物件做承接(一两个细节即可,不要罗列),不要空喊「上次聊得很好」。 \n "
" 2. **不要**用与刚开新对话相同的「您好/你好呀」式硬开场;像「上次你说到 X, 今天想接着讲讲吗? 」更合适。 \n "
" 3. 钩子要**具体、好回答、有画面感**,落在人生回忆里;不要问「最近怎么样」「今天忙吗」这种纯社交寒暄。 \n "
" 4. 若历史里没有可用细节,可从「当前阶段还可以聊」里挑一个轻巧落点;仍要避免泛泛盘问。 \n "
" 5. 简短:两三句内,不要排比、不要长段。 \n "
)
return f """ { head } { tone_paragraph } { profile_section } { topic_hint }
{ style_note }
## 格式
- 可用 [ SPLIT ] 分成最多 2 条 ; 或一条里 「 承接 + 钩子 」 。
- { chat_output_rules ( ) } 不要替用户编回答 。
直接输出 ( 仅自然口语 , 无 Markdown ) : """
_STAGE_TOPIC_CHIP_BANK : Dict [ str , List [ tuple [ str , str ] ] ] = {
" childhood " : [
( " place " , " 童年长大的地方 " ) ,
( " people " , " 童年里重要的人 " ) ,
( " daily_life " , " 童年的一天 " ) ,
( " turning_event " , " 童年最难忘的一件事 " ) ,
( " emotion " , " 童年最深的感受 " ) ,
] ,
" education " : [
( " school " , " 学生时代的学校 " ) ,
( " city " , " 求学的城市 " ) ,
( " motivation " , " 读书时的动力 " ) ,
( " challenge " , " 求学路上的难关 " ) ,
( " change " , " 求学带来的变化 " ) ,
] ,
" career " : [
( " job " , " 做过的工作 " ) ,
( " environment " , " 工作的环境 " ) ,
( " decision " , " 职业里的关键决定 " ) ,
( " pressure " , " 工作中的压力 " ) ,
( " growth " , " 职业上的成长 " ) ,
] ,
" family " : [
( " relationship " , " 家人之间的关系 " ) ,
( " conflict " , " 家里的矛盾与化解 " ) ,
( " support " , " 家人之间的相互支持 " ) ,
( " responsibility " , " 肩上的家庭责任 " ) ,
] ,
" later_life " : [
( " value " , " 现在最看重的事 " ) ,
( " regret " , " 心里的遗憾 " ) ,
( " pride " , " 最骄傲的事 " ) ,
( " lesson " , " 想留下的人生经验 " ) ,
] ,
}
def build_topic_chips (
current_stage : str ,
empty_slots : List [ str ] ,
* ,
max_chips : int = 4 ,
2026-05-12 11:10:21 +08:00
language : str = " zh " ,
2026-05-07 15:39:33 +00:00
) - > List [ Dict [ str , str ] ] :
""" 根据当前阶段与空 slot 列表生成 quick-start 话题 chips。
返回结构 : [ { " id " : slot_key , " label " : 短标签 , " text " : 用户点击后发出的句子 } ]
"""
2026-05-12 11:10:21 +08:00
slot_labels = slot_name_map_for ( language )
2026-05-07 15:39:33 +00:00
stage_bank = _STAGE_TOPIC_CHIP_BANK . get ( current_stage ) or [ ]
seen : set [ str ] = set ( )
chips : List [ Dict [ str , str ] ] = [ ]
# 优先从「当前阶段空 slot」挑选( 与开场提问方向一致)
empty_set = { s for s in empty_slots if s }
2026-05-12 11:10:21 +08:00
for slot_key , zh_label in stage_bank :
2026-05-07 15:39:33 +00:00
if slot_key in empty_set and slot_key not in seen :
2026-05-12 11:10:21 +08:00
label = (
zh_label if language != " en " else slot_labels . get ( slot_key , zh_label )
)
text = (
f " I ' d like to talk about { label } . "
if language == " en "
else f " 我想聊聊 { label } "
2026-05-07 15:39:33 +00:00
)
2026-05-12 11:10:21 +08:00
chips . append ( { " id " : slot_key , " label " : label , " text " : text } )
2026-05-07 15:39:33 +00:00
seen . add ( slot_key )
if len ( chips ) > = max_chips :
return chips
# 不足则用阶段默认话题补齐
2026-05-12 11:10:21 +08:00
for slot_key , zh_label in stage_bank :
2026-05-07 15:39:33 +00:00
if slot_key in seen :
continue
2026-05-12 11:10:21 +08:00
label = (
zh_label if language != " en " else slot_labels . get ( slot_key , zh_label )
)
text = (
f " I ' d like to talk about { label } . "
if language == " en "
else f " 我想聊聊 { label } "
2026-05-07 15:39:33 +00:00
)
2026-05-12 11:10:21 +08:00
chips . append ( { " id " : slot_key , " label " : label , " text " : text } )
2026-05-07 15:39:33 +00:00
seen . add ( slot_key )
if len ( chips ) > = max_chips :
return chips
return chips
2026-04-06 22:22:50 +08:00
__all__ = [
" SLOT_NAME_MAP " ,
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>
2026-05-11 16:16:49 +08:00
" SLOT_NAME_MAP_EN " ,
" slot_name_map_for " ,
2026-05-07 15:39:33 +00:00
" build_topic_chips " ,
2026-04-06 22:22:50 +08:00
" get_guided_conversation_prompt " ,
" get_opening_prompt " ,
2026-05-07 15:39:33 +00:00
" get_re_greeting_prompt " ,
2026-04-06 22:22:50 +08:00
]