Make Dream QA responses story-led
#35
by ADJCJH - opened
- dream_customs/pipeline.py +714 -15
- dream_customs/prompts.py +16 -3
- tests/test_pipeline.py +160 -2
dream_customs/pipeline.py
CHANGED
|
@@ -19,6 +19,8 @@ from dream_customs.render import render_pact_card, render_today_tip_card
|
|
| 19 |
from dream_customs.safety import needs_escalation, safety_note
|
| 20 |
from dream_customs.schema import CustomsSession, DreamIntake, DreamQAState, EvidenceItem, PactCard, TimelineEvent, TodayTipCard
|
| 21 |
|
|
|
|
|
|
|
| 22 |
|
| 23 |
def _normalize_language(language: str = "en") -> str:
|
| 24 |
return "zh" if language == "zh" else "en"
|
|
@@ -108,6 +110,12 @@ _ZH_ANCHOR_MARKERS = [
|
|
| 108 |
"掉进海",
|
| 109 |
"海里",
|
| 110 |
"海",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"小孩找不到家",
|
| 112 |
"小孩",
|
| 113 |
"找不到家",
|
|
@@ -210,6 +218,12 @@ _ZH_TO_EN_PHRASES = {
|
|
| 210 |
"掉进海": "falling into the sea",
|
| 211 |
"海里": "sea",
|
| 212 |
"海": "sea",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
"小孩找不到家": "child unable to find home",
|
| 214 |
"小孩": "child",
|
| 215 |
"找不到家": "unable to find home",
|
|
@@ -304,6 +318,44 @@ _ZH_TO_EN_PHRASES = {
|
|
| 304 |
"梦境": "dream",
|
| 305 |
}
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
def _dedupe_preserve_order(items: List[str]) -> List[str]:
|
| 309 |
seen = set()
|
|
@@ -462,10 +514,23 @@ def _english_anchor_text(text: str) -> str:
|
|
| 462 |
return clean
|
| 463 |
|
| 464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
def _anchors_for_language(intake: DreamIntake, language: str = "en") -> List[str]:
|
| 466 |
anchors = _extract_dream_anchors(intake)
|
| 467 |
if _is_zh(language):
|
| 468 |
-
return anchors
|
| 469 |
localized = [_english_anchor_text(anchor) for anchor in anchors]
|
| 470 |
return _dedupe_preserve_order([anchor for anchor in localized if anchor])
|
| 471 |
|
|
@@ -514,6 +579,103 @@ def _secondary_anchor(intake: DreamIntake, language: str = "en") -> str:
|
|
| 514 |
return anchors[1] if len(anchors) > 1 else _primary_anchor(intake, language)
|
| 515 |
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
def _anchor_with_article(anchor: str) -> str:
|
| 518 |
clean = (anchor or "").strip()
|
| 519 |
if clean.lower().startswith(("the ", "a ", "an ")):
|
|
@@ -543,9 +705,314 @@ def _summary_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
|
| 543 |
return f"你梦见{clean}"
|
| 544 |
|
| 545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 547 |
if intake.main_question.strip():
|
| 548 |
-
return intake.main_question
|
|
|
|
|
|
|
|
|
|
| 549 |
task = _task_focus(intake.merged_text(), language)
|
| 550 |
if task:
|
| 551 |
if not _is_zh(language):
|
|
@@ -576,12 +1043,31 @@ def _fallback_interpretation(intake: DreamIntake, language: str = "en") -> str:
|
|
| 576 |
|
| 577 |
|
| 578 |
def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
| 580 |
if not _is_zh(language):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
return (
|
| 582 |
f"For today, borrow one action from {_anchor_with_article(primary)}: open the task, write only the first line, "
|
| 583 |
"and let that be enough for now."
|
| 584 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
return f"今天先从「{primary}」借一个动作:只做最小的第一步,不急着把整件事完成。"
|
| 586 |
|
| 587 |
|
|
@@ -963,6 +1449,143 @@ def _grounded_followup_question(intake: DreamIntake, language: str = "en") -> st
|
|
| 963 |
return f"如果「{primary}」能递给你一个更小的第一步,今天那一步会是什么?"
|
| 964 |
|
| 965 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 967 |
polished = card.model_copy(deep=True)
|
| 968 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
|
@@ -1089,14 +1712,20 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1089 |
setattr(polished, field, _clean_placeholder_phrase(getattr(polished, field)))
|
| 1090 |
if not polished.dream_summary.strip() or _is_placeholder_anchor(polished.dream_summary) or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 1091 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 1092 |
-
|
|
|
|
|
|
|
|
|
|
| 1093 |
not polished.main_question.strip()
|
| 1094 |
or _is_placeholder_anchor(polished.main_question)
|
| 1095 |
or not _text_uses_anchor(polished.main_question, anchors)
|
| 1096 |
):
|
| 1097 |
polished.main_question = _main_question_from_intake(intake, language)
|
|
|
|
| 1098 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 1099 |
-
if
|
|
|
|
|
|
|
| 1100 |
polished.interpretation = answer_interpretation
|
| 1101 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1102 |
anchor = _answer_bridge_anchor(anchors)
|
|
@@ -1115,8 +1744,11 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1115 |
elif not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
|
| 1116 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 1117 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
|
|
|
| 1118 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 1119 |
-
if
|
|
|
|
|
|
|
| 1120 |
polished.today_tip = answer_tip
|
| 1121 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1122 |
anchor = anchors[0]
|
|
@@ -1140,8 +1772,11 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1140 |
):
|
| 1141 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 1142 |
hard_action_markers = ["address it immediately", "fix it immediately", "solve it immediately"]
|
|
|
|
| 1143 |
answer_action = _answer_based_tiny_action(answers, language)
|
| 1144 |
-
if
|
|
|
|
|
|
|
| 1145 |
polished.tiny_action = answer_action
|
| 1146 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1147 |
polished.tiny_action = (
|
|
@@ -1161,11 +1796,34 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1161 |
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 1162 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 1163 |
):
|
|
|
|
|
|
|
| 1164 |
if _is_zh(language):
|
| 1165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1166 |
else:
|
| 1167 |
-
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
polished.caring_note = (
|
| 1170 |
"你不需要一醒来就解决整个梦,先把一个细节照亮就很好。"
|
| 1171 |
if _is_zh(language)
|
|
@@ -1509,6 +2167,29 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 1509 |
)
|
| 1510 |
return next_session
|
| 1511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
prompt = (
|
| 1513 |
followup_question_prompt(next_session.intake, next_session.question_history, next_session.answer_history, language)
|
| 1514 |
if force_another
|
|
@@ -1540,7 +2221,23 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 1540 |
deduped_questions = [_grounded_followup_question(next_session.intake, language)]
|
| 1541 |
fresh_questions = deduped_questions
|
| 1542 |
|
| 1543 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1544 |
next_session.phase = "ask"
|
| 1545 |
next_session.qa_state = build_qa_state(
|
| 1546 |
next_session.intake,
|
|
@@ -1552,10 +2249,12 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 1552 |
next_session.events.append(
|
| 1553 |
_event(
|
| 1554 |
"assistant",
|
| 1555 |
-
|
| 1556 |
-
|
| 1557 |
-
|
| 1558 |
-
|
|
|
|
|
|
|
| 1559 |
meta=str(negotiation.get("visitor_name", "")),
|
| 1560 |
status="question",
|
| 1561 |
)
|
|
|
|
| 19 |
from dream_customs.safety import needs_escalation, safety_note
|
| 20 |
from dream_customs.schema import CustomsSession, DreamIntake, DreamQAState, EvidenceItem, PactCard, TimelineEvent, TodayTipCard
|
| 21 |
|
| 22 |
+
MAX_DREAM_DECOMPOSITION_QUESTIONS = 3
|
| 23 |
+
|
| 24 |
|
| 25 |
def _normalize_language(language: str = "en") -> str:
|
| 26 |
return "zh" if language == "zh" else "en"
|
|
|
|
| 110 |
"掉进海",
|
| 111 |
"海里",
|
| 112 |
"海",
|
| 113 |
+
"前任发消息",
|
| 114 |
+
"发消息又消失",
|
| 115 |
+
"发消息",
|
| 116 |
+
"消息",
|
| 117 |
+
"前任",
|
| 118 |
+
"消失",
|
| 119 |
"小孩找不到家",
|
| 120 |
"小孩",
|
| 121 |
"找不到家",
|
|
|
|
| 218 |
"掉进海": "falling into the sea",
|
| 219 |
"海里": "sea",
|
| 220 |
"海": "sea",
|
| 221 |
+
"前任发消息": "former partner sending a message",
|
| 222 |
+
"发消息又消失": "message that disappeared",
|
| 223 |
+
"发消息": "sending a message",
|
| 224 |
+
"消息": "message",
|
| 225 |
+
"前任": "former partner",
|
| 226 |
+
"消失": "disappearing",
|
| 227 |
"小孩找不到家": "child unable to find home",
|
| 228 |
"小孩": "child",
|
| 229 |
"找不到家": "unable to find home",
|
|
|
|
| 318 |
"梦境": "dream",
|
| 319 |
}
|
| 320 |
|
| 321 |
+
_EN_TO_ZH_PHRASES = {
|
| 322 |
+
"a simple sketch of a person standing on wavy": "海浪上的小人",
|
| 323 |
+
"simple sketch of a person standing on wavy": "海浪上的小人",
|
| 324 |
+
"person standing on wavy": "海浪上的小人",
|
| 325 |
+
"wavy lines representing water": "海浪",
|
| 326 |
+
"wavy lines": "海浪",
|
| 327 |
+
"wavy": "海浪",
|
| 328 |
+
"a dreamlike representation of a sea under night": "夜晚的海",
|
| 329 |
+
"dreamlike representation of a sea": "夜晚的海",
|
| 330 |
+
"sea under night": "夜晚的海",
|
| 331 |
+
"dark sea dream": "漆黑的海",
|
| 332 |
+
"dark sea": "漆黑的海",
|
| 333 |
+
"wavy water": "海浪",
|
| 334 |
+
"waves": "海浪",
|
| 335 |
+
"wave": "海浪",
|
| 336 |
+
"sea": "海",
|
| 337 |
+
"crescent moon": "月牙",
|
| 338 |
+
"moon": "月亮",
|
| 339 |
+
"stick figure": "小人",
|
| 340 |
+
"melted elevator buttons": "融化的电梯按钮",
|
| 341 |
+
"melted elevator button": "融化的电梯按钮",
|
| 342 |
+
"elevator buttons": "电梯按钮",
|
| 343 |
+
"elevator button": "电梯按钮",
|
| 344 |
+
"elevator doors": "电梯门",
|
| 345 |
+
"elevator": "电梯",
|
| 346 |
+
"button": "按钮",
|
| 347 |
+
"floor 14": "数字 14",
|
| 348 |
+
"number 14": "数字 14",
|
| 349 |
+
"lost child at subway station": "地铁站里迷路的小孩",
|
| 350 |
+
"lost child at subway": "地铁里迷路的小孩",
|
| 351 |
+
"subway station": "地铁站",
|
| 352 |
+
"subway": "地铁",
|
| 353 |
+
"child figure": "小孩",
|
| 354 |
+
"child": "小孩",
|
| 355 |
+
"home": "回家的方向",
|
| 356 |
+
"arrow": "箭头",
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
|
| 360 |
def _dedupe_preserve_order(items: List[str]) -> List[str]:
|
| 361 |
seen = set()
|
|
|
|
| 514 |
return clean
|
| 515 |
|
| 516 |
|
| 517 |
+
def _zh_anchor_text(text: str) -> str:
|
| 518 |
+
clean = re.sub(r"\s+", " ", (text or "").strip(" .,:;!?\"'()[]{}"))
|
| 519 |
+
if not clean:
|
| 520 |
+
return ""
|
| 521 |
+
lowered = clean.lower()
|
| 522 |
+
for source, target in sorted(_EN_TO_ZH_PHRASES.items(), key=lambda item: len(item[0]), reverse=True):
|
| 523 |
+
if source in lowered:
|
| 524 |
+
return target
|
| 525 |
+
if re.search(r"[A-Za-z]", clean) and not re.search(r"[\u4e00-\u9fff]", clean):
|
| 526 |
+
return ""
|
| 527 |
+
return clean
|
| 528 |
+
|
| 529 |
+
|
| 530 |
def _anchors_for_language(intake: DreamIntake, language: str = "en") -> List[str]:
|
| 531 |
anchors = _extract_dream_anchors(intake)
|
| 532 |
if _is_zh(language):
|
| 533 |
+
return _dedupe_preserve_order([anchor for anchor in (_zh_anchor_text(anchor) for anchor in anchors) if anchor])
|
| 534 |
localized = [_english_anchor_text(anchor) for anchor in anchors]
|
| 535 |
return _dedupe_preserve_order([anchor for anchor in localized if anchor])
|
| 536 |
|
|
|
|
| 579 |
return anchors[1] if len(anchors) > 1 else _primary_anchor(intake, language)
|
| 580 |
|
| 581 |
|
| 582 |
+
def _story_text(intake: DreamIntake, answers: str = "") -> str:
|
| 583 |
+
return "\n".join(
|
| 584 |
+
part
|
| 585 |
+
for part in [
|
| 586 |
+
intake.dream_text,
|
| 587 |
+
intake.voice_transcript,
|
| 588 |
+
" ".join(intake.visual_clues),
|
| 589 |
+
intake.mood,
|
| 590 |
+
intake.main_question,
|
| 591 |
+
intake.uncertainty,
|
| 592 |
+
intake.user_context,
|
| 593 |
+
answers or "",
|
| 594 |
+
]
|
| 595 |
+
if part and part.strip()
|
| 596 |
+
).lower()
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def _contains_any(text: str, terms: List[str]) -> bool:
|
| 600 |
+
return any(term in text for term in terms)
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def _dream_theme(intake: DreamIntake, answers: str = "") -> str:
|
| 604 |
+
text = _story_text(intake, answers)
|
| 605 |
+
if _contains_any(text, ["小孩", "child", "找不到家", "lost", "home", "回家", "地铁", "subway"]):
|
| 606 |
+
return "lost_home"
|
| 607 |
+
if _contains_any(text, ["海", "海浪", "水", "月牙", "moon", "sea", "wave", "water", "dark sea"]):
|
| 608 |
+
return "dark_water"
|
| 609 |
+
if _contains_any(text, ["电梯", "按钮", "14", "elevator", "button", "floor"]):
|
| 610 |
+
return "stuck_elevator"
|
| 611 |
+
if _contains_any(text, ["图书馆", "楼梯", "便签", "call home", "library", "staircase", "sticky note"]):
|
| 612 |
+
return "library_signal"
|
| 613 |
+
if _contains_any(text, ["前任", "消息", "消失", "former partner", "ex", "message", "disappear"]):
|
| 614 |
+
return "message_loss"
|
| 615 |
+
if _contains_any(text, ["追", "被追", "chase", "chased", "running away"]):
|
| 616 |
+
return "chased"
|
| 617 |
+
if _contains_any(text, ["考试", "作业", "教室", "老师", "exam", "assignment", "classroom", "teacher", "homework"]):
|
| 618 |
+
return "school_pressure"
|
| 619 |
+
if _contains_any(text, ["路牌", "两条路", "大雾", "找不到路", "sign", "two roads", "fog", "lost road"]):
|
| 620 |
+
return "road_choice"
|
| 621 |
+
return "open"
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
def _join_anchors(anchors: List[str], language: str = "en", limit: int = 3) -> str:
|
| 625 |
+
visible = [anchor for anchor in anchors[:limit] if anchor]
|
| 626 |
+
if not visible:
|
| 627 |
+
return "梦境片段" if _is_zh(language) else "dream fragments"
|
| 628 |
+
if _is_zh(language):
|
| 629 |
+
return "、".join(visible)
|
| 630 |
+
if len(visible) == 1:
|
| 631 |
+
return visible[0]
|
| 632 |
+
return ", ".join(visible[:-1]) + f", and {visible[-1]}"
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str = "en", answers: str = "") -> str:
|
| 636 |
+
theme = _dream_theme(intake, answers)
|
| 637 |
+
if _is_zh(language):
|
| 638 |
+
themed = {
|
| 639 |
+
"lost_home": "地铁站里迷路的小孩和回家的方向",
|
| 640 |
+
"dark_water": "夜晚的海、海浪和月牙下的小人",
|
| 641 |
+
"stuck_elevator": "电梯、融化的按钮和数字 14",
|
| 642 |
+
"library_signal": "旧图书馆、红色楼梯和那张便签",
|
| 643 |
+
"message_loss": "那条消息、前任和突然消失",
|
| 644 |
+
"chased": "森林、白色猫和空白路牌",
|
| 645 |
+
"school_pressure": "教室、作业和来不及交上的感觉",
|
| 646 |
+
"road_choice": "雾里的路牌和两条路",
|
| 647 |
+
}
|
| 648 |
+
else:
|
| 649 |
+
themed = {
|
| 650 |
+
"lost_home": "the lost child, the subway, and the way home",
|
| 651 |
+
"dark_water": "the dark water, the waves, and the small figure under the moon",
|
| 652 |
+
"stuck_elevator": "the elevator, the melted button, and floor 14",
|
| 653 |
+
"library_signal": "the old library, the red staircase, and the note",
|
| 654 |
+
"message_loss": "the message, the former partner, and the disappearance",
|
| 655 |
+
"chased": "the chase, the call for help, and the missing route",
|
| 656 |
+
"school_pressure": "the classroom, the assignment, and the late feeling",
|
| 657 |
+
"road_choice": "the foggy sign and the two roads",
|
| 658 |
+
}
|
| 659 |
+
if theme == "chased" and anchors:
|
| 660 |
+
return _join_anchors(anchors, language)
|
| 661 |
+
return themed.get(theme) or _join_anchors(anchors, language)
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
def _answer_snippet(answers: str, language: str = "en") -> str:
|
| 665 |
+
lines = [
|
| 666 |
+
line.strip()
|
| 667 |
+
for line in (answers or "").splitlines()
|
| 668 |
+
if line.strip() and not _is_skip_answer(line, language)
|
| 669 |
+
]
|
| 670 |
+
if not lines:
|
| 671 |
+
return ""
|
| 672 |
+
clean = re.sub(r"\s+", " ", lines[-1]).strip(" ::「」\"'()[]{}")
|
| 673 |
+
if _is_zh(language):
|
| 674 |
+
return clean[:34].rstrip(",。;、 ")
|
| 675 |
+
words = clean.split()
|
| 676 |
+
return " ".join(words[:14])
|
| 677 |
+
|
| 678 |
+
|
| 679 |
def _anchor_with_article(anchor: str) -> str:
|
| 680 |
clean = (anchor or "").strip()
|
| 681 |
if clean.lower().startswith(("the ", "a ", "an ")):
|
|
|
|
| 705 |
return f"你梦见{clean}"
|
| 706 |
|
| 707 |
|
| 708 |
+
def _user_supplied_text(intake: DreamIntake, answers: str = "", include_mood: bool = False) -> str:
|
| 709 |
+
parts = [
|
| 710 |
+
intake.dream_text,
|
| 711 |
+
intake.voice_transcript,
|
| 712 |
+
intake.main_question,
|
| 713 |
+
intake.uncertainty,
|
| 714 |
+
intake.user_context,
|
| 715 |
+
answers or "",
|
| 716 |
+
]
|
| 717 |
+
if include_mood:
|
| 718 |
+
parts.append(intake.mood)
|
| 719 |
+
return "\n".join(part.strip() for part in parts if part and part.strip())
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def _question_sentence_candidates(text: str) -> List[str]:
|
| 723 |
+
pieces = re.split(r"[。!?!?\n\r]+|(?<=\.)\s+|[,,;;]", text or "")
|
| 724 |
+
return [piece.strip(" ::「」\"'()[]{}") for piece in pieces if piece.strip()]
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
def _clean_user_question(text: str, language: str = "en") -> str:
|
| 728 |
+
clean = re.sub(r"\s+", " ", (text or "").strip(" ::「」\"'()[]{}"))
|
| 729 |
+
if not clean:
|
| 730 |
+
return ""
|
| 731 |
+
if _is_zh(language):
|
| 732 |
+
clean = re.sub(r"^(我)?(醒来后)?(最)?(想知道|想问|在想|担心|害怕)[::,,\s]*", "", clean)
|
| 733 |
+
clean = re.sub(r"^(只)?想知道[::,,\s]*", "", clean)
|
| 734 |
+
clean = clean.strip(" ::「」\"'()[]{}")
|
| 735 |
+
return clean if clean.endswith(("?", "?")) else f"{clean}?"
|
| 736 |
+
clean = re.sub(
|
| 737 |
+
r"^(i\s+)?(woke\s+up\s+)?(want\s+to\s+know|wonder|am\s+wondering|need\s+to\s+know|worry|fear)\s*(if|whether|why|what|how)?\s*",
|
| 738 |
+
lambda match: (match.group(4) or "").strip() + " ",
|
| 739 |
+
clean,
|
| 740 |
+
flags=re.IGNORECASE,
|
| 741 |
+
).strip()
|
| 742 |
+
clean = clean[0].upper() + clean[1:] if clean else ""
|
| 743 |
+
if clean and not clean.endswith("?") and re.match(r"^(why|what|how|does|do|did|is|am|are|can|could|should|would)\b", clean, re.IGNORECASE):
|
| 744 |
+
clean += "?"
|
| 745 |
+
return clean
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
def _extract_explicit_user_question(intake: DreamIntake, answers: str = "", language: str = "en") -> str:
|
| 749 |
+
if intake.main_question.strip():
|
| 750 |
+
return _clean_user_question(intake.main_question, language)
|
| 751 |
+
text = _user_supplied_text(intake, answers)
|
| 752 |
+
if _is_zh(language):
|
| 753 |
+
markers = (
|
| 754 |
+
"为什么",
|
| 755 |
+
"是不是",
|
| 756 |
+
"会不会",
|
| 757 |
+
"怎么办",
|
| 758 |
+
"我该",
|
| 759 |
+
"能不能",
|
| 760 |
+
"代表",
|
| 761 |
+
"说明",
|
| 762 |
+
"预兆",
|
| 763 |
+
"征兆",
|
| 764 |
+
"想知道",
|
| 765 |
+
"想问",
|
| 766 |
+
"撑不住",
|
| 767 |
+
"走出来",
|
| 768 |
+
"这么难受",
|
| 769 |
+
)
|
| 770 |
+
else:
|
| 771 |
+
markers = (
|
| 772 |
+
"why",
|
| 773 |
+
"what should",
|
| 774 |
+
"how should",
|
| 775 |
+
"does this mean",
|
| 776 |
+
"do i",
|
| 777 |
+
"am i",
|
| 778 |
+
"is this",
|
| 779 |
+
"could this",
|
| 780 |
+
"can this",
|
| 781 |
+
"should i",
|
| 782 |
+
"i wonder",
|
| 783 |
+
"want to know",
|
| 784 |
+
"not coping",
|
| 785 |
+
"cope",
|
| 786 |
+
"a sign",
|
| 787 |
+
"omen",
|
| 788 |
+
)
|
| 789 |
+
for sentence in _question_sentence_candidates(text):
|
| 790 |
+
lowered = sentence.lower()
|
| 791 |
+
if _is_skip_answer(sentence, language):
|
| 792 |
+
continue
|
| 793 |
+
if any(marker in lowered for marker in markers):
|
| 794 |
+
return _clean_user_question(sentence, language)
|
| 795 |
+
return ""
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def _emotion_labels_from_text(text: str, language: str = "en") -> List[str]:
|
| 799 |
+
lowered = (text or "").lower()
|
| 800 |
+
if _is_zh(language):
|
| 801 |
+
groups = [
|
| 802 |
+
("害怕", ["害怕", "吓", "恐惧", "心慌", "心跳很快", "惊醒"]),
|
| 803 |
+
("压力", ["压力", "撑不住", "扛不住", "崩溃", "压垮", "焦虑", "太累", "疲惫"]),
|
| 804 |
+
("难过", ["难过", "难受", "伤心", "失落", "想哭", "委屈", "孤独"]),
|
| 805 |
+
("自责", ["自责", "内疚", "愧疚", "后悔", "责怪自己"]),
|
| 806 |
+
("需要安慰", ["安慰", "被安慰", "抱抱", "关心", "陪陪", "鸡汤"]),
|
| 807 |
+
]
|
| 808 |
+
else:
|
| 809 |
+
groups = [
|
| 810 |
+
("fear", ["scared", "afraid", "terrified", "panic", "panicked", "frightened"]),
|
| 811 |
+
("pressure", ["overwhelmed", "not coping", "can't cope", "cannot cope", "burned out", "too much", "anxious"]),
|
| 812 |
+
("sadness", ["sad", "grief", "hurt", "lonely", "heartbroken", "upset"]),
|
| 813 |
+
("guilt", ["guilty", "ashamed", "blame myself", "regret"]),
|
| 814 |
+
("comfort", ["comfort", "reassurance", "not productivity", "not advice", "not a pep talk"]),
|
| 815 |
+
]
|
| 816 |
+
labels: List[str] = []
|
| 817 |
+
for label, terms in groups:
|
| 818 |
+
if any(term in lowered for term in terms):
|
| 819 |
+
labels.append(label)
|
| 820 |
+
return _dedupe_preserve_order(labels)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
def _emotion_phrase(labels: List[str], language: str = "en") -> str:
|
| 824 |
+
if not labels:
|
| 825 |
+
return "这个感受" if _is_zh(language) else "this feeling"
|
| 826 |
+
if _is_zh(language):
|
| 827 |
+
return "、".join(labels)
|
| 828 |
+
if len(labels) == 1:
|
| 829 |
+
return labels[0]
|
| 830 |
+
return ", ".join(labels[:-1]) + f", and {labels[-1]}"
|
| 831 |
+
|
| 832 |
+
|
| 833 |
+
def _needs_comfort(text: str, language: str = "en") -> bool:
|
| 834 |
+
lowered = (text or "").lower()
|
| 835 |
+
if _is_zh(language):
|
| 836 |
+
return any(term in lowered for term in ["安慰", "被安慰", "抱抱", "关心", "别催", "不是鸡汤"])
|
| 837 |
+
return any(term in lowered for term in ["comfort", "reassurance", "not productivity", "not a pep talk", "not advice"])
|
| 838 |
+
|
| 839 |
+
|
| 840 |
+
def _should_use_emotion_led_response(intake: DreamIntake, answers: str, language: str = "en") -> bool:
|
| 841 |
+
user_text = _user_supplied_text(intake, answers)
|
| 842 |
+
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 843 |
+
labels = _emotion_labels_from_text(user_text, language)
|
| 844 |
+
if _is_low_context_intake(intake) and _is_skip_answer(answers, language) and not explicit_question and not _needs_comfort(user_text, language):
|
| 845 |
+
return False
|
| 846 |
+
return bool(explicit_question or labels or _needs_comfort(user_text, language))
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
def _direct_question_reassurance(question: str, labels: List[str], language: str = "en") -> str:
|
| 850 |
+
lowered = (question or "").lower()
|
| 851 |
+
if _is_zh(language):
|
| 852 |
+
if any(term in lowered for term in ["撑不住", "扛不住", "崩溃"]):
|
| 853 |
+
return "它不等于你一定撑不住了;更像是在提醒你,最近的压力已经值得被认真照顾。"
|
| 854 |
+
if "走出来" in lowered:
|
| 855 |
+
return "它不急着证明你有没有走出来;更像是在说明这段难受还需要一点被看见的时间。"
|
| 856 |
+
if any(term in lowered for term in ["预兆", "征兆", "坏事", "会不会发生"]):
|
| 857 |
+
return "它不适合被当作预兆;我们先把它当作醒来后仍在身体里的担心来照顾。"
|
| 858 |
+
if any(term in lowered for term in ["我是不是", "是不是我"]):
|
| 859 |
+
return "它不是给你贴标签的证据;它更像是在把一个需要被接住的感受放到你面前。"
|
| 860 |
+
if "为什么" in lowered or "这么难受" in lowered:
|
| 861 |
+
return "这么难受不是你反应过度;梦可能只是把还没放下的感受放大给你看。"
|
| 862 |
+
emotion = _emotion_phrase(labels, language)
|
| 863 |
+
return f"你提到的{emotion}不是小事,它值得先被接住,而不是立刻被解释掉。"
|
| 864 |
+
if any(term in lowered for term in ["not coping", "cope", "coping", "falling apart"]):
|
| 865 |
+
return "This does not prove you are failing to cope; it may be showing that your pressure deserves care before answers."
|
| 866 |
+
if "sign" in lowered or "omen" in lowered or "bad will happen" in lowered:
|
| 867 |
+
return "This is safer to treat as a fear to care for, not as evidence that something bad will happen."
|
| 868 |
+
if "am i" in lowered or "does this mean i" in lowered:
|
| 869 |
+
return "This dream is not evidence for a label about you; it is one feeling asking to be met gently."
|
| 870 |
+
if "why" in lowered:
|
| 871 |
+
return "The ache makes sense; the dream may be enlarging a feeling that has not had enough room yet."
|
| 872 |
+
emotion = _emotion_phrase(labels, language)
|
| 873 |
+
return f"The {emotion} you named deserves to be met first, not explained away too quickly."
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
def _emotion_led_interpretation(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 877 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 878 |
+
return ""
|
| 879 |
+
question = _extract_explicit_user_question(intake, answers, language)
|
| 880 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
|
| 881 |
+
emotion = _emotion_phrase(labels, language)
|
| 882 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 883 |
+
direct = _direct_question_reassurance(question, labels, language)
|
| 884 |
+
if _is_zh(language):
|
| 885 |
+
opener = f"你问的是「{question}」。" if question else ""
|
| 886 |
+
return (
|
| 887 |
+
f"{opener}{direct} 第一层,先承认醒来后的{emotion}是真实的,不需要被责怪。"
|
| 888 |
+
f"第二层,梦里的「{anchor}」也许把这种感受变成了一个可以看的画面。"
|
| 889 |
+
"第三层,今天不急着找唯一答案,先给自己一个能站稳的小支点。"
|
| 890 |
+
)
|
| 891 |
+
opener = f"You asked, \"{question}\" " if question else ""
|
| 892 |
+
return (
|
| 893 |
+
f"{opener}{direct} First, let the {emotion} be real without blaming yourself. "
|
| 894 |
+
f"Second, the {_anchor_with_article(anchor)} may be the dream's concrete shape for that feeling. "
|
| 895 |
+
"Third, for today the goal is not to solve the whole dream, but to give yourself one steadier place to stand."
|
| 896 |
+
)
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 900 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 901 |
+
return ""
|
| 902 |
+
if _has_prophecy_frame(_user_supplied_text(intake, answers)):
|
| 903 |
+
return ""
|
| 904 |
+
theme = _dream_theme(intake, answers)
|
| 905 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 906 |
+
answer_snippet = _answer_snippet(answers, language)
|
| 907 |
+
if _is_zh(language):
|
| 908 |
+
if theme == "lost_home":
|
| 909 |
+
extra = f"如果刚才的回答里「{answer_snippet}」最重,就从它开始。" if answer_snippet else ""
|
| 910 |
+
return (
|
| 911 |
+
f"今天先顺着「{anchor}」照顾那种需要被带路的感觉,而不是急着解释梦的含义。"
|
| 912 |
+
f"{extra}给今天补一个很小的“路标”:问一个人、查一个入口,或写下下一站在哪里。"
|
| 913 |
+
)
|
| 914 |
+
if theme == "dark_water":
|
| 915 |
+
return (
|
| 916 |
+
f"今天先把「{anchor}」当成醒来后还在身体里的浪,不当成危险证明。"
|
| 917 |
+
"先选一个让自己回到岸边的动作:开灯、洗脸、发一句“我醒来有点慌,先缓一下”。"
|
| 918 |
+
)
|
| 919 |
+
if theme == "stuck_elevator":
|
| 920 |
+
return (
|
| 921 |
+
f"今天不要逼自己抵达所有楼层;先把「{anchor}」变成一个只按一次的按钮。"
|
| 922 |
+
"只确认当前最卡的一层是什么,再允许自己停在那里做一个很小动作。"
|
| 923 |
+
)
|
| 924 |
+
if theme == "library_signal":
|
| 925 |
+
return (
|
| 926 |
+
f"今天把「{anchor}」当成一张提醒你回到安定处的便签。"
|
| 927 |
+
"不用立刻整理完整答案,只挑一个让你有“回家感”的人、物或角落,靠近它五分钟。"
|
| 928 |
+
)
|
| 929 |
+
if theme == "message_loss":
|
| 930 |
+
return (
|
| 931 |
+
f"今天先把「{anchor}」看成一段还没被好好收起的联系。"
|
| 932 |
+
"不用真的发出去,先写一封不发送的回信,让那句没说完的话有地方停靠。"
|
| 933 |
+
)
|
| 934 |
+
return (
|
| 935 |
+
f"今天的小建议是:先别把「{anchor}」解释成结论;把它当成梦递过来的线索,"
|
| 936 |
+
"问问它在替你保护哪一种感受,再给那种感受一个很小的照顾动作。"
|
| 937 |
+
)
|
| 938 |
+
if theme == "lost_home":
|
| 939 |
+
extra = f" If your answer points to \"{answer_snippet}\", start there." if answer_snippet else ""
|
| 940 |
+
return (
|
| 941 |
+
f"For today, treat {anchor} as a need for guidance, not as a verdict about you.{extra} "
|
| 942 |
+
"Add one tiny wayfinding marker: ask one person, check one entrance, or name the next stop."
|
| 943 |
+
)
|
| 944 |
+
if theme == "dark_water":
|
| 945 |
+
return (
|
| 946 |
+
f"For today, treat {anchor} as a wave still in your body, not proof of danger. "
|
| 947 |
+
"Choose one shore-like action: turn on a light, wash your face, or tell someone you woke up unsettled."
|
| 948 |
+
)
|
| 949 |
+
if theme == "stuck_elevator":
|
| 950 |
+
return (
|
| 951 |
+
f"For today, do not make yourself reach every floor. Let {anchor} become one button: "
|
| 952 |
+
"name the stuck floor, then do only one small action there."
|
| 953 |
+
)
|
| 954 |
+
if theme == "library_signal":
|
| 955 |
+
return (
|
| 956 |
+
f"For today, treat {anchor} like a note pointing you back to steadiness. "
|
| 957 |
+
"Pick one person, place, or object that feels like home and spend five minutes near it."
|
| 958 |
+
)
|
| 959 |
+
if theme == "message_loss":
|
| 960 |
+
return (
|
| 961 |
+
f"For today, treat {anchor} as an unfinished contact, not a demand to reopen everything. "
|
| 962 |
+
"Write one unsent reply so the unsaid sentence has somewhere to rest."
|
| 963 |
+
)
|
| 964 |
+
return (
|
| 965 |
+
f"For today, do not turn {anchor} into a fixed conclusion. Treat it as a clue about one feeling "
|
| 966 |
+
"that needs care, then give that feeling one small caring action."
|
| 967 |
+
)
|
| 968 |
+
|
| 969 |
+
|
| 970 |
+
def _emotion_led_tiny_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 971 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 972 |
+
return ""
|
| 973 |
+
theme = _dream_theme(intake, answers)
|
| 974 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 975 |
+
if _is_zh(language):
|
| 976 |
+
if theme == "lost_home":
|
| 977 |
+
return f"用 5 分钟给「{anchor}」画一张三格小路线:我在找什么、我能问谁或查哪里、今天只走到哪一站。"
|
| 978 |
+
if theme == "dark_water":
|
| 979 |
+
return f"用 5 分钟做一个“上岸”动作:看着「{anchor}」,说出三个房间里的真实物件,再喝一口水或开一盏灯。"
|
| 980 |
+
if theme == "stuck_elevator":
|
| 981 |
+
return f"用 5 分钟写一个“只按这一层”的按钮:今天卡在哪一层、我能做的最小动作、做完就停。"
|
| 982 |
+
if theme == "library_signal":
|
| 983 |
+
return f"用 5 分钟把「{anchor}」改写成一张现实便签:我现在可以回到哪里,或联系谁,让自己稳一点。"
|
| 984 |
+
if theme == "message_loss":
|
| 985 |
+
return f"用 5 分钟写一封不发送的短信草稿,开头只写:梦里的「{anchor}」让我想承认的是……"
|
| 986 |
+
return f"用 5 分钟做一个三格便签:梦里的「{anchor}」最亮的细节、它带出的感受、今天能照顾它的一小步。"
|
| 987 |
+
if theme == "lost_home":
|
| 988 |
+
return f"Spend five minutes making a three-box route for {anchor}: what I am looking for, who or where I can ask, and the next stop only."
|
| 989 |
+
if theme == "dark_water":
|
| 990 |
+
return f"Spend five minutes doing one shore action for {anchor}: name three real objects in the room, then drink water or turn on a light."
|
| 991 |
+
if theme == "stuck_elevator":
|
| 992 |
+
return f"Spend five minutes making a one-floor button for {anchor}: the stuck floor, the smallest action, and permission to stop after it."
|
| 993 |
+
if theme == "library_signal":
|
| 994 |
+
return f"Spend five minutes turning {anchor} into a real note: where can I return, or who can I contact, to feel steadier?"
|
| 995 |
+
if theme == "message_loss":
|
| 996 |
+
return f"Spend five minutes drafting one unsent message that begins: what {anchor} makes me want to admit is..."
|
| 997 |
+
return f"Spend five minutes making three boxes for {anchor}: brightest detail, feeling it carries, and one small care step for today."
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str = "en") -> str:
|
| 1001 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 1002 |
+
return ""
|
| 1003 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
|
| 1004 |
+
emotion = _emotion_phrase(labels, language)
|
| 1005 |
+
if _is_zh(language):
|
| 1006 |
+
return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
|
| 1007 |
+
return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
|
| 1008 |
+
|
| 1009 |
+
|
| 1010 |
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 1011 |
if intake.main_question.strip():
|
| 1012 |
+
return _clean_user_question(intake.main_question, language)
|
| 1013 |
+
explicit_question = _extract_explicit_user_question(intake, "", language)
|
| 1014 |
+
if explicit_question:
|
| 1015 |
+
return explicit_question
|
| 1016 |
task = _task_focus(intake.merged_text(), language)
|
| 1017 |
if task:
|
| 1018 |
if not _is_zh(language):
|
|
|
|
| 1043 |
|
| 1044 |
|
| 1045 |
def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
|
| 1046 |
+
anchors = _anchors_for_language(intake, language)
|
| 1047 |
+
primary = anchors[0] if anchors else _primary_anchor(intake, language)
|
| 1048 |
+
anchor = _story_anchor_phrase(intake, anchors, language)
|
| 1049 |
+
theme = _dream_theme(intake)
|
| 1050 |
if not _is_zh(language):
|
| 1051 |
+
if theme == "lost_home":
|
| 1052 |
+
return f"For today, let {anchor} become a small wayfinding cue: choose one place to ask, one entrance to check, or one next stop to name."
|
| 1053 |
+
if theme == "dark_water":
|
| 1054 |
+
return f"For today, let {anchor} become a shore cue: name what felt too large, then choose one ordinary thing that helps your body feel here again."
|
| 1055 |
+
if theme == "library_signal":
|
| 1056 |
+
return f"For today, let {anchor} become a note back to steadiness: choose one person, place, or object that feels like home and spend five minutes near it."
|
| 1057 |
+
if theme == "message_loss":
|
| 1058 |
+
return f"For today, let {anchor} become one unsent sentence, not a full conversation: write what is still tender and let it rest."
|
| 1059 |
return (
|
| 1060 |
f"For today, borrow one action from {_anchor_with_article(primary)}: open the task, write only the first line, "
|
| 1061 |
"and let that be enough for now."
|
| 1062 |
)
|
| 1063 |
+
if theme == "lost_home":
|
| 1064 |
+
return f"今天先让「{anchor}」变成一个小路标:只确认一个可以问的人、一个可以查的入口,或一个下一站。"
|
| 1065 |
+
if theme == "dark_water":
|
| 1066 |
+
return f"今天先让「{anchor}」变成一个上岸提示:说出那个太大的感受,再选一件能让身体回到当下的小事。"
|
| 1067 |
+
if theme == "library_signal":
|
| 1068 |
+
return f"今天先让「{anchor}」变成一张回到安定处的便签:靠近一个有回家感的人、地方或物件五分钟。"
|
| 1069 |
+
if theme == "message_loss":
|
| 1070 |
+
return f"今天先让「{anchor}」变成一句不发送的话,而不是一整场对话:写下还柔软的那句,然后让它停在那里。"
|
| 1071 |
return f"今天先从「{primary}」借一个动作:只做最小的第一步,不急着把整件事完成。"
|
| 1072 |
|
| 1073 |
|
|
|
|
| 1449 |
return f"如果「{primary}」能递给你一个更小的第一步,今天那一步会是什么?"
|
| 1450 |
|
| 1451 |
|
| 1452 |
+
def _first_input_focus_sentence(intake: DreamIntake, language: str = "en") -> str:
|
| 1453 |
+
explicit_question = _extract_explicit_user_question(intake, "", language)
|
| 1454 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, "", include_mood=True), language)
|
| 1455 |
+
emotion = _emotion_phrase(labels, language)
|
| 1456 |
+
anchors = _anchors_for_language(intake, language)
|
| 1457 |
+
story_anchor = _story_anchor_phrase(intake, anchors, language)
|
| 1458 |
+
if _is_zh(language):
|
| 1459 |
+
if explicit_question and labels:
|
| 1460 |
+
return f"你最在意的是「{explicit_question}」,而且醒来后的{emotion}需要先被接住。"
|
| 1461 |
+
if explicit_question:
|
| 1462 |
+
return f"你最在意的是「{explicit_question}」,不是只想听一个固定象征解释。"
|
| 1463 |
+
if labels:
|
| 1464 |
+
return f"这个梦留下的{emotion}比符号本身更重要,我会先顺着感受看。"
|
| 1465 |
+
return f"核心线索先落在「{story_anchor}」上,我会先确认它和你今天的需要怎么连起来。"
|
| 1466 |
+
if explicit_question and labels:
|
| 1467 |
+
return f"your main question is \"{explicit_question}\", and the {emotion} after waking needs to be met first."
|
| 1468 |
+
if explicit_question:
|
| 1469 |
+
return f"your main question is \"{explicit_question}\", not a generic symbol reading."
|
| 1470 |
+
if labels:
|
| 1471 |
+
return f"the {emotion} after waking matters more than a fixed symbol meaning."
|
| 1472 |
+
return f"the strongest trail begins with {story_anchor}, so I will connect it to what you may need today."
|
| 1473 |
+
|
| 1474 |
+
|
| 1475 |
+
def _decomposition_question(
|
| 1476 |
+
intake: DreamIntake,
|
| 1477 |
+
existing_count: int,
|
| 1478 |
+
answers: str,
|
| 1479 |
+
model_question: str,
|
| 1480 |
+
language: str = "en",
|
| 1481 |
+
) -> str:
|
| 1482 |
+
anchors = _anchors_for_language(intake, language)
|
| 1483 |
+
story_anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 1484 |
+
theme = _dream_theme(intake, answers)
|
| 1485 |
+
declared_question = _question_for_declared_real_task(intake, language) if existing_count == 0 else ""
|
| 1486 |
+
if declared_question:
|
| 1487 |
+
return declared_question
|
| 1488 |
+
if _is_zh(language):
|
| 1489 |
+
stage_questions = {
|
| 1490 |
+
"lost_home": [
|
| 1491 |
+
"梦里那个迷路的小孩,更像是你自己需要被带路,还是某件现实里暂时找不到家的事?",
|
| 1492 |
+
"如果把地铁站或回家方向看成一个场景,最让你心里一紧的是“没人带路”“找不到入口”,还是“怕把谁弄丢”?",
|
| 1493 |
+
"最后只确认今天的需要:你更想被安慰一下,还是想得到一个很小的找路动作?",
|
| 1494 |
+
],
|
| 1495 |
+
"dark_water": [
|
| 1496 |
+
"醒来后最强的是害怕、孤单、喘不过气,还是别的身体感受?",
|
| 1497 |
+
"画面里的海浪、月牙或小人,��一个最像你醒来后还忘不掉的部分?",
|
| 1498 |
+
"最后只确认今天的需要:你更想要安定身体,还是想理解为什么会这么慌?",
|
| 1499 |
+
],
|
| 1500 |
+
"stuck_elevator": [
|
| 1501 |
+
"电梯、按钮或数字 14 里,哪一个最像你最近卡住的感觉?",
|
| 1502 |
+
"这个卡住更像“来不及开始”,还是“已经按了按钮却没有回应”?",
|
| 1503 |
+
"最后只确认今天的需要:你想要一个小行动,还是先要一句能让压力降下来的话?",
|
| 1504 |
+
],
|
| 1505 |
+
"library_signal": [
|
| 1506 |
+
"旧图书馆、楼梯或便签里,哪一个最像梦在提醒你别忽略的线索?",
|
| 1507 |
+
"那张便签或“回家”的感觉,更像想联系某个人,还是想回到一种安定状态?",
|
| 1508 |
+
"最后只确认今天的需要:你想整理一个现实线索,还是先给自己一点回家的感觉?",
|
| 1509 |
+
],
|
| 1510 |
+
"message_loss": [
|
| 1511 |
+
"那条消息最刺痛你的地方,是它出现了、消失了,还是它来自某个具体的人?",
|
| 1512 |
+
"这个梦更像在问“我还在等什么”,还是在问“我该怎么照顾还没放下的部分”?",
|
| 1513 |
+
"最后只确认今天的需要:你想要一句安慰,还是一个不打扰别人的小行动?",
|
| 1514 |
+
],
|
| 1515 |
+
}
|
| 1516 |
+
fallback = [
|
| 1517 |
+
f"在「{story_anchor}」里,醒来后最强烈的感受是什么?",
|
| 1518 |
+
f"如果只选一个细节继续看,你会选「{story_anchor}」里的哪一处?为什么?",
|
| 1519 |
+
"最后只确认今天的需要:你更想被安慰、被提醒,还是得到一个很小的行动?",
|
| 1520 |
+
]
|
| 1521 |
+
else:
|
| 1522 |
+
stage_questions = {
|
| 1523 |
+
"lost_home": [
|
| 1524 |
+
"Does the lost child feel more like a part of you that needs guidance, or like a real situation that has no clear way home yet?",
|
| 1525 |
+
"In the subway and way-home scene, what tightens most: nobody guiding you, no visible entrance, or fear of losing someone?",
|
| 1526 |
+
"Last check before the tip: would comfort help most today, or one tiny way-finding action?",
|
| 1527 |
+
],
|
| 1528 |
+
"dark_water": [
|
| 1529 |
+
"After waking, was the strongest feeling fear, loneliness, breathlessness, or something else in your body?",
|
| 1530 |
+
"Which image stays with you most: the waves, the moon, or the small figure?",
|
| 1531 |
+
"Last check before the tip: do you need help calming your body, or understanding why the image felt so intense?",
|
| 1532 |
+
],
|
| 1533 |
+
"stuck_elevator": [
|
| 1534 |
+
"Which detail feels closest to your current stuck point: the elevator, the melted button, or floor 14?",
|
| 1535 |
+
"Does the stuckness feel more like being late to start, or like pressing the button and getting no response?",
|
| 1536 |
+
"Last check before the tip: would one small action help most, or a sentence that lowers the pressure first?",
|
| 1537 |
+
],
|
| 1538 |
+
}
|
| 1539 |
+
fallback = [
|
| 1540 |
+
f"In {story_anchor}, what feeling stayed strongest after waking?",
|
| 1541 |
+
f"If we keep only one detail from {story_anchor}, which one matters most, and why?",
|
| 1542 |
+
"Last check before the tip: do you want comfort, a reminder, or one very small action?",
|
| 1543 |
+
]
|
| 1544 |
+
candidates = stage_questions.get(theme, fallback)
|
| 1545 |
+
index = min(max(existing_count, 0), MAX_DREAM_DECOMPOSITION_QUESTIONS - 1)
|
| 1546 |
+
question = candidates[index] if index < len(candidates) else model_question
|
| 1547 |
+
if question:
|
| 1548 |
+
return question
|
| 1549 |
+
return model_question or _grounded_followup_question(intake, language)
|
| 1550 |
+
|
| 1551 |
+
|
| 1552 |
+
def _compose_decomposition_response(
|
| 1553 |
+
intake: DreamIntake,
|
| 1554 |
+
existing_count: int,
|
| 1555 |
+
question: str,
|
| 1556 |
+
answers: str,
|
| 1557 |
+
language: str = "en",
|
| 1558 |
+
) -> str:
|
| 1559 |
+
anchors = _anchors_for_language(intake, language)
|
| 1560 |
+
story_anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 1561 |
+
answer_snippet = _answer_snippet(answers, language)
|
| 1562 |
+
if _is_zh(language):
|
| 1563 |
+
if existing_count == 0:
|
| 1564 |
+
focus = _first_input_focus_sentence(intake, language)
|
| 1565 |
+
return (
|
| 1566 |
+
f"我先听到的是:{focus}\n\n"
|
| 1567 |
+
f"我会把这个梦拆成三层来看:醒来后的感受、梦里最抓人的画面({story_anchor})、"
|
| 1568 |
+
"以及它今天想帮你照顾的需要。\n\n"
|
| 1569 |
+
f"先问一个会影响最后建议的问题:{question}"
|
| 1570 |
+
)
|
| 1571 |
+
if existing_count == 1:
|
| 1572 |
+
prefix = f"我把你的回答也放进来了:{answer_snippet}。" if answer_snippet else "我再补一层具体画面。"
|
| 1573 |
+
return f"{prefix} 这一轮只看「{story_anchor}」里最关键的连接:{question}"
|
| 1574 |
+
return f"最后只确认一个方向,之后我就生成今日小 Tips:{question}"
|
| 1575 |
+
if existing_count == 0:
|
| 1576 |
+
focus = _first_input_focus_sentence(intake, language)
|
| 1577 |
+
return (
|
| 1578 |
+
f"What I hear first is that {focus}\n\n"
|
| 1579 |
+
f"I will unpack this in three layers: the feeling after waking, the concrete image ({story_anchor}), "
|
| 1580 |
+
f"and what it may be asking you to care for today.\n\n"
|
| 1581 |
+
f"One question that will shape the final tip: {question}"
|
| 1582 |
+
)
|
| 1583 |
+
if existing_count == 1:
|
| 1584 |
+
prefix = f"I am adding your answer: {answer_snippet}." if answer_snippet else "I will add one more concrete layer."
|
| 1585 |
+
return f"{prefix} For this round, I am looking at the key link in {story_anchor}: {question}"
|
| 1586 |
+
return f"One last direction check, then I will write the Today Tip: {question}"
|
| 1587 |
+
|
| 1588 |
+
|
| 1589 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 1590 |
polished = card.model_copy(deep=True)
|
| 1591 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
|
|
|
| 1712 |
setattr(polished, field, _clean_placeholder_phrase(getattr(polished, field)))
|
| 1713 |
if not polished.dream_summary.strip() or _is_placeholder_anchor(polished.dream_summary) or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 1714 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 1715 |
+
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 1716 |
+
if explicit_question:
|
| 1717 |
+
polished.main_question = explicit_question
|
| 1718 |
+
elif (
|
| 1719 |
not polished.main_question.strip()
|
| 1720 |
or _is_placeholder_anchor(polished.main_question)
|
| 1721 |
or not _text_uses_anchor(polished.main_question, anchors)
|
| 1722 |
):
|
| 1723 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 1724 |
+
emotion_interpretation = _emotion_led_interpretation(intake, answers, anchors, language)
|
| 1725 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 1726 |
+
if emotion_interpretation:
|
| 1727 |
+
polished.interpretation = emotion_interpretation
|
| 1728 |
+
elif answer_interpretation:
|
| 1729 |
polished.interpretation = answer_interpretation
|
| 1730 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1731 |
anchor = _answer_bridge_anchor(anchors)
|
|
|
|
| 1744 |
elif not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
|
| 1745 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 1746 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 1747 |
+
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 1748 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 1749 |
+
if emotion_tip:
|
| 1750 |
+
polished.today_tip = emotion_tip
|
| 1751 |
+
elif answer_tip:
|
| 1752 |
polished.today_tip = answer_tip
|
| 1753 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1754 |
anchor = anchors[0]
|
|
|
|
| 1772 |
):
|
| 1773 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 1774 |
hard_action_markers = ["address it immediately", "fix it immediately", "solve it immediately"]
|
| 1775 |
+
emotion_action = _emotion_led_tiny_action(intake, answers, anchors, language)
|
| 1776 |
answer_action = _answer_based_tiny_action(answers, language)
|
| 1777 |
+
if emotion_action:
|
| 1778 |
+
polished.tiny_action = emotion_action
|
| 1779 |
+
elif answer_action:
|
| 1780 |
polished.tiny_action = answer_action
|
| 1781 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1782 |
polished.tiny_action = (
|
|
|
|
| 1796 |
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 1797 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 1798 |
):
|
| 1799 |
+
anchor_phrase = _story_anchor_phrase(intake, anchors, language, answers)
|
| 1800 |
+
theme = _dream_theme(intake, answers)
|
| 1801 |
if _is_zh(language):
|
| 1802 |
+
if theme == "lost_home":
|
| 1803 |
+
polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」补一张小路标:我在找什么、能问哪里、下一站是什么。"
|
| 1804 |
+
elif theme == "dark_water":
|
| 1805 |
+
polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」找一个上岸动作:看见三个真实物件,再做一件让身体安定的小事。"
|
| 1806 |
+
elif theme == "library_signal":
|
| 1807 |
+
polished.tiny_action = f"用 5 分钟把「{anchor_phrase}」变成现实便签:今天我可以靠近哪一个安定来源?"
|
| 1808 |
+
elif theme == "message_loss":
|
| 1809 |
+
polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」写一句不发送的话,然后把手机放下。"
|
| 1810 |
+
else:
|
| 1811 |
+
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 1812 |
else:
|
| 1813 |
+
if theme == "lost_home":
|
| 1814 |
+
polished.tiny_action = f"Spend five minutes adding a small route marker for {anchor_phrase}: what I seek, where I can ask, and the next stop."
|
| 1815 |
+
elif theme == "dark_water":
|
| 1816 |
+
polished.tiny_action = f"Spend five minutes finding a shore action for {anchor_phrase}: name three real objects, then do one grounding thing."
|
| 1817 |
+
elif theme == "library_signal":
|
| 1818 |
+
polished.tiny_action = f"Spend five minutes turning {anchor_phrase} into a real note: which steady source can I move closer to today?"
|
| 1819 |
+
elif theme == "message_loss":
|
| 1820 |
+
polished.tiny_action = f"Spend five minutes writing one unsent sentence for {anchor_phrase}, then put the phone down."
|
| 1821 |
+
else:
|
| 1822 |
+
polished.tiny_action = f"Spend five minutes writing the first small step connected to the {anchors[0]}."
|
| 1823 |
+
emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
|
| 1824 |
+
if emotion_caring_note:
|
| 1825 |
+
polished.caring_note = emotion_caring_note
|
| 1826 |
+
elif not polished.caring_note.strip():
|
| 1827 |
polished.caring_note = (
|
| 1828 |
"你不需要一醒来就解决整个梦,先把一个细节照亮就很好。"
|
| 1829 |
if _is_zh(language)
|
|
|
|
| 2167 |
)
|
| 2168 |
return next_session
|
| 2169 |
|
| 2170 |
+
existing_count = len(next_session.question_history)
|
| 2171 |
+
if existing_count >= MAX_DREAM_DECOMPOSITION_QUESTIONS:
|
| 2172 |
+
next_session.phase = "ask"
|
| 2173 |
+
next_session.qa_state = build_qa_state(
|
| 2174 |
+
next_session.intake,
|
| 2175 |
+
questions=next_session.question_history,
|
| 2176 |
+
answers=next_session.answer_history,
|
| 2177 |
+
language=language,
|
| 2178 |
+
)
|
| 2179 |
+
next_session.events.append(
|
| 2180 |
+
_event(
|
| 2181 |
+
"assistant",
|
| 2182 |
+
"线索已经足够" if _is_zh(language) else "Enough context gathered",
|
| 2183 |
+
(
|
| 2184 |
+
"我已经问到 3 个核心问题了;现在更适合生成今日小 Tips,而不是继续追问。"
|
| 2185 |
+
if _is_zh(language)
|
| 2186 |
+
else "I have asked three core questions; it is better to generate the Today Tip than keep questioning."
|
| 2187 |
+
),
|
| 2188 |
+
status="ready",
|
| 2189 |
+
)
|
| 2190 |
+
)
|
| 2191 |
+
return next_session
|
| 2192 |
+
|
| 2193 |
prompt = (
|
| 2194 |
followup_question_prompt(next_session.intake, next_session.question_history, next_session.answer_history, language)
|
| 2195 |
if force_another
|
|
|
|
| 2221 |
deduped_questions = [_grounded_followup_question(next_session.intake, language)]
|
| 2222 |
fresh_questions = deduped_questions
|
| 2223 |
|
| 2224 |
+
model_question = fresh_questions[0] if fresh_questions else _grounded_followup_question(next_session.intake, language)
|
| 2225 |
+
focused_question = _decomposition_question(
|
| 2226 |
+
next_session.intake,
|
| 2227 |
+
existing_count,
|
| 2228 |
+
next_session.answers_text(),
|
| 2229 |
+
model_question,
|
| 2230 |
+
language,
|
| 2231 |
+
)
|
| 2232 |
+
visible_question = _compose_decomposition_response(
|
| 2233 |
+
next_session.intake,
|
| 2234 |
+
existing_count,
|
| 2235 |
+
focused_question,
|
| 2236 |
+
next_session.answers_text(),
|
| 2237 |
+
language,
|
| 2238 |
+
)
|
| 2239 |
+
if visible_question not in next_session.question_history:
|
| 2240 |
+
next_session.question_history.append(visible_question)
|
| 2241 |
next_session.phase = "ask"
|
| 2242 |
next_session.qa_state = build_qa_state(
|
| 2243 |
next_session.intake,
|
|
|
|
| 2249 |
next_session.events.append(
|
| 2250 |
_event(
|
| 2251 |
"assistant",
|
| 2252 |
+
"梦境助手理解与追问" if _is_zh(language) and existing_count == 0 else (
|
| 2253 |
+
"梦境助手追问" if _is_zh(language) else (
|
| 2254 |
+
"Dream QA understanding and question" if existing_count == 0 else "Dream QA question"
|
| 2255 |
+
)
|
| 2256 |
+
),
|
| 2257 |
+
visible_question,
|
| 2258 |
meta=str(negotiation.get("visitor_name", "")),
|
| 2259 |
status="question",
|
| 2260 |
)
|
dream_customs/prompts.py
CHANGED
|
@@ -51,12 +51,21 @@ def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
|
|
| 51 |
return f"""
|
| 52 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 53 |
Write a non-diagnostic interpretation draft and exactly one primary Today Tip / 今日小 Tips.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
|
| 55 |
-
The today_tip must cite at least one concrete dream anchor
|
|
|
|
|
|
|
| 56 |
Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
|
| 57 |
-
Keep the whole result short, warm, and specific to the user's answer.
|
| 58 |
-
The tiny_action must be a 5-minute
|
| 59 |
Avoid demanding phrases such as "immediately", "must", "fix it", or "solve it".
|
|
|
|
| 60 |
Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
|
| 61 |
{_language_instruction(language)}
|
| 62 |
|
|
@@ -169,6 +178,8 @@ def negotiation_prompt(intake: DreamIntake, language: str = "en") -> str:
|
|
| 169 |
return f"""
|
| 170 |
You are the Dream QA question guide. The user is not asking for diagnosis.
|
| 171 |
Help the user record the dream, clarify the main question, and answer one gentle follow-up before the final 今日小 Tips.
|
|
|
|
|
|
|
| 172 |
The tone should be warm, plain, and specific. Do not make medical claims.
|
| 173 |
Ask questions that an ordinary person can understand without knowing any app lore.
|
| 174 |
Prefer questions about the strongest feeling, one confusing scene, or one safe next-day reference.
|
|
@@ -195,6 +206,8 @@ def followup_question_prompt(
|
|
| 195 |
return f"""
|
| 196 |
You are the Dream QA question guide. Ask one more gentle follow-up question.
|
| 197 |
Do not diagnose. Do not repeat previous questions.
|
|
|
|
|
|
|
| 198 |
The question must be plain and useful: ask what the user wants to understand, what feeling was strongest,
|
| 199 |
or whether one concrete dream detail connects to today.
|
| 200 |
Do not use unclear metaphors about fate, symbols, hidden meanings, stamps, release, or permits.
|
|
|
|
| 51 |
return f"""
|
| 52 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 53 |
Write a non-diagnostic interpretation draft and exactly one primary Today Tip / 今日小 Tips.
|
| 54 |
+
First answer the user's stated question directly. If the user sounds scared, sad,
|
| 55 |
+
overwhelmed, guilty, lonely, or asks for comfort, follow that emotion before giving any action.
|
| 56 |
+
The interpretation must be step-by-step: use 2 to 4 short layers that move from
|
| 57 |
+
the user's feeling, to concrete dream anchors, to the 1-3 follow-up answers, to one gentle way to care for today.
|
| 58 |
+
Do not collapse every dream into productivity advice such as opening a task,
|
| 59 |
+
writing a first line, or making the first step smaller.
|
| 60 |
Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
|
| 61 |
+
The today_tip and tiny_action must cite at least one concrete dream anchor and must change with the user's story,
|
| 62 |
+
visual evidence, and follow-up answers. Do not reuse stock exercises such as "write two lines" unless the user's
|
| 63 |
+
own dream or answer makes that exact action feel specific.
|
| 64 |
Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
|
| 65 |
+
Keep the whole result short, warm, emotionally responsive, and specific to the user's answer.
|
| 66 |
+
The tiny_action must be a 5-minute self-check or small care step, not a command to solve the whole problem.
|
| 67 |
Avoid demanding phrases such as "immediately", "must", "fix it", or "solve it".
|
| 68 |
+
If the user asks for comfort, caring_note should be warm, specific, and validating.
|
| 69 |
Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
|
| 70 |
{_language_instruction(language)}
|
| 71 |
|
|
|
|
| 178 |
return f"""
|
| 179 |
You are the Dream QA question guide. The user is not asking for diagnosis.
|
| 180 |
Help the user record the dream, clarify the main question, and answer one gentle follow-up before the final 今日小 Tips.
|
| 181 |
+
Use a gentle grill-me style: first reflect what you understood, then ask the single question that would most improve
|
| 182 |
+
the final advice. Dream QA can ask at most 3 decomposition questions total, so every question must earn its place.
|
| 183 |
The tone should be warm, plain, and specific. Do not make medical claims.
|
| 184 |
Ask questions that an ordinary person can understand without knowing any app lore.
|
| 185 |
Prefer questions about the strongest feeling, one confusing scene, or one safe next-day reference.
|
|
|
|
| 206 |
return f"""
|
| 207 |
You are the Dream QA question guide. Ask one more gentle follow-up question.
|
| 208 |
Do not diagnose. Do not repeat previous questions.
|
| 209 |
+
Treat this as one of at most 3 decomposition rounds. Ask only the missing piece that would make the final Today Tip
|
| 210 |
+
more personal: feeling, concrete image, or what kind of support the user wants today.
|
| 211 |
The question must be plain and useful: ask what the user wants to understand, what feeling was strongest,
|
| 212 |
or whether one concrete dream detail connects to today.
|
| 213 |
Do not use unclear metaphors about fate, symbols, hidden meanings, stamps, release, or permits.
|
tests/test_pipeline.py
CHANGED
|
@@ -178,6 +178,73 @@ def test_generate_today_tip_keeps_answer_history_and_removes_placeholder_text():
|
|
| 178 |
assert "does not indicate any real-life concerns" not in combined
|
| 179 |
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
def test_generate_today_tip_adds_safety_note_for_repeated_insomnia_without_self_harm():
|
| 182 |
intake = build_intake(
|
| 183 |
dream_text="我昨晚反复梦见自己在一条漆黑的走廊里走,醒来后心跳很快,已经连续3晚都睡不好。",
|
|
@@ -346,13 +413,21 @@ def test_ask_answer_skip_draft_revise_and_seal_actions():
|
|
| 346 |
)
|
| 347 |
session = ask_questions(session, FakeTextClient())
|
| 348 |
assert session.phase == "ask"
|
| 349 |
-
assert len(session.question_history) ==
|
|
|
|
| 350 |
|
| 351 |
session = answer_question(session, "I want one small start.")
|
| 352 |
assert session.answer_history[-1] == "I want one small start."
|
| 353 |
|
| 354 |
session = ask_questions(session, FakeTextClient(), force_another=True)
|
| 355 |
-
assert len(session.question_history) ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
session = skip_question(session)
|
| 358 |
assert "skip" in session.answer_history[-1].lower()
|
|
@@ -536,6 +611,89 @@ def test_visual_witness_clues_drive_questions_and_today_tip():
|
|
| 536 |
assert "objects" not in combined
|
| 537 |
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
def test_witness_failure_keeps_text_path_alive():
|
| 540 |
class BrokenWitnessVision:
|
| 541 |
def extract_witness(self, image_path):
|
|
|
|
| 178 |
assert "does not indicate any real-life concerns" not in combined
|
| 179 |
|
| 180 |
|
| 181 |
+
def test_generate_today_tip_follows_user_question_and_comfort_need_in_chinese():
|
| 182 |
+
intake = build_intake(
|
| 183 |
+
dream_text="我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
|
| 184 |
+
mood="害怕",
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
card = generate_today_tip(intake, "我最近工作压力很大,真的很想被安慰一下。", FakeTextClient(), language="zh")
|
| 188 |
+
combined = "\n".join(
|
| 189 |
+
[
|
| 190 |
+
card.main_question,
|
| 191 |
+
card.interpretation,
|
| 192 |
+
card.today_tip,
|
| 193 |
+
card.tiny_action,
|
| 194 |
+
card.caring_note,
|
| 195 |
+
]
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
assert "撑不住" in card.main_question
|
| 199 |
+
assert "海" in combined
|
| 200 |
+
assert "压力" in combined or "害怕" in combined
|
| 201 |
+
assert "第一层" in card.interpretation and "第二层" in card.interpretation
|
| 202 |
+
assert "太脆弱" in card.caring_note
|
| 203 |
+
assert "电梯" not in combined
|
| 204 |
+
assert "只打开那件事" not in combined
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def test_generate_today_tip_answers_sad_relationship_question_without_productivity_template():
|
| 208 |
+
intake = build_intake(
|
| 209 |
+
dream_text="我梦到前任发消息又消失了,我醒来很难过,想知道是不是我还没走出来。",
|
| 210 |
+
mood="难过",
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
card = generate_today_tip(intake, "我不想要鸡汤,只想知道为什么这么难受。", FakeTextClient(), language="zh")
|
| 214 |
+
combined = "\n".join([card.main_question, card.interpretation, card.today_tip, card.caring_note])
|
| 215 |
+
|
| 216 |
+
assert "为什么这么难受" in card.main_question or "没走出来" in card.main_question
|
| 217 |
+
assert "难受" in combined or "难过" in combined
|
| 218 |
+
assert "消息" in combined
|
| 219 |
+
assert "第一句话" not in combined
|
| 220 |
+
assert "打开任务" not in combined
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def test_generate_today_tip_keeps_english_emotional_question_from_becoming_task_advice():
|
| 224 |
+
intake = build_intake(
|
| 225 |
+
dream_text="I dreamed I was drowning in dark water and woke scared. Does this mean I am not coping?",
|
| 226 |
+
mood="scared",
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
card = generate_today_tip(intake, "I need comfort, not productivity advice.", FakeTextClient(), language="en")
|
| 230 |
+
combined = "\n".join(
|
| 231 |
+
[
|
| 232 |
+
card.main_question,
|
| 233 |
+
card.interpretation,
|
| 234 |
+
card.today_tip,
|
| 235 |
+
card.tiny_action,
|
| 236 |
+
card.caring_note,
|
| 237 |
+
]
|
| 238 |
+
).lower()
|
| 239 |
+
|
| 240 |
+
assert "not coping" in card.main_question.lower()
|
| 241 |
+
assert "water" in combined
|
| 242 |
+
assert "first" in card.interpretation.lower() and "second" in card.interpretation.lower()
|
| 243 |
+
assert "not weak" in combined or "comforted" in combined
|
| 244 |
+
assert "open the task" not in combined
|
| 245 |
+
assert "first line" not in combined
|
| 246 |
+
|
| 247 |
+
|
| 248 |
def test_generate_today_tip_adds_safety_note_for_repeated_insomnia_without_self_harm():
|
| 249 |
intake = build_intake(
|
| 250 |
dream_text="我昨晚反复梦见自己在一条漆黑的走廊里走,醒来后心跳很快,已经连续3晚都睡不好。",
|
|
|
|
| 413 |
)
|
| 414 |
session = ask_questions(session, FakeTextClient())
|
| 415 |
assert session.phase == "ask"
|
| 416 |
+
assert len(session.question_history) == 1
|
| 417 |
+
assert "three layers" in session.question_history[0]
|
| 418 |
|
| 419 |
session = answer_question(session, "I want one small start.")
|
| 420 |
assert session.answer_history[-1] == "I want one small start."
|
| 421 |
|
| 422 |
session = ask_questions(session, FakeTextClient(), force_another=True)
|
| 423 |
+
assert len(session.question_history) == 2
|
| 424 |
+
assert "For this round" in session.question_history[-1]
|
| 425 |
+
|
| 426 |
+
session = ask_questions(session, FakeTextClient(), force_another=True)
|
| 427 |
+
assert len(session.question_history) == 3
|
| 428 |
+
|
| 429 |
+
session = ask_questions(session, FakeTextClient(), force_another=True)
|
| 430 |
+
assert len(session.question_history) == 3
|
| 431 |
|
| 432 |
session = skip_question(session)
|
| 433 |
assert "skip" in session.answer_history[-1].lower()
|
|
|
|
| 611 |
assert "objects" not in combined
|
| 612 |
|
| 613 |
|
| 614 |
+
def test_zh_text_and_image_keep_user_question_while_using_visual_anchors():
|
| 615 |
+
class SeaDreamVision:
|
| 616 |
+
def extract_witness(self, image_path):
|
| 617 |
+
return VisionWitness(
|
| 618 |
+
scene_summary=(
|
| 619 |
+
"A simple sketch of a person standing on wavy lines under a crescent moon, "
|
| 620 |
+
"with the text 'dark sea dream' above."
|
| 621 |
+
),
|
| 622 |
+
objects=["stick figure", "wavy lines representing water", "crescent moon"],
|
| 623 |
+
visible_text=["dark sea dream"],
|
| 624 |
+
mood_cues=["small figure in a large dark place"],
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
def extract_clues(self, image_path):
|
| 628 |
+
return ["flat fallback should not win"]
|
| 629 |
+
|
| 630 |
+
session = add_evidence(
|
| 631 |
+
create_session(language="zh"),
|
| 632 |
+
dream_text="我醒来很害怕,这张草图是梦里最清楚的画面。我想知道为什么它让我这么慌。",
|
| 633 |
+
image_path="sea.png",
|
| 634 |
+
mood="害怕",
|
| 635 |
+
vision_client=SeaDreamVision(),
|
| 636 |
+
asr_client=FakeASRClient(),
|
| 637 |
+
language="zh",
|
| 638 |
+
)
|
| 639 |
+
card = generate_today_tip(session.intake, "", FakeTextClient(), language="zh")
|
| 640 |
+
combined = "\n".join([card.main_question, ",".join(card.dream_anchors), card.interpretation, card.today_tip])
|
| 641 |
+
|
| 642 |
+
assert "为什么它让我这么慌?" in card.main_question
|
| 643 |
+
assert any(anchor in card.dream_anchors for anchor in ["夜晚的海", "海浪", "月牙", "漆黑的海"])
|
| 644 |
+
assert "blue hallway" not in combined
|
| 645 |
+
assert "a simple sketch" not in combined.lower()
|
| 646 |
+
assert "dreamlike representation" not in combined.lower()
|
| 647 |
+
assert "这么难受不是你反应过度" in card.interpretation
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def test_zh_first_response_and_tip_follow_story_and_image_not_template():
|
| 651 |
+
class LostChildVision:
|
| 652 |
+
def extract_witness(self, image_path):
|
| 653 |
+
return VisionWitness(
|
| 654 |
+
scene_summary="A child figure stands inside a subway station with arrows pointing toward HOME.",
|
| 655 |
+
objects=["child figure", "subway station", "arrows"],
|
| 656 |
+
visible_text=["HOME"],
|
| 657 |
+
mood_cues=["lost", "small figure in a large station"],
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
def extract_clues(self, image_path):
|
| 661 |
+
return ["flat fallback should not win"]
|
| 662 |
+
|
| 663 |
+
session = add_evidence(
|
| 664 |
+
create_session(language="zh"),
|
| 665 |
+
dream_text="我梦见一个小孩在地铁站找不到家,我醒来很害怕,想知道为什么这个画面让我这么难受。",
|
| 666 |
+
image_path="lost-child.png",
|
| 667 |
+
mood="害怕",
|
| 668 |
+
vision_client=LostChildVision(),
|
| 669 |
+
asr_client=FakeASRClient(),
|
| 670 |
+
language="zh",
|
| 671 |
+
)
|
| 672 |
+
session = ask_questions(session, FakeTextClient(), language="zh")
|
| 673 |
+
first_response = session.question_history[0]
|
| 674 |
+
|
| 675 |
+
assert "我先听到的是" in first_response
|
| 676 |
+
assert "三层" in first_response
|
| 677 |
+
assert "小孩" in first_response
|
| 678 |
+
assert "回家" in first_response or "地铁" in first_response
|
| 679 |
+
|
| 680 |
+
session = answer_question(session, "更像我自己最近不知道该往哪里走,也很想有人带一下。", language="zh")
|
| 681 |
+
card = generate_today_tip(
|
| 682 |
+
session.intake,
|
| 683 |
+
session.answers_text(),
|
| 684 |
+
FakeTextClient(),
|
| 685 |
+
language="zh",
|
| 686 |
+
followup_questions=session.question_history,
|
| 687 |
+
)
|
| 688 |
+
combined = "\n".join([card.interpretation, card.today_tip, card.tiny_action, card.caring_note])
|
| 689 |
+
|
| 690 |
+
assert "小孩" in combined
|
| 691 |
+
assert "回家" in combined or "地铁" in combined or "路标" in combined
|
| 692 |
+
assert "需要被带路" in card.today_tip or "路标" in card.today_tip
|
| 693 |
+
assert "写两行" not in combined
|
| 694 |
+
assert "当作害怕的形状" not in combined
|
| 695 |
+
|
| 696 |
+
|
| 697 |
def test_witness_failure_keeps_text_path_alive():
|
| 698 |
class BrokenWitnessVision:
|
| 699 |
def extract_witness(self, image_path):
|