kriyanshi Cursor commited on
Commit
9cbd5f0
·
1 Parent(s): e26a40b

Document parameterized replay and add Gmail/YouTube bindings.

Browse files

Update 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>

Files changed (4) hide show
  1. BLOG.md +27 -14
  2. README.md +30 -5
  3. data/skill_schemas.json +29 -2
  4. 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. [What's next](#whats-next-trajectory-parameterization)
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
- ## What's next: trajectory parameterization
628
 
629
- ### The current gap
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
- But trajectories are still recorded with **fixed entities** — the WhatsApp trajectory says "message hi to biraj" and the `set_text` actions contain `"hi"` and `"biraj"`. Replay uses those literal values, not the extracted parameters.
639
 
640
- ### The planned solution
641
 
642
- **Slot-filling at replay time**: when the model returns `{"contact": "mom", "message": "i'm on my way"}`, the replay engine:
643
 
644
- 1. Identifies parameterizable steps in the trajectory (text input actions)
645
- 2. Substitutes extracted values into `set_text` actions
646
- 3. Uses smart node resolution to find the contact field, search box, etc.
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
- The trajectory becomes a **template** rather than a fixed recording. Record once with placeholder entities, replay with any contact, message, time, or destination.
655
 
656
- ### Other future work
 
 
 
 
 
 
 
 
 
 
 
 
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 → Pocket Automator trajectory JSON → 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
  **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
- | **Demo UI** | Gradio (`app.py`) |
63
- | **Recorder** | [Pocket Automator](https://github.com/kriyanshii/pocket-automator) — Android accessibility capture & replay |
 
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
- Trajectory parameterization (slot-filling at replay time) is planned next.
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
- Preview-only for the Gradio demo today; Pocket Automator will mirror this at replay time.
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
- trajectory = load_trajectory("whatsapp_send_message")
162
  bound = apply_parameters(
163
- trajectory,
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()