Files
life-echo/api/app/agents/chat/prompts_profile.py

363 lines
14 KiB
Python
Raw Normal View History

"""
用户基础资料收集提示词
"""
2026-03-19 14:36:14 +08:00
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 12 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)
2026-03-19 14:36:14 +08:00
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
)
2026-03-19 14:36:14 +08:00
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 12 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,
)
2026-03-19 14:36:14 +08:00
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. **资料穿插**仅当用户本轮主要在确认闲聊或话题与缺失资料完全无关时再在末尾**温和插入 01 **还需要了解里的问题
4. **轮换**若上一轮你已就某一类资料追问过见历史里助手发言本轮**不要再问同一类**改问其他缺失项或本轮只承接不提资料
5. 每次最多 **12 **资料相关问点能用推断就不要重复确认已知地/
严格禁止
- **严禁再次询问已知信息中已列出的内容**
- {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