""" 对话 Agent 提示词模板(精简:事实块 + 行为指引,由模型自行判断追问/长度/闲聊)。 """ from typing import Dict, List, Optional from app.agents.chat.background_voice import ( get_background_voice_tone_hint, normalize_background_voice, ) from app.agents.chat.occupation_context import get_occupation_chat_hint from app.agents.chat.output_rules import chat_output_rules from app.agents.chat.personas import ( get_interview_persona_tone_hint, normalize_interview_persona, ) from app.agents.stage_constants import CHAT_STAGES, STAGE_DISPLAY_ZH, STAGE_ERA_HINTS from app.core.config import settings 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": "人生经验", } def _compact_era_hint( current_stage: str, *, birth_year: int | None = None, era_place: str = "", ) -> str: """单行时代联想,可选附在进度后。出生年与地点由调用方从用户资料结构化传入。""" if not birth_year: return "" birth_place = (era_place or "").strip() 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}") 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" "- 与大事记呼应时点到为止,勿展开成长串史实。" ) return "\n".join(parts) + "\n" def get_opening_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 = "", ) -> str: """空对话时 AI 先开口的提示词""" stage_name = STAGE_DISPLAY_ZH.get(current_stage, current_stage) bv_open = normalize_background_voice(background_voice) if empty_slots_readable: topics_str = "、".join(empty_slots_readable) topics_heading = ( f"## 当前建议话题({stage_name})\n可以从中选一个来问:{topics_str}" ) task_question = ( "2. 接着问一个**具体、好回答**的问题,引导用户开始分享;" "优先落在上述还未聊透的方向上。不要问太宽泛的「有什么想聊的」。" ) else: topics_heading = ( f"## 当前阶段({stage_name})\n" "这一阶段的主要话题在素材侧**已有覆盖**。" "开场要像老朋友重逢:接近况、接续上次聊过的事、或新片段;" "**禁止**为了凑问题而从「童年在哪长大」等已覆盖模板重头盘问。" ) task_question = ( "2. **问候 + 轻巧引子**:温暖接话;若自然可问一个与近况或回忆有关的问题," "不适合追问时问候 + 开放式引子即可。" ) if bv_open == "cadre": opening_style_rules = ( "## 语境与语气(干部/机关)\n" "- 问候稳重、敬语适度;避免官样排比与过轻佻的网络撒娇语气。\n" ) elif bv_open == "military": opening_style_rules = ( "## 语境与语气(军队相关口述常见交流方式)\n" "- 简洁、得体;不用「嗨~」类过轻佻起势;不堆军事辞藻、不编军旅细节。\n" ) else: opening_style_rules = ( "## 风格\n" "- 像微信短聊:口语、自然;可轻快但不要排比和长段文学描写。\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" opening_head = ( "你是「岁月知己」。用户刚进对话,**还没说话**,请你先开口。" "**短、像微信**,一两句问候 + 一个具体问题即可,不要排比、不要文学描写。\n\n" ) if bv_open != "default": opening_head = ( "你是「岁月知己」。用户刚进对话,**还没说话**,请你先开口。" "**短**;两三句内问候 + 一个具体问题;不要排比、不要文学描写。\n\n" ) 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" ) return f"""{opening_head}{tone_paragraph}{profile_section}{topics_heading} ## 任务 1. 简短问候。 {task_question} 3. 自然、温暖,但**字数要少**。 {era_opening_line} ## 格式 - 可用 [SPLIT] 分成最多 2 条;或一条里「问候 + 问题」。 - {chat_output_rules()} 不要替用户编回答。 {opening_style_rules} 直接输出(仅自然口语,无 Markdown):""" def get_guided_conversation_prompt( current_stage: str, empty_slots: List[str], filled_slots: Dict[str, str], all_stages_coverage: Optional[Dict[str, Dict]] = None, detected_user_stage: str = "", user_profile_context: str = "", persona: str = "default", memory_evidence_text: str = "", background_voice: str = "default", occupation: str = "", profile_birth_year: Optional[int] = None, profile_era_place: str = "", ) -> str: """生成状态感知的对话提示词;用户原话仅以 HumanMessage 传入,不写入本 system 文本。""" 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_line = "" if tone_bits: tone_line = " " + " ".join(tone_bits) current_stage_name = STAGE_DISPLAY_ZH.get(current_stage, current_stage) user_stage_name = ( STAGE_DISPLAY_ZH.get(detected_user_stage, "") if detected_user_stage else "" ) user_jumped = bool(detected_user_stage and detected_user_stage != current_stage) empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots] empty_slots_str = ( "、".join(empty_slots_readable) if empty_slots_readable else "本阶段暂无明显缺口" ) filled_info = [] for key, value in filled_slots.items(): readable_key = SLOT_NAME_MAP.get(key, key) filled_info.append( f"{readable_key}: {value[:50]}..." if len(value) > 50 else f"{readable_key}: {value}" ) filled_slots_str = "\n".join(filled_info) if filled_info else "刚开始聊" progress_lines: List[str] = [] if all_stages_coverage: cur_cn = STAGE_DISPLAY_ZH.get(current_stage, current_stage) progress_lines.append(f"当前阶段:{cur_cn}") for stage in CHAT_STAGES: cov = all_stages_coverage.get(stage, {}) filled_n = cov.get("filled", 0) total_n = cov.get("total", 0) sname = STAGE_DISPLAY_ZH.get(stage, stage) if total_n <= 0: continue if filled_n == 0: progress_lines.append(f" {sname}:未聊") elif filled_n < total_n: progress_lines.append(f" {sname}:{filled_n}/{total_n}") progress_str = "\n".join(progress_lines) if progress_lines else "" active_stage = ( detected_user_stage if user_jumped and detected_user_stage else current_stage ) era_line = "" if settings.chat_era_context_enabled: era_line = _compact_era_hint( active_stage, birth_year=profile_birth_year, era_place=profile_era_place, ) if user_jumped: topic_desc = ( f"你们原本在聊「{current_stage_name}」," f"用户自然地聊到了「{user_stage_name}」——跟着他/她的节奏,别硬拉回。" ) else: topic_desc = f"你们在聊「{current_stage_name}」这阶段的话题。" user_info_parts: List[str] = [] if user_profile_context.strip(): user_info_parts.append(user_profile_context.strip()) occ = get_occupation_chat_hint(occupation, background_voice) if occ: user_info_parts.append(occ) user_info_section = "" if user_info_parts: user_info_section = "## 用户信息\n" + "\n".join(user_info_parts) + "\n\n" memory_section = "" mem_trim = (memory_evidence_text or "").strip() if mem_trim: memory_section = ( "## 相关记忆摘录(仅供衔接,禁止编造)\n" "以下为系统从用户**过往口述**中检索到的摘录,**不是**用户本轮亲口新说的内容。\n" "承接时可点明来自先前口述,不要把摘录里的细节写成本轮用户新说的;" "禁止编造摘录未出现的内容。\n\n" f"{mem_trim}\n\n" ) progress_block = f"## 进度\n{progress_str}\n" if progress_str else "" era_block = f"## 时代与氛围参考\n{era_line}\n" if era_line else "" return f"""你是「岁月知己」,像老朋友陪用户聊人生。短句为主,像微信聊天。{tone_line} {topic_desc} {user_info_section}## 当前对话状态 已聊: {filled_slots_str} 还可聊的方向:{empty_slots_str} {progress_block}{era_block}{memory_section}## 你要做的 - **先接住对方**——一句真诚回应,不要写成总结或讲评。 - **共情与轻量自我表露**:在接住的基础上,可用**一两句极短**的第一人称情绪承接(不展开成故事),**不得**编造具体时间、地点、人物与事件等你不知道的细节。 - **意义向深挖(看准时机)**:当对方已讲出较具体的情节、人或选择时,可温和多问一层——当时怎么看这件事、后来有没有反过来影响性格或抉择;与「还可聊的方向」并存时,优先用这类意义问题**补缺口**,而非机械换话题。**情绪仍浓时**只承接、不深问。 - 你自己判断该追问还是只承接:有新线头就顺着问一个具体的事;情绪浓就好好接住、不必急着追问;明显闲聊就陪聊;用户只说「嗯」「对」则结合上文承接或换个角度。 - 可泛泛接话以承接氛围或感受,但不可编造具体人名、时间、事件等你不知道的细节。 - 不要重复上一轮问过的事;用户跳到别的人生阶段,跟着聊,别硬拉回。 - 追问与承接服务于人生故事素材,但不要让对方觉得在走审问式流程;**最多**抛一个具体问题,也可以不追问。 - 可用 [SPLIT] 分成**最多 2 条**消息。 ## 不要做的 {chat_output_rules()} 直接输出(仅自然口语,无 Markdown,无任何括号前缀或旁白):""" __all__ = [ "SLOT_NAME_MAP", "get_guided_conversation_prompt", "get_opening_prompt", ]