"""访谈提示词:精简结构与人格/语气融合回归。""" from langchain_core.messages import AIMessage, HumanMessage, SystemMessage from app.agents.chat.helpers import format_history_string from app.agents.chat.interview_state_hints import ( AUTOBIOGRAPHICAL_BOUNDARY_FALLBACK_ZH, DUPLICATE_QUESTION_GUARD_FALLBACK_ZH, apply_autobiographical_boundary_guard, apply_duplicate_question_guard, extract_scene_cues, segments_are_only_duplicate_guard_fallback, ) from app.agents.chat.output_rules import chat_output_rules from app.agents.chat.personas import normalize_interview_persona from app.agents.chat.prompts_conversation import ( get_guided_conversation_prompt, get_opening_prompt, ) from app.agents.chat.slot_question_bank import SLOT_QUESTION_OUTLINES from app.agents.state_schema import ( KnownFact, MemoirStateSchema, PersonaThread, default_slots, prompt_empty_slots_for_current_stage, ) def test_guided_prompt_does_not_embed_raw_user_message_in_system_text(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", ) assert "__USER_SECRET_PHRASE_XYZ__" not in p # Signature no longer takes user_message; secret would only leak via profile p2 = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="__USER_SECRET_PROFILE__", persona="default", ) assert "__USER_SECRET_PROFILE__" in p2 def test_guided_prompt_injects_slot_question_outline_for_empty_slots(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place", "people"], filled_slots={"emotion": "挺开心的"}, detected_user_stage="childhood", user_profile_context="", persona="default", ) assert "本阶段问题大纲" in p assert "叙述槽" in p or "采集目标" in p assert "成长的地方" in p assert "重要的人" in p def test_guided_prompt_emotion_priority_over_outline(): p = get_guided_conversation_prompt( current_stage="education", empty_slots=["school"], filled_slots={}, detected_user_stage="education", user_profile_context="", persona="default", ) assert "情绪" in p and "大纲" in p def test_guided_prompt_includes_memoir_quality_rubric_hints(): p = get_guided_conversation_prompt( current_stage="career", empty_slots=["growth"], filled_slots={"job": "做过工程师"}, detected_user_stage="career", user_profile_context="", persona="default", ) assert "成稿质量导向" in p assert "真实性与覆盖" in p assert "出版就绪度" in p def test_guided_prompt_follow_immersion_whitespace_style_examples(): p = get_guided_conversation_prompt( current_stage="belief", empty_slots=["value"], filled_slots={}, detected_user_stage="belief", user_profile_context="", persona="default", ) assert "跟随" in p and "沉浸" in p assert "留白" in p assert "风格参考" in p assert "这让我想起" in p def test_guided_prompt_narrative_weave_and_transition(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", ) assert "编织" in p or "内在线" in p assert "通感" in p or "比喻" in p assert "下面我们聊聊" in p def test_guided_prompt_host_tone_and_context_forward(): p = get_guided_conversation_prompt( current_stage="career", empty_slots=["job"], filled_slots={}, detected_user_stage="career", user_profile_context="", persona="default", ) assert "主持" in p or "播报" in p assert "上下文" in p or "既定事实" in p assert "行为" in p and "影响" in p def test_guided_prompt_leaves_turn_level_question_contract_to_turn_plan() -> None: p = get_guided_conversation_prompt( current_stage="career", empty_slots=["job"], filled_slots={}, detected_user_stage="career", user_profile_context="", persona="default", ) assert "随后**必须**用**一条**" not in p assert "短承接后须带回一条" not in p assert "仍须**勾回回忆叙事**" not in p assert "具体问几问、是否必须追问,见顶部" in p def test_education_and_family_change_outlines_differ(): edu = SLOT_QUESTION_OUTLINES[("education", "change")] fam = SLOT_QUESTION_OUTLINES[("family", "change")] assert edu[0] != fam[0] def test_guided_prompt_mentions_empathy_and_scene_strategy(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", ) assert "接住" in p assert "画面" in p or "细节" in p assert "深挖" in p assert "串联" in p def test_guided_prompt_era_popculture_open_questions_when_birth_year(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", profile_birth_year=1985, profile_era_place="潍坊", ) assert "时代与氛围参考" in p assert "流行文化" in p assert "开放式" in p def test_opening_prompt_includes_era_task_when_birth_year_configured(): p = get_opening_prompt( current_stage="childhood", empty_slots_readable=["成长的地方"], user_profile_context="出生年份:1985年", persona="default", profile_birth_year=1985, profile_era_place="潍坊", ) assert "年代氛围" in p def test_guided_prompt_persona_tone_warm_listener(): p = get_guided_conversation_prompt( current_stage="education", empty_slots=["school"], filled_slots={}, detected_user_stage="education", user_profile_context="", persona="warm_listener", ) assert "倾听" in p or "柔和" in p def test_guided_prompt_persona_curious_guide(): p = get_guided_conversation_prompt( current_stage="education", empty_slots=["school"], filled_slots={}, detected_user_stage="education", user_profile_context="", persona="curious_guide", ) assert "细节" in p def test_normalize_interview_persona_unknown_falls_back(): assert normalize_interview_persona("not_a_real_persona") == "default" assert normalize_interview_persona("") == "default" def test_guided_prompt_contains_memory_section_when_evidence(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", memory_evidence_text="[摘要:rolling] 1990年生于上海。", ) assert "记忆线索" in p or "追问角度" in p assert "1990年生于上海" in p def test_guided_prompt_military_tone_in_system(): p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", background_voice="military", ) assert "简洁" in p or "利落" in p or "得体" in p def test_guided_prompt_includes_known_facts_persona_threads_and_recent_questions(): p = get_guided_conversation_prompt( current_stage="career", empty_slots=["job", "decision"], filled_slots={"growth": "越做越确定自己适合产品"}, detected_user_stage="career", user_profile_context="", persona="default", known_facts=[ KnownFact( label="本轮新信息", value="我后来去了瑞士读书", stage="education" ), ], persona_threads=[ PersonaThread(trait="执着坚持", evidence="为了训练咬牙坚持了很多年"), ], recent_questions=["你当时为什么会想去瑞士?"], ) assert "已确认事实" in p assert "我后来去了瑞士读书" in p assert "人物主线" in p assert "执着坚持" in p assert "最近已经问过的问题" in p assert "为什么会想去瑞士" in p def test_prompt_empty_slots_excludes_slots_already_covered_by_known_facts(): state = MemoirStateSchema( stage_order=["education"], current_stage="education", covered_stages=[], slots={"education": default_slots()["education"]}, known_facts=[ KnownFact( label="求学城市", value="后来在瑞士读书", stage="education", slot_name="city", ) ], ) prompt_empty = prompt_empty_slots_for_current_stage(state) assert "city" not in prompt_empty assert "school" in prompt_empty def test_duplicate_question_guard_downgrades_recent_repeat_question(): state = MemoirStateSchema( stage_order=["education"], current_stage="education", covered_stages=[], slots={"education": default_slots()["education"]}, known_facts=[ KnownFact(label="本轮新信息", value="我后来去了瑞士读书", stage="education") ], ) cleaned, touched = apply_duplicate_question_guard( ["我记住了。你后来去了瑞士读书吗?"], state=state, recent_questions=["你后来去了瑞士读书吗?"], ) assert touched is True assert cleaned == ["我记住了。"] def test_segments_are_only_duplicate_guard_fallback_single_stub(): assert segments_are_only_duplicate_guard_fallback( [DUPLICATE_QUESTION_GUARD_FALLBACK_ZH] ) assert not segments_are_only_duplicate_guard_fallback(["承接。后来呢?"]) def test_segments_are_only_duplicate_guard_fallback_requires_exact_stub(): assert not segments_are_only_duplicate_guard_fallback(["我记住了。"]) assert not segments_are_only_duplicate_guard_fallback( [DUPLICATE_QUESTION_GUARD_FALLBACK_ZH, "第二泡"] ) def test_extract_scene_cues_picks_up_sensory_keywords(): cues = extract_scene_cues("我们小时候在河里游泳,冬天溜冰") assert any("凉" in c or "水" in c for c in cues) assert any("冰" in c or "咔嚓" in c for c in cues) def test_extract_scene_cues_empty_for_abstract_text(): assert extract_scene_cues("我觉得人生需要坚持") == [] def test_default_persona_now_has_tone_hint(): from app.agents.chat.personas import get_interview_persona_tone_hint hint = get_interview_persona_tone_hint("default") assert hint assert "画面" in hint or "细节" in hint def test_opening_prompt_military_style_rules_not_dialogue_samples() -> None: p = get_opening_prompt( current_stage="childhood", empty_slots_readable=["成长的地方"], user_profile_context="", persona="default", background_voice="military", ) assert "军队相关" in p assert "示例" not in p def test_format_history_string_includes_system_for_debug_logs() -> None: s = format_history_string( [ SystemMessage(content="SYS_INSTRUCTIONS"), HumanMessage(content="hi"), AIMessage(content="hello"), ] ) assert "System: SYS_INSTRUCTIONS" in s assert "Human: hi" in s assert "Assistant: hello" in s def test_format_history_string_omit_system_body() -> None: s = format_history_string( [ SystemMessage(content="SYS_INSTRUCTIONS"), HumanMessage(content="hi"), ], omit_system_body=True, ) assert "SYS_INSTRUCTIONS" not in s assert "System: None: p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="", persona="default", ) assert "身份边界" in p assert "真实人生传记" in p assert "共同回忆" in p assert "泛指" in p def test_guided_prompt_blocks_using_user_context_as_assistant_identity() -> None: p = get_guided_conversation_prompt( current_stage="childhood", empty_slots=["place"], filled_slots={}, detected_user_stage="childhood", user_profile_context="成长地:上海", persona="default", ) assert "我是上海人" in p assert "不能把用户的成长地答成" in p assert "你刚提到上海" in p or "你之前说过那段童年" in p def test_chat_output_rules_bans_assistant_autobiography() -> None: rules = chat_output_rules() assert "禁止" in rules assert "声称助手本人" in rules or "助手本人" in rules assert "共同回忆" in rules assert "你是哪里人" in rules assert "你刚提到" in rules def test_autobiographical_boundary_guard_replaces_crush_claim() -> None: out, touched = apply_autobiographical_boundary_guard( ["是啊,朱丽叶就是我当时暗恋的女生。"] ) assert touched is True assert out == [AUTOBIOGRAPHICAL_BOUNDARY_FALLBACK_ZH] def test_autobiographical_boundary_guard_replaces_childhood_claim() -> None: out, touched = apply_autobiographical_boundary_guard( ["我小时候也演过这个,还挺紧张的。"] ) assert touched is True assert out == [AUTOBIOGRAPHICAL_BOUNDARY_FALLBACK_ZH] def test_autobiographical_boundary_guard_allows_generic_empathy() -> None: safe = [ "我能想象那会儿站在台上,手心里全是汗。", "换作很多人可能也会记很久。", ] out, touched = apply_autobiographical_boundary_guard(safe) assert touched is False assert out == safe def test_autobiographical_boundary_guard_mixed_segments() -> None: out, touched = apply_autobiographical_boundary_guard( ["嗯,你刚才那段我接住了。", "我小时候也演过。"] ) assert touched is True assert out[0] == "嗯,你刚才那段我接住了。" assert out[1] == AUTOBIOGRAPHICAL_BOUNDARY_FALLBACK_ZH def test_autobiographical_boundary_guard_catches_iyan_role_without_demo() -> None: out, touched = apply_autobiographical_boundary_guard( ["那次话剧里我演罗密欧,对手戏挺难的。"] ) assert touched is True assert out == [AUTOBIOGRAPHICAL_BOUNDARY_FALLBACK_ZH] def test_autobiographical_boundary_guard_allows_wo_yanshi_demo() -> None: out, touched = apply_autobiographical_boundary_guard( ["我演示一下这个按钮怎么点。"] ) assert touched is False assert out == ["我演示一下这个按钮怎么点。"]