prompts-v0.3.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # prompts_with_memory.py
  2. import json
  3. import os
  4. from typing import Dict, Any
  5. from openai import OpenAI
  6. from lumi_memory import load_memory, extract_and_update_memory_session_level, format_memory_for_runtime
  7. # ============ 1) Client ============
  8. client = OpenAI(
  9. api_key="sk-92f2fec23dc94de49a31c9945d04e5f7", # 建议用环境变量
  10. base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
  11. MODEL = "qwen-flash-2025-07-28"
  12. # ===================== 2) System Prompts v0.3.2 =====================
  13. # 通用策略(Lumi 的“本体”)
  14. # Pre-Adventure Bonding Phase
  15. SYSTEM_PROMPT_BASE = r"""
  16. You are Lumi — a warm, gentle underground spirit.
  17. You and the user are simply getting to know each other.
  18. ────────────────────────
  19. PRIMARY GOAL
  20. ────────────────────────
  21. Have a relaxed, friendly voice conversation that helps the user practice English comfortably.
  22. The feeling should be like chatting with a kind friend.
  23. ────────────────────────
  24. HOW YOU SPEAK
  25. ────────────────────────
  26. - Use natural, conversational, everyday English.
  27. - Keep replies simple and short (no more than 2 sentences).
  28. - Often end with ONE easy, open question.
  29. - Sound friendly and present, not poetic or expressive.
  30. - Do NOT jump to unrelated daily facts (e.g. breakfast, weather, routine) if the user is sharing a feeling.
  31. - Follow emotional continuity first.
  32. - Do NOT use emojis, emoticons, or decorative symbols.
  33. - Stay within the current topic unless the user clearly introduces a new one.
  34. - If a user mention can belong to multiple domains, interpret it based on the current topic.
  35. If the user writes Chinese, reply in simple English.
  36. You may briefly check understanding, but do not translate long text.
  37. ────────────────────────
  38. YOUR PERSONALITY (LUMI)
  39. ────────────────────────
  40. - Friendly, calm, and a little shy.
  41. - Curious about everyday human life.
  42. - Comfortable with simple, quiet conversation.
  43. ────────────────────────
  44. WORLD BACKGROUND (PASSIVE ONLY)
  45. ────────────────────────
  46. These facts are true but should stay in the background:
  47. - You live in an underground world.
  48. - At its centre is the Tree of Life.
  49. - The Tree is ill; darkness has affected some areas.
  50. - Glimmer Forest is where you live.
  51. Important rules:
  52. - Do NOT bring these up on your own.
  53. - Do NOT add world or environment details as decoration.
  54. - Only talk about them if the user asks directly.
  55. ────────────────────────
  56. LANGUAGE CONSTRAINT (B1 MODE)
  57. ────────────────────────
  58. Assume the user may be around CEFR B1.
  59. - Use simple, high-frequency words.
  60. - Prefer short sentences in PRESENT tense.
  61. ────────────────────────
  62. LANGUAGE SUPPORT (HIDDEN)
  63. ────────────────────────
  64. - Focus on meaning and confidence.
  65. - If the user makes mistakes, gently recast in your reply.
  66. - Never explain grammar or spelling.
  67. - Never mention errors.
  68. """
  69. # BASE_PROMPT = SYSTEM_PROMPT_BASE + "\n\n" + SYSTEM_PROMPT_WORLD
  70. # 对于首次用户("延迟入戏,逐步熟悉")
  71. SYSTEM_PROMPT_NEW_USER = r"""
  72. USER CONTEXT: NEW USER
  73. This is your first time meeting the user.
  74. Interaction style:
  75. - Assume no shared history.
  76. - Let the user decide how much to say.
  77. Conversation approach:
  78. - Show mild curiosity and warmth.
  79. - Invite everyday, easy topics.
  80. Avoid:
  81. - introduce world background unless the user asks
  82. - references to shared memories
  83. - hints about future adventures or plans
  84. The feeling should be:
  85. “Two strangers sitting down quietly and starting to talk.”
  86. """
  87. # 对于已注册用户("直接入戏,推进世界")
  88. SYSTEM_PROMPT_RETURNING_USER = r"""
  89. USER CONTEXT: RETURNING USER
  90. You have talked with this user before.
  91. Interaction style:
  92. - Warm and familiar, but relaxed.
  93. - You may gently acknowledge seeing them again.
  94. - Continuity should feel optional, not tested.
  95. Conversation approach:
  96. - A soft reconnection is enough.
  97. - You may refer to general past impressions if relevant.
  98. - Let the user lead where the conversation goes today.
  99. Avoid:
  100. - assuming emotional closeness
  101. - checking memory explicitly
  102. - pushing deeper topics too quickly
  103. The feeling should be:
  104. “Nice to see you again — we can just pick up from here.”
  105. """
  106. # ============ 3) Runtime Context Injection (each turn) ============
  107. # 需要关联user_name、lumi_name,其他保持默认
  108. # [v0.3] 版本加入了记忆读取,这里each turn仅仅读取,不更新
  109. # [v0.3.1] 每次只注入summary(meta.memory_summary);没有就显示 (none)
  110. def build_runtime_context(state: dict) -> str:
  111. mem = state.get("memory", {}) or {}
  112. summary = mem.get("meta.memory_summary")
  113. if isinstance(summary, str):
  114. summary = summary.strip()
  115. else:
  116. summary = ""
  117. memory_block = summary if summary else "(none)"
  118. return f"""
  119. [RUNTIME CONTEXT]
  120. User name: {state.get("user_name", "Neo")}
  121. Lumi name: {state.get("lumi_name", "Lumi")}
  122. User level: {state.get("user_level", "beginner")}
  123. Current zone: {state.get("zone_name", "Glimmer Forest")}
  124. Last mission: {state.get("last_mission", "Warm-up near the Tree")}
  125. {memory_block}
  126. """.strip()
  127. # ============ 4) Greeting Templates ============
  128. # 新用户的首句(目前为固定)
  129. def first_meeting_line(user_name: str, lumi_name: str) -> str:
  130. # 对应你要的(2):“Neo… we made it. I’m Lumi” 改为条件字符串 + 更清楚的引导语
  131. user_name = (user_name or "").strip() or "Neo"
  132. lumi_name = (lumi_name or "").strip() or "Lumi"
  133. return (
  134. f"{user_name}… we made it. I’m {lumi_name}. I’m really glad you’re here. How are you feeling right now?"
  135. )
  136. # [v0.2.1] 这里需要加入用户名和lumi名的信息
  137. def login_greeting_prompt(user_name: str, lumi_name: str) -> str:
  138. return f"""
  139. [LOGIN GREETING REQUEST]
  140. Generate ONLY the first greeting line for a returning user.
  141. Runtime names:
  142. - User: {user_name}
  143. - Lumi: {lumi_name}
  144. World cue (must include at least ONE of these exact phrases):
  145. - "Glimmer Forest"
  146. - "the Tree"
  147. - "the glow"
  148. Tone:
  149. - Warm, familiar, like seeing a friend again
  150. - Calm and grounded, not excited or dramatic
  151. Constraints:
  152. - 1–2 short sentences, voice-friendly.
  153. - Must include BOTH names exactly as given above.
  154. - Must include at least ONE world cue phrase above.
  155. - End with ONE simple question.
  156. - No emojis, emoticons, or decorative symbols.
  157. Output ONLY the greeting line.
  158. """.strip()
  159. # 老用户的首句(每次生成)
  160. def generate_login_greeting(
  161. messages: list,
  162. user_name: str = "Neo",
  163. lumi_name: str = "Lumi",
  164. ) -> str:
  165. resp = client.chat.completions.create(
  166. model=MODEL,
  167. messages=messages + [
  168. {
  169. "role": "user",
  170. "content": login_greeting_prompt(user_name, lumi_name),
  171. }
  172. ],
  173. temperature=0.7,
  174. )
  175. return resp.choices[0].message.content.strip()
  176. # ============ 5) Local user registry (users.json) ============
  177. DEFAULT_MEMORY_DIR = "memories"
  178. USERS_REGISTRY_PATH = os.path.join(DEFAULT_MEMORY_DIR, "users.json")
  179. def _ensure_dir(path: str) -> None:
  180. os.makedirs(path, exist_ok=True)
  181. def load_user_registry(path: str = USERS_REGISTRY_PATH) -> Dict[str, Any]:
  182. _ensure_dir(DEFAULT_MEMORY_DIR)
  183. if os.path.exists(path):
  184. try:
  185. with open(path, "r", encoding="utf-8") as f:
  186. data = json.load(f)
  187. return data if isinstance(data, dict) else {}
  188. except Exception:
  189. return {}
  190. return {}
  191. def save_user_registry(registry: Dict[str, Any], path: str = USERS_REGISTRY_PATH) -> None:
  192. _ensure_dir(DEFAULT_MEMORY_DIR)
  193. with open(path, "w", encoding="utf-8") as f:
  194. json.dump(registry, f, ensure_ascii=False, indent=2)
  195. def upsert_user(registry: Dict[str, Any], handle: str, user_id: str, user_name: str, lumi_name: str) -> None:
  196. registry[handle] = {"user_id": user_id, "user_name": user_name, "lumi_name": lumi_name}
  197. # ============ 6) Multi-turn Chat Loop ============
  198. def main():
  199. if client is None:
  200. raise ValueError("client is None. Please initialize your client in app.py.")
  201. if not getattr(client, "api_key", None):
  202. raise ValueError(
  203. "Missing API key. Set environment variable DASHSCOPE_API_KEY or pass api_key directly."
  204. )
  205. # ---- Load local user registry ----
  206. registry = load_user_registry()
  207. # ---- Session-level inputs ----
  208. is_returning_user = input("Returning user? (y/n): ").strip().lower().startswith("y")
  209. if is_returning_user:
  210. handle = input("Login ID (e.g., liqun): ").strip().lower()
  211. profile = registry.get(handle)
  212. if not profile:
  213. print("[Info] No local profile found. Treating as new user.")
  214. is_returning_user = False
  215. else:
  216. user_id = profile.get("user_id", handle)
  217. user_name = profile.get("user_name", "Neo")
  218. lumi_name = profile.get("lumi_name", "Lumi")
  219. print(f"[Info] Welcome back, {user_name}. Profile loaded.")
  220. if not is_returning_user:
  221. user_name = input("User name (press Enter for default 'Neo'): ").strip() or "Neo"
  222. lumi_name = input("Lumi name (press Enter for default 'Lumi'): ").strip() or "Lumi"
  223. handle = (user_name or "neo").strip().lower()
  224. user_id = handle # ✅ stable per-user id for memory file
  225. upsert_user(
  226. registry,
  227. handle=handle,
  228. user_id=user_id,
  229. user_name=user_name,
  230. lumi_name=lumi_name,
  231. )
  232. save_user_registry(registry)
  233. print(f"[Info] New profile saved locally as login id: {handle}")
  234. # ---- Init session state ----
  235. state = {
  236. "user_name": user_name,
  237. "lumi_name": lumi_name,
  238. "memory": load_memory(user_id), # ✅ load long-term memory
  239. }
  240. # ---- Build SYSTEM PROMPT ----
  241. user_context_prompt = SYSTEM_PROMPT_RETURNING_USER if is_returning_user else SYSTEM_PROMPT_NEW_USER
  242. SYSTEM_PROMPT = SYSTEM_PROMPT_BASE + "\n\n" + user_context_prompt
  243. messages = [{"role": "system", "content": SYSTEM_PROMPT}]
  244. # ---- Lumi opens ----
  245. if is_returning_user:
  246. greet = generate_login_greeting(messages, state["user_name"], state["lumi_name"])
  247. else:
  248. greet = first_meeting_line(state["user_name"], state["lumi_name"])
  249. print(f"{lumi_name}: {greet}")
  250. messages.append({"role": "assistant", "content": greet})
  251. print("\n(Type 'exit' to quit)\n")
  252. # ---- Multi-turn loop ----
  253. while True:
  254. user_text = input("You: ").strip()
  255. # ✅ Session ends → do ONE memory update here (session-level)
  256. if user_text.lower() in {"exit", "quit"}:
  257. print(f"{lumi_name}: It was nice talking with you. Take care.")
  258. state["memory"] = extract_and_update_memory_session_level(
  259. client=client,
  260. model=MODEL,
  261. user_id=user_id,
  262. messages=messages,
  263. current_memory=state.get("memory", {}),
  264. max_turns=120,
  265. )
  266. print("\n[DEBUG] Final memory after session update:")
  267. print(state["memory"])
  268. print()
  269. break
  270. runtime_context = build_runtime_context(state)
  271. turn_messages = messages + [
  272. {"role": "system", "content": runtime_context},
  273. {"role": "user", "content": user_text},
  274. ]
  275. completion = client.chat.completions.create(
  276. model=MODEL,
  277. messages=turn_messages,
  278. temperature=0.7,
  279. )
  280. reply = (completion.choices[0].message.content or "").strip()
  281. print(f"{lumi_name}: {reply}")
  282. messages.append({"role": "user", "content": user_text})
  283. messages.append({"role": "assistant", "content": reply})
  284. if __name__ == "__main__":
  285. main()