Document parameterized replay and add Gmail/YouTube bindings.
Browse filesUpdate README and BLOG for the validated classify→bind→replay flow, and extend skill schemas plus ParameterBinder for Gmail and YouTube preview.
Co-authored-by: Cursor <cursoragent@cursor.com>
- BLOG.md +27 -14
- README.md +30 -5
- data/skill_schemas.json +29 -2
- src/parameter_binder.py +56 -3
BLOG.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 18 |
6. [Deployment and demo](#step-4-deploy-inference-on-modal-demo-on-gradio)
|
| 19 |
7. [Evaluation and benchmarks](#evaluation-how-we-measure-generalization)
|
| 20 |
8. [Why this approach works](#why-this-approach-works)
|
| 21 |
-
9. [
|
| 22 |
10. [Try it yourself](#try-it-yourself)
|
| 23 |
|
| 24 |
---
|
|
@@ -624,9 +624,9 @@ This project targets **personal automation on hardware you own** — the Backyar
|
|
| 624 |
|
| 625 |
---
|
| 626 |
|
| 627 |
-
##
|
| 628 |
|
| 629 |
-
### The
|
| 630 |
|
| 631 |
V2 extracts parameters at inference time:
|
| 632 |
|
|
@@ -635,15 +635,15 @@ V2 extracts parameters at inference time:
|
|
| 635 |
→ {"contact": "mom", "message": "i'm on my way"}
|
| 636 |
```
|
| 637 |
|
| 638 |
-
|
| 639 |
|
| 640 |
-
###
|
| 641 |
|
| 642 |
-
**
|
| 643 |
|
| 644 |
-
1.
|
| 645 |
-
2.
|
| 646 |
-
3.
|
| 647 |
|
| 648 |
This closes the loop:
|
| 649 |
|
|
@@ -651,14 +651,26 @@ This closes the loop:
|
|
| 651 |
Natural language → structured intent → parameterized replay on any device
|
| 652 |
```
|
| 653 |
|
| 654 |
-
|
| 655 |
|
| 656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
|
|
|
|
|
|
|
| 658 |
- **On-device inference** — run the 3B model locally without Modal
|
| 659 |
-
- **More skills** — maps, photos, settings toggles, banking apps
|
| 660 |
- **Multi-step intents** — "set alarm and text mom I'll be late"
|
| 661 |
-
- **Confidence calibration** — know when to ask the user for clarification
|
| 662 |
- **UI change detection** — alert when a trajectory needs re-recording
|
| 663 |
|
| 664 |
---
|
|
@@ -702,13 +714,14 @@ python app.py
|
|
| 702 |
```
|
| 703 |
app.py # Gradio demo (hackathon submission UI)
|
| 704 |
data/
|
| 705 |
-
skill_schemas.json # Parameter definitions per skill
|
| 706 |
skills.jsonl # Canonical skill ↔ task mapping
|
| 707 |
train_intent.jsonl # ~15k SFT examples (generated locally)
|
| 708 |
eval_intent_prompts.json # Held-out intent eval set
|
| 709 |
pocket_benchmark_prompts.json # 200 real-world messy prompts
|
| 710 |
src/
|
| 711 |
skill_router.py # Skill name → trajectory JSON
|
|
|
|
| 712 |
skill_utils.py # JSON parsing, aliases, fallbacks
|
| 713 |
classifier_prompt.py # System prompts for V1 and V2
|
| 714 |
evaluate_intent.py # Local evaluation
|
|
|
|
| 18 |
6. [Deployment and demo](#step-4-deploy-inference-on-modal-demo-on-gradio)
|
| 19 |
7. [Evaluation and benchmarks](#evaluation-how-we-measure-generalization)
|
| 20 |
8. [Why this approach works](#why-this-approach-works)
|
| 21 |
+
9. [Parameterized replay](#parameterized-replay-classify--bind--replay)
|
| 22 |
10. [Try it yourself](#try-it-yourself)
|
| 23 |
|
| 24 |
---
|
|
|
|
| 624 |
|
| 625 |
---
|
| 626 |
|
| 627 |
+
## Parameterized replay: classify → bind → replay
|
| 628 |
|
| 629 |
+
### The gap V2 closed
|
| 630 |
|
| 631 |
V2 extracts parameters at inference time:
|
| 632 |
|
|
|
|
| 635 |
→ {"contact": "mom", "message": "i'm on my way"}
|
| 636 |
```
|
| 637 |
|
| 638 |
+
Recorded trajectories still contain **fixed entities** — the WhatsApp export types `"Biraj"` and `"Hi"`. Without binding, replay ignores the model output.
|
| 639 |
|
| 640 |
+
### Slot-filling at replay time
|
| 641 |
|
| 642 |
+
**ParameterBinder** substitutes runtime values into trajectory steps before replay:
|
| 643 |
|
| 644 |
+
1. Load bindings from `data/skill_schemas.json` (which step maps to which parameter)
|
| 645 |
+
2. Rewrite `set_text` values and post-search `click` labels
|
| 646 |
+
3. Hand the bound trajectory to `ReplayPlanner` → `ReplayEngine`
|
| 647 |
|
| 648 |
This closes the loop:
|
| 649 |
|
|
|
|
| 651 |
Natural language → structured intent → parameterized replay on any device
|
| 652 |
```
|
| 653 |
|
| 654 |
+
**Validated end-to-end flow (WhatsApp on device):**
|
| 655 |
|
| 656 |
+
```
|
| 657 |
+
Modal /predict (or pasted JSON)
|
| 658 |
+
→ parameter dialog in Pocket Automator
|
| 659 |
+
→ ParameterBinder.apply(trajectory, parameters, bindings)
|
| 660 |
+
→ ReplayPlanner.plan → ReplayEngine.replay
|
| 661 |
+
→ WhatsApp taps with the extracted contact and message
|
| 662 |
+
```
|
| 663 |
+
|
| 664 |
+
The Gradio Space runs the same binding logic in Python (`src/parameter_binder.py`) so the trajectory JSON preview matches what replay will execute.
|
| 665 |
+
|
| 666 |
+
Bindings are defined per skill. **WhatsApp**, **Gmail**, and **YouTube** are supported in preview; Pocket Automator mirrors the schema on device.
|
| 667 |
+
|
| 668 |
+
### What's next
|
| 669 |
|
| 670 |
+
- **Self-contained exports** — embed `bindings` + `recordedParameters` in exported trajectory JSON (Phase B.8)
|
| 671 |
+
- **More skills** — Contacts, Calendar, Spotify search, etc.
|
| 672 |
- **On-device inference** — run the 3B model locally without Modal
|
|
|
|
| 673 |
- **Multi-step intents** — "set alarm and text mom I'll be late"
|
|
|
|
| 674 |
- **UI change detection** — alert when a trajectory needs re-recording
|
| 675 |
|
| 676 |
---
|
|
|
|
| 714 |
```
|
| 715 |
app.py # Gradio demo (hackathon submission UI)
|
| 716 |
data/
|
| 717 |
+
skill_schemas.json # Parameter definitions and trajectory bindings per skill
|
| 718 |
skills.jsonl # Canonical skill ↔ task mapping
|
| 719 |
train_intent.jsonl # ~15k SFT examples (generated locally)
|
| 720 |
eval_intent_prompts.json # Held-out intent eval set
|
| 721 |
pocket_benchmark_prompts.json # 200 real-world messy prompts
|
| 722 |
src/
|
| 723 |
skill_router.py # Skill name → trajectory JSON
|
| 724 |
+
parameter_binder.py # Runtime parameter → trajectory step substitution
|
| 725 |
skill_utils.py # JSON parsing, aliases, fallbacks
|
| 726 |
classifier_prompt.py # System prompts for V1 and V2
|
| 727 |
evaluate_intent.py # Local evaluation
|
README.md
CHANGED
|
@@ -29,7 +29,11 @@ You say *"text mom on whatsapp i'm on my way"* — a voice assistant might web-s
|
|
| 29 |
"play my workout playlist" → spotify_play_playlist → trajectories/spotify_play_playlist.json
|
| 30 |
```
|
| 31 |
|
| 32 |
-
**Tech:** fine-tuned [Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) via 4-bit QLoRA + SFT ([Unsloth](https://github.com/unslothai/unsloth) on Modal) → skill router →
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
**Submission links**
|
| 35 |
|
|
@@ -59,8 +63,9 @@ UI traces in `trajectories/` were captured with **[Pocket Automator](https://git
|
|
| 59 |
| **Base model** | [Qwen/Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) |
|
| 60 |
| **Fine-tune** | 4-bit QLoRA + SFT with [Unsloth](https://github.com/unslothai/unsloth) on Modal (`modal_apps/train_modal.py`) |
|
| 61 |
| **Inference** | Modal GPU API (`modal_apps/predict_api.py`) — returns skill + parameters |
|
| 62 |
-
| **
|
| 63 |
-
| **
|
|
|
|
| 64 |
| **Data** | 15 Android trajectories → `data/skills.jsonl` → ~510 prompt variations in `data/train.jsonl` |
|
| 65 |
|
| 66 |
## Quick start (local dev)
|
|
@@ -106,6 +111,7 @@ data/
|
|
| 106 |
skills.jsonl # Canonical skill ↔ task mapping
|
| 107 |
src/
|
| 108 |
skill_router.py # Skill name → trajectory JSON
|
|
|
|
| 109 |
skill_utils.py # Shared JSON parsing helpers
|
| 110 |
evaluate.py # Local CPU/MPS evaluation
|
| 111 |
modal_apps/ # Modal training + inference (not named "modal" — avoids import clash)
|
|
@@ -149,8 +155,27 @@ V1 maps prompts to a skill label only. V2 extracts structured intents:
|
|
| 149 |
|
| 150 |
The Gradio demo and Modal `/predict` API both return skill + parameters.
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
### Data
|
| 153 |
-
- `data/skill_schemas.json` — parameter definitions per skill
|
| 154 |
- `data/train_intent.jsonl` — ~15k synthetic SFT examples (generated locally via script; gitignored — upload to Modal for training)
|
| 155 |
- `data/eval_intent_prompts.json` — held-out intent eval set
|
| 156 |
- `data/pocket_benchmark_prompts.json` — 200 real-world messy prompts
|
|
@@ -171,7 +196,7 @@ modal run modal_apps/evaluate_pocket_benchmark_modal.py
|
|
| 171 |
| Parameter accuracy | 86.0% |
|
| 172 |
| Exact JSON match | 85.5% |
|
| 173 |
|
| 174 |
-
|
| 175 |
|
| 176 |
## License
|
| 177 |
|
|
|
|
| 29 |
"play my workout playlist" → spotify_play_playlist → trajectories/spotify_play_playlist.json
|
| 30 |
```
|
| 31 |
|
| 32 |
+
**Tech:** fine-tuned [Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) via 4-bit QLoRA + SFT ([Unsloth](https://github.com/unslothai/unsloth) on Modal) → skill router → parameterized trajectory → Pocket Automator replay on device. Fifteen real Android flows expand to ~15k synthetic intent examples for training; inference runs on Modal, demo UI on Gradio.
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
Modal /predict (or pasted JSON) → parameter dialog → ParameterBinder → replay → device taps
|
| 36 |
+
```
|
| 37 |
|
| 38 |
**Submission links**
|
| 39 |
|
|
|
|
| 63 |
| **Base model** | [Qwen/Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) |
|
| 64 |
| **Fine-tune** | 4-bit QLoRA + SFT with [Unsloth](https://github.com/unslothai/unsloth) on Modal (`modal_apps/train_modal.py`) |
|
| 65 |
| **Inference** | Modal GPU API (`modal_apps/predict_api.py`) — returns skill + parameters |
|
| 66 |
+
| **Parameter binding** | `src/parameter_binder.py` + `data/skill_schemas.json` bindings — substitutes runtime values into trajectory steps |
|
| 67 |
+
| **Demo UI** | Gradio (`app.py`) — shows parameterized trajectory preview |
|
| 68 |
+
| **Recorder / replay** | [Pocket Automator](https://github.com/kriyanshii/pocket-automator) — accessibility capture, parameter dialog, `ParameterBinder`, replay |
|
| 69 |
| **Data** | 15 Android trajectories → `data/skills.jsonl` → ~510 prompt variations in `data/train.jsonl` |
|
| 70 |
|
| 71 |
## Quick start (local dev)
|
|
|
|
| 111 |
skills.jsonl # Canonical skill ↔ task mapping
|
| 112 |
src/
|
| 113 |
skill_router.py # Skill name → trajectory JSON
|
| 114 |
+
parameter_binder.py # Runtime parameter → trajectory step substitution
|
| 115 |
skill_utils.py # Shared JSON parsing helpers
|
| 116 |
evaluate.py # Local CPU/MPS evaluation
|
| 117 |
modal_apps/ # Modal training + inference (not named "modal" — avoids import clash)
|
|
|
|
| 155 |
|
| 156 |
The Gradio demo and Modal `/predict` API both return skill + parameters.
|
| 157 |
|
| 158 |
+
### Parameterized replay
|
| 159 |
+
|
| 160 |
+
V2 extracts `{skill, parameters}` at inference time. **Slot-filling at replay** substitutes those values into recorded `set_text` / post-search click steps before replay.
|
| 161 |
+
|
| 162 |
+
**End-to-end flow (validated on WhatsApp):**
|
| 163 |
+
|
| 164 |
+
```
|
| 165 |
+
"text mom on whatsapp i'm on my way"
|
| 166 |
+
→ {"skill": "whatsapp_send_message", "parameters": {"contact": "mom", "message": "i'm on my way"}}
|
| 167 |
+
→ ParameterBinder (Gradio preview + Pocket Automator on device)
|
| 168 |
+
→ replay with "mom" / "i'm on my way", not the recorded "Biraj" / "Hi"
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
Bindings live in `data/skill_schemas.json` per skill. Supported in preview today: **WhatsApp**, **Gmail**, **YouTube**. Pocket Automator mirrors the same binding rules at replay time via its parameter dialog.
|
| 172 |
+
|
| 173 |
+
```bash
|
| 174 |
+
python -m src.parameter_binder # self-test bindings
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
### Data
|
| 178 |
+
- `data/skill_schemas.json` — parameter definitions and trajectory bindings per skill
|
| 179 |
- `data/train_intent.jsonl` — ~15k synthetic SFT examples (generated locally via script; gitignored — upload to Modal for training)
|
| 180 |
- `data/eval_intent_prompts.json` — held-out intent eval set
|
| 181 |
- `data/pocket_benchmark_prompts.json` — 200 real-world messy prompts
|
|
|
|
| 196 |
| Parameter accuracy | 86.0% |
|
| 197 |
| Exact JSON match | 85.5% |
|
| 198 |
|
| 199 |
+
**Next:** self-contained trajectory exports (bindings embedded in export JSON) and bindings for remaining skills.
|
| 200 |
|
| 201 |
## License
|
| 202 |
|
data/skill_schemas.json
CHANGED
|
@@ -143,7 +143,21 @@
|
|
| 143 |
"required": true,
|
| 144 |
"description": "YouTube search query"
|
| 145 |
}
|
| 146 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
},
|
| 148 |
"contacts_search": {
|
| 149 |
"description": "Search for a contact in the phone contacts app",
|
|
@@ -168,6 +182,19 @@
|
|
| 168 |
"required": true,
|
| 169 |
"description": "Email body text"
|
| 170 |
}
|
| 171 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
}
|
|
|
|
| 143 |
"required": true,
|
| 144 |
"description": "YouTube search query"
|
| 145 |
}
|
| 146 |
+
},
|
| 147 |
+
"bindings": [
|
| 148 |
+
{
|
| 149 |
+
"parameter": "query",
|
| 150 |
+
"action": "set_text",
|
| 151 |
+
"resource_id_suffix": "search_edit_text",
|
| 152 |
+
"package_contains": "youtube"
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"parameter": "query",
|
| 156 |
+
"action": "click",
|
| 157 |
+
"after_search": true,
|
| 158 |
+
"set_content_description": false
|
| 159 |
+
}
|
| 160 |
+
]
|
| 161 |
},
|
| 162 |
"contacts_search": {
|
| 163 |
"description": "Search for a contact in the phone contacts app",
|
|
|
|
| 182 |
"required": true,
|
| 183 |
"description": "Email body text"
|
| 184 |
}
|
| 185 |
+
},
|
| 186 |
+
"bindings": [
|
| 187 |
+
{
|
| 188 |
+
"parameter": "recipient",
|
| 189 |
+
"action": "set_text",
|
| 190 |
+
"compose_recipient": true
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"parameter": "message",
|
| 194 |
+
"action": "set_text",
|
| 195 |
+
"resource_id_suffix": "editor",
|
| 196 |
+
"package_contains": "gm"
|
| 197 |
+
}
|
| 198 |
+
]
|
| 199 |
}
|
| 200 |
}
|
src/parameter_binder.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Substitute runtime intent parameters into recorded trajectory steps.
|
| 3 |
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
|
@@ -34,6 +34,22 @@ _COMPOSE_BODY_SUFFIXES = (
|
|
| 34 |
"body",
|
| 35 |
)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
def load_skill_schemas() -> dict[str, Any]:
|
| 39 |
return json.loads(SKILL_SCHEMAS_PATH.read_text(encoding="utf-8"))
|
|
@@ -107,6 +123,9 @@ def _binding_matches(
|
|
| 107 |
if package_contains and package_contains.lower() not in (step.get("packageName") or "").lower():
|
| 108 |
return False
|
| 109 |
|
|
|
|
|
|
|
|
|
|
| 110 |
return True
|
| 111 |
|
| 112 |
|
|
@@ -158,9 +177,9 @@ def apply_parameters(
|
|
| 158 |
def _self_test() -> None:
|
| 159 |
from src.skill_router import load_trajectory
|
| 160 |
|
| 161 |
-
|
| 162 |
bound = apply_parameters(
|
| 163 |
-
|
| 164 |
"whatsapp_send_message",
|
| 165 |
{"contact": "mom", "message": "i'm on my way"},
|
| 166 |
)
|
|
@@ -189,6 +208,40 @@ def _self_test() -> None:
|
|
| 189 |
]
|
| 190 |
assert post_search_clicks and all(click.get("text") == "mom" for click in post_search_clicks)
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
if __name__ == "__main__":
|
| 194 |
_self_test()
|
|
|
|
| 1 |
"""
|
| 2 |
Substitute runtime intent parameters into recorded trajectory steps.
|
| 3 |
|
| 4 |
+
Used by the Gradio preview and mirrored in Pocket Automator's ParameterBinder at replay time.
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
|
|
|
| 34 |
"body",
|
| 35 |
)
|
| 36 |
|
| 37 |
+
_COMPOSE_EXCLUDED_SUFFIXES = (
|
| 38 |
+
"subject",
|
| 39 |
+
"editor",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _is_compose_recipient_step(step: dict[str, Any]) -> bool:
|
| 44 |
+
action = step.get("action") or {}
|
| 45 |
+
if action.get("type") != "set_text":
|
| 46 |
+
return False
|
| 47 |
+
package_name = (step.get("packageName") or "").lower()
|
| 48 |
+
if "gm" not in package_name and "gmail" not in package_name:
|
| 49 |
+
return False
|
| 50 |
+
resource_id = action.get("resourceId") or ""
|
| 51 |
+
return not any(_suffix_matches(resource_id, suffix) for suffix in _COMPOSE_EXCLUDED_SUFFIXES)
|
| 52 |
+
|
| 53 |
|
| 54 |
def load_skill_schemas() -> dict[str, Any]:
|
| 55 |
return json.loads(SKILL_SCHEMAS_PATH.read_text(encoding="utf-8"))
|
|
|
|
| 123 |
if package_contains and package_contains.lower() not in (step.get("packageName") or "").lower():
|
| 124 |
return False
|
| 125 |
|
| 126 |
+
if binding.get("compose_recipient"):
|
| 127 |
+
return _is_compose_recipient_step(step)
|
| 128 |
+
|
| 129 |
return True
|
| 130 |
|
| 131 |
|
|
|
|
| 177 |
def _self_test() -> None:
|
| 178 |
from src.skill_router import load_trajectory
|
| 179 |
|
| 180 |
+
whatsapp = load_trajectory("whatsapp_send_message")
|
| 181 |
bound = apply_parameters(
|
| 182 |
+
whatsapp,
|
| 183 |
"whatsapp_send_message",
|
| 184 |
{"contact": "mom", "message": "i'm on my way"},
|
| 185 |
)
|
|
|
|
| 208 |
]
|
| 209 |
assert post_search_clicks and all(click.get("text") == "mom" for click in post_search_clicks)
|
| 210 |
|
| 211 |
+
gmail = load_trajectory("gmail_send_email")
|
| 212 |
+
bound_gmail = apply_parameters(
|
| 213 |
+
gmail,
|
| 214 |
+
"gmail_send_email",
|
| 215 |
+
{"recipient": "alice@example.com", "message": "running late"},
|
| 216 |
+
)
|
| 217 |
+
recipient_values = [
|
| 218 |
+
step["action"]["value"]
|
| 219 |
+
for step in bound_gmail["steps"]
|
| 220 |
+
if _is_compose_recipient_step(step)
|
| 221 |
+
]
|
| 222 |
+
body_values = [
|
| 223 |
+
step["action"]["value"]
|
| 224 |
+
for step in bound_gmail["steps"]
|
| 225 |
+
if step["action"]["type"] == "set_text"
|
| 226 |
+
and _suffix_matches(step["action"].get("resourceId"), "editor")
|
| 227 |
+
]
|
| 228 |
+
assert recipient_values and all(value == "alice@example.com" for value in recipient_values), recipient_values
|
| 229 |
+
assert body_values and all(value == "running late" for value in body_values), body_values
|
| 230 |
+
|
| 231 |
+
youtube = load_trajectory("youtube_search")
|
| 232 |
+
bound_youtube = apply_parameters(
|
| 233 |
+
youtube,
|
| 234 |
+
"youtube_search",
|
| 235 |
+
{"query": "pasta recipes"},
|
| 236 |
+
)
|
| 237 |
+
youtube_search_values = [
|
| 238 |
+
step["action"]["value"]
|
| 239 |
+
for step in bound_youtube["steps"]
|
| 240 |
+
if step["action"]["type"] == "set_text"
|
| 241 |
+
and _suffix_matches(step["action"].get("resourceId"), "search_edit_text")
|
| 242 |
+
]
|
| 243 |
+
assert youtube_search_values and all(value == "pasta recipes" for value in youtube_search_values), youtube_search_values
|
| 244 |
+
|
| 245 |
|
| 246 |
if __name__ == "__main__":
|
| 247 |
_self_test()
|