2026-04-06 22:22:50 +08:00
|
|
|
|
"""访谈提示词:精简结构与人格/语气融合回归。"""
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
2026-04-02 12:00:00 +08:00
|
|
|
|
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
|
|
|
|
|
|
2026-04-08 21:36:12 +08:00
|
|
|
|
from app.agents.chat.interview_state_hints import (
|
|
|
|
|
|
apply_duplicate_question_guard,
|
|
|
|
|
|
extract_scene_cues,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.agents.state_schema import KnownFact, MemoirStateSchema, PersonaThread, default_slots
|
2026-04-02 12:00:00 +08:00
|
|
|
|
from app.agents.chat.helpers import format_history_string
|
2026-03-31 23:55:26 +08:00
|
|
|
|
from app.agents.chat.personas import normalize_interview_persona
|
|
|
|
|
|
from app.agents.chat.prompts_conversation import (
|
|
|
|
|
|
get_guided_conversation_prompt,
|
|
|
|
|
|
get_opening_prompt,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-02 12:00:00 +08:00
|
|
|
|
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
|
2026-04-06 22:22:50 +08:00
|
|
|
|
# Signature no longer takes user_message; secret would only leak via profile
|
|
|
|
|
|
p2 = get_guided_conversation_prompt(
|
2026-03-31 23:55:26 +08:00
|
|
|
|
current_stage="childhood",
|
|
|
|
|
|
empty_slots=["place"],
|
|
|
|
|
|
filled_slots={},
|
|
|
|
|
|
detected_user_stage="childhood",
|
2026-04-06 22:22:50 +08:00
|
|
|
|
user_profile_context="__USER_SECRET_PROFILE__",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
persona="default",
|
|
|
|
|
|
)
|
2026-04-06 22:22:50 +08:00
|
|
|
|
assert "__USER_SECRET_PROFILE__" in p2
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 21:36:12 +08:00
|
|
|
|
def test_guided_prompt_mentions_empathy_and_scene_strategy():
|
2026-03-31 23:55:26 +08:00
|
|
|
|
p = get_guided_conversation_prompt(
|
|
|
|
|
|
current_stage="childhood",
|
|
|
|
|
|
empty_slots=["place"],
|
|
|
|
|
|
filled_slots={},
|
|
|
|
|
|
detected_user_stage="childhood",
|
|
|
|
|
|
user_profile_context="",
|
|
|
|
|
|
persona="default",
|
|
|
|
|
|
)
|
2026-04-08 21:36:12 +08:00
|
|
|
|
assert "接住" in p
|
|
|
|
|
|
assert "画面" in p or "细节" in p
|
|
|
|
|
|
assert "深挖" in p
|
|
|
|
|
|
assert "串联" in p
|
2026-04-08 17:10:09 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-06 22:22:50 +08:00
|
|
|
|
def test_guided_prompt_persona_tone_warm_listener():
|
2026-03-31 23:55:26 +08:00
|
|
|
|
p = get_guided_conversation_prompt(
|
2026-04-06 22:22:50 +08:00
|
|
|
|
current_stage="education",
|
|
|
|
|
|
empty_slots=["school"],
|
2026-03-31 23:55:26 +08:00
|
|
|
|
filled_slots={},
|
2026-04-06 22:22:50 +08:00
|
|
|
|
detected_user_stage="education",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
user_profile_context="",
|
2026-04-06 22:22:50 +08:00
|
|
|
|
persona="warm_listener",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
)
|
2026-04-06 22:22:50 +08:00
|
|
|
|
assert "倾听" in p or "柔和" in p
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-06 22:22:50 +08:00
|
|
|
|
def test_guided_prompt_persona_curious_guide():
|
2026-03-31 23:55:26 +08:00
|
|
|
|
p = get_guided_conversation_prompt(
|
2026-04-06 22:22:50 +08:00
|
|
|
|
current_stage="education",
|
|
|
|
|
|
empty_slots=["school"],
|
2026-03-31 23:55:26 +08:00
|
|
|
|
filled_slots={},
|
2026-04-06 22:22:50 +08:00
|
|
|
|
detected_user_stage="education",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
user_profile_context="",
|
2026-04-06 22:22:50 +08:00
|
|
|
|
persona="curious_guide",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
)
|
2026-04-06 22:22:50 +08:00
|
|
|
|
assert "细节" in p
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-06 22:22:50 +08:00
|
|
|
|
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():
|
2026-03-31 23:55:26 +08:00
|
|
|
|
p = get_guided_conversation_prompt(
|
|
|
|
|
|
current_stage="childhood",
|
|
|
|
|
|
empty_slots=["place"],
|
|
|
|
|
|
filled_slots={},
|
|
|
|
|
|
detected_user_stage="childhood",
|
|
|
|
|
|
user_profile_context="",
|
|
|
|
|
|
persona="default",
|
2026-04-06 22:22:50 +08:00
|
|
|
|
memory_evidence_text="[摘要:rolling] 1990年生于上海。",
|
2026-03-31 23:55:26 +08:00
|
|
|
|
)
|
2026-04-06 22:22:50 +08:00
|
|
|
|
assert "相关记忆摘录" in p
|
|
|
|
|
|
assert "过往口述" in p
|
|
|
|
|
|
assert "1990年生于上海" in p
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-06 22:22:50 +08:00
|
|
|
|
def test_guided_prompt_military_tone_in_system():
|
2026-03-31 23:55:26 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
2026-04-06 22:22:50 +08:00
|
|
|
|
assert "简洁" in p or "利落" in p or "得体" in p
|
2026-03-31 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 21:36:12 +08:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "city" not in state.prompt_empty_slots_for_current_stage()
|
|
|
|
|
|
assert "school" in state.prompt_empty_slots_for_current_stage()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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_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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 17:10:09 +08:00
|
|
|
|
def test_opening_prompt_military_style_rules_not_dialogue_samples() -> None:
|
2026-03-31 23:55:26 +08:00
|
|
|
|
p = get_opening_prompt(
|
|
|
|
|
|
current_stage="childhood",
|
|
|
|
|
|
empty_slots_readable=["成长的地方"],
|
|
|
|
|
|
user_profile_context="",
|
|
|
|
|
|
persona="default",
|
|
|
|
|
|
background_voice="military",
|
|
|
|
|
|
)
|
2026-04-08 17:10:09 +08:00
|
|
|
|
assert "军队相关" in p
|
|
|
|
|
|
assert "示例" not in p
|
2026-04-02 12:00:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-03 13:49:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: <omitted total_len=16" in s
|
|
|
|
|
|
assert "sha12=" in s
|
|
|
|
|
|
assert "Human: hi" in s
|