fix: harden Dream QA agent path
#29
by ADJCJH - opened
- app.py +48 -0
- dream_customs/pipeline.py +81 -12
- dream_customs/safety.py +20 -1
- dream_customs/ui/actions.py +17 -1
- dream_customs/ui/styles.py +38 -0
- tests/test_agent_readiness.py +11 -0
- tests/test_pipeline.py +51 -1
- tests/test_safety.py +8 -0
app.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
import os
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from dream_customs import zerogpu # noqa: F401
|
| 4 |
from dream_customs.ui.app import build_demo
|
| 5 |
|
|
@@ -7,6 +11,50 @@ from dream_customs.ui.app import build_demo
|
|
| 7 |
demo = build_demo()
|
| 8 |
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
if __name__ == "__main__":
|
| 11 |
demo.launch(
|
| 12 |
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
+
from gradio.routes import App
|
| 4 |
+
from fastapi import Request
|
| 5 |
+
from fastapi.responses import JSONResponse, RedirectResponse
|
| 6 |
+
|
| 7 |
from dream_customs import zerogpu # noqa: F401
|
| 8 |
from dream_customs.ui.app import build_demo
|
| 9 |
|
|
|
|
| 11 |
demo = build_demo()
|
| 12 |
|
| 13 |
|
| 14 |
+
_ORIGINAL_CREATE_APP = App.create_app
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _install_gradio_api_aliases(app, blocks) -> None:
|
| 18 |
+
existing_paths = {getattr(route, "path", "") for route in app.routes}
|
| 19 |
+
|
| 20 |
+
if "/gradio_api/info" not in existing_paths:
|
| 21 |
+
@app.get("/gradio_api/info", include_in_schema=False)
|
| 22 |
+
async def gradio_api_info_alias() -> JSONResponse:
|
| 23 |
+
"""Compatibility for Hugging Face's generated agents.md schema URL."""
|
| 24 |
+
|
| 25 |
+
return JSONResponse(blocks.get_api_info())
|
| 26 |
+
|
| 27 |
+
async def _redirect_to_gradio4_path(request: Request) -> RedirectResponse:
|
| 28 |
+
suffix = request.url.path.removeprefix("/gradio_api")
|
| 29 |
+
target = suffix or "/"
|
| 30 |
+
if request.url.query:
|
| 31 |
+
target = f"{target}?{request.url.query}"
|
| 32 |
+
return RedirectResponse(url=target, status_code=307)
|
| 33 |
+
|
| 34 |
+
for alias_path in (
|
| 35 |
+
"/gradio_api/queue/join",
|
| 36 |
+
"/gradio_api/queue/data",
|
| 37 |
+
"/gradio_api/upload",
|
| 38 |
+
):
|
| 39 |
+
if alias_path not in existing_paths:
|
| 40 |
+
app.add_api_route(
|
| 41 |
+
alias_path,
|
| 42 |
+
_redirect_to_gradio4_path,
|
| 43 |
+
methods=["GET", "POST"],
|
| 44 |
+
include_in_schema=False,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _create_app_with_gradio_api_aliases(blocks, app_kwargs=None, auth_dependency=None):
|
| 49 |
+
app = _ORIGINAL_CREATE_APP(blocks, app_kwargs=app_kwargs, auth_dependency=auth_dependency)
|
| 50 |
+
_install_gradio_api_aliases(app, blocks)
|
| 51 |
+
return app
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
App.create_app = staticmethod(_create_app_with_gradio_api_aliases)
|
| 55 |
+
_install_gradio_api_aliases(demo.app, demo)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
if __name__ == "__main__":
|
| 59 |
demo.launch(
|
| 60 |
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
dream_customs/pipeline.py
CHANGED
|
@@ -100,6 +100,17 @@ _ANCHOR_STOPWORDS = {
|
|
| 100 |
}
|
| 101 |
|
| 102 |
_ZH_ANCHOR_MARKERS = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
"电梯按钮",
|
| 104 |
"融化的按钮",
|
| 105 |
"按钮融化",
|
|
@@ -118,6 +129,17 @@ _ZH_ANCHOR_MARKERS = [
|
|
| 118 |
]
|
| 119 |
|
| 120 |
_ZH_TO_EN_PHRASES = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
"电梯按钮": "elevator button",
|
| 122 |
"融化的按钮": "melted button",
|
| 123 |
"按钮融化": "melted button",
|
|
@@ -145,6 +167,26 @@ def _dedupe_preserve_order(items: List[str]) -> List[str]:
|
|
| 145 |
return result
|
| 146 |
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
| 149 |
raw_text = " ".join(
|
| 150 |
[
|
|
@@ -167,7 +209,8 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 167 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 168 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 169 |
r"gate|gates|floor|floors|stamp|stamps|number|numbers|exam|exams|"
|
| 170 |
-
r"pencil|pencils|train|trains|door|doors|
|
|
|
|
| 171 |
r"stairwell|stairwells|room|rooms|key|keys|note|notes|rain|moon|moons|curtain|curtains"
|
| 172 |
r")\b"
|
| 173 |
)
|
|
@@ -179,7 +222,8 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 179 |
|
| 180 |
noun_pattern = re.compile(
|
| 181 |
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor|"
|
| 182 |
-
r"exam|pencil|train|door|
|
|
|
|
| 183 |
)
|
| 184 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 185 |
candidates.extend(clue.lower() for clue in intake.visual_clues if clue.strip())
|
|
@@ -251,7 +295,9 @@ def _summary_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
|
| 251 |
clean = clean[:69].rstrip() + "..."
|
| 252 |
if not _is_zh(language):
|
| 253 |
return clean if clean.lower().startswith(("i ", "you ")) else f"You dreamed about {clean}"
|
| 254 |
-
|
|
|
|
|
|
|
| 255 |
|
| 256 |
|
| 257 |
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
|
@@ -350,7 +396,16 @@ def _anchor_in_text(text: str, anchors: List[str]) -> bool:
|
|
| 350 |
|
| 351 |
def _text_uses_anchor(text: str, anchors: List[str]) -> bool:
|
| 352 |
clean = (text or "").lower()
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
|
| 356 |
def _is_generic_visitor_name(text: str, intake: DreamIntake) -> bool:
|
|
@@ -589,7 +644,7 @@ def build_qa_state(
|
|
| 589 |
answers: Optional[List[str]] = None,
|
| 590 |
language: str = "en",
|
| 591 |
) -> DreamQAState:
|
| 592 |
-
anchors = _anchors_for_language(intake, language)
|
| 593 |
return DreamQAState(
|
| 594 |
dream_summary=_summary_from_intake(intake, language),
|
| 595 |
main_question=_main_question_from_intake(intake, language),
|
|
@@ -602,8 +657,8 @@ def build_qa_state(
|
|
| 602 |
|
| 603 |
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "", language: str = "en") -> TodayTipCard:
|
| 604 |
polished = card.model_copy(deep=True)
|
| 605 |
-
intake_anchors = _anchors_for_language(intake, language)
|
| 606 |
-
card_anchors = (
|
| 607 |
polished.dream_anchors
|
| 608 |
if _is_zh(language)
|
| 609 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
|
@@ -613,11 +668,17 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 613 |
else:
|
| 614 |
anchors = card_anchors or intake_anchors
|
| 615 |
if not anchors:
|
| 616 |
-
anchors = [_primary_anchor(intake, language)]
|
|
|
|
|
|
|
| 617 |
polished.dream_anchors = anchors
|
| 618 |
-
if not polished.dream_summary.strip():
|
| 619 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 620 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 622 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 623 |
if answer_interpretation:
|
|
@@ -634,6 +695,7 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 634 |
elif (
|
| 635 |
not polished.today_tip.strip()
|
| 636 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
|
|
|
| 637 |
or not _anchor_in_text(polished.today_tip, anchors)
|
| 638 |
):
|
| 639 |
polished.today_tip = _grounded_today_tip(intake, language)
|
|
@@ -641,11 +703,18 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 641 |
answer_action = _answer_based_tiny_action(answers, language)
|
| 642 |
if answer_action and (
|
| 643 |
not polished.tiny_action.strip()
|
|
|
|
|
|
|
| 644 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 645 |
or "email" in (answers or "").lower()
|
| 646 |
):
|
| 647 |
polished.tiny_action = answer_action
|
| 648 |
-
elif
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
if _is_zh(language):
|
| 650 |
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 651 |
else:
|
|
@@ -657,7 +726,7 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 657 |
else "You do not have to solve the whole dream this morning; noticing one detail is enough."
|
| 658 |
)
|
| 659 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 660 |
-
polished.safety_note = safety_note() if needs_escalation(merged) else ""
|
| 661 |
if not _is_zh(language):
|
| 662 |
polished = _clean_english_today_tip_language(polished)
|
| 663 |
return polished
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
_ZH_ANCHOR_MARKERS = [
|
| 103 |
+
"高楼边缘",
|
| 104 |
+
"高楼",
|
| 105 |
+
"边缘",
|
| 106 |
+
"红色的门",
|
| 107 |
+
"红色门",
|
| 108 |
+
"大雪地",
|
| 109 |
+
"雪地",
|
| 110 |
+
"地铁站",
|
| 111 |
+
"地铁",
|
| 112 |
+
"安全帽",
|
| 113 |
+
"男生",
|
| 114 |
"电梯按钮",
|
| 115 |
"融化的按钮",
|
| 116 |
"按钮融化",
|
|
|
|
| 129 |
]
|
| 130 |
|
| 131 |
_ZH_TO_EN_PHRASES = {
|
| 132 |
+
"高楼边缘": "high-rise edge",
|
| 133 |
+
"高楼": "high-rise",
|
| 134 |
+
"边缘": "edge",
|
| 135 |
+
"红色的门": "red door",
|
| 136 |
+
"红色门": "red door",
|
| 137 |
+
"大雪地": "snowfield",
|
| 138 |
+
"雪地": "snow",
|
| 139 |
+
"地铁站": "subway station",
|
| 140 |
+
"地铁": "subway",
|
| 141 |
+
"安全帽": "hard hat",
|
| 142 |
+
"男生": "young man",
|
| 143 |
"电梯按钮": "elevator button",
|
| 144 |
"融化的按钮": "melted button",
|
| 145 |
"按钮融化": "melted button",
|
|
|
|
| 167 |
return result
|
| 168 |
|
| 169 |
|
| 170 |
+
_PLACEHOLDER_ANCHORS = {
|
| 171 |
+
"梦里的那个细节",
|
| 172 |
+
"梦里的细节",
|
| 173 |
+
"那个细节",
|
| 174 |
+
"dream detail",
|
| 175 |
+
"that dream detail",
|
| 176 |
+
"that that dream detail",
|
| 177 |
+
"the dream detail",
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _is_placeholder_anchor(text: str) -> bool:
|
| 182 |
+
clean = re.sub(r"\s+", " ", (text or "").strip().lower())
|
| 183 |
+
return not clean or clean in _PLACEHOLDER_ANCHORS or "dream detail" in clean or "梦里的那个细节" in clean
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _remove_placeholder_anchors(items: List[str]) -> List[str]:
|
| 187 |
+
return [item for item in items if not _is_placeholder_anchor(item)]
|
| 188 |
+
|
| 189 |
+
|
| 190 |
def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
| 191 |
raw_text = " ".join(
|
| 192 |
[
|
|
|
|
| 209 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 210 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 211 |
r"gate|gates|floor|floors|stamp|stamps|number|numbers|exam|exams|"
|
| 212 |
+
r"pencil|pencils|train|trains|door|doors|subway|station|stations|snow|snowfield|"
|
| 213 |
+
r"hat|hats|phone|phones|water|shoe|shoes|"
|
| 214 |
r"stairwell|stairwells|room|rooms|key|keys|note|notes|rain|moon|moons|curtain|curtains"
|
| 215 |
r")\b"
|
| 216 |
)
|
|
|
|
| 222 |
|
| 223 |
noun_pattern = re.compile(
|
| 224 |
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor|"
|
| 225 |
+
r"exam|pencil|train|door|subway|station|snow|snowfield|hat|phone|water|shoe|stairwell|"
|
| 226 |
+
r"room|key|note|rain|moon|curtain|sleep|dream)\b"
|
| 227 |
)
|
| 228 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 229 |
candidates.extend(clue.lower() for clue in intake.visual_clues if clue.strip())
|
|
|
|
| 295 |
clean = clean[:69].rstrip() + "..."
|
| 296 |
if not _is_zh(language):
|
| 297 |
return clean if clean.lower().startswith(("i ", "you ")) else f"You dreamed about {clean}"
|
| 298 |
+
if clean.startswith(("你", "我", "I ", "i ")) or any(marker in clean[:12] for marker in ("梦见", "梦到")):
|
| 299 |
+
return clean
|
| 300 |
+
return f"你梦见{clean}"
|
| 301 |
|
| 302 |
|
| 303 |
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
|
|
|
| 396 |
|
| 397 |
def _text_uses_anchor(text: str, anchors: List[str]) -> bool:
|
| 398 |
clean = (text or "").lower()
|
| 399 |
+
for anchor in anchors:
|
| 400 |
+
item = anchor.lower().strip()
|
| 401 |
+
if not item or _is_placeholder_anchor(item):
|
| 402 |
+
continue
|
| 403 |
+
if item in clean:
|
| 404 |
+
return True
|
| 405 |
+
tokens = [token for token in re.split(r"[\s,,。::;;、]+", item) if len(token) >= 2]
|
| 406 |
+
if any(token in clean for token in tokens):
|
| 407 |
+
return True
|
| 408 |
+
return False
|
| 409 |
|
| 410 |
|
| 411 |
def _is_generic_visitor_name(text: str, intake: DreamIntake) -> bool:
|
|
|
|
| 644 |
answers: Optional[List[str]] = None,
|
| 645 |
language: str = "en",
|
| 646 |
) -> DreamQAState:
|
| 647 |
+
anchors = _remove_placeholder_anchors(_anchors_for_language(intake, language))
|
| 648 |
return DreamQAState(
|
| 649 |
dream_summary=_summary_from_intake(intake, language),
|
| 650 |
main_question=_main_question_from_intake(intake, language),
|
|
|
|
| 657 |
|
| 658 |
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "", language: str = "en") -> TodayTipCard:
|
| 659 |
polished = card.model_copy(deep=True)
|
| 660 |
+
intake_anchors = _remove_placeholder_anchors(_anchors_for_language(intake, language))
|
| 661 |
+
card_anchors = _remove_placeholder_anchors(
|
| 662 |
polished.dream_anchors
|
| 663 |
if _is_zh(language)
|
| 664 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
|
|
|
| 668 |
else:
|
| 669 |
anchors = card_anchors or intake_anchors
|
| 670 |
if not anchors:
|
| 671 |
+
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
| 672 |
+
if not anchors:
|
| 673 |
+
anchors = ["梦境片段" if _is_zh(language) else "dream fragment"]
|
| 674 |
polished.dream_anchors = anchors
|
| 675 |
+
if not polished.dream_summary.strip() or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 676 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 677 |
+
if (
|
| 678 |
+
not polished.main_question.strip()
|
| 679 |
+
or _is_placeholder_anchor(polished.main_question)
|
| 680 |
+
or not _text_uses_anchor(polished.main_question, anchors)
|
| 681 |
+
):
|
| 682 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 683 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 684 |
if answer_interpretation:
|
|
|
|
| 695 |
elif (
|
| 696 |
not polished.today_tip.strip()
|
| 697 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
| 698 |
+
or _is_placeholder_anchor(polished.today_tip)
|
| 699 |
or not _anchor_in_text(polished.today_tip, anchors)
|
| 700 |
):
|
| 701 |
polished.today_tip = _grounded_today_tip(intake, language)
|
|
|
|
| 703 |
answer_action = _answer_based_tiny_action(answers, language)
|
| 704 |
if answer_action and (
|
| 705 |
not polished.tiny_action.strip()
|
| 706 |
+
or _is_placeholder_anchor(polished.tiny_action)
|
| 707 |
+
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 708 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 709 |
or "email" in (answers or "").lower()
|
| 710 |
):
|
| 711 |
polished.tiny_action = answer_action
|
| 712 |
+
elif (
|
| 713 |
+
not polished.tiny_action.strip()
|
| 714 |
+
or _is_placeholder_anchor(polished.tiny_action)
|
| 715 |
+
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 716 |
+
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 717 |
+
):
|
| 718 |
if _is_zh(language):
|
| 719 |
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 720 |
else:
|
|
|
|
| 726 |
else "You do not have to solve the whole dream this morning; noticing one detail is enough."
|
| 727 |
)
|
| 728 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 729 |
+
polished.safety_note = safety_note(language) if needs_escalation(merged) else ""
|
| 730 |
if not _is_zh(language):
|
| 731 |
polished = _clean_english_today_tip_language(polished)
|
| 732 |
return polished
|
dream_customs/safety.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
ESCALATION_TERMS = (
|
| 2 |
"hurt myself",
|
| 3 |
"kill myself",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"suicide",
|
| 5 |
"self-harm",
|
| 6 |
"hurt someone",
|
|
@@ -10,6 +16,13 @@ ESCALATION_TERMS = (
|
|
| 10 |
"想伤害自己",
|
| 11 |
"自杀",
|
| 12 |
"自残",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"伤害别人",
|
| 14 |
"很多天睡不着",
|
| 15 |
"无法正常生活",
|
|
@@ -22,7 +35,13 @@ def needs_escalation(text: str) -> bool:
|
|
| 22 |
return any(term in lowered for term in ESCALATION_TERMS)
|
| 23 |
|
| 24 |
|
| 25 |
-
def safety_note() -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
return (
|
| 27 |
"This dream sounds heavier than a playful reflection tool should handle. "
|
| 28 |
"If you feel unsafe, cannot sleep for many nights, or worry you may hurt "
|
|
|
|
| 1 |
ESCALATION_TERMS = (
|
| 2 |
"hurt myself",
|
| 3 |
"kill myself",
|
| 4 |
+
"do not want to wake up",
|
| 5 |
+
"don't want to wake up",
|
| 6 |
+
"didn't want to wake up",
|
| 7 |
+
"hopeless",
|
| 8 |
+
"can't go on",
|
| 9 |
+
"cannot go on",
|
| 10 |
"suicide",
|
| 11 |
"self-harm",
|
| 12 |
"hurt someone",
|
|
|
|
| 16 |
"想伤害自己",
|
| 17 |
"自杀",
|
| 18 |
"自残",
|
| 19 |
+
"不想醒来",
|
| 20 |
+
"不想活",
|
| 21 |
+
"活不下去",
|
| 22 |
+
"轻生",
|
| 23 |
+
"绝望",
|
| 24 |
+
"撑不住",
|
| 25 |
+
"崩溃",
|
| 26 |
"伤害别人",
|
| 27 |
"很多天睡不着",
|
| 28 |
"无法正常生活",
|
|
|
|
| 35 |
return any(term in lowered for term in ESCALATION_TERMS)
|
| 36 |
|
| 37 |
|
| 38 |
+
def safety_note(language: str = "en") -> str:
|
| 39 |
+
if language == "zh":
|
| 40 |
+
return (
|
| 41 |
+
"这段梦和醒来后的感受听起来已经超过了一个轻量梦境反思工具适合独自处理的范围。"
|
| 42 |
+
"如果你感到不安全、连续很多天睡不着,或担心自己可能伤害自己或别人,请现在联系一个可信任的人,"
|
| 43 |
+
"或寻求专业支持。"
|
| 44 |
+
)
|
| 45 |
return (
|
| 46 |
"This dream sounds heavier than a playful reflection tool should handle. "
|
| 47 |
"If you feel unsafe, cannot sleep for many nights, or worry you may hurt "
|
dream_customs/ui/actions.py
CHANGED
|
@@ -83,7 +83,7 @@ def _view_payload(
|
|
| 83 |
card = session.sealed_tip or session.draft_tip
|
| 84 |
error = _latest_error(session)
|
| 85 |
status = "error" if error else "tip" if session.sealed_tip else "ask" if session.question_history else "record"
|
| 86 |
-
|
| 87 |
"status": status,
|
| 88 |
"phase": session.phase,
|
| 89 |
"language": language,
|
|
@@ -96,6 +96,22 @@ def _view_payload(
|
|
| 96 |
"notice": _notice_for_status(status, error, language),
|
| 97 |
"debug": json.loads(_debug_json(session, text_backend, vision_backend, **settings)),
|
| 98 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def _view(
|
|
|
|
| 83 |
card = session.sealed_tip or session.draft_tip
|
| 84 |
error = _latest_error(session)
|
| 85 |
status = "error" if error else "tip" if session.sealed_tip else "ask" if session.question_history else "record"
|
| 86 |
+
payload = {
|
| 87 |
"status": status,
|
| 88 |
"phase": session.phase,
|
| 89 |
"language": language,
|
|
|
|
| 96 |
"notice": _notice_for_status(status, error, language),
|
| 97 |
"debug": json.loads(_debug_json(session, text_backend, vision_backend, **settings)),
|
| 98 |
}
|
| 99 |
+
if card:
|
| 100 |
+
payload.update(
|
| 101 |
+
{
|
| 102 |
+
"dream_summary": card.dream_summary,
|
| 103 |
+
"main_question": card.main_question,
|
| 104 |
+
"dream_anchors": card.dream_anchors,
|
| 105 |
+
"followup_questions": card.followup_questions,
|
| 106 |
+
"user_answers": card.user_answers,
|
| 107 |
+
"interpretation": card.interpretation,
|
| 108 |
+
"today_tip": card.today_tip,
|
| 109 |
+
"tiny_action": card.tiny_action,
|
| 110 |
+
"caring_note": card.caring_note,
|
| 111 |
+
"safety_note": card.safety_note,
|
| 112 |
+
}
|
| 113 |
+
)
|
| 114 |
+
return payload
|
| 115 |
|
| 116 |
|
| 117 |
def _view(
|
dream_customs/ui/styles.py
CHANGED
|
@@ -970,6 +970,44 @@ a.built-with[href*="gradio.app"] {
|
|
| 970 |
font-size: 0.82rem !important;
|
| 971 |
}
|
| 972 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
@keyframes dc-mic-pulse {
|
| 974 |
0% {
|
| 975 |
box-shadow: 0 0 0 0 rgba(11, 91, 100, 0.32);
|
|
|
|
| 970 |
font-size: 0.82rem !important;
|
| 971 |
}
|
| 972 |
|
| 973 |
+
.dc-debug-panel,
|
| 974 |
+
.dc-debug-panel .block,
|
| 975 |
+
.dc-debug-panel .wrap,
|
| 976 |
+
.dc-debug-panel .container,
|
| 977 |
+
.dc-debug-panel .cm-editor,
|
| 978 |
+
.dc-debug-panel .cm-scroller,
|
| 979 |
+
.dc-debug-panel .cm-content,
|
| 980 |
+
.dc-dev,
|
| 981 |
+
.dc-dev .block,
|
| 982 |
+
.dc-dev .wrap,
|
| 983 |
+
.dc-dev .container,
|
| 984 |
+
.dc-dev-advanced,
|
| 985 |
+
.dc-dev-advanced .block,
|
| 986 |
+
.dc-dev-advanced .wrap,
|
| 987 |
+
.dc-dev-advanced .container {
|
| 988 |
+
max-width: 100% !important;
|
| 989 |
+
min-width: 0 !important;
|
| 990 |
+
overflow-x: hidden !important;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.dc-debug-panel .cm-scroller,
|
| 994 |
+
.dc-debug-panel pre {
|
| 995 |
+
overflow-x: auto !important;
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
.dc-debug-panel .cm-line,
|
| 999 |
+
.dc-debug-panel .cm-content,
|
| 1000 |
+
.dc-debug-panel code,
|
| 1001 |
+
.dc-debug-panel pre {
|
| 1002 |
+
overflow-wrap: anywhere !important;
|
| 1003 |
+
white-space: pre-wrap !important;
|
| 1004 |
+
word-break: break-word !important;
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
.dc-debug-panel .cm-line {
|
| 1008 |
+
max-width: 100% !important;
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
@keyframes dc-mic-pulse {
|
| 1012 |
0% {
|
| 1013 |
box-shadow: 0 0 0 0 rgba(11, 91, 100, 0.32);
|
tests/test_agent_readiness.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
| 1 |
from scripts.evaluate_agent_readiness import _agent_api_result, _load_agents_doc
|
| 2 |
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
def test_agents_doc_is_present_and_agent_facing():
|
| 5 |
result = _load_agents_doc("agents.md", None, timeout=1)
|
| 6 |
|
|
|
|
| 1 |
from scripts.evaluate_agent_readiness import _agent_api_result, _load_agents_doc
|
| 2 |
|
| 3 |
|
| 4 |
+
def test_gradio_api_info_alias_matches_agent_endpoint():
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
|
| 7 |
+
from app import demo
|
| 8 |
+
|
| 9 |
+
response = TestClient(demo.app).get("/gradio_api/info")
|
| 10 |
+
|
| 11 |
+
assert response.status_code == 200
|
| 12 |
+
assert "/agent_dream_qa" in response.json()["named_endpoints"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
def test_agents_doc_is_present_and_agent_facing():
|
| 16 |
result = _load_agents_doc("agents.md", None, timeout=1)
|
| 17 |
|
tests/test_pipeline.py
CHANGED
|
@@ -13,13 +13,14 @@ from dream_customs.pipeline import (
|
|
| 13 |
draft_pact,
|
| 14 |
generate_negotiation,
|
| 15 |
generate_pact,
|
|
|
|
| 16 |
intake_from_modalities,
|
| 17 |
revise_pact,
|
| 18 |
seal_pact,
|
| 19 |
skip_question,
|
| 20 |
)
|
| 21 |
from dream_customs.prompts import pact_prompt
|
| 22 |
-
from dream_customs.schema import PactCard, VisionWitness
|
| 23 |
|
| 24 |
|
| 25 |
def test_dated_permit_id_uses_runtime_date_and_preserves_serial():
|
|
@@ -90,6 +91,55 @@ def test_generate_pact_adds_safety_note_for_distress():
|
|
| 90 |
assert card.safety_note
|
| 91 |
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
def test_generate_pact_polishes_unclear_model_output_into_daily_tip():
|
| 94 |
class UnclearTextClient:
|
| 95 |
def generate_pact(self, prompt):
|
|
|
|
| 13 |
draft_pact,
|
| 14 |
generate_negotiation,
|
| 15 |
generate_pact,
|
| 16 |
+
generate_today_tip,
|
| 17 |
intake_from_modalities,
|
| 18 |
revise_pact,
|
| 19 |
seal_pact,
|
| 20 |
skip_question,
|
| 21 |
)
|
| 22 |
from dream_customs.prompts import pact_prompt
|
| 23 |
+
from dream_customs.schema import PactCard, TodayTipCard, VisionWitness
|
| 24 |
|
| 25 |
|
| 26 |
def test_dated_permit_id_uses_runtime_date_and_preserves_serial():
|
|
|
|
| 91 |
assert card.safety_note
|
| 92 |
|
| 93 |
|
| 94 |
+
def test_generate_today_tip_repairs_placeholder_and_stale_model_anchors():
|
| 95 |
+
class StaleHostedTextClient:
|
| 96 |
+
def generate_today_tip(self, prompt):
|
| 97 |
+
return TodayTipCard(
|
| 98 |
+
dream_summary="I dreamed the elevator button melted and the floor number stayed on 14.",
|
| 99 |
+
main_question="What might that dream detail be asking me to notice today?",
|
| 100 |
+
dream_anchors=["that dream detail"],
|
| 101 |
+
followup_questions=["What did the elevator feel like?"],
|
| 102 |
+
user_answers=[],
|
| 103 |
+
interpretation="Maybe that dream detail points to a stuck point.",
|
| 104 |
+
today_tip="For today, borrow one action from that dream detail and write one line.",
|
| 105 |
+
tiny_action="Spend five minutes with that dream detail.",
|
| 106 |
+
caring_note="Noticing one dream detail is enough.",
|
| 107 |
+
safety_note="",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
intake = build_intake(
|
| 111 |
+
dream_text="昨晚我梦见自己在一片大雪地里找不到出口,反复回头看到一扇红色的门却打不开。",
|
| 112 |
+
mood="Neutral",
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
card = generate_today_tip(intake, "", StaleHostedTextClient(), language="zh")
|
| 116 |
+
combined = "\n".join(
|
| 117 |
+
[
|
| 118 |
+
card.dream_summary,
|
| 119 |
+
card.main_question,
|
| 120 |
+
",".join(card.dream_anchors),
|
| 121 |
+
card.interpretation,
|
| 122 |
+
card.today_tip,
|
| 123 |
+
card.tiny_action,
|
| 124 |
+
]
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
assert "梦里的那个细节" not in combined
|
| 128 |
+
assert "that dream detail" not in combined
|
| 129 |
+
assert "电梯" not in combined
|
| 130 |
+
assert "红色" in combined or "雪地" in combined
|
| 131 |
+
assert card.dream_anchors[0] in {"红色的门", "红色门", "大雪地", "雪地"}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_generate_today_tip_adds_chinese_safety_note_for_hopeless_text():
|
| 135 |
+
intake = build_intake(dream_text="我梦到站在高楼边缘,醒来后不想醒来,很绝望。")
|
| 136 |
+
|
| 137 |
+
card = generate_today_tip(intake, "", FakeTextClient(), language="zh")
|
| 138 |
+
|
| 139 |
+
assert card.safety_note
|
| 140 |
+
assert "可信任的人" in card.safety_note
|
| 141 |
+
|
| 142 |
+
|
| 143 |
def test_generate_pact_polishes_unclear_model_output_into_daily_tip():
|
| 144 |
class UnclearTextClient:
|
| 145 |
def generate_pact(self, prompt):
|
tests/test_safety.py
CHANGED
|
@@ -13,5 +13,13 @@ def test_chinese_severe_insomnia_escalates():
|
|
| 13 |
assert needs_escalation("我已经很多天睡不着,感觉无法正常生活。")
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def test_safety_note_mentions_professional_support():
|
| 17 |
assert "professional support" in safety_note().lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
assert needs_escalation("我已经很多天睡不着,感觉无法正常生活。")
|
| 14 |
|
| 15 |
|
| 16 |
+
def test_chinese_hopeless_text_escalates():
|
| 17 |
+
assert needs_escalation("醒来后我不想醒来,很绝望,感觉自己撑不住。")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
def test_safety_note_mentions_professional_support():
|
| 21 |
assert "professional support" in safety_note().lower()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_chinese_safety_note_mentions_support():
|
| 25 |
+
assert "可信任的人" in safety_note("zh")
|