fix: ground dream customs story flow
#12
by ADJCJH - opened
- docs/superpowers/plans/2026-06-08-dream-customs-story-ux-polish.md +291 -0
- dream_customs/pipeline.py +231 -8
- dream_customs/prompts.py +17 -3
- dream_customs/ui/app.py +3 -1
- dream_customs/ui/copy.py +4 -0
- dream_customs/ui/styles.py +23 -3
- tests/test_pipeline.py +82 -0
- tests/test_ui_actions.py +19 -1
docs/superpowers/plans/2026-06-08-dream-customs-story-ux-polish.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dream Customs Story UX Polish Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
| 4 |
+
|
| 5 |
+
**Goal:** Make the public Dream Customs flow feel like one complete dream-customs ritual by grounding generated cards in the submitted dream, hiding developer controls from ordinary users, and turning hosted-model wait time into story-aware status copy.
|
| 6 |
+
|
| 7 |
+
**Architecture:** Keep Hugging Face Space as the public Gradio frontend and Modal as the hidden hosted MiniCPM route. Add deterministic local grounding helpers in `dream_customs/pipeline.py` so weak hosted output is repaired before rendering. Keep the UI thin and mobile-first by moving backend controls behind a collapsed developer accordion and adding user-facing processing copy without adding new backend dependencies.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Python, Pydantic, Gradio Blocks, pytest, Hugging Face Space, Modal hosted endpoints.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## File Structure
|
| 14 |
+
|
| 15 |
+
- Modify `dream_customs/pipeline.py`: extract concrete dream details, detect generic model output, and repair pact fields with dream-specific visitor/action/release copy.
|
| 16 |
+
- Modify `dream_customs/prompts.py`: tighten MiniCPM text prompts so generated questions and pact cards must reuse concrete dream details.
|
| 17 |
+
- Modify `dream_customs/ui/app.py`: collapse developer settings by default and add story-aware loading/status text near the main action.
|
| 18 |
+
- Modify `dream_customs/ui/copy.py`: add public-facing processing copy.
|
| 19 |
+
- Modify `dream_customs/ui/styles.py`: style the processing note and make the collapsed developer panel quieter on desktop and mobile.
|
| 20 |
+
- Modify `tests/test_pipeline.py`: add regression tests for dream-grounded hosted output repair.
|
| 21 |
+
- Modify `tests/test_ui_actions.py`: add regression tests for public UI defaults and processing copy.
|
| 22 |
+
|
| 23 |
+
## Task 0: Baseline And Branch Hygiene
|
| 24 |
+
|
| 25 |
+
**Files:**
|
| 26 |
+
- Read: `AGENTS.md`
|
| 27 |
+
- Read: `PRODUCT.md`
|
| 28 |
+
- Read: `docs/spec.md`
|
| 29 |
+
- Read: `dream_customs/pipeline.py`
|
| 30 |
+
- Read: `dream_customs/ui/app.py`
|
| 31 |
+
|
| 32 |
+
- [x] **Step 1: Confirm repository and remotes**
|
| 33 |
+
|
| 34 |
+
Run:
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
pwd
|
| 38 |
+
git status --short --branch
|
| 39 |
+
git remote -v
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Expected:
|
| 43 |
+
|
| 44 |
+
```text
|
| 45 |
+
/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon
|
| 46 |
+
## feature/dream-customs-ui-voice-settings...origin/feature/dream-customs-ui-voice-settings
|
| 47 |
+
origin https://github.com/adjcjh777/dream-customs-build-small.git
|
| 48 |
+
space https://huggingface.co/spaces/build-small-hackathon/dream-customs
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
- [x] **Step 2: Sync latest code**
|
| 52 |
+
|
| 53 |
+
Run:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
git pull
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Expected:
|
| 60 |
+
|
| 61 |
+
```text
|
| 62 |
+
Already up to date.
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Task 1: Ground Weak Pact Output In Dream Details
|
| 66 |
+
|
| 67 |
+
**Files:**
|
| 68 |
+
- Modify: `tests/test_pipeline.py`
|
| 69 |
+
- Modify: `dream_customs/pipeline.py`
|
| 70 |
+
|
| 71 |
+
- [x] **Step 1: Add failing regression test for generic hosted output**
|
| 72 |
+
|
| 73 |
+
Add a test in `tests/test_pipeline.py` that uses a weak text client returning a generic visitor, generic health tip, generic odd task, and bare time release for this input:
|
| 74 |
+
|
| 75 |
+
```text
|
| 76 |
+
I dreamed I was at a customs window carrying a suitcase full of wet paper. The clerk asked me to declare every unfinished promise before sunrise.
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Expected repaired card:
|
| 80 |
+
|
| 81 |
+
- `visitor_name` contains either `Wet Paper` or `Unfinished Promise`.
|
| 82 |
+
- `alliance_reading` mentions at least one of `wet paper`, `customs window`, or `unfinished promise`.
|
| 83 |
+
- `practical_suggestion` mentions a first step or promise, not generic hydration.
|
| 84 |
+
- `weird_task` mentions paper, promise, suitcase, or customs.
|
| 85 |
+
- `bedtime_release` is not a bare time such as `7:00 PM`.
|
| 86 |
+
|
| 87 |
+
- [x] **Step 2: Run focused failing test**
|
| 88 |
+
|
| 89 |
+
Run:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
.venv/bin/python -m pytest tests/test_pipeline.py::test_generate_pact_repairs_generic_hosted_output_with_dream_details -q
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Expected: FAIL because current polishing does not repair all generic-but-valid hosted output.
|
| 96 |
+
|
| 97 |
+
- [x] **Step 3: Implement dream-detail extraction and repair**
|
| 98 |
+
|
| 99 |
+
In `dream_customs/pipeline.py`:
|
| 100 |
+
|
| 101 |
+
- Add `_extract_dream_anchors(intake)` returning up to three concrete lower-case noun phrases from dream text, voice transcript, and visual clues.
|
| 102 |
+
- Add `_is_generic_daily_tip(text)` for generic health/productivity suggestions that do not reference the dream.
|
| 103 |
+
- Add `_is_bare_time_or_generic_release(text)`.
|
| 104 |
+
- Extend `_polish_card_for_daily_use()` so weak visitor names, alliance readings, practical suggestions, weird tasks, and bedtime releases are repaired with the anchors.
|
| 105 |
+
- Keep existing safety behavior and dated permit stamping unchanged.
|
| 106 |
+
|
| 107 |
+
- [x] **Step 4: Run focused test until green**
|
| 108 |
+
|
| 109 |
+
Run:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
.venv/bin/python -m pytest tests/test_pipeline.py::test_generate_pact_repairs_generic_hosted_output_with_dream_details -q
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Expected: PASS.
|
| 116 |
+
|
| 117 |
+
## Task 2: Tighten MiniCPM Prompt Contracts
|
| 118 |
+
|
| 119 |
+
**Files:**
|
| 120 |
+
- Modify: `tests/test_pipeline.py`
|
| 121 |
+
- Modify: `dream_customs/prompts.py`
|
| 122 |
+
|
| 123 |
+
- [x] **Step 1: Add prompt contract test**
|
| 124 |
+
|
| 125 |
+
Add a test proving `pact_prompt()` includes explicit grounding constraints:
|
| 126 |
+
|
| 127 |
+
- reuse at least two concrete dream details;
|
| 128 |
+
- avoid generic wellness filler;
|
| 129 |
+
- `bedtime_release` must be a sentence, not a time;
|
| 130 |
+
- `visitor_name` must be an object/event from the dream, not a human name unless a person appeared.
|
| 131 |
+
|
| 132 |
+
- [x] **Step 2: Run focused failing test**
|
| 133 |
+
|
| 134 |
+
Run:
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
.venv/bin/python -m pytest tests/test_pipeline.py::test_pact_prompt_requires_dream_grounded_card -q
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Expected: FAIL until prompt copy is tightened.
|
| 141 |
+
|
| 142 |
+
- [x] **Step 3: Update prompts**
|
| 143 |
+
|
| 144 |
+
In `dream_customs/prompts.py`, add concrete instructions to `negotiation_prompt()`, `followup_question_prompt()`, `pact_prompt()`, and `pact_revision_prompt()` that keep the model plain, non-clinical, and dream-specific.
|
| 145 |
+
|
| 146 |
+
- [x] **Step 4: Run prompt contract test**
|
| 147 |
+
|
| 148 |
+
Run:
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
.venv/bin/python -m pytest tests/test_pipeline.py::test_pact_prompt_requires_dream_grounded_card -q
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Expected: PASS.
|
| 155 |
+
|
| 156 |
+
## Task 3: Hide Developer Settings From Ordinary Users
|
| 157 |
+
|
| 158 |
+
**Files:**
|
| 159 |
+
- Modify: `tests/test_ui_actions.py`
|
| 160 |
+
- Modify: `dream_customs/ui/app.py`
|
| 161 |
+
- Modify: `dream_customs/ui/styles.py`
|
| 162 |
+
|
| 163 |
+
- [x] **Step 1: Add UI source regression test**
|
| 164 |
+
|
| 165 |
+
Add a test that inspects `dream_customs.ui.app` source and asserts `Runtime settings` accordion is created with `open=False`.
|
| 166 |
+
|
| 167 |
+
- [x] **Step 2: Run focused failing test**
|
| 168 |
+
|
| 169 |
+
Run:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
.venv/bin/python -m pytest tests/test_ui_actions.py::test_runtime_settings_are_collapsed_for_public_flow -q
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
Expected: FAIL while the accordion is still `open=True`.
|
| 176 |
+
|
| 177 |
+
- [x] **Step 3: Collapse and quiet the developer panel**
|
| 178 |
+
|
| 179 |
+
Change `with gr.Accordion("Runtime settings", open=True, elem_classes=["dc-dev"]):` to `open=False`, and update CSS so collapsed runtime settings look like a quiet secondary control rather than the next stage of the user flow.
|
| 180 |
+
|
| 181 |
+
- [x] **Step 4: Run focused UI test**
|
| 182 |
+
|
| 183 |
+
Run:
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
.venv/bin/python -m pytest tests/test_ui_actions.py::test_runtime_settings_are_collapsed_for_public_flow -q
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
Expected: PASS.
|
| 190 |
+
|
| 191 |
+
## Task 4: Add Story-Aware Processing Copy
|
| 192 |
+
|
| 193 |
+
**Files:**
|
| 194 |
+
- Modify: `tests/test_ui_actions.py`
|
| 195 |
+
- Modify: `dream_customs/ui/copy.py`
|
| 196 |
+
- Modify: `dream_customs/ui/app.py`
|
| 197 |
+
- Modify: `dream_customs/ui/styles.py`
|
| 198 |
+
|
| 199 |
+
- [x] **Step 1: Add copy regression test**
|
| 200 |
+
|
| 201 |
+
Add a test that imports `PROCESSING_NOTE` and asserts it mentions the clerk/model route without using secret/backend jargon such as `token`, `endpoint`, or `debug`.
|
| 202 |
+
|
| 203 |
+
- [x] **Step 2: Run focused failing test**
|
| 204 |
+
|
| 205 |
+
Run:
|
| 206 |
+
|
| 207 |
+
```bash
|
| 208 |
+
.venv/bin/python -m pytest tests/test_ui_actions.py::test_processing_note_is_story_copy_not_backend_jargon -q
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
Expected: FAIL until `PROCESSING_NOTE` exists.
|
| 212 |
+
|
| 213 |
+
- [x] **Step 3: Add and render processing note**
|
| 214 |
+
|
| 215 |
+
Add `PROCESSING_NOTE` to `dream_customs/ui/copy.py` and render it near the submit buttons in `dream_customs/ui/app.py`.
|
| 216 |
+
|
| 217 |
+
Recommended copy:
|
| 218 |
+
|
| 219 |
+
```text
|
| 220 |
+
When you file a dream, the clerk reads the fragment and drafts a pass. Model-backed routes may take a few seconds; text-only fallback stays ready.
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
- [x] **Step 4: Style processing note**
|
| 224 |
+
|
| 225 |
+
Add `.dc-processing-note` styles in `dream_customs/ui/styles.py` with small, readable text and no layout shift on mobile.
|
| 226 |
+
|
| 227 |
+
- [x] **Step 5: Run focused copy test**
|
| 228 |
+
|
| 229 |
+
Run:
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
.venv/bin/python -m pytest tests/test_ui_actions.py::test_processing_note_is_story_copy_not_backend_jargon -q
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
Expected: PASS.
|
| 236 |
+
|
| 237 |
+
## Task 5: Full Verification And Deployment
|
| 238 |
+
|
| 239 |
+
**Files:**
|
| 240 |
+
- Verify: all modified files
|
| 241 |
+
|
| 242 |
+
- [x] **Step 1: Run full test suite**
|
| 243 |
+
|
| 244 |
+
Run:
|
| 245 |
+
|
| 246 |
+
```bash
|
| 247 |
+
.venv/bin/python -m pytest -q
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
Expected: all tests pass.
|
| 251 |
+
|
| 252 |
+
- [x] **Step 2: Run whitespace check**
|
| 253 |
+
|
| 254 |
+
Run:
|
| 255 |
+
|
| 256 |
+
```bash
|
| 257 |
+
git diff --check
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
Expected: no output.
|
| 261 |
+
|
| 262 |
+
- [x] **Step 3: Run local Gradio smoke**
|
| 263 |
+
|
| 264 |
+
Run:
|
| 265 |
+
|
| 266 |
+
```bash
|
| 267 |
+
GRADIO_SERVER_PORT=7862 .venv/bin/python app.py
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
Open `http://127.0.0.1:7862`, submit the customs-window wet-paper dream, skip optional question, and verify:
|
| 271 |
+
|
| 272 |
+
- runtime settings are collapsed by default;
|
| 273 |
+
- the processing note is visible before submit;
|
| 274 |
+
- the final pass references wet paper/customs/unfinished promises;
|
| 275 |
+
- mobile width has no obvious overflow.
|
| 276 |
+
|
| 277 |
+
- [ ] **Step 4: Commit and push GitHub branch**
|
| 278 |
+
|
| 279 |
+
Run:
|
| 280 |
+
|
| 281 |
+
```bash
|
| 282 |
+
git add docs/superpowers/plans/2026-06-08-dream-customs-story-ux-polish.md dream_customs/pipeline.py dream_customs/prompts.py dream_customs/ui/app.py dream_customs/ui/copy.py dream_customs/ui/styles.py tests/test_pipeline.py tests/test_ui_actions.py
|
| 283 |
+
git commit -m "fix: ground dream customs story flow"
|
| 284 |
+
git push origin feature/dream-customs-ui-voice-settings
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
- [ ] **Step 5: Sync Hugging Face Space and verify live app**
|
| 288 |
+
|
| 289 |
+
Push or create a Space PR to `space/main` according to the current auth state. Then verify the live Space `/config` and public app page show the updated runtime settings behavior and pass generation flow.
|
| 290 |
+
|
| 291 |
+
Stop and report if HF auth, forced public `main` overwrite, or PR merge permissions block completion.
|
dream_customs/pipeline.py
CHANGED
|
@@ -45,6 +45,114 @@ def _stamp_card_for_today(card: PactCard) -> PactCard:
|
|
| 45 |
return stamped
|
| 46 |
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
def _looks_unclear_or_dream_literal(text: str) -> bool:
|
| 49 |
clean = (text or "").strip()
|
| 50 |
if len(clean) < 12:
|
|
@@ -72,6 +180,41 @@ def _looks_unclear_or_dream_literal(text: str) -> bool:
|
|
| 72 |
return any(term in clean for term in dream_literals)
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def _safe_practical_suggestion(intake: DreamIntake) -> str:
|
| 76 |
mood = intake.mood.strip().lower()
|
| 77 |
if mood in {"uneasy", "foggy", "tired", "焦虑", "迷雾", "累"}:
|
|
@@ -86,27 +229,96 @@ def _safe_weird_task(intake: DreamIntake) -> str:
|
|
| 86 |
return "Write your smallest task on paper, draw a tiny clearance stamp beside it, and work on it for just five minutes."
|
| 87 |
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 90 |
polished = card.model_copy(deep=True)
|
| 91 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
|
| 95 |
if _looks_unclear_or_dream_literal(polished.practical_suggestion):
|
| 96 |
polished.practical_suggestion = _safe_practical_suggestion(intake)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
if _looks_unclear_or_dream_literal(polished.weird_task) and polished.weird_task.strip() == polished.practical_suggestion.strip():
|
| 99 |
polished.weird_task = _safe_weird_task(intake)
|
| 100 |
elif len((polished.weird_task or "").strip()) < 8:
|
| 101 |
polished.weird_task = _safe_weird_task(intake)
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
if polished.risk_level.strip() in {"低", "中", "高", "low", "medium", "high"}:
|
| 109 |
polished.risk_level = "medium: handle gently, without treating it as a warning sign"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
if not needs_escalation(merged):
|
| 112 |
polished.safety_note = ""
|
|
@@ -318,6 +530,17 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 318 |
fresh_questions = ["If today only needs one smaller first step, what should that step be?"]
|
| 319 |
if not fresh_questions:
|
| 320 |
fresh_questions = questions[:3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
next_session.question_history.extend(fresh_questions[:3])
|
| 323 |
next_session.phase = "negotiating"
|
|
|
|
| 45 |
return stamped
|
| 46 |
|
| 47 |
|
| 48 |
+
_ANCHOR_STOPWORDS = {
|
| 49 |
+
"about",
|
| 50 |
+
"after",
|
| 51 |
+
"again",
|
| 52 |
+
"asked",
|
| 53 |
+
"before",
|
| 54 |
+
"behind",
|
| 55 |
+
"being",
|
| 56 |
+
"carrying",
|
| 57 |
+
"declare",
|
| 58 |
+
"dream",
|
| 59 |
+
"dreamed",
|
| 60 |
+
"dreamt",
|
| 61 |
+
"every",
|
| 62 |
+
"feeling",
|
| 63 |
+
"fragment",
|
| 64 |
+
"from",
|
| 65 |
+
"full",
|
| 66 |
+
"into",
|
| 67 |
+
"last",
|
| 68 |
+
"left",
|
| 69 |
+
"night",
|
| 70 |
+
"paper",
|
| 71 |
+
"promise",
|
| 72 |
+
"through",
|
| 73 |
+
"today",
|
| 74 |
+
"window",
|
| 75 |
+
"with",
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _dedupe_preserve_order(items: List[str]) -> List[str]:
|
| 80 |
+
seen = set()
|
| 81 |
+
result: List[str] = []
|
| 82 |
+
for item in items:
|
| 83 |
+
clean = re.sub(r"\s+", " ", item.strip(" .,:;!?\"'()[]{}")).lower()
|
| 84 |
+
if clean and clean not in seen:
|
| 85 |
+
result.append(clean)
|
| 86 |
+
seen.add(clean)
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
| 91 |
+
text = " ".join(
|
| 92 |
+
[
|
| 93 |
+
intake.dream_text,
|
| 94 |
+
intake.voice_transcript,
|
| 95 |
+
" ".join(intake.visual_clues),
|
| 96 |
+
" ".join(intake.recurring_symbols),
|
| 97 |
+
]
|
| 98 |
+
).lower()
|
| 99 |
+
candidates: List[str] = []
|
| 100 |
+
pair_pattern = re.compile(
|
| 101 |
+
r"\b([a-z][a-z'-]+)\s+("
|
| 102 |
+
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 103 |
+
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 104 |
+
r"gate|gates|floor|floors|stamp|stamps|number|numbers"
|
| 105 |
+
r")\b"
|
| 106 |
+
)
|
| 107 |
+
for match in pair_pattern.finditer(text):
|
| 108 |
+
modifier, noun = match.groups()
|
| 109 |
+
phrase = f"{modifier} {noun.rstrip('s')}"
|
| 110 |
+
if modifier not in _ANCHOR_STOPWORDS:
|
| 111 |
+
candidates.append(phrase)
|
| 112 |
+
|
| 113 |
+
noun_pattern = re.compile(
|
| 114 |
+
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor)\b"
|
| 115 |
+
)
|
| 116 |
+
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 117 |
+
candidates.extend(clue.lower() for clue in intake.visual_clues if clue.strip())
|
| 118 |
+
|
| 119 |
+
return _dedupe_preserve_order(candidates)[:3]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _primary_anchor(intake: DreamIntake) -> str:
|
| 123 |
+
anchors = _extract_dream_anchors(intake)
|
| 124 |
+
return anchors[0] if anchors else "night visitor"
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _secondary_anchor(intake: DreamIntake) -> str:
|
| 128 |
+
anchors = _extract_dream_anchors(intake)
|
| 129 |
+
return anchors[1] if len(anchors) > 1 else _primary_anchor(intake)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _title_anchor(text: str) -> str:
|
| 133 |
+
return " ".join(part.capitalize() for part in text.split())
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _text_uses_anchor(text: str, anchors: List[str]) -> bool:
|
| 137 |
+
clean = (text or "").lower()
|
| 138 |
+
return any(anchor in clean for anchor in anchors)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _is_generic_visitor_name(text: str, intake: DreamIntake) -> bool:
|
| 142 |
+
clean = (text or "").strip()
|
| 143 |
+
if not clean:
|
| 144 |
+
return True
|
| 145 |
+
lowered = clean.lower()
|
| 146 |
+
merged = intake.merged_text().lower()
|
| 147 |
+
generic_names = {"dreamer", "night visitor", "elena", "visitor", "the visitor"}
|
| 148 |
+
if lowered in generic_names:
|
| 149 |
+
return True
|
| 150 |
+
anchors = _extract_dream_anchors(intake)
|
| 151 |
+
if anchors and not _text_uses_anchor(lowered, anchors) and lowered not in merged and len(clean.split()) <= 2:
|
| 152 |
+
return True
|
| 153 |
+
return False
|
| 154 |
+
|
| 155 |
+
|
| 156 |
def _looks_unclear_or_dream_literal(text: str) -> bool:
|
| 157 |
clean = (text or "").strip()
|
| 158 |
if len(clean) < 12:
|
|
|
|
| 180 |
return any(term in clean for term in dream_literals)
|
| 181 |
|
| 182 |
|
| 183 |
+
def _is_generic_daily_tip(text: str, anchors: List[str]) -> bool:
|
| 184 |
+
clean = (text or "").lower()
|
| 185 |
+
generic_markers = [
|
| 186 |
+
"hydrate",
|
| 187 |
+
"dehydration",
|
| 188 |
+
"piece of fruit",
|
| 189 |
+
"cognitive function",
|
| 190 |
+
"morning routine",
|
| 191 |
+
"take a short walk",
|
| 192 |
+
"eat something",
|
| 193 |
+
"drink water",
|
| 194 |
+
]
|
| 195 |
+
return any(marker in clean for marker in generic_markers) and not _text_uses_anchor(clean, anchors)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _is_generic_weird_task(text: str, anchors: List[str]) -> bool:
|
| 199 |
+
clean = (text or "").lower()
|
| 200 |
+
generic_markers = [
|
| 201 |
+
"count the number of birds",
|
| 202 |
+
"requires no special skills",
|
| 203 |
+
"harmless and playful activity",
|
| 204 |
+
"salute the kettle",
|
| 205 |
+
]
|
| 206 |
+
return any(marker in clean for marker in generic_markers) and not _text_uses_anchor(clean, anchors)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _is_bare_time_or_generic_release(text: str) -> bool:
|
| 210 |
+
clean = (text or "").strip()
|
| 211 |
+
if re.fullmatch(r"\d{1,2}:\d{2}\s*(?:am|pm|AM|PM)?", clean):
|
| 212 |
+
return True
|
| 213 |
+
if len(clean.split()) <= 3:
|
| 214 |
+
return True
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
|
| 218 |
def _safe_practical_suggestion(intake: DreamIntake) -> str:
|
| 219 |
mood = intake.mood.strip().lower()
|
| 220 |
if mood in {"uneasy", "foggy", "tired", "焦虑", "迷雾", "累"}:
|
|
|
|
| 229 |
return "Write your smallest task on paper, draw a tiny clearance stamp beside it, and work on it for just five minutes."
|
| 230 |
|
| 231 |
|
| 232 |
+
def _grounded_practical_suggestion(intake: DreamIntake) -> str:
|
| 233 |
+
primary = _primary_anchor(intake)
|
| 234 |
+
secondary = _secondary_anchor(intake)
|
| 235 |
+
if "promise" in primary or "promise" in secondary:
|
| 236 |
+
return (
|
| 237 |
+
"Choose one unfinished promise and shrink it into a first step small enough to finish in "
|
| 238 |
+
"10 minutes."
|
| 239 |
+
)
|
| 240 |
+
return (
|
| 241 |
+
f"Pick one real task that feels like the {primary}, then define only its first step for the next 10 minutes."
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _grounded_weird_task(intake: DreamIntake) -> str:
|
| 246 |
+
primary = _primary_anchor(intake)
|
| 247 |
+
secondary = _secondary_anchor(intake)
|
| 248 |
+
if "paper" in primary or "paper" in secondary:
|
| 249 |
+
return "Write one unfinished promise on a scrap of paper, fold it like a tiny suitcase, and stamp it cleared."
|
| 250 |
+
if "customs" in primary or "customs" in secondary:
|
| 251 |
+
return "Make a one-line customs form for today's smallest task and mark it cleared after five minutes."
|
| 252 |
+
return f"Draw the {primary} as a tiny customs stamp, press it once, and work for five minutes."
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def _grounded_bedtime_release(intake: DreamIntake) -> str:
|
| 256 |
+
primary = _primary_anchor(intake)
|
| 257 |
+
secondary = _secondary_anchor(intake)
|
| 258 |
+
return f"Tonight, the {primary} and the {secondary} are logged, cleared, and allowed to rest until morning."
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _grounded_alliance_reading(intake: DreamIntake) -> str:
|
| 262 |
+
primary = _primary_anchor(intake)
|
| 263 |
+
secondary = _secondary_anchor(intake)
|
| 264 |
+
return (
|
| 265 |
+
f"You can treat the {primary} and the {secondary} as last night's way of asking for one promise "
|
| 266 |
+
"to become smaller and easier to carry today."
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def _grounded_question(intake: DreamIntake, question: str) -> str:
|
| 271 |
+
anchors = _extract_dream_anchors(intake)
|
| 272 |
+
if not anchors or _text_uses_anchor(question, anchors):
|
| 273 |
+
return question
|
| 274 |
+
primary = _primary_anchor(intake)
|
| 275 |
+
secondary = _secondary_anchor(intake)
|
| 276 |
+
return (
|
| 277 |
+
f"When you picture the {primary} and the {secondary}, what is one real-life promise "
|
| 278 |
+
"or task you want to make easier today?"
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _grounded_followup_question(intake: DreamIntake) -> str:
|
| 283 |
+
primary = _primary_anchor(intake)
|
| 284 |
+
return f"If the {primary} could hand you one smaller first step for today, what would that step be?"
|
| 285 |
+
|
| 286 |
+
|
| 287 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 288 |
polished = card.model_copy(deep=True)
|
| 289 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 290 |
+
anchors = _extract_dream_anchors(intake)
|
| 291 |
+
if _is_generic_visitor_name(polished.visitor_name, intake):
|
| 292 |
+
polished.visitor_name = _title_anchor(_primary_anchor(intake))
|
| 293 |
|
| 294 |
if _looks_unclear_or_dream_literal(polished.practical_suggestion):
|
| 295 |
polished.practical_suggestion = _safe_practical_suggestion(intake)
|
| 296 |
+
elif _is_generic_daily_tip(polished.practical_suggestion, anchors):
|
| 297 |
+
polished.practical_suggestion = _grounded_practical_suggestion(intake)
|
| 298 |
+
elif anchors and not _text_uses_anchor(polished.practical_suggestion, anchors):
|
| 299 |
+
polished.practical_suggestion = _grounded_practical_suggestion(intake)
|
| 300 |
|
| 301 |
if _looks_unclear_or_dream_literal(polished.weird_task) and polished.weird_task.strip() == polished.practical_suggestion.strip():
|
| 302 |
polished.weird_task = _safe_weird_task(intake)
|
| 303 |
elif len((polished.weird_task or "").strip()) < 8:
|
| 304 |
polished.weird_task = _safe_weird_task(intake)
|
| 305 |
+
elif _is_generic_weird_task(polished.weird_task, anchors):
|
| 306 |
+
polished.weird_task = _grounded_weird_task(intake)
|
| 307 |
+
elif anchors and not _text_uses_anchor(polished.weird_task, anchors):
|
| 308 |
+
polished.weird_task = _grounded_weird_task(intake)
|
| 309 |
+
|
| 310 |
+
if (
|
| 311 |
+
len((polished.alliance_reading or "").strip()) < 12
|
| 312 |
+
or "联盟成员" in polished.alliance_reading
|
| 313 |
+
or (anchors and not _text_uses_anchor(polished.alliance_reading, anchors))
|
| 314 |
+
):
|
| 315 |
+
polished.alliance_reading = _grounded_alliance_reading(intake)
|
| 316 |
if polished.risk_level.strip() in {"低", "中", "高", "low", "medium", "high"}:
|
| 317 |
polished.risk_level = "medium: handle gently, without treating it as a warning sign"
|
| 318 |
+
if _is_bare_time_or_generic_release(polished.bedtime_release) or (
|
| 319 |
+
anchors and not _text_uses_anchor(polished.bedtime_release, anchors)
|
| 320 |
+
):
|
| 321 |
+
polished.bedtime_release = _grounded_bedtime_release(intake)
|
| 322 |
|
| 323 |
if not needs_escalation(merged):
|
| 324 |
polished.safety_note = ""
|
|
|
|
| 530 |
fresh_questions = ["If today only needs one smaller first step, what should that step be?"]
|
| 531 |
if not fresh_questions:
|
| 532 |
fresh_questions = questions[:3]
|
| 533 |
+
if fresh_questions:
|
| 534 |
+
fresh_questions = [_grounded_question(next_session.intake, fresh_questions[0])] + fresh_questions[1:]
|
| 535 |
+
seen_questions = set(next_session.question_history)
|
| 536 |
+
deduped_questions: List[str] = []
|
| 537 |
+
for question in fresh_questions:
|
| 538 |
+
if question and question not in seen_questions:
|
| 539 |
+
deduped_questions.append(question)
|
| 540 |
+
seen_questions.add(question)
|
| 541 |
+
if force_another and not deduped_questions:
|
| 542 |
+
deduped_questions = [_grounded_followup_question(next_session.intake)]
|
| 543 |
+
fresh_questions = deduped_questions
|
| 544 |
|
| 545 |
next_session.question_history.extend(fresh_questions[:3])
|
| 546 |
next_session.phase = "negotiating"
|
dream_customs/prompts.py
CHANGED
|
@@ -18,6 +18,9 @@ The tone should be gentle, plain, and specific. Do not make medical claims.
|
|
| 18 |
Ask questions that an ordinary person can understand without knowing the app lore.
|
| 19 |
Prefer questions about today's mood, one small real-life concern, or one safe action.
|
| 20 |
Do not ask vague symbolic questions such as what a stamp wants to release.
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
Dream intake:
|
| 23 |
{intake.merged_text()}
|
|
@@ -36,6 +39,8 @@ Do not diagnose. Do not repeat previous questions.
|
|
| 36 |
The question must be plain and useful: ask what the user wants to make easier today,
|
| 37 |
or whether there is one realistic thing they want help starting.
|
| 38 |
Do not use unclear metaphors about stamps, release, fate, symbols, or hidden meanings.
|
|
|
|
|
|
|
| 39 |
|
| 40 |
Dream intake:
|
| 41 |
{intake.merged_text()}
|
|
@@ -59,11 +64,16 @@ You are the Dream Customs diplomat. Generate a final Today's Pact card.
|
|
| 59 |
Do not diagnose. Do not claim the dream has one certain meaning.
|
| 60 |
The card must be useful for the user's real day, not only poetic.
|
| 61 |
Give:
|
| 62 |
-
-
|
| 63 |
-
-
|
|
|
|
|
|
|
|
|
|
| 64 |
- safety_note: empty string unless the user mentions self-harm, harming others, severe distress, severe insomnia, panic, or inability to function.
|
| 65 |
Use warm, non-clinical language.
|
| 66 |
-
Avoid generic names like "Dreamer"; name the dream visitor with a short, vivid phrase.
|
|
|
|
|
|
|
| 67 |
Write the final card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 68 |
|
| 69 |
Dream intake:
|
|
@@ -85,6 +95,10 @@ Keep the same dream visitor unless the user's new material clearly changes it.
|
|
| 85 |
Do not diagnose. Do not make the dream sound certain or frightening.
|
| 86 |
Keep practical_suggestion safe, concrete, and useful for daily life.
|
| 87 |
Keep weird_task playful but understandable and separate from the practical suggestion.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
Use safety_note only for severe distress or safety risk; otherwise return an empty string.
|
| 89 |
Write the revised card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 90 |
|
|
|
|
| 18 |
Ask questions that an ordinary person can understand without knowing the app lore.
|
| 19 |
Prefer questions about today's mood, one small real-life concern, or one safe action.
|
| 20 |
Do not ask vague symbolic questions such as what a stamp wants to release.
|
| 21 |
+
Ground every question in a concrete detail from the intake when possible, such as an object,
|
| 22 |
+
place, action, color, or phrase the user actually provided.
|
| 23 |
+
Do not invent a human name unless the user mentioned a person.
|
| 24 |
|
| 25 |
Dream intake:
|
| 26 |
{intake.merged_text()}
|
|
|
|
| 39 |
The question must be plain and useful: ask what the user wants to make easier today,
|
| 40 |
or whether there is one realistic thing they want help starting.
|
| 41 |
Do not use unclear metaphors about stamps, release, fate, symbols, or hidden meanings.
|
| 42 |
+
Reuse one concrete dream detail from the intake so the user can feel the question belongs
|
| 43 |
+
to this dream rather than to a generic reflection form.
|
| 44 |
|
| 45 |
Dream intake:
|
| 46 |
{intake.merged_text()}
|
|
|
|
| 64 |
Do not diagnose. Do not claim the dream has one certain meaning.
|
| 65 |
The card must be useful for the user's real day, not only poetic.
|
| 66 |
Give:
|
| 67 |
+
- visitor_name: a short object/event phrase from the dream, not a human name unless a person appears.
|
| 68 |
+
- alliance_reading: reuse at least two concrete dream details from the intake and say what the dream may be trying to protect today.
|
| 69 |
+
- practical_suggestion: one safe, concrete next step for today. Tie it to the user's dream details or answers. avoid generic wellness filler such as hydration, fruit, exercise, or sleep hygiene unless the user explicitly asked for that.
|
| 70 |
+
- weird_task: one harmless, playful, slightly odd thing doable in 5 minutes. It must reuse at least one concrete dream detail and be understandable.
|
| 71 |
+
- bedtime_release: bedtime_release must be a sentence-length release phrase, not a time, schedule, diagnosis, or command.
|
| 72 |
- safety_note: empty string unless the user mentions self-harm, harming others, severe distress, severe insomnia, panic, or inability to function.
|
| 73 |
Use warm, non-clinical language.
|
| 74 |
+
Avoid generic names like "Dreamer"; name the dream visitor with a short, vivid phrase from the user's own dream.
|
| 75 |
+
The final card must reuse at least two concrete dream details across visitor_name,
|
| 76 |
+
alliance_reading, practical_suggestion, weird_task, and bedtime_release.
|
| 77 |
Write the final card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 78 |
|
| 79 |
Dream intake:
|
|
|
|
| 95 |
Do not diagnose. Do not make the dream sound certain or frightening.
|
| 96 |
Keep practical_suggestion safe, concrete, and useful for daily life.
|
| 97 |
Keep weird_task playful but understandable and separate from the practical suggestion.
|
| 98 |
+
The revised card must reuse at least two concrete dream details from the intake.
|
| 99 |
+
visitor_name should be an object or event from the dream, not a human name unless a person appears.
|
| 100 |
+
bedtime_release must be a sentence, not a time or schedule.
|
| 101 |
+
Avoid generic wellness filler unless the user explicitly requested it.
|
| 102 |
Use safety_note only for severe distress or safety risk; otherwise return an empty string.
|
| 103 |
Write the revised card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 104 |
|
dream_customs/ui/app.py
CHANGED
|
@@ -35,6 +35,7 @@ from dream_customs.ui.copy import (
|
|
| 35 |
EXAMPLE_MOOD,
|
| 36 |
DEFAULT_MOOD,
|
| 37 |
MOOD_OPTIONS,
|
|
|
|
| 38 |
)
|
| 39 |
from dream_customs.ui.styles import CSS
|
| 40 |
|
|
@@ -371,6 +372,7 @@ def build_demo() -> gr.Blocks:
|
|
| 371 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 372 |
example_button = gr.Button("Try the sample →", variant="secondary")
|
| 373 |
submit_button = gr.Button("Issue today's pass →", variant="primary")
|
|
|
|
| 374 |
with gr.Accordion("Additional material", open=False, elem_classes=["dc-attachment-drawer"]):
|
| 375 |
image_input = gr.Image(label="Image clue", type="filepath", height=160)
|
| 376 |
|
|
@@ -420,7 +422,7 @@ def build_demo() -> gr.Blocks:
|
|
| 420 |
</div>
|
| 421 |
""".strip()
|
| 422 |
)
|
| 423 |
-
with gr.Accordion("Runtime settings", open=
|
| 424 |
gr.HTML(
|
| 425 |
"""
|
| 426 |
<div class="dc-dev-help">
|
|
|
|
| 35 |
EXAMPLE_MOOD,
|
| 36 |
DEFAULT_MOOD,
|
| 37 |
MOOD_OPTIONS,
|
| 38 |
+
PROCESSING_NOTE,
|
| 39 |
)
|
| 40 |
from dream_customs.ui.styles import CSS
|
| 41 |
|
|
|
|
| 372 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 373 |
example_button = gr.Button("Try the sample →", variant="secondary")
|
| 374 |
submit_button = gr.Button("Issue today's pass →", variant="primary")
|
| 375 |
+
gr.HTML(f"<p class='dc-processing-note'>{escape(PROCESSING_NOTE)}</p>")
|
| 376 |
with gr.Accordion("Additional material", open=False, elem_classes=["dc-attachment-drawer"]):
|
| 377 |
image_input = gr.Image(label="Image clue", type="filepath", height=160)
|
| 378 |
|
|
|
|
| 422 |
</div>
|
| 423 |
""".strip()
|
| 424 |
)
|
| 425 |
+
with gr.Accordion("Runtime settings", open=False, elem_classes=["dc-dev"]):
|
| 426 |
gr.HTML(
|
| 427 |
"""
|
| 428 |
<div class="dc-dev-help">
|
dream_customs/ui/copy.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
APP_TITLE = "Dream Customs"
|
| 2 |
APP_SUBTITLE = "Turn last night's strange fragment into one gentle clearance pass for today."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
DREAM_PLACEHOLDER = "For example: I kept missing an elevator, and the buttons melted like wax."
|
| 5 |
ANSWER_PLACEHOLDER = "Add one sentence if it helps, or leave this blank and skip ahead."
|
|
|
|
| 1 |
APP_TITLE = "Dream Customs"
|
| 2 |
APP_SUBTITLE = "Turn last night's strange fragment into one gentle clearance pass for today."
|
| 3 |
+
PROCESSING_NOTE = (
|
| 4 |
+
"When you file a dream, the clerk reads the fragment and drafts a pass. "
|
| 5 |
+
"Model-backed routes may take a few seconds; text-only fallback stays ready."
|
| 6 |
+
)
|
| 7 |
|
| 8 |
DREAM_PLACEHOLDER = "For example: I kept missing an elevator, and the buttons melted like wax."
|
| 9 |
ANSWER_PLACEHOLDER = "Add one sentence if it helps, or leave this blank and skip ahead."
|
dream_customs/ui/styles.py
CHANGED
|
@@ -591,6 +591,20 @@ button.secondary {
|
|
| 591 |
font-size: 1.05rem !important;
|
| 592 |
}
|
| 593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
.dc-attachment-drawer {
|
| 595 |
background: rgba(230, 221, 208, 0.52) !important;
|
| 596 |
border: 1px solid var(--dc-line) !important;
|
|
@@ -810,12 +824,18 @@ a.built-with[href*="gradio.app"] {
|
|
| 810 |
}
|
| 811 |
|
| 812 |
.dc-dev {
|
| 813 |
-
background: rgba(255, 249, 238, 0.
|
| 814 |
-
border: 1px
|
| 815 |
border-radius: var(--dc-radius-md) !important;
|
| 816 |
box-shadow: none;
|
| 817 |
margin-top: 0;
|
| 818 |
-
padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
}
|
| 820 |
|
| 821 |
.dc-dev-help {
|
|
|
|
| 591 |
font-size: 1.05rem !important;
|
| 592 |
}
|
| 593 |
|
| 594 |
+
.dc-processing-note {
|
| 595 |
+
color: var(--dc-muted);
|
| 596 |
+
font-size: 0.84rem;
|
| 597 |
+
line-height: 1.45;
|
| 598 |
+
margin: 12px 2px 0;
|
| 599 |
+
max-width: 44rem;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.dc-processing-note::before {
|
| 603 |
+
color: var(--dc-coral-dark);
|
| 604 |
+
content: "While the stamp dries: ";
|
| 605 |
+
font-weight: 850;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
.dc-attachment-drawer {
|
| 609 |
background: rgba(230, 221, 208, 0.52) !important;
|
| 610 |
border: 1px solid var(--dc-line) !important;
|
|
|
|
| 824 |
}
|
| 825 |
|
| 826 |
.dc-dev {
|
| 827 |
+
background: rgba(255, 249, 238, 0.46) !important;
|
| 828 |
+
border: 1px dashed rgba(189, 168, 143, 0.55) !important;
|
| 829 |
border-radius: var(--dc-radius-md) !important;
|
| 830 |
box-shadow: none;
|
| 831 |
margin-top: 0;
|
| 832 |
+
padding: 10px !important;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.dc-dev > button {
|
| 836 |
+
color: rgba(19, 41, 47, 0.74) !important;
|
| 837 |
+
font-size: 0.84rem !important;
|
| 838 |
+
min-height: 42px !important;
|
| 839 |
}
|
| 840 |
|
| 841 |
.dc-dev-help {
|
tests/test_pipeline.py
CHANGED
|
@@ -16,6 +16,7 @@ from dream_customs.pipeline import (
|
|
| 16 |
seal_pact,
|
| 17 |
skip_question,
|
| 18 |
)
|
|
|
|
| 19 |
from dream_customs.schema import PactCard
|
| 20 |
|
| 21 |
|
|
@@ -57,6 +58,23 @@ def test_generate_negotiation_returns_questions():
|
|
| 57 |
assert len(negotiation["questions"]) == 3
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
def test_generate_pact_returns_card_and_html():
|
| 61 |
intake = build_intake(dream_text="I missed an elevator.", mood="anxious")
|
| 62 |
card, html = generate_pact(intake, "I want a small start.", FakeTextClient())
|
|
@@ -95,6 +113,70 @@ def test_generate_pact_polishes_unclear_model_output_into_daily_tip():
|
|
| 95 |
assert "Life tip" in html
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def test_add_evidence_updates_session_with_text_image_audio_and_mood():
|
| 99 |
session = add_evidence(
|
| 100 |
create_session(),
|
|
|
|
| 16 |
seal_pact,
|
| 17 |
skip_question,
|
| 18 |
)
|
| 19 |
+
from dream_customs.prompts import pact_prompt
|
| 20 |
from dream_customs.schema import PactCard
|
| 21 |
|
| 22 |
|
|
|
|
| 58 |
assert len(negotiation["questions"]) == 3
|
| 59 |
|
| 60 |
|
| 61 |
+
def test_ask_questions_grounds_generic_question_in_dream_detail():
|
| 62 |
+
session = add_evidence(
|
| 63 |
+
create_session(),
|
| 64 |
+
dream_text=(
|
| 65 |
+
"I dreamed I was at a customs window carrying a suitcase full of wet paper. "
|
| 66 |
+
"The clerk asked me to declare every unfinished promise before sunrise."
|
| 67 |
+
),
|
| 68 |
+
mood="Uneasy",
|
| 69 |
+
vision_client=FakeVisionClient(),
|
| 70 |
+
asr_client=FakeASRClient(),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
session = ask_questions(session, FakeTextClient())
|
| 74 |
+
|
| 75 |
+
assert "customs window" in session.question_history[0].lower() or "wet paper" in session.question_history[0].lower()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
def test_generate_pact_returns_card_and_html():
|
| 79 |
intake = build_intake(dream_text="I missed an elevator.", mood="anxious")
|
| 80 |
card, html = generate_pact(intake, "I want a small start.", FakeTextClient())
|
|
|
|
| 113 |
assert "Life tip" in html
|
| 114 |
|
| 115 |
|
| 116 |
+
def test_generate_pact_repairs_generic_hosted_output_with_dream_details():
|
| 117 |
+
class GenericHostedTextClient:
|
| 118 |
+
def generate_pact(self, prompt):
|
| 119 |
+
return PactCard(
|
| 120 |
+
visitor_name="Elena",
|
| 121 |
+
permit_id="DC-015",
|
| 122 |
+
contraband=["unfiled worry", "one stamp asking to be noticed"],
|
| 123 |
+
risk_level="medium",
|
| 124 |
+
alliance_reading=(
|
| 125 |
+
"You can treat this as a small signal from last night's feelings, not a prophecy. "
|
| 126 |
+
"Today, protect a realistic pace."
|
| 127 |
+
),
|
| 128 |
+
practical_suggestion=(
|
| 129 |
+
"Hydrate and eat a piece of fruit to support your morning routine, as dehydration "
|
| 130 |
+
"can affect cognitive function."
|
| 131 |
+
),
|
| 132 |
+
weird_task=(
|
| 133 |
+
"Count the number of birds in the sky before bed, as it is a harmless and playful "
|
| 134 |
+
"activity that requires no special skills."
|
| 135 |
+
),
|
| 136 |
+
bedtime_release="7:00 PM",
|
| 137 |
+
safety_note="",
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
intake = build_intake(
|
| 141 |
+
dream_text=(
|
| 142 |
+
"I dreamed I was at a customs window carrying a suitcase full of wet paper. "
|
| 143 |
+
"The clerk asked me to declare every unfinished promise before sunrise."
|
| 144 |
+
),
|
| 145 |
+
mood="Uneasy",
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
card, html = generate_pact(intake, "", GenericHostedTextClient())
|
| 149 |
+
joined = "\n".join(
|
| 150 |
+
[
|
| 151 |
+
card.visitor_name,
|
| 152 |
+
card.alliance_reading,
|
| 153 |
+
card.practical_suggestion,
|
| 154 |
+
card.weird_task,
|
| 155 |
+
card.bedtime_release,
|
| 156 |
+
]
|
| 157 |
+
).lower()
|
| 158 |
+
|
| 159 |
+
assert "wet paper" in joined or "unfinished promise" in joined
|
| 160 |
+
assert "customs window" in card.alliance_reading.lower() or "wet paper" in card.alliance_reading.lower()
|
| 161 |
+
assert "promise" in card.practical_suggestion.lower() or "first step" in card.practical_suggestion.lower()
|
| 162 |
+
assert any(anchor in card.weird_task.lower() for anchor in ["paper", "promise", "suitcase", "customs"])
|
| 163 |
+
assert card.bedtime_release != "7:00 PM"
|
| 164 |
+
assert "Hydrate and eat a piece of fruit" not in html
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def test_pact_prompt_requires_dream_grounded_card():
|
| 168 |
+
intake = build_intake(
|
| 169 |
+
dream_text="I carried wet paper through a customs window before sunrise.",
|
| 170 |
+
mood="Uneasy",
|
| 171 |
+
)
|
| 172 |
+
prompt = pact_prompt(intake, "I want to keep one promise small today.")
|
| 173 |
+
|
| 174 |
+
assert "reuse at least two concrete dream details" in prompt
|
| 175 |
+
assert "avoid generic wellness filler" in prompt
|
| 176 |
+
assert "bedtime_release must be a sentence" in prompt
|
| 177 |
+
assert "not a human name unless a person appears" in prompt
|
| 178 |
+
|
| 179 |
+
|
| 180 |
def test_add_evidence_updates_session_with_text_image_audio_and_mood():
|
| 181 |
session = add_evidence(
|
| 182 |
create_session(),
|
tests/test_ui_actions.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
import json
|
|
|
|
| 2 |
from datetime import date
|
| 3 |
|
| 4 |
from dream_customs.ui.actions import answer_to_card_action, initial_mobile_state, skip_to_card_action, submit_dream_action
|
|
|
|
| 5 |
from dream_customs.ui.app import _reset
|
| 6 |
-
from dream_customs.ui.copy import DEFAULT_MOOD
|
| 7 |
|
| 8 |
|
| 9 |
def test_mobile_defaults_to_model_backends():
|
|
@@ -14,6 +16,22 @@ def test_mobile_defaults_to_model_backends():
|
|
| 14 |
assert view["debug"]["vision_backend"] == "model"
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def test_mobile_reset_restores_calm_mood():
|
| 18 |
settings_values = [
|
| 19 |
"",
|
|
|
|
| 1 |
import json
|
| 2 |
+
import inspect
|
| 3 |
from datetime import date
|
| 4 |
|
| 5 |
from dream_customs.ui.actions import answer_to_card_action, initial_mobile_state, skip_to_card_action, submit_dream_action
|
| 6 |
+
import dream_customs.ui.app as ui_app
|
| 7 |
from dream_customs.ui.app import _reset
|
| 8 |
+
from dream_customs.ui.copy import DEFAULT_MOOD, PROCESSING_NOTE
|
| 9 |
|
| 10 |
|
| 11 |
def test_mobile_defaults_to_model_backends():
|
|
|
|
| 16 |
assert view["debug"]["vision_backend"] == "model"
|
| 17 |
|
| 18 |
|
| 19 |
+
def test_runtime_settings_are_collapsed_for_public_flow():
|
| 20 |
+
source = inspect.getsource(ui_app.build_demo)
|
| 21 |
+
|
| 22 |
+
assert 'gr.Accordion("Runtime settings", open=False' in source
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_processing_note_is_story_copy_not_backend_jargon():
|
| 26 |
+
lowered = PROCESSING_NOTE.lower()
|
| 27 |
+
|
| 28 |
+
assert "clerk" in lowered
|
| 29 |
+
assert "model" in lowered
|
| 30 |
+
assert "token" not in lowered
|
| 31 |
+
assert "endpoint" not in lowered
|
| 32 |
+
assert "debug" not in lowered
|
| 33 |
+
|
| 34 |
+
|
| 35 |
def test_mobile_reset_restores_calm_mood():
|
| 36 |
settings_values = [
|
| 37 |
"",
|