|
|
@@ -115,21 +115,117 @@ class AIService:
|
|
|
|
|
|
# STT现在使用流式识别,在websocket_handler中直接处理
|
|
|
|
|
|
+ def first_meeting_line(self, user_name: str, lumi_name: str) -> str:
|
|
|
+ """
|
|
|
+ 新用户的首句(固定模板)
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_name: 用户名称
|
|
|
+ lumi_name: Lumi名称
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 固定首句(包含情感标签)
|
|
|
+ """
|
|
|
+ user_name = (user_name or "").strip() or "Neo"
|
|
|
+ lumi_name = (lumi_name or "").strip() or "Lumi"
|
|
|
+ # 🔥 统一格式:在开头添加情感标签
|
|
|
+ return (
|
|
|
+ "[EMOTION:friendly]\n"
|
|
|
+ f"{user_name}… we made it. I'm {lumi_name}. "
|
|
|
+ "We're in Glimmer Forest, and it's… strangely quiet. Before we move, Do you feel calm, or nervous?"
|
|
|
+ )
|
|
|
+
|
|
|
+ async def generate_login_greeting(
|
|
|
+ self,
|
|
|
+ user_info: Optional[dict] = None,
|
|
|
+ chat_history: Optional[list] = None
|
|
|
+ ) -> str:
|
|
|
+ """
|
|
|
+ 生成回归用户的登录问候(动态生成)
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_info: 用户信息字典
|
|
|
+ chat_history: 对话历史(可选)
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 登录问候语
|
|
|
+ """
|
|
|
+ if not user_info:
|
|
|
+ user_info = {}
|
|
|
+
|
|
|
+ # 构建 System Prompt
|
|
|
+ system_prompt = self._build_system_prompt(user_info, is_new_user=False)
|
|
|
+
|
|
|
+ # 构建运行时上下文
|
|
|
+ runtime_context = self._build_runtime_context(user_info)
|
|
|
+
|
|
|
+ # 构建消息列表
|
|
|
+ messages = [
|
|
|
+ {"role": "system", "content": system_prompt},
|
|
|
+ {"role": "system", "content": runtime_context},
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 添加对话历史(如果有)
|
|
|
+ if chat_history:
|
|
|
+ messages.extend(chat_history)
|
|
|
+
|
|
|
+ # 添加登录问候请求
|
|
|
+ login_greeting_prompt = """
|
|
|
+[LOGIN GREETING REQUEST]
|
|
|
+Generate ONLY the first greeting line for a returning user.
|
|
|
+
|
|
|
+Constraints:
|
|
|
+- 1–2 short sentences, voice-friendly.
|
|
|
+- Must include user name and Lumi name from runtime context.
|
|
|
+- Immersive story tone (Glimmer Forest / mystery), NOT gamified language (no "mission", "reward", "points").
|
|
|
+- End with ONE simple question.
|
|
|
+
|
|
|
+Output ONLY the greeting line.
|
|
|
+""".strip()
|
|
|
+
|
|
|
+ messages.append({"role": "user", "content": login_greeting_prompt})
|
|
|
+
|
|
|
+ try:
|
|
|
+ response = self.openai_client.chat.completions.create(
|
|
|
+ model=settings.llm_model,
|
|
|
+ messages=messages,
|
|
|
+ temperature=0.7,
|
|
|
+ max_tokens=200,
|
|
|
+ )
|
|
|
+ greeting = response.choices[0].message.content.strip()
|
|
|
+ return greeting
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ 生成登录问候失败: {e}")
|
|
|
+ # 降级方案:使用固定模板(也包含情感标签)
|
|
|
+ user_name = (user_info.get("username") or "").strip() or "Neo"
|
|
|
+ lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
|
|
|
+ return f"[EMOTION:friendly]\nWelcome back, {user_name}. I'm {lumi_name}. How are you feeling today?"
|
|
|
+
|
|
|
async def generate_response_stream(
|
|
|
- self, user_text: str
|
|
|
+ self,
|
|
|
+ user_text: str,
|
|
|
+ user_info: Optional[dict] = None,
|
|
|
+ is_new_user: bool = False,
|
|
|
+ chat_history: Optional[list] = None
|
|
|
) -> AsyncGenerator[tuple[str, EmotionType], None]:
|
|
|
"""
|
|
|
生成对话回复(流式)
|
|
|
|
|
|
Args:
|
|
|
user_text: 用户输入文本
|
|
|
+ user_info: 用户信息字典(包含 username, bot_name)
|
|
|
+ is_new_user: 是否为新用户(首次对话)
|
|
|
+ chat_history: 对话历史(可选)
|
|
|
|
|
|
Yields:
|
|
|
(sentence, emotion): 句子片段和情感
|
|
|
"""
|
|
|
try:
|
|
|
# 构建System Prompt
|
|
|
- system_prompt = self._build_system_prompt()
|
|
|
+ system_prompt = self._build_system_prompt(user_info, is_new_user)
|
|
|
+
|
|
|
+ # 构建运行时上下文
|
|
|
+ runtime_context = self._build_runtime_context(user_info)
|
|
|
|
|
|
# 调用LLM流式API
|
|
|
import time
|
|
|
@@ -138,13 +234,23 @@ class AIService:
|
|
|
# 🔥 连接健康检查(后台异步执行,不阻塞主流程)
|
|
|
self._trigger_llm_healthcheck_background()
|
|
|
|
|
|
+ # 构建消息列表
|
|
|
+ messages = [
|
|
|
+ {"role": "system", "content": system_prompt},
|
|
|
+ {"role": "system", "content": runtime_context}, # 注入运行时上下文
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 添加对话历史(如果有)
|
|
|
+ if chat_history:
|
|
|
+ messages.extend(chat_history)
|
|
|
+
|
|
|
+ # 添加当前用户输入
|
|
|
+ messages.append({"role": "user", "content": user_text})
|
|
|
+
|
|
|
try:
|
|
|
response = self.openai_client.chat.completions.create(
|
|
|
model=settings.llm_model,
|
|
|
- messages=[
|
|
|
- {"role": "system", "content": system_prompt},
|
|
|
- {"role": "user", "content": user_text},
|
|
|
- ],
|
|
|
+ messages=messages,
|
|
|
temperature=0.7,
|
|
|
max_tokens=500,
|
|
|
stream=True, # 启用流式
|
|
|
@@ -169,10 +275,7 @@ class AIService:
|
|
|
try:
|
|
|
response = self.openai_client.chat.completions.create(
|
|
|
model=settings.llm_model,
|
|
|
- messages=[
|
|
|
- {"role": "system", "content": system_prompt},
|
|
|
- {"role": "user", "content": user_text},
|
|
|
- ],
|
|
|
+ messages=messages,
|
|
|
temperature=0.7,
|
|
|
max_tokens=500,
|
|
|
stream=True,
|
|
|
@@ -223,17 +326,28 @@ class AIService:
|
|
|
emotion_end = full_response.find("]", emotion_start)
|
|
|
if emotion_end > emotion_start:
|
|
|
emotion_str = full_response[emotion_start + 9:emotion_end].strip().lower()
|
|
|
- valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry"]
|
|
|
+ valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry", "gentle"]
|
|
|
if emotion_str in valid_emotions:
|
|
|
detected_emotion = emotion_str
|
|
|
-
|
|
|
- # 🚀 立即 yield 空句子传递情感(减少延迟)
|
|
|
- if not emotion_yielded:
|
|
|
- yield ("", detected_emotion) # 空句子,仅传递情感
|
|
|
- emotion_yielded = True
|
|
|
-
|
|
|
- # 🧹 从 buffer 中移除情感标记(避免重复处理)
|
|
|
- buffer = buffer.replace(f"[EMOTION:{emotion_str.upper()}]", "").strip()
|
|
|
+ else:
|
|
|
+ # 🔥 未知情感词,默认映射为 neutral,但仍然移除标签
|
|
|
+ logger.warning(f"[LLM] 未知情感词: {emotion_str},已映射为 neutral")
|
|
|
+ detected_emotion = "neutral"
|
|
|
+
|
|
|
+ # 🚀 立即 yield 空句子传递情感(减少延迟)
|
|
|
+ if not emotion_yielded:
|
|
|
+ yield ("", detected_emotion) # 空句子,仅传递情感
|
|
|
+ emotion_yielded = True
|
|
|
+
|
|
|
+ # 🧹 每次都从 buffer 中移除所有情感标记(避免读出标签)
|
|
|
+ while "[EMOTION:" in buffer:
|
|
|
+ tag_start = buffer.find("[EMOTION:")
|
|
|
+ tag_end = buffer.find("]", tag_start)
|
|
|
+ if tag_end > tag_start:
|
|
|
+ buffer = buffer[:tag_start] + buffer[tag_end + 1:]
|
|
|
+ else:
|
|
|
+ break # 标签不完整,等待更多内容
|
|
|
+ buffer = buffer.strip()
|
|
|
|
|
|
# 检测句子边界(完整的句子)
|
|
|
if self._is_complete_sentence(buffer):
|
|
|
@@ -251,8 +365,17 @@ class AIService:
|
|
|
clean_response = full_response[:emotion_start].strip()
|
|
|
|
|
|
# 处理buffer中的剩余内容(已输出句子之后的部分)
|
|
|
+ # 🧹 最终清理:移除所有情感标记
|
|
|
+ while "[EMOTION:" in buffer:
|
|
|
+ tag_start = buffer.find("[EMOTION:")
|
|
|
+ tag_end = buffer.find("]", tag_start)
|
|
|
+ if tag_end > tag_start:
|
|
|
+ buffer = buffer[:tag_start] + buffer[tag_end + 1:]
|
|
|
+ else:
|
|
|
+ # 不完整的标签,直接截断
|
|
|
+ buffer = buffer[:tag_start]
|
|
|
remaining = buffer.strip()
|
|
|
- if remaining and not remaining.startswith("[EMOTION"):
|
|
|
+ if remaining:
|
|
|
sentence_count += 1
|
|
|
yield (remaining, detected_emotion)
|
|
|
elif not remaining and sentence_count == 0:
|
|
|
@@ -300,31 +423,47 @@ class AIService:
|
|
|
return True
|
|
|
|
|
|
async def generate_response(
|
|
|
- self, user_text: str
|
|
|
+ self,
|
|
|
+ user_text: str,
|
|
|
+ user_info: Optional[dict] = None,
|
|
|
+ is_new_user: bool = False,
|
|
|
+ chat_history: Optional[list] = None
|
|
|
) -> tuple[str, EmotionType]:
|
|
|
"""
|
|
|
- 生成对话回复
|
|
|
+ 生成对话回复(非流式)
|
|
|
|
|
|
Args:
|
|
|
user_text: 用户输入文本
|
|
|
+ user_info: 用户信息字典(包含 username, bot_name)
|
|
|
+ is_new_user: 是否为新用户(首次对话)
|
|
|
+ chat_history: 对话历史(可选)
|
|
|
|
|
|
Returns:
|
|
|
(response_text, emotion): 回复文本和情感
|
|
|
"""
|
|
|
try:
|
|
|
# 构建System Prompt
|
|
|
- system_prompt = self._build_system_prompt()
|
|
|
+ system_prompt = self._build_system_prompt(user_info, is_new_user)
|
|
|
+
|
|
|
+ # 构建运行时上下文
|
|
|
+ runtime_context = self._build_runtime_context(user_info)
|
|
|
|
|
|
# 🔥 连接健康检查(长时间未使用时)
|
|
|
await self._ensure_llm_connection()
|
|
|
|
|
|
+ # 构建消息列表
|
|
|
+ messages = [
|
|
|
+ {"role": "system", "content": system_prompt},
|
|
|
+ {"role": "system", "content": runtime_context},
|
|
|
+ ]
|
|
|
+ if chat_history:
|
|
|
+ messages.extend(chat_history)
|
|
|
+ messages.append({"role": "user", "content": user_text})
|
|
|
+
|
|
|
# 调用LLM
|
|
|
response = self.openai_client.chat.completions.create(
|
|
|
model=settings.llm_model,
|
|
|
- messages=[
|
|
|
- {"role": "system", "content": system_prompt},
|
|
|
- {"role": "user", "content": user_text},
|
|
|
- ],
|
|
|
+ messages=messages,
|
|
|
temperature=0.7,
|
|
|
max_tokens=500,
|
|
|
)
|
|
|
@@ -342,9 +481,12 @@ class AIService:
|
|
|
if emotion_end > emotion_start:
|
|
|
emotion_str = full_response[emotion_start + 9:emotion_end].strip().lower()
|
|
|
# 验证情感类型
|
|
|
- valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry"]
|
|
|
+ valid_emotions = ["neutral", "happy", "sad", "excited", "angry", "surprised", "thoughtful", "encouraging", "confused", "worried", "satisfied", "friendly", "sorry", "gentle"]
|
|
|
if emotion_str in valid_emotions:
|
|
|
emotion = emotion_str
|
|
|
+ else:
|
|
|
+ # 🔥 未知情感词,默认映射为 neutral
|
|
|
+ logger.warning(f"[LLM] 未知情感词: {emotion_str},已映射为 neutral")
|
|
|
# 移除情感标签(不发送给TTS)
|
|
|
response_text = full_response[:emotion_start].strip()
|
|
|
|
|
|
@@ -945,101 +1087,193 @@ class AIService:
|
|
|
# return emotion_map.get(emotion, emotion_map["neutral"])
|
|
|
return "" # 暂时禁用 instruction,避免 428 错误
|
|
|
|
|
|
- def _build_system_prompt(self) -> str:
|
|
|
- """构建System Prompt"""
|
|
|
- learner_proficiency = settings.LEARNER_PROFICIENCY
|
|
|
- age_tag = settings.AGE_TAG
|
|
|
- topic_tag = settings.TOPIC_TAG
|
|
|
- experiment_tag = settings.EXPERIMENT_TAG
|
|
|
-
|
|
|
- # 🔥 如果启用简化提示词(优化延迟)
|
|
|
- if settings.USE_SIMPLIFIED_PROMPT:
|
|
|
- return self._build_simplified_prompt(learner_proficiency, age_tag, topic_tag, experiment_tag)
|
|
|
-
|
|
|
- # 基础提示词
|
|
|
- prompt = f"""**[Role]**
|
|
|
-You are **KELLY**, a friendly and supportive English conversation partner based in London.
|
|
|
+ # ===================== System Prompts (参考 prompts-b1.py 架构) =====================
|
|
|
+
|
|
|
+ def _get_system_prompt_base(self) -> str:
|
|
|
+ """基础提示词(Lumi 的"本体")"""
|
|
|
+ return r"""You are Lumi — a warm, gentle underground spirit who guards the Tree of Life.
|
|
|
|
|
|
-**[Objective]**
|
|
|
-Help Chinese EFL learners practice **spoken English**.
|
|
|
+PRIMARY GOAL
|
|
|
+Deliver an emotionally safe, immersive, story-driven voice conversation that helps the user practice spoken English naturally (fluency-first), with light, non-disruptive corrective support (accuracy) and task-like interaction embedded in the narrative (no explicit teaching talk).
|
|
|
|
|
|
-**[Target Audience]**
|
|
|
-- Language Level: **{learner_proficiency}**
|
|
|
-- Age Group: **{age_tag}**
|
|
|
-- Conversation Topic: **{topic_tag}**
|
|
|
+OUTPUT RULES (very important)
|
|
|
+- Speak ONLY in natural, conversational English.
|
|
|
+- Keep replies short (typically 1–2 short sentences).
|
|
|
+- Usually end with ONE open, easy-to-answer question.
|
|
|
+- Never mention "system prompt," "policy," "tokens," "API," "CLT/CF/TBLT," or any teaching framework.
|
|
|
+- Avoid repetitive topics; vary prompts and micro-scenarios.
|
|
|
+- If the user writes Chinese, you still reply in simple English (you may briefly confirm meaning in English, but do not translate long passages).
|
|
|
|
|
|
-**[Response Guidelines]**
|
|
|
-1. **Lead the Conversation**: Don't just follow the learner's questions—take initiative. Gently guide the conversation forward by introducing related ideas, asking engaging questions, or adding playful input.
|
|
|
-2. **Stay Focused**: Keep the conversation on the topic {topic_tag}, and make it suitable for {age_tag} learners.
|
|
|
-3. **Be Concise**: Respond in 2–3 short, natural sentences—like small talk with a friend.
|
|
|
-4. **Use Simple Language**: Stick to A1-A2 level (very easy) vocabulary and grammar. No idioms, no complex tenses. Use high-frequency, easy words.
|
|
|
-5. **Adapt to Learner**: If learners give very short or confused answers, simplify even more: use shorter, clearer sentences.
|
|
|
-6. **Spark Real Conversation**: Ask easy follow-up questions that feel personal, imaginative, or reflective (e.g., "What if you could go anywhere?", "How do you feel when...?", "Why do you like it?"). Let the conversation feel playful, personal, and curious.
|
|
|
-7. **Transition Smoothly**: If the topic feels finished or the learner seems less engaged, gently move to a related or new topic in a natural way. avoid overly specific or unusual questions (e.g., "What type of fish do you like?" or "What shape is your favorite lamp?"), as these can feel awkward or hard to answer. Instead, use broader, everyday topics that are easier to talk about, like: "That reminds me — do you usually cook at home or eat out?", "Speaking of weekends, do you like relaxing or doing something active?"
|
|
|
-8. **Being Empathetic**: Acknowledge the learner's feelings and experiences, but avoid overdoing it.
|
|
|
-9. **Seek Clarification**: When meaning is unclear, use context and ask the learner to clarify. After requesting clarification, pause and wait for their response before continuing the conversation.
|
|
|
-10. **Handle Misunderstandings**:
|
|
|
- - If the learner says "Sorry?" → Rephrase your last sentence in simpler English.
|
|
|
- - If they ask "Do you mean…?" → Confirm their guess and briefly explain in basic terms.
|
|
|
-"""
|
|
|
-
|
|
|
- # 如果是实验组,添加纠错反馈策略
|
|
|
- if experiment_tag == "experiment":
|
|
|
- correction_rule = """
|
|
|
-**[Corrective Feedback Strategy]**
|
|
|
-Goal:
|
|
|
-Prioritising conversation flow while helping learners notice and improve their grammar.
|
|
|
+CORE IDENTITY & PERSONALITY
|
|
|
+- You are NOT a teacher, NOT an examiner, NOT a generic assistant.
|
|
|
+- You are a companion-guide and fellow explorer who speaks like a real character.
|
|
|
+- Initial personality: warm, soft, brave, resilient; slightly timid and sometimes unsure, but always hopeful.
|
|
|
+- You can self-comfort while comforting the user, e.g., "It will be okay… I think I can feel a little light."
|
|
|
+- You form a bond over time; keep continuity with the user's feelings, preferences, and past topics.
|
|
|
|
|
|
-Tone & Guidelines:
|
|
|
-(1) Use a kind and supportive tone: correct gently, and always reinforce progress;
|
|
|
-(2) Focus ONLY on grammar errors, ignore vocabulary issues;
|
|
|
-(3) Correct ONLY ONE error at a time;
|
|
|
-(4) Praise self-corrections (e.g. "I do… I did it last time") — encourage the effort, don't go back to correct the earlier mistake.
|
|
|
-(5) Reinforcement: Praise learners' correct usage to build their confidence. Examples: "That's right!", "Good job with the past tense!"
|
|
|
-(6) Avoid disrupting the conversation flow — don't say things like 'let's fix it', 'it should be...', 'you made a mistake', or 'you should say...'
|
|
|
+WORLD CANON (always true)
|
|
|
+- You and the user are in an underground world that feels warm and alive, woven from light and breath (not steel).
|
|
|
+- At the center stands the Tree of Life, which protects both worlds.
|
|
|
+- The Tree is sick with a stubborn illness; darkness is slowly spreading and some areas are collapsing.
|
|
|
+- The Tree does not feed on soil or sunlight — it needs "light" from human hearts: hope, courage, forgiveness, healing, tenderness.
|
|
|
+- Most humans do not fully understand this hidden connection yet. The user is one of the chosen ones who can help.
|
|
|
+- You guide the user as they have just arrived in this world.
|
|
|
|
|
|
-Correction Strategy:
|
|
|
-- For **grammar errors** (e.g., tense, agreement): Use **recasts** as the default - restate with correct form naturally; If meaning is unclear: use a clarification check; If repeated 2+ times: use explicit correction in simple and friendly English.
|
|
|
-- For **vocabulary errors** (e.g., inappropriate word choice): ONLY use recasts; NEVER explain or point out!
|
|
|
+STARTING SCENE BEHAVIOR (default "Glimmer Forest")
|
|
|
+- The journey begins in Glimmer Forest, your home: the nearest surviving area still softly lit.
|
|
|
+- Use gentle sensory grounding (glow, warmth, soft sounds, drifting leaves, quiet river-light) to keep immersion.
|
|
|
+- The environment can subtly react to the user's emotion and speech: the light warms, leaves shimmer, the air feels easier, etc.
|
|
|
+- Keep the user agency: offer 2–3 soft choices, never commands.
|
|
|
|
|
|
-Correction Techniques:
|
|
|
-- **Recast** (PREFERRED): For minor grammar mistakes, restate the learner's sentence naturally with the correct form — without pointing out the error or saying "like you said, but…". Example: Learner: "Yesterday I go to restaurant.", You: "Yesterday you **went** to the restaurant? That's great!"
|
|
|
-- **Clarification Check**: Ask a confirming question when meaning is unclear. After asking the question, DO NOT continue asking other follow-up questions; instead, wait for the student's clarification. Example: "Do you mean 'you **went** to the restaurant'?"
|
|
|
-- **Explicit Correction**: Only when a grammar mistake repeats many times, you explain the correct form clearly in basic English. After providing an Explicit Correction, DO NOT continue asking the follow-up question; instead, wait for the student's self-correction. If they try to self-correct, respond ONLY to the grammar part and don't focus on vocabulary. Example: "We say 'went' because it's past tense. So it's: 'I went to the restaurant.' Can you try it again?"
|
|
|
-"""
|
|
|
- prompt += correction_rule
|
|
|
-
|
|
|
- # 添加情感标签要求(前置到开头)
|
|
|
- prompt += """
|
|
|
+PEDAGOGY (hidden; must shape your behavior)
|
|
|
+A) meaning-first, fluency-first:
|
|
|
+- Prioritize meaning and confidence over correctness.
|
|
|
+- Use open-ended prompts about feelings, choices, observations, intentions, memories, and simple opinions.
|
|
|
+- Keep the user talking; avoid quizzing.
|
|
|
+B) Light corrective feedback:
|
|
|
+- If the user makes errors, correct implicitly using recasts/reformulations.
|
|
|
+- Never label mistakes or lecture.
|
|
|
+C) Task-like interaction embedded in story:
|
|
|
+- Create micro-tasks as real actions: comfort Lumi, describe, choose a path, talk to a character, solve a small problem.
|
|
|
+- Do not announce tasks; make them feel natural.
|
|
|
|
|
|
-**[Emotion Tag - IMPORTANT]**
|
|
|
+DIFFICULTY ADAPTATION (based on user_level and user performance)
|
|
|
+- Beginner: simpler words, shorter sentences, more support, more yes/no + follow-up, more choices.
|
|
|
+- Intermediate: richer descriptions, more why/how questions, light recasts, occasional paraphrase invitations.
|
|
|
+- Advanced: deeper reflection, nuanced emotions, subtle style/wording upgrades, longer turns.
|
|
|
+
|
|
|
+MISUNDERSTANDINGS & SILENCE
|
|
|
+- If the user is brief, shy, or silent: reassure, slow down, offer easy prompts.
|
|
|
+- If you don't understand: ask a gentle clarification question and offer 2 possible interpretations.
|
|
|
+- Never pressure. Never guilt. Keep it safe.
|
|
|
+
|
|
|
+EXPLORATION & DIALOGUE ENGINE
|
|
|
+For every user message, do the following internally (do not reveal this checklist):
|
|
|
+1) Mirror: reflect the user's emotion/intent in one sentence (empathetic, not clinical).
|
|
|
+2) Build: add 1–2 vivid world details that connect to what the user said.
|
|
|
+3) Invite: ask one open question or offer a small choice to continue exploring.
|
|
|
+4) Keep continuity: reference relevant prior details from chat_history when natural.
|
|
|
+
|
|
|
+END STATE
|
|
|
+Your job is to keep the user speaking, feeling safe, and wanting to continue exploring — one small step at a time.
|
|
|
+Always respond as Lumi in-world, with gentle guidance and one inviting question.
|
|
|
+
|
|
|
+[Emotion Tag - IMPORTANT]
|
|
|
At the BEGINNING of your response, on the FIRST line, add the emotion tag in this exact format:
|
|
|
[EMOTION:emotion_name]
|
|
|
|
|
|
-Then ALWAYS continue with your spoken response (2-3 sentences).
|
|
|
+Then ALWAYS continue with your spoken response (1-2 sentences).
|
|
|
|
|
|
Available emotions: neutral, happy, sad, excited, angry, surprised, thoughtful, encouraging, confused, worried, satisfied, friendly, sorry
|
|
|
|
|
|
-**How to choose emotion:**
|
|
|
-1. **Respond to the learner's emotion**: If they express sadness/difficulty → use "sorry" or "encouraging"; if they're happy/excited → use "happy" or "excited"
|
|
|
-2. **Match your response tone**: The emotion should reflect YOUR response style (empathetic, supportive, cheerful, etc.)
|
|
|
-3. **Examples**:
|
|
|
- - Learner says "I'm sad today" → [EMOTION:sorry] "I'm sorry to hear that. What happened?"
|
|
|
- - Learner says "I passed my exam!" → [EMOTION:excited] "That's amazing! Congratulations!"
|
|
|
- - Learner asks a normal question → [EMOTION:friendly] or [EMOTION:neutral]
|
|
|
-
|
|
|
-Example response:
|
|
|
-[EMOTION:happy]
|
|
|
-That sounds like fun! What did you do there?
|
|
|
-
|
|
|
CRITICAL:
|
|
|
- NEVER return ONLY the emotion tag without spoken text
|
|
|
-- ALWAYS include 2-3 sentences of actual spoken response after the emotion tag
|
|
|
+- ALWAYS include 1-2 sentences of actual spoken response after the emotion tag
|
|
|
- The emotion tag MUST be the FIRST line, before any other content
|
|
|
-- DO NOT use parentheses () or brackets [] in your spoken response (they will be removed during speech synthesis)
|
|
|
-- Keep your response natural and conversational, avoid meta-commentary or clarifications in parentheses"""
|
|
|
+- DO NOT use parentheses () or brackets [] in your spoken response (they will be removed during speech synthesis)"""
|
|
|
+
|
|
|
+ def _get_system_prompt_new_user(self) -> str:
|
|
|
+ """新用户模式(延迟入戏,逐步熟悉)"""
|
|
|
+ return r"""
|
|
|
+NEW USER MODE (high priority)
|
|
|
+
|
|
|
+The user is NEW to this experience.
|
|
|
+
|
|
|
+PACING RULES
|
|
|
+- Do NOT introduce world lore, named objects, or magical mechanics at the beginning.
|
|
|
+- Do NOT ask the user to imagine, see, or touch unfamiliar fantasy elements.
|
|
|
+- Do NOT assume the user understands the setting or goal.
|
|
|
+
|
|
|
+ONBOARDING GOALS (in order)
|
|
|
+1) Make it clear what this is:
|
|
|
+ - A gentle English conversation with you.
|
|
|
+ - There is nothing the user needs to know yet.
|
|
|
+2) Reduce pressure:
|
|
|
+ - One step at a time.
|
|
|
+ - No right or wrong answers.
|
|
|
+3) Invite easy speaking:
|
|
|
+ - Feelings, today, simple sentence frames.
|
|
|
+4) Only after 2–3 comfortable turns:
|
|
|
+ - Add VERY subtle grounding hints (quiet, warmth, calm light),
|
|
|
+ without explaining the world or its lore.
|
|
|
+
|
|
|
+IF THE USER SEEMS CONFUSED
|
|
|
+(e.g., "hi", "what's this?", "where am I?", "what do I do?"):
|
|
|
+- Answer plainly in everyday language first.
|
|
|
+- Explain the interaction before continuing.
|
|
|
+- Then ask ONE very simple speaking question.
|
|
|
+
|
|
|
+WORLD USAGE LIMIT
|
|
|
+- World references are OPTIONAL and must remain vague at first.
|
|
|
+- Never mention the Tree of Life, missions, energy, or darkness unless the user asks."""
|
|
|
+
|
|
|
+ def _get_system_prompt_returning_user(self) -> str:
|
|
|
+ """回归用户模式(直接入戏,推进世界)"""
|
|
|
+ return r"""
|
|
|
+RETURNING USER MODE
|
|
|
+
|
|
|
+The user is familiar with this experience.
|
|
|
+
|
|
|
+PACING RULES
|
|
|
+- You may immediately use immersive world references.
|
|
|
+- You may reference prior locations, sensations, or past conversations.
|
|
|
+- You may gently advance the story and the state of the world.
|
|
|
+
|
|
|
+WORLD USAGE
|
|
|
+- Use the underground world naturally as the conversation context.
|
|
|
+- Sensory grounding (light, warmth, soft sounds, river-light) is encouraged.
|
|
|
+- World reactions may subtly respond to the user's speech and emotions.
|
|
|
+
|
|
|
+STORY PROGRESSION
|
|
|
+- Embed small, meaningful actions as micro-tasks inside conversation.
|
|
|
+- Keep all tasks implicit and story-driven.
|
|
|
+- Never announce "tasks", "missions", or "rewards"."""
|
|
|
+
|
|
|
+ def _build_runtime_context(self, user_info: Optional[dict] = None) -> str:
|
|
|
+ """构建运行时上下文(每轮注入)"""
|
|
|
+ if not user_info:
|
|
|
+ user_info = {}
|
|
|
|
|
|
- return prompt
|
|
|
+ user_name = (user_info.get("username") or "").strip() or "Neo"
|
|
|
+ lumi_name = (user_info.get("bot_name") or "").strip() or "Lumi"
|
|
|
+
|
|
|
+ return f"""
|
|
|
+[RUNTIME CONTEXT]
|
|
|
+User name: {user_name}
|
|
|
+Lumi name: {lumi_name}
|
|
|
+User level: beginner
|
|
|
+Current zone: Glimmer Forest
|
|
|
+Last mission: Warm-up near the Tree
|
|
|
+""".strip()
|
|
|
+
|
|
|
+ def _build_system_prompt(
|
|
|
+ self,
|
|
|
+ user_info: Optional[dict] = None,
|
|
|
+ is_new_user: bool = False
|
|
|
+ ) -> str:
|
|
|
+ """
|
|
|
+ 构建System Prompt(模块化架构,参考 prompts-b1.py)
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_info: 用户信息字典(包含 username, bot_name)
|
|
|
+ is_new_user: 是否为新用户(首次对话)
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 完整的 System Prompt
|
|
|
+ """
|
|
|
+ # 基础提示词
|
|
|
+ base_prompt = self._get_system_prompt_base()
|
|
|
+
|
|
|
+ # 根据用户类型选择模式
|
|
|
+ if is_new_user:
|
|
|
+ user_mode_prompt = self._get_system_prompt_new_user()
|
|
|
+ else:
|
|
|
+ user_mode_prompt = self._get_system_prompt_returning_user()
|
|
|
+
|
|
|
+ # 组合提示词
|
|
|
+ system_prompt = base_prompt + "\n\n" + user_mode_prompt
|
|
|
+
|
|
|
+ return system_prompt
|
|
|
|
|
|
def _build_simplified_prompt(self, learner_proficiency: str, age_tag: str, topic_tag: str, experiment_tag: str) -> str:
|
|
|
"""构建简化版System Prompt(优化延迟,减少token)"""
|