Make Dream QA responses emotion led
#31
by ADJCJH - opened
- dream_customs/pipeline.py +265 -6
- dream_customs/prompts.py +9 -2
- tests/test_pipeline.py +67 -0
dream_customs/pipeline.py
CHANGED
|
@@ -108,6 +108,12 @@ _ZH_ANCHOR_MARKERS = [
|
|
| 108 |
"掉进海",
|
| 109 |
"海里",
|
| 110 |
"海",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"小孩找不到家",
|
| 112 |
"小孩",
|
| 113 |
"找不到家",
|
|
@@ -210,6 +216,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",
|
|
@@ -543,9 +555,241 @@ 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):
|
|
@@ -1089,14 +1333,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 +1365,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 +1393,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 = (
|
|
@@ -1165,7 +1421,10 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1165 |
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 1166 |
else:
|
| 1167 |
polished.tiny_action = f"Spend five minutes writing the first small step connected to the {anchors[0]}."
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
| 1169 |
polished.caring_note = (
|
| 1170 |
"你不需要一醒来就解决整个梦,先把一个细节照亮就很好。"
|
| 1171 |
if _is_zh(language)
|
|
|
|
| 108 |
"掉进海",
|
| 109 |
"海里",
|
| 110 |
"海",
|
| 111 |
+
"前任发消息",
|
| 112 |
+
"发消息又消失",
|
| 113 |
+
"发消息",
|
| 114 |
+
"消息",
|
| 115 |
+
"前任",
|
| 116 |
+
"消失",
|
| 117 |
"小孩找不到家",
|
| 118 |
"小孩",
|
| 119 |
"找不到家",
|
|
|
|
| 216 |
"掉进海": "falling into the sea",
|
| 217 |
"海里": "sea",
|
| 218 |
"海": "sea",
|
| 219 |
+
"前任发消息": "former partner sending a message",
|
| 220 |
+
"发消息又消失": "message that disappeared",
|
| 221 |
+
"发消息": "sending a message",
|
| 222 |
+
"消息": "message",
|
| 223 |
+
"前任": "former partner",
|
| 224 |
+
"消失": "disappearing",
|
| 225 |
"小孩找不到家": "child unable to find home",
|
| 226 |
"小孩": "child",
|
| 227 |
"找不到家": "unable to find home",
|
|
|
|
| 555 |
return f"你梦见{clean}"
|
| 556 |
|
| 557 |
|
| 558 |
+
def _user_supplied_text(intake: DreamIntake, answers: str = "", include_mood: bool = False) -> str:
|
| 559 |
+
parts = [
|
| 560 |
+
intake.dream_text,
|
| 561 |
+
intake.voice_transcript,
|
| 562 |
+
intake.main_question,
|
| 563 |
+
intake.uncertainty,
|
| 564 |
+
intake.user_context,
|
| 565 |
+
answers or "",
|
| 566 |
+
]
|
| 567 |
+
if include_mood:
|
| 568 |
+
parts.append(intake.mood)
|
| 569 |
+
return "\n".join(part.strip() for part in parts if part and part.strip())
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _question_sentence_candidates(text: str) -> List[str]:
|
| 573 |
+
pieces = re.split(r"[。!?!?\n\r]+|(?<=\.)\s+|[,,;;]", text or "")
|
| 574 |
+
return [piece.strip(" ::「」\"'()[]{}") for piece in pieces if piece.strip()]
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def _clean_user_question(text: str, language: str = "en") -> str:
|
| 578 |
+
clean = re.sub(r"\s+", " ", (text or "").strip(" ::「」\"'()[]{}"))
|
| 579 |
+
if not clean:
|
| 580 |
+
return ""
|
| 581 |
+
if _is_zh(language):
|
| 582 |
+
clean = re.sub(r"^(我)?(醒来后)?(最)?(想知道|想问|在想|担心|害怕)[::,,\s]*", "", clean)
|
| 583 |
+
clean = re.sub(r"^(只)?想知道[::,,\s]*", "", clean)
|
| 584 |
+
clean = clean.strip(" ::「」\"'()[]{}")
|
| 585 |
+
return clean if clean.endswith(("?", "?")) else f"{clean}?"
|
| 586 |
+
clean = re.sub(
|
| 587 |
+
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*",
|
| 588 |
+
lambda match: (match.group(4) or "").strip() + " ",
|
| 589 |
+
clean,
|
| 590 |
+
flags=re.IGNORECASE,
|
| 591 |
+
).strip()
|
| 592 |
+
clean = clean[0].upper() + clean[1:] if clean else ""
|
| 593 |
+
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):
|
| 594 |
+
clean += "?"
|
| 595 |
+
return clean
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def _extract_explicit_user_question(intake: DreamIntake, answers: str = "", language: str = "en") -> str:
|
| 599 |
+
if intake.main_question.strip():
|
| 600 |
+
return _clean_user_question(intake.main_question, language)
|
| 601 |
+
text = _user_supplied_text(intake, answers)
|
| 602 |
+
if _is_zh(language):
|
| 603 |
+
markers = (
|
| 604 |
+
"为什么",
|
| 605 |
+
"是不是",
|
| 606 |
+
"会不会",
|
| 607 |
+
"怎么办",
|
| 608 |
+
"我该",
|
| 609 |
+
"能不能",
|
| 610 |
+
"代表",
|
| 611 |
+
"说明",
|
| 612 |
+
"预兆",
|
| 613 |
+
"征兆",
|
| 614 |
+
"想知道",
|
| 615 |
+
"想问",
|
| 616 |
+
"撑不住",
|
| 617 |
+
"走出来",
|
| 618 |
+
"这么难受",
|
| 619 |
+
)
|
| 620 |
+
else:
|
| 621 |
+
markers = (
|
| 622 |
+
"why",
|
| 623 |
+
"what should",
|
| 624 |
+
"how should",
|
| 625 |
+
"does this mean",
|
| 626 |
+
"do i",
|
| 627 |
+
"am i",
|
| 628 |
+
"is this",
|
| 629 |
+
"could this",
|
| 630 |
+
"can this",
|
| 631 |
+
"should i",
|
| 632 |
+
"i wonder",
|
| 633 |
+
"want to know",
|
| 634 |
+
"not coping",
|
| 635 |
+
"cope",
|
| 636 |
+
"a sign",
|
| 637 |
+
"omen",
|
| 638 |
+
)
|
| 639 |
+
for sentence in _question_sentence_candidates(text):
|
| 640 |
+
lowered = sentence.lower()
|
| 641 |
+
if _is_skip_answer(sentence, language):
|
| 642 |
+
continue
|
| 643 |
+
if any(marker in lowered for marker in markers):
|
| 644 |
+
return _clean_user_question(sentence, language)
|
| 645 |
+
return ""
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def _emotion_labels_from_text(text: str, language: str = "en") -> List[str]:
|
| 649 |
+
lowered = (text or "").lower()
|
| 650 |
+
if _is_zh(language):
|
| 651 |
+
groups = [
|
| 652 |
+
("害怕", ["害怕", "吓", "恐惧", "心慌", "心跳很快", "惊醒"]),
|
| 653 |
+
("压力", ["压力", "撑不住", "扛不住", "崩溃", "压垮", "焦虑", "太累", "疲惫"]),
|
| 654 |
+
("难过", ["难过", "难受", "伤心", "失落", "想哭", "委屈", "孤独"]),
|
| 655 |
+
("自责", ["自责", "内疚", "愧疚", "后悔", "责怪自己"]),
|
| 656 |
+
("需要安慰", ["安慰", "被安慰", "抱抱", "关心", "陪陪", "鸡汤"]),
|
| 657 |
+
]
|
| 658 |
+
else:
|
| 659 |
+
groups = [
|
| 660 |
+
("fear", ["scared", "afraid", "terrified", "panic", "panicked", "frightened"]),
|
| 661 |
+
("pressure", ["overwhelmed", "not coping", "can't cope", "cannot cope", "burned out", "too much", "anxious"]),
|
| 662 |
+
("sadness", ["sad", "grief", "hurt", "lonely", "heartbroken", "upset"]),
|
| 663 |
+
("guilt", ["guilty", "ashamed", "blame myself", "regret"]),
|
| 664 |
+
("comfort", ["comfort", "reassurance", "not productivity", "not advice", "not a pep talk"]),
|
| 665 |
+
]
|
| 666 |
+
labels: List[str] = []
|
| 667 |
+
for label, terms in groups:
|
| 668 |
+
if any(term in lowered for term in terms):
|
| 669 |
+
labels.append(label)
|
| 670 |
+
return _dedupe_preserve_order(labels)
|
| 671 |
+
|
| 672 |
+
|
| 673 |
+
def _emotion_phrase(labels: List[str], language: str = "en") -> str:
|
| 674 |
+
if not labels:
|
| 675 |
+
return "这个感受" if _is_zh(language) else "this feeling"
|
| 676 |
+
if _is_zh(language):
|
| 677 |
+
return "、".join(labels)
|
| 678 |
+
if len(labels) == 1:
|
| 679 |
+
return labels[0]
|
| 680 |
+
return ", ".join(labels[:-1]) + f", and {labels[-1]}"
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def _needs_comfort(text: str, language: str = "en") -> bool:
|
| 684 |
+
lowered = (text or "").lower()
|
| 685 |
+
if _is_zh(language):
|
| 686 |
+
return any(term in lowered for term in ["安慰", "被安慰", "抱抱", "关心", "别催", "不是鸡汤"])
|
| 687 |
+
return any(term in lowered for term in ["comfort", "reassurance", "not productivity", "not a pep talk", "not advice"])
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def _should_use_emotion_led_response(intake: DreamIntake, answers: str, language: str = "en") -> bool:
|
| 691 |
+
user_text = _user_supplied_text(intake, answers)
|
| 692 |
+
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 693 |
+
labels = _emotion_labels_from_text(user_text, language)
|
| 694 |
+
if _is_low_context_intake(intake) and _is_skip_answer(answers, language) and not explicit_question and not _needs_comfort(user_text, language):
|
| 695 |
+
return False
|
| 696 |
+
return bool(explicit_question or labels or _needs_comfort(user_text, language))
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
def _direct_question_reassurance(question: str, labels: List[str], language: str = "en") -> str:
|
| 700 |
+
lowered = (question or "").lower()
|
| 701 |
+
if _is_zh(language):
|
| 702 |
+
if any(term in lowered for term in ["撑不住", "扛不住", "崩溃"]):
|
| 703 |
+
return "它不等于你一定撑不住了;更像是在提醒你,最近的压力已经值得被认真照顾。"
|
| 704 |
+
if "走出来" in lowered:
|
| 705 |
+
return "它不急着证明你有没有走出来;更像是在说明这段难受还需要一点被看见的时间。"
|
| 706 |
+
if any(term in lowered for term in ["预兆", "征兆", "坏事", "会不会发生"]):
|
| 707 |
+
return "它不适合被当作预兆;我们先把它当作醒来后仍在身体里的担心来照顾。"
|
| 708 |
+
if any(term in lowered for term in ["我是不是", "是不是我"]):
|
| 709 |
+
return "它不是给你贴标签的证据;它更像是在把一个需要被接住的感受放到你面前。"
|
| 710 |
+
if "为什么" in lowered or "这么难受" in lowered:
|
| 711 |
+
return "这么难受不是你反应过度;梦可能只是把还没放下的感受放大给你看。"
|
| 712 |
+
emotion = _emotion_phrase(labels, language)
|
| 713 |
+
return f"你提到的{emotion}不是小事,它值得先被接住,而不是立刻被解释掉。"
|
| 714 |
+
if any(term in lowered for term in ["not coping", "cope", "coping", "falling apart"]):
|
| 715 |
+
return "This does not prove you are failing to cope; it may be showing that your pressure deserves care before answers."
|
| 716 |
+
if "sign" in lowered or "omen" in lowered or "bad will happen" in lowered:
|
| 717 |
+
return "This is safer to treat as a fear to care for, not as evidence that something bad will happen."
|
| 718 |
+
if "am i" in lowered or "does this mean i" in lowered:
|
| 719 |
+
return "This dream is not evidence for a label about you; it is one feeling asking to be met gently."
|
| 720 |
+
if "why" in lowered:
|
| 721 |
+
return "The ache makes sense; the dream may be enlarging a feeling that has not had enough room yet."
|
| 722 |
+
emotion = _emotion_phrase(labels, language)
|
| 723 |
+
return f"The {emotion} you named deserves to be met first, not explained away too quickly."
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
def _emotion_led_interpretation(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 727 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 728 |
+
return ""
|
| 729 |
+
question = _extract_explicit_user_question(intake, answers, language)
|
| 730 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
|
| 731 |
+
emotion = _emotion_phrase(labels, language)
|
| 732 |
+
anchor = _answer_bridge_anchor(anchors)
|
| 733 |
+
direct = _direct_question_reassurance(question, labels, language)
|
| 734 |
+
if _is_zh(language):
|
| 735 |
+
opener = f"你问的是「{question}」。" if question else ""
|
| 736 |
+
return (
|
| 737 |
+
f"{opener}{direct} 第一层,先承认醒来后的{emotion}是真实的,不需要被责怪。"
|
| 738 |
+
f"第二层,梦里的「{anchor}」也许把这种感受变成了一个可以看的画面。"
|
| 739 |
+
"第三层,今天不急着找唯一答案,先给自己一个能站稳的小支点。"
|
| 740 |
+
)
|
| 741 |
+
opener = f"You asked, \"{question}\" " if question else ""
|
| 742 |
+
return (
|
| 743 |
+
f"{opener}{direct} First, let the {emotion} be real without blaming yourself. "
|
| 744 |
+
f"Second, the {_anchor_with_article(anchor)} may be the dream's concrete shape for that feeling. "
|
| 745 |
+
"Third, for today the goal is not to solve the whole dream, but to give yourself one steadier place to stand."
|
| 746 |
+
)
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 750 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 751 |
+
return ""
|
| 752 |
+
if _has_prophecy_frame(_user_supplied_text(intake, answers)):
|
| 753 |
+
return ""
|
| 754 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
|
| 755 |
+
emotion = _emotion_phrase(labels, language)
|
| 756 |
+
anchor = anchors[0]
|
| 757 |
+
if _is_zh(language):
|
| 758 |
+
return (
|
| 759 |
+
f"今天先把「{anchor}」当作{emotion}的形状,而不是结论:写一句“我现在被什么淹到/刺痛”,"
|
| 760 |
+
"再写一句“我此刻可以抓住的一个小安定”。"
|
| 761 |
+
)
|
| 762 |
+
return (
|
| 763 |
+
f"For today, treat the {anchor} as the shape of {emotion}, not a verdict: write one line naming "
|
| 764 |
+
"what feels heavy, then one line naming a small steady thing you can hold now."
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
def _emotion_led_tiny_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 769 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 770 |
+
return ""
|
| 771 |
+
anchor = anchors[0]
|
| 772 |
+
if _is_zh(language):
|
| 773 |
+
return f"用 5 分钟写两行:1.「{anchor}」让我最难受的是…… 2. 现在我能给自己的一点安定是……"
|
| 774 |
+
return f"Spend five minutes writing two lines: 1. The {anchor} hurts most because... 2. One steady thing I can offer myself now is..."
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str = "en") -> str:
|
| 778 |
+
if not _should_use_emotion_led_response(intake, answers, language):
|
| 779 |
+
return ""
|
| 780 |
+
labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
|
| 781 |
+
emotion = _emotion_phrase(labels, language)
|
| 782 |
+
if _is_zh(language):
|
| 783 |
+
return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
|
| 784 |
+
return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
|
| 785 |
+
|
| 786 |
+
|
| 787 |
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 788 |
if intake.main_question.strip():
|
| 789 |
+
return _clean_user_question(intake.main_question, language)
|
| 790 |
+
explicit_question = _extract_explicit_user_question(intake, "", language)
|
| 791 |
+
if explicit_question:
|
| 792 |
+
return explicit_question
|
| 793 |
task = _task_focus(intake.merged_text(), language)
|
| 794 |
if task:
|
| 795 |
if not _is_zh(language):
|
|
|
|
| 1333 |
setattr(polished, field, _clean_placeholder_phrase(getattr(polished, field)))
|
| 1334 |
if not polished.dream_summary.strip() or _is_placeholder_anchor(polished.dream_summary) or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 1335 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 1336 |
+
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 1337 |
+
if explicit_question:
|
| 1338 |
+
polished.main_question = explicit_question
|
| 1339 |
+
elif (
|
| 1340 |
not polished.main_question.strip()
|
| 1341 |
or _is_placeholder_anchor(polished.main_question)
|
| 1342 |
or not _text_uses_anchor(polished.main_question, anchors)
|
| 1343 |
):
|
| 1344 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 1345 |
+
emotion_interpretation = _emotion_led_interpretation(intake, answers, anchors, language)
|
| 1346 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 1347 |
+
if emotion_interpretation:
|
| 1348 |
+
polished.interpretation = emotion_interpretation
|
| 1349 |
+
elif answer_interpretation:
|
| 1350 |
polished.interpretation = answer_interpretation
|
| 1351 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1352 |
anchor = _answer_bridge_anchor(anchors)
|
|
|
|
| 1365 |
elif not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
|
| 1366 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 1367 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 1368 |
+
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 1369 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 1370 |
+
if emotion_tip:
|
| 1371 |
+
polished.today_tip = emotion_tip
|
| 1372 |
+
elif answer_tip:
|
| 1373 |
polished.today_tip = answer_tip
|
| 1374 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1375 |
anchor = anchors[0]
|
|
|
|
| 1393 |
):
|
| 1394 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 1395 |
hard_action_markers = ["address it immediately", "fix it immediately", "solve it immediately"]
|
| 1396 |
+
emotion_action = _emotion_led_tiny_action(intake, answers, anchors, language)
|
| 1397 |
answer_action = _answer_based_tiny_action(answers, language)
|
| 1398 |
+
if emotion_action:
|
| 1399 |
+
polished.tiny_action = emotion_action
|
| 1400 |
+
elif answer_action:
|
| 1401 |
polished.tiny_action = answer_action
|
| 1402 |
elif _has_prophecy_frame(intake.merged_text()):
|
| 1403 |
polished.tiny_action = (
|
|
|
|
| 1421 |
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 1422 |
else:
|
| 1423 |
polished.tiny_action = f"Spend five minutes writing the first small step connected to the {anchors[0]}."
|
| 1424 |
+
emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
|
| 1425 |
+
if emotion_caring_note:
|
| 1426 |
+
polished.caring_note = emotion_caring_note
|
| 1427 |
+
elif not polished.caring_note.strip():
|
| 1428 |
polished.caring_note = (
|
| 1429 |
"你不需要一醒来就解决整个梦,先把一个细节照亮就很好。"
|
| 1430 |
if _is_zh(language)
|
dream_customs/prompts.py
CHANGED
|
@@ -51,12 +51,19 @@ 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 |
|
|
|
|
| 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 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 must cite at least one concrete dream anchor.
|
| 62 |
Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
|
| 63 |
+
Keep the whole result short, warm, emotionally responsive, and specific to the user's answer.
|
| 64 |
+
The tiny_action must be a 5-minute self-check or small care step, not a command to solve the whole problem.
|
| 65 |
Avoid demanding phrases such as "immediately", "must", "fix it", or "solve it".
|
| 66 |
+
If the user asks for comfort, caring_note should be warm, specific, and validating.
|
| 67 |
Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
|
| 68 |
{_language_instruction(language)}
|
| 69 |
|
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晚都睡不好。",
|
|
|
|
| 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晚都睡不好。",
|