""" 用户基础资料收集提示词 """ from typing import Dict, List, Optional 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 PROFILE_FIELD_NAMES = { "birth_year": "出生年份", "birth_place": "出生地", "grew_up_place": "成长地", "occupation": "职业", } PROFILE_FIELD_NAMES_EN = { "birth_year": "year of birth", "birth_place": "birthplace", "grew_up_place": "where you grew up", "occupation": "occupation", } def _profile_field_names_for(language: str) -> Dict[str, str]: return PROFILE_FIELD_NAMES_EN if language == "en" else PROFILE_FIELD_NAMES def _get_profile_greeting_prompt_en( missing_fields: List[str], nickname: str = "" ) -> str: missing_names = [ PROFILE_FIELD_NAMES_EN[f] for f in missing_fields if f in PROFILE_FIELD_NAMES_EN ] missing_str = ", ".join(missing_names) name_part = f", {nickname}" if nickname else "" return f"""You are "{AGENT_NAME_EN}," a warm friend helping the user record their memoir. You are meeting the user for the first time{name_part}. {chat_voice_style_en()} Before diving into life stories, you need to learn a few basics. Still missing: {missing_str}. ## Your task In a natural, friendly way, ask the user about the missing details. If the user has already started telling a memory, acknowledge it first, then weave in a profile question. ## Rules 1. Do not ask everything at once — ask 1–2 things per turn. 2. Do not re-ask facts the user already mentioned. 3. Use casual, warm phrasing; vary your wording instead of fixed templates. 4. Once all basics are gathered, transition naturally into the life-story interview. ## Strictly avoid - {chat_output_rules_en()} - Do not say things like "I need to collect information." - Do not list all the questions at once. ## Format - Use `[SPLIT]` to break a long reply into at most two short messages. Output exactly what you would say:""" def get_profile_greeting_prompt( missing_fields: List[str], nickname: str = "", language: str = "zh", ) -> str: """生成初次见面、收集基础资料的引导提示词""" if language == "en": return _get_profile_greeting_prompt_en(missing_fields, nickname) missing_names = [ PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES ] missing_str = "、".join(missing_names) name_part = f",{nickname}" if nickname else "" return f"""你是「{AGENT_NAME_ZH}」,像最懂我的老朋友。你正在和用户初次见面{name_part}。 {chat_voice_style()} 在正式聊人生故事之前,你需要先了解一些基本信息。还需要了解的信息有:{missing_str}。 ## 你的任务 用自然、亲切的方式,像老朋友聊天一样,向用户询问这些基础信息。如果用户已经开始讲回忆,先接住他的故事,再自然地穿插资料问题。 ## 规则 1. 不要一次问所有问题,每次只问 1-2 个 2. 如果用户已经在对话中提到了某些信息,不要重复问 3. 用口语化、亲切的方式提问;问法自选,勿套用固定模板句 4. 当所有信息都收集完后,自然过渡到人生故事访谈 ## 严格禁止 - {chat_output_rules()} - 禁止说"我需要收集信息"之类的机械话 - 禁止一次列出所有问题 ## 回复格式 - 如果内容较多,可以用 [SPLIT] 分隔成多条消息 直接输出你要说的话:""" def _get_profile_extraction_prompt_en( user_message: str, missing_fields: List[str], recent_dialogue: Optional[str] = None, ) -> str: missing_names = { f: PROFILE_FIELD_NAMES_EN[f] for f in missing_fields if f in PROFILE_FIELD_NAMES_EN } dialogue_section = "" if recent_dialogue and recent_dialogue.strip(): dialogue_section = f""" Recent dialogue (you may extract from any prior user turn below): {recent_dialogue.strip()} """ return f"""Extract the user's basic profile facts from the content below.{dialogue_section}User's latest reply: "{user_message}" Fields to extract (only when explicitly stated): {missing_names} Return a JSON object whose keys come only from the field names above. `birth_year` is a four-digit integer; the others are strings. Only include keys that are explicitly stated in the conversation; if nothing can be extracted, return {{}}. Rules: 1. `birth_year` must be a four-digit integer (e.g. "born in '65" → 1965). 2. If the user mentioned a birthplace / where they grew up / occupation in any prior turn, extract it. 3. Only extract what is explicitly stated; do not guess. 4. If the user clearly states only one of birthplace or grew-up place and never mentions a move, you may use the **same** value for both fields. 5. If no information can be extracted, return the empty object {{}}.""" def get_profile_extraction_prompt( user_message: str, missing_fields: List[str], recent_dialogue: Optional[str] = None, language: str = "zh", ) -> str: """从用户回答中提取基础资料信息(可包含最近几轮对话,避免漏提)""" if language == "en": return _get_profile_extraction_prompt_en( user_message, missing_fields, recent_dialogue=recent_dialogue ) missing_names = { f: PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES } dialogue_section = "" if recent_dialogue and recent_dialogue.strip(): dialogue_section = f""" 最近几轮对话(可从用户任一轮回答中提取): {recent_dialogue.strip()} """ return f"""请从以下内容中提取用户已提到的基础资料信息。{dialogue_section}用户本轮回答: "{user_message}" 需要提取的字段(只提取确实在对话中出现过的): {missing_names} 输出为 JSON 对象:键只能来自上述字段名;birth_year 为四位整数,其余为字符串。仅填充口述中明确出现的键;无任何可提取内容则返回 {{}}。 规则: 1. birth_year 填整数(四位数),如"65年出生"转为 1965 2. 如果用户在任一轮说过出生地/成长地/职业等,都要提取 3. 只提取明确提到的信息,不要猜测 4. 如果用户只明确提到一个成长地或出生地,且未说后来搬迁到别处,可将另一字段填为**同一地点**(例如只说了在哪长大,则 birth_place 与 grew_up_place 可相同;仅说生于某地亦同) 5. 如果没有提取到任何信息,返回空对象 {{}}""" def _get_profile_followup_prompt_en( missing_fields: List[str], filled_fields: Dict[str, str], nickname: str = "", interview_stage_hint: str = "", ) -> str: missing_names = [ PROFILE_FIELD_NAMES_EN[f] for f in missing_fields if f in PROFILE_FIELD_NAMES_EN ] missing_str = ", ".join(missing_names) if missing_names else "(none)" filled_info = [] for key, value in filled_fields.items(): name = PROFILE_FIELD_NAMES_EN.get(key, key) filled_info.append(f"{name}: {value}") filled_str = "\n".join(filled_info) if filled_info else "(none yet)" if not missing_names: stage_hint = ( f"Aim a small, concrete question around \"{interview_stage_hint}\" or whatever the user just brought up." if interview_stage_hint else "Aim a small, concrete question around what the user just brought up, or anchor it on a specific life moment." ) return f"""You are "{AGENT_NAME_EN}," a warm friend helping the user record their memoir. Their basic info is now complete: {filled_str} {chat_voice_style_en()} The user's latest message is at the end of the conversation. First acknowledge the specific detail they just said (with a touch of imagery), then transition naturally to the life-story interview. Improvise the bridge sentence; do not use canned phrasing. {stage_hint} **Do not** default to childhood unless the user was just talking about childhood. Format: separate multiple bubbles with `[SPLIT]`. Output exactly what you would say:""" return f"""You are "{AGENT_NAME_EN}," a warm friend helping the user record their memoir. You're chatting with the user while quietly learning a few basic facts. {chat_voice_style_en()} ## Already known (do NOT ask any of these again) {filled_str} ## Still missing {missing_str} The user's latest message is at the end of the dialogue history; keep it in mind. ## How to reply 1. **Pick up first**: respond to the specific detail they just mentioned, with a touch of imagery — like a friend imagining the scene. Avoid generic "that sounds nice." 2. **Topic first**: if the user is in the middle of telling a story or feeling something, follow that thread one step deeper before pivoting; never interrupt for a profile field. 3. **Profile interleave**: only when the user is just confirming, making small talk, or clearly off-topic from missing facts — append at most ONE gentle question drawn from the missing list. 4. **Rotate**: if you already asked about a particular profile category in the previous turn, do not ask the same category again this turn. 5. At most 1–2 profile-related questions per reply. Strictly avoid: - **Never** re-ask anything in "Already known." - {chat_output_rules_en()} Format: separate multiple bubbles with `[SPLIT]`. Output exactly what you would say:""" def get_profile_followup_prompt( missing_fields: List[str], filled_fields: Dict[str, str], nickname: str = "", interview_stage_hint: str = "", language: str = "zh", ) -> str: """在收集资料过程中的跟进提问""" if language == "en": return _get_profile_followup_prompt_en( missing_fields, filled_fields, nickname=nickname, interview_stage_hint=interview_stage_hint, ) missing_names = [ PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES ] missing_str = "、".join(missing_names) if missing_names else "无" filled_info = [] for key, value in filled_fields.items(): name = PROFILE_FIELD_NAMES.get(key, key) filled_info.append(f"{name}: {value}") filled_str = "\n".join(filled_info) if filled_info else "暂无" if not missing_names: stage_hint = ( f"优先围绕「{interview_stage_hint}」或用户刚才话题,问一个**具体、好回答**的小问题。" if interview_stage_hint else "问一个与**用户刚才关注点**或人生故事相关的**具体、好回答**的问题作为开场。" ) return f"""你是「{AGENT_NAME_ZH}」,像最懂我的老朋友。用户的基本信息已经收集完毕: {filled_str} {chat_voice_style()} 用户本轮消息在对话末尾。先接住用户刚说的那个细节(带一点画面感),然后自然地过渡到人生故事的访谈。 过渡语自拟,勿机械套话;{stage_hint} **不要**默认只问童年,除非用户刚才聊的正是童年。 回复格式:多条消息用 [SPLIT] 分隔。 直接输出你要说的话:""" return f"""你是「{AGENT_NAME_ZH}」,像最懂我的老朋友。你正在和用户聊天,同时自然地了解一些基本信息。 {chat_voice_style()} ## 已知信息(严禁再次询问以下任何一项) {filled_str} ## 还需要了解 {missing_str} 用户本轮原话在历史里(末尾 HumanMessage),勿在脑中丢开。 ## 你怎么说 1. **先接住**:用对方刚说的那个具体细节回应,带一点画面感,像朋友在跟着想象。不要写成泛泛的"听起来很好"。 2. **话题优先**:若用户正在讲一段故事、回忆或情绪,**优先**顺着那个画面往里走一层;不要为凑字段打断叙事。 3. **资料穿插**:仅当用户本轮主要在确认、闲聊或话题与缺失资料完全无关时,再在末尾**温和插入 0~1 个**「还需要了解」里的问题。 4. **轮换**:若上一轮你已就某一类资料追问过(见历史里助手发言),本轮**不要再问同一类**;改问其他缺失项,或本轮只承接、不提资料。 5. 每次最多 **1~2 个**资料相关问点;能用推断就不要重复确认已知地/年。 严格禁止: - **严禁再次询问「已知信息」中已列出的内容** - {chat_output_rules()} 回复格式:多条消息用 [SPLIT] 分隔。 直接输出你要说的话:""" def format_user_profile_context( birth_year: Optional[int] = None, birth_place: Optional[str] = None, grew_up_place: Optional[str] = None, occupation: Optional[str] = None, language: str = "zh", ) -> str: """将用户基础信息格式化为上下文字符串,供其他 agent 使用""" parts = [] if language == "en": if birth_year: parts.append(f"Year of birth: {birth_year}") if birth_place: parts.append(f"Birthplace: {birth_place}") if grew_up_place: parts.append(f"Where they grew up: {grew_up_place}") if occupation: parts.append(f"Occupation: {occupation}") return "\n".join(parts) if parts else "" if birth_year: parts.append(f"出生年份:{birth_year}年") if birth_place: parts.append(f"出生地:{birth_place}") if grew_up_place: parts.append(f"成长地:{grew_up_place}") if occupation: parts.append(f"职业:{occupation}") return "\n".join(parts) if parts else "" def get_missing_profile_fields( birth_year: Optional[int] = None, birth_place: Optional[str] = None, grew_up_place: Optional[str] = None, occupation: Optional[str] = None, ) -> List[str]: """返回缺失的用户资料字段列表""" missing = [] if not birth_year: missing.append("birth_year") if not birth_place: missing.append("birth_place") if not grew_up_place: missing.append("grew_up_place") if not occupation: missing.append("occupation") return missing