kriyanshi commited on
Commit
d481ece
·
1 Parent(s): 40a90bb

Update hackathon submission docs with blog link and README tags.

Browse files

Add HF blog URL to README and BLOG, plus track/badge tags and submission links for Build Small.

BLOG.md ADDED
@@ -0,0 +1,743 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # From "Play My Workout Playlist" to a Real Android Tap Plan
2
+
3
+ **How a 3B-parameter model turns messy phone requests into replayable UI automation — without shipping your life to a cloud API.**
4
+
5
+ *Built for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon) — Backyard AI track, sponsored by Modal.*
6
+
7
+ **Published on Hugging Face:** [From "Play My Workout Playlist" to a Real Android Tap Plan](https://huggingface.co/blog/build-small-hackathon/android-skill-router)
8
+
9
+ ---
10
+
11
+ ## Table of contents
12
+
13
+ 1. [The problem](#the-problem-with-phone-automation-today)
14
+ 2. [The architecture](#the-architecture-classify--route--replay)
15
+ 3. [Recording trajectories](#step-1-record-real-ui-flows-on-android)
16
+ 4. [Training the classifier](#step-2-train-a-tiny-classifier-not-a-general-agent)
17
+ 5. [Synthetic data at scale](#step-3-synthetic-data-at-scale)
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
+ ---
25
+
26
+ ## The problem with phone automation today
27
+
28
+ You say: *"text mom on whatsapp i'm on my way."*
29
+
30
+ A voice assistant might reply with a web search, a generic "I can't do that," or a cloud API call that only works if WhatsApp cooperates. What you actually want is simpler and more direct: open WhatsApp, find Mom, type the message, send it.
31
+
32
+ That gap — between **natural language** and **deterministic UI actions on a real device** — is what **Android Skill Router** is built to close.
33
+
34
+ ### Why cloud agents fall short for personal automation
35
+
36
+ Most phone automation today follows one of two paths:
37
+
38
+ | Approach | Strength | Weakness |
39
+ | --- | --- | --- |
40
+ | **Cloud voice assistants** | Understand broad language | Can't tap your apps; privacy concerns; needs network |
41
+ | **Macro/script tools** | Deterministic replay | Require exact trigger phrases; no natural language |
42
+ | **Vision-based agents** | Flexible | Slow, expensive, hallucinate UI coordinates |
43
+
44
+ Android Skill Router takes a third path: **a small local classifier that understands messy language, paired with pre-recorded UI trajectories that an accessibility runtime replays exactly.**
45
+
46
+ The core insight:
47
+
48
+ > You don't need a 70B frontier model to *do* the tapping. You need a 3B model to understand *what you mean*, then hand off to a fixed replay plan.
49
+
50
+ ```
51
+ "play my workout playlist"
52
+ → spotify_play_playlist
53
+ → trajectories/spotify_play_playlist.json
54
+ → Pocket Automator replays taps on device
55
+ ```
56
+
57
+ This is the classifier layer of the **[Pocket Automator](https://github.com/kriyanshii/pocket-automator)** stack: record once on your phone, route forever with a tiny local model.
58
+
59
+ ---
60
+
61
+ ## The architecture: classify → route → replay
62
+
63
+ The system has three layers, each deliberately small and composable.
64
+
65
+ ```mermaid
66
+ flowchart LR
67
+ A[Natural language prompt] --> B[Fine-tuned Qwen2.5-3B]
68
+ B --> C["Structured intent\n{skill, parameters}"]
69
+ C --> D[Skill Router]
70
+ D --> E[Trajectory JSON]
71
+ E --> F[Pocket Automator replay]
72
+ ```
73
+
74
+ ### Layer 1: Intent classifier
75
+
76
+ A fine-tuned **Qwen2.5-3B-Instruct** model receives a user prompt and returns structured JSON:
77
+
78
+ ```json
79
+ {
80
+ "skill": "whatsapp_send_message",
81
+ "parameters": {
82
+ "contact": "mom",
83
+ "message": "i'm on my way"
84
+ }
85
+ }
86
+ ```
87
+
88
+ The model handles slang, typos, incomplete phrasing, and app disambiguation (WhatsApp vs Gmail vs Slack). It never invents UI steps — only picks from 15 known skills and extracts parameter slots.
89
+
90
+ ### Layer 2: Skill router
91
+
92
+ A deterministic lookup table maps skill names to trajectory files:
93
+
94
+ ```python
95
+ SKILL_TO_TRAJECTORY = {
96
+ "whatsapp_send_message": "trajectories/whatsapp_send_message.json",
97
+ "spotify_play_playlist": "trajectories/spotify_play_playlist.json",
98
+ # ... 15 skills total
99
+ }
100
+ ```
101
+
102
+ If the model returns `whatsapp_send_message`, the router loads `trajectories/whatsapp_send_message.json`. No guessing, no hallucination. If the skill doesn't exist or the file is missing, the system fails loudly with a clear error.
103
+
104
+ The router also includes **defensive parsing**: skill aliases (`send_whatsapp` → `whatsapp_send_message`), JSON extraction from noisy model output, and keyword fallbacks when the model returns an unknown label.
105
+
106
+ ### Layer 3: Trajectory replay
107
+
108
+ Each trajectory is a JSON file exported from **[Pocket Automator](https://github.com/kriyanshii/pocket-automator)** — an Android accessibility recorder. It contains:
109
+
110
+ - A **task description** (the original human intent)
111
+ - The **target app package** (`com.whatsapp`, `com.spotify.music`, etc.)
112
+ - A sequence of **steps**, each with a full UI tree snapshot and an action
113
+
114
+ Example step from a WhatsApp trajectory:
115
+
116
+ ```json
117
+ {
118
+ "timestamp": 4024,
119
+ "screen": { /* full accessibility tree */ },
120
+ "action": {
121
+ "type": "click",
122
+ "resourceId": "com.motorola.launcher3:id/icon",
123
+ "contentDescription": "WhatsApp",
124
+ "path": [0, 0, 0, 0, 2, 0, 0]
125
+ },
126
+ "packageName": "com.motorola.launcher3"
127
+ }
128
+ ```
129
+
130
+ Action types include `click`, `set_text`, and scroll gestures. Pocket Automator resolves nodes at replay time using resource IDs, content descriptions, and tree paths — so minor UI changes don't break the flow.
131
+
132
+ ### The separation of concerns
133
+
134
+ | Component | Responsibility | Can fail? |
135
+ | --- | --- | --- |
136
+ | Language model | Understand intent | Gracefully — fallbacks exist |
137
+ | Skill router | Map intent → file | Never — deterministic lookup |
138
+ | Trajectory | Store ground-truth UI steps | Never — fixed recording |
139
+ | Pocket Automator | Execute on device | Only if UI changed drastically |
140
+
141
+ This is the design bet: **language understanding is fuzzy; automation must be exact.**
142
+
143
+ ---
144
+
145
+ ## Step 1: Record real UI flows on Android
146
+
147
+ Every skill starts on hardware you own. No synthetic UI trees, no emulated taps — real recordings from a real Motorola device.
148
+
149
+ ### Pocket Automator: the Android recorder
150
+
151
+ **[Pocket Automator](https://github.com/kriyanshii/pocket-automator)** is an Android accessibility app that:
152
+
153
+ 1. **Records** taps, text input, and scrolls while you use any app
154
+ 2. **Captures** the full accessibility tree at each step (node IDs, bounds, class names, text)
155
+ 3. **Exports** recordings as JSON for training pipelines
156
+ 4. **Replays** saved recordings with smart node resolution
157
+
158
+ Requirements: Android 10+ (API 29), accessibility service enabled, overlay permission.
159
+
160
+ ### The recording workflow
161
+
162
+ 1. Open Pocket Automator and tap **Record**
163
+ 2. Name your task (e.g., "message hi to biraj on WhatsApp")
164
+ 3. Perform the task naturally on your phone
165
+ 4. Stop recording from the floating overlay
166
+ 5. Export the JSON to your development machine
167
+ 6. Place it in `trajectories/` and run `scripts/generate_skill_dataset.py`
168
+
169
+ The script reads each trajectory's `task` and `app` fields, derives a snake_case skill name, and writes `data/skills.jsonl`:
170
+
171
+ ```json
172
+ {"skill": "whatsapp_send_message", "task": "message hi to biraj on WhatsApp"}
173
+ {"skill": "spotify_play_playlist", "task": "play liked songs playlist from Spotify"}
174
+ {"skill": "create_alarm", "task": "create alarm for 7 am tomorrow"}
175
+ ```
176
+
177
+ Skill name derivation uses app package and task keywords — WhatsApp tasks become `whatsapp_send_message`, Spotify pause tasks become `spotify_pause`, and so on.
178
+
179
+ ### The 15 skills
180
+
181
+ | Skill | App | Example task |
182
+ | --- | --- | --- |
183
+ | `create_alarm` | Clock | Set alarm for 7 am tomorrow |
184
+ | `calendar_create_event` | Calendar | Create event tomorrow 4 pm |
185
+ | `wifi_enable` | Settings | Enable Wi-Fi |
186
+ | `bluetooth_enable` | Settings | Turn on Bluetooth |
187
+ | `whatsapp_send_message` | WhatsApp | Message a contact |
188
+ | `gmail_send_email` | Gmail | Send email to recipient |
189
+ | `slack_open_channel` | Slack | Open a channel |
190
+ | `spotify_play_playlist` | Spotify | Play a playlist |
191
+ | `spotify_search_play` | Spotify | Search and play music |
192
+ | `spotify_pause` | Spotify | Pause playback |
193
+ | `uber_request_ride` | Uber | Request ride to destination |
194
+ | `youtube_search` | YouTube | Search for videos |
195
+ | `linkedin_search_person` | LinkedIn | Search for a person |
196
+ | `contacts_search` | Contacts | Find a contact |
197
+ | `camera_take_photo` | Camera | Take a picture |
198
+
199
+ Each trajectory file is large (often 5,000+ lines) because it includes the full accessibility tree at every step. That's intentional — replay engines need rich node metadata to resolve targets reliably.
200
+
201
+ ### Why real recordings matter
202
+
203
+ Synthetic UI automation data is brittle. Real recordings capture:
204
+
205
+ - **Launcher states** — how your home screen looks with your app icons
206
+ - **Keyboard transitions** — when the soft keyboard appears during text input
207
+ - **Scroll positions** — where list items sit after scrolling
208
+ - **Timing** — natural pauses between actions
209
+
210
+ These details can't be generated. They're the ground truth that makes replay work on your specific device.
211
+
212
+ ---
213
+
214
+ ## Step 2: Train a tiny classifier, not a general agent
215
+
216
+ The model is **[Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct)** — deliberately under 4B parameters for the Build Small Hackathon's *Tiny Titan* achievement.
217
+
218
+ ### Why 3B is enough
219
+
220
+ The classification task is narrow:
221
+
222
+ - **15 skill labels** (not open-ended tool use)
223
+ - **Structured JSON output** (not free-form text)
224
+ - **Parameter slot-filling** (contact, message, time — not reasoning chains)
225
+
226
+ A 3B instruct model already understands apps, contacts, times, and natural language phrasing. Fine-tuning teaches it *your* skill taxonomy and output format — not general Android knowledge.
227
+
228
+ ### Training configuration
229
+
230
+ Training runs on **Modal** GPUs via `modal_apps/train_modal.py`:
231
+
232
+ | Hyperparameter | Value |
233
+ | --- | --- |
234
+ | Base model | Qwen2.5-3B-Instruct |
235
+ | Method | 4-bit QLoRA + SFT (Unsloth) |
236
+ | LoRA rank | 32 |
237
+ | LoRA alpha | 32 |
238
+ | Target modules | q/k/v/o_proj, gate/up/down_proj |
239
+ | Epochs | 5 |
240
+ | Batch size | 8 |
241
+ | Learning rate | 2e-4 |
242
+ | Optimizer | AdamW 8-bit |
243
+ | Max sequence length | 2048 |
244
+ | GPU | Modal A10G |
245
+
246
+ The training pipeline:
247
+
248
+ 1. Upload `data/train_intent.jsonl` to a Modal Volume
249
+ 2. Load base model in 4-bit quantization
250
+ 3. Apply QLoRA adapters to attention and MLP layers
251
+ 4. Format examples with Qwen 2.5 chat template
252
+ 5. Train with TRL's `SFTTrainer`
253
+ 6. Save LoRA adapter to `/model/adapter`
254
+ 7. Save merged 16-bit model to `/model/merged`
255
+
256
+ ```bash
257
+ python scripts/generate_intent_dataset.py
258
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
259
+ modal volume get android-dataset-model adapter ./trained_model/adapter
260
+ ```
261
+
262
+ ### V1 → V2: from labels to intents
263
+
264
+ **V1 (skill classification only)** mapped prompts to a skill name:
265
+
266
+ ```
267
+ "play my workout playlist" → {"skill": "spotify_play_playlist"}
268
+ ```
269
+
270
+ Training data: ~510 examples in `data/train.jsonl` (~30 variations per skill).
271
+
272
+ **V2 (structured intent extraction)** adds parameter slot-filling:
273
+
274
+ ```
275
+ "text mom on whatsapp i'm on my way"
276
+ → {"skill": "whatsapp_send_message", "parameters": {"contact": "mom", "message": "i'm on my way"}}
277
+ ```
278
+
279
+ Training data: ~15,000 examples in `data/train_intent.jsonl` (~1,000 per skill).
280
+
281
+ ### Parameter schemas
282
+
283
+ Each skill declares its parameters in `data/skill_schemas.json`:
284
+
285
+ ```json
286
+ {
287
+ "whatsapp_send_message": {
288
+ "description": "Send a WhatsApp message to a contact",
289
+ "parameters": {
290
+ "contact": {"type": "string", "required": true},
291
+ "message": {"type": "string", "required": true}
292
+ }
293
+ },
294
+ "create_alarm": {
295
+ "description": "Set an alarm at a specific time",
296
+ "parameters": {
297
+ "time": {"type": "string", "required": true},
298
+ "day": {"type": "string", "required": false}
299
+ }
300
+ },
301
+ "wifi_enable": {
302
+ "description": "Enable Wi-Fi on the device",
303
+ "parameters": {}
304
+ }
305
+ }
306
+ ```
307
+
308
+ Skills with no variable inputs (`wifi_enable`, `bluetooth_enable`, `spotify_pause`, `camera_take_photo`) return empty parameter objects.
309
+
310
+ ### The system prompt
311
+
312
+ The model receives a tight, deterministic instruction:
313
+
314
+ ```
315
+ You extract structured Android automation intents from natural language.
316
+ Reply with JSON only: {"skill": "<skill_name>", "parameters": {<extracted_fields>}}.
317
+ Pick exactly one skill. Extract all relevant parameters mentioned in the request
318
+ (contact names, messages, times, destinations, channel names, search queries, etc.).
319
+ Use an empty object for parameters when the skill needs none.
320
+ Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.)
321
+ to pick the correct skill.
322
+ ```
323
+
324
+ No chain-of-thought. No tool descriptions. No examples in the prompt. Just JSON.
325
+
326
+ ### Training example format
327
+
328
+ Each row in `train_intent.jsonl` is a three-turn chat:
329
+
330
+ ```json
331
+ {
332
+ "messages": [
333
+ {"role": "system", "content": "You extract structured Android automation intents..."},
334
+ {"role": "user", "content": "whatsapp message Vikram see you tonight"},
335
+ {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\",\"parameters\":{\"contact\":\"Vikram\",\"message\":\"see you tonight\"}}"}
336
+ ]
337
+ }
338
+ ```
339
+
340
+ The assistant always responds with compact JSON — no markdown fences, no explanation.
341
+
342
+ ---
343
+
344
+ ## Step 3: Synthetic data at scale
345
+
346
+ Fifteen real trajectories can't train a robust classifier alone. The project generates **~15,000 synthetic SFT examples** locally via `scripts/generate_intent_dataset.py`.
347
+
348
+ ### How data generation works
349
+
350
+ The generator follows a four-step pipeline:
351
+
352
+ ```
353
+ skill_schemas.json + skills.jsonl
354
+
355
+ Entity pools (contacts, messages, times, destinations...)
356
+
357
+ Template variations (24+ templates per skill)
358
+
359
+ train_intent.jsonl (~1000 examples/skill)
360
+ eval_intent_prompts.json (~6 held-out prompts/skill)
361
+ ```
362
+
363
+ ### Entity pools
364
+
365
+ Realistic but synthetic entities ensure diversity without privacy concerns:
366
+
367
+ | Pool | Examples |
368
+ | --- | --- |
369
+ | **Contacts** | Ri, Biraj, Mom, Parag Shah, grandma, my roommate |
370
+ | **Messages** | "see you soon", "running late", "project update attached" |
371
+ | **Alarm times** | 5 am, 6:30 am, 7 am, noon, 10 pm |
372
+ | **Alarm days** | today, tomorrow, monday, next friday |
373
+ | **Destinations** | airport, train station, home, office |
374
+ | **Playlists** | workout, liked songs, chill vibes, focus |
375
+ | **Channels** | engineering, general, data contributors |
376
+ | **Search queries** | pasta recipes, jazz music, ghibli food |
377
+
378
+ ### Template variations
379
+
380
+ Each skill has 15–30 prompt templates with placeholder slots:
381
+
382
+ **WhatsApp templates:**
383
+ ```
384
+ "message {message} to {contact} on whatsapp"
385
+ "text {contact} {message} on whatsapp"
386
+ "whatsapp {contact} saying {message}"
387
+ "ping {contact} on whatsapp with {message}"
388
+ ```
389
+
390
+ **Alarm templates:**
391
+ ```
392
+ "create alarm for {time} {day}"
393
+ "wake me up at {time} {day}"
394
+ "set a {time} alarm for {day}"
395
+ "{time} alarm {day} please"
396
+ ```
397
+
398
+ **Uber templates:**
399
+ ```
400
+ "get an uber to {destination}"
401
+ "uber me to {destination}"
402
+ "book a cab to {destination} via uber"
403
+ ```
404
+
405
+ Templates are crossed with random entity samples to produce unique training pairs. The same intent can appear as:
406
+
407
+ - "set an alarm for 7 am tomorrow"
408
+ - "wake me up at seven tomorrow morning"
409
+ - "7am alarm pls"
410
+ - "please alarm 7 am tomorrow thanks"
411
+
412
+ ### V1 training data (skill-only)
413
+
414
+ The earlier `scripts/generate_training_data.py` produces ~510 examples for V1 classification:
415
+
416
+ - 30 variations per skill from `skills.jsonl` task descriptions
417
+ - Guaranteed inclusion of Gradio demo prompts
418
+ - Regex-based parsing of task strings to derive alarm times, contacts, etc.
419
+
420
+ ### Held-out evaluation sets
421
+
422
+ Two evaluation sets prevent overfitting to templates:
423
+
424
+ | File | Size | Purpose |
425
+ | --- | --- | --- |
426
+ | `data/eval_intent_prompts.json` | ~90 prompts | Structured eval during training |
427
+ | `data/pocket_benchmark_prompts.json` | 200 prompts | Real-world messy language benchmark |
428
+
429
+ The Pocket Automator benchmark is intentionally unlike training data — slang, typos, incomplete phrasing, conversational filler:
430
+
431
+ ```
432
+ "yo set an alrm for like 5:45 tmrw morning pls"
433
+ "need to b up at 6ish on monday ngl"
434
+ "hit up zoe on whatsapp say im omw"
435
+ "wa msg marcus 'running 20 min late'"
436
+ "lowkey need 11:11 pm alarm tonight"
437
+ "deadass need alarm sunday noon"
438
+ ```
439
+
440
+ Each benchmark case is tagged with `domain` (alarms, whatsapp, spotify...) and `styles` (slang, typo, incomplete, conversational). Prompts are filtered against training data to ensure zero overlap.
441
+
442
+ ---
443
+
444
+ ## Step 4: Deploy inference on Modal, demo on Gradio
445
+
446
+ ### Modal inference API
447
+
448
+ Training and inference both run on **Modal** — serverless GPU infrastructure with persistent volumes.
449
+
450
+ `modal_apps/predict_api.py` deploys a FastAPI endpoint:
451
+
452
+ ```bash
453
+ modal deploy modal_apps/predict_api.py
454
+ # → https://<workspace>--android-skill-predict-api-skillpredictor-web.modal.run
455
+ ```
456
+
457
+ Architecture:
458
+
459
+ - **Container class** `SkillPredictor` loads the QLoRA model once via `@modal.enter()`
460
+ - **4-bit quantized** base model + LoRA adapter from Modal Volume
461
+ - **Greedy decoding** (`do_sample=False`) for deterministic JSON output
462
+ - **128 max new tokens** — enough for any intent JSON
463
+ - **5-minute scale-down window** — containers stay warm between requests
464
+
465
+ Request/response:
466
+
467
+ ```bash
468
+ curl -X POST https://.../predict \
469
+ -H "Content-Type: application/json" \
470
+ -d '{"prompt": "text mom on whatsapp i am on my way"}'
471
+ ```
472
+
473
+ ```json
474
+ {
475
+ "skill": "whatsapp_send_message",
476
+ "parameters": {
477
+ "contact": "mom",
478
+ "message": "i am on my way"
479
+ }
480
+ }
481
+ ```
482
+
483
+ The API applies the same post-processing as local evaluation: JSON extraction, skill normalization, alias resolution, and keyword fallbacks.
484
+
485
+ ### Gradio demo
486
+
487
+ The **Gradio demo** (`app.py`) is the hackathon submission UI, hosted on Hugging Face Spaces.
488
+
489
+ Flow:
490
+
491
+ 1. User types a natural language prompt (or picks an example)
492
+ 2. App POSTs to Modal `/predict` endpoint
493
+ 3. Response is parsed: skill label, parameter tiles, confidence display
494
+ 4. Skill router loads the matching trajectory from `trajectories/`
495
+ 5. UI shows task description, app package, step count, and trajectory preview
496
+
497
+ Example prompts built into the demo:
498
+
499
+ - "play my workout playlist"
500
+ - "turn bluetooth on"
501
+ - "wake me up tomorrow morning"
502
+ - "send ri a message on whatsapp"
503
+ - "book an uber to the airport"
504
+
505
+ The Space doesn't ship model weights — inference stays on Modal. Only a `MODAL_PREDICT_URL` secret is needed.
506
+
507
+ ### Local development
508
+
509
+ Three commands to run everything locally:
510
+
511
+ ```bash
512
+ # 1. Generate training data
513
+ python scripts/generate_intent_dataset.py
514
+
515
+ # 2. Train on Modal GPU
516
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
517
+
518
+ # 3. Deploy inference + run demo
519
+ modal deploy modal_apps/predict_api.py
520
+ export MODAL_PREDICT_URL="https://..."
521
+ python app.py
522
+ ```
523
+
524
+ Evaluation can run locally on CPU/MPS if you download the adapter:
525
+
526
+ ```bash
527
+ modal volume get android-dataset-model adapter ./trained_model/adapter
528
+ python -m src.evaluate_intent
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Evaluation: how we measure generalization
534
+
535
+ ### Metrics
536
+
537
+ Three metrics capture different levels of correctness:
538
+
539
+ | Metric | Definition | What it measures |
540
+ | --- | --- | --- |
541
+ | **Skill accuracy** | Predicted skill matches expected | App/action disambiguation |
542
+ | **Parameter accuracy** | All expected parameters match (normalized) | Slot-filling quality |
543
+ | **Exact JSON match** | Skill + all parameters match exactly | End-to-end correctness |
544
+
545
+ Parameter matching uses normalized lowercase comparison — `"Mom"` matches `"mom"`, extra whitespace is stripped.
546
+
547
+ ### Pocket Automator benchmark results
548
+
549
+ Evaluation on **200 held-out prompts** with slang, typos, and conversational phrasing:
550
+
551
+ | Metric | Score |
552
+ | --- | --- |
553
+ | **Skill accuracy** | 99.0% |
554
+ | **Parameter accuracy** | 86.0% |
555
+ | **Exact JSON match** | 85.5% |
556
+
557
+ The model almost never picks the wrong app or action. Parameter extraction is harder — preserving informal time expressions like `"6ish"` vs normalizing to `"6 am"` — but 86% is strong for a 3B model with no cloud fallback.
558
+
559
+ ### Where errors happen
560
+
561
+ Parameter failures tend to cluster around:
562
+
563
+ - **Informal time expressions**: "6ish on monday" vs `"time": "6 am", "day": "monday"`
564
+ - **Abbreviated days**: "tmrw" vs "tomorrow morning"
565
+ - **Message truncation**: model drops filler words the benchmark expects verbatim
566
+ - **Contact nicknames**: "roomie" vs a full name
567
+
568
+ Skill errors (1%) mostly involve near-miss disambiguation — Spotify search-and-play vs play-playlist when the prompt is ambiguous.
569
+
570
+ ### Evaluation commands
571
+
572
+ ```bash
573
+ # On Modal GPU
574
+ modal run modal_apps/evaluate_intent_modal.py
575
+ modal run modal_apps/evaluate_pocket_benchmark_modal.py
576
+
577
+ # Locally
578
+ python -m src.evaluate_intent
579
+ python -m src.evaluate_pocket_benchmark
580
+ ```
581
+
582
+ The pocket benchmark runner produces a confusion matrix, per-domain breakdown, and a failure report saved to `data/pocket_benchmark_report.txt`.
583
+
584
+ ---
585
+
586
+ ## Why this approach works
587
+
588
+ ### 1. Local-first, privacy-preserving
589
+
590
+ A 3B model can run on-device (via llama.cpp, MLC, or similar) or on a small GPU. Your "text mom I'm running late" never needs to hit a frontier API. The entire inference stack fits in ~2GB of VRAM with 4-bit quantization.
591
+
592
+ ### 2. Deterministic replay, not hallucinated taps
593
+
594
+ The model outputs a skill label and parameters. The trajectory is a fixed file recorded on a real device. No invented button coordinates, no drift between runs. If the model says `whatsapp_send_message`, you get the exact same tap sequence every time.
595
+
596
+ This is fundamentally different from vision-based agents that re-locate UI elements on every run and can click the wrong thing.
597
+
598
+ ### 3. Cheap to extend
599
+
600
+ Adding a new skill is a repeatable pipeline:
601
+
602
+ 1. Record one trajectory with Pocket Automator
603
+ 2. Add parameter schema to `data/skill_schemas.json`
604
+ 3. Add skill mapping to `src/skill_router.py`
605
+ 4. Regenerate training data: `python scripts/generate_intent_dataset.py`
606
+ 5. Fine-tune: `modal run modal_apps/train_modal.py --dataset train_intent.jsonl`
607
+
608
+ No prompt engineering session. No re-architecting the model. Just more data and another training run.
609
+
610
+ ### 4. Separation of concerns
611
+
612
+ | Component | Responsibility | Swappable? |
613
+ | --- | --- | --- |
614
+ | Language model | Understand intent | Yes — any 3B instruct model |
615
+ | Skill router | Map intent → file | Yes — add skills without retraining |
616
+ | Pocket Automator | Execute UI steps | Yes — any accessibility replay engine |
617
+ | Trajectory JSON | Store ground truth | Yes — re-record when UI changes |
618
+
619
+ Each piece can be improved independently. Better model? Swap the adapter. UI changed? Re-record one trajectory. New app? Add a skill.
620
+
621
+ ### 5. Designed for the "backyard"
622
+
623
+ This project targets **personal automation on hardware you own** — the Backyard AI track. It's not trying to automate every Android app in existence. It's trying to automate *your* apps, *your* flows, *your* phrasing, with a model small enough to run locally.
624
+
625
+ ---
626
+
627
+ ## What's next: trajectory parameterization
628
+
629
+ ### The current gap
630
+
631
+ V2 extracts parameters at inference time:
632
+
633
+ ```
634
+ "text mom on whatsapp i'm on my way"
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
+
650
+ ```
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
+ ---
665
+
666
+ ## Try it yourself
667
+
668
+ ### Links
669
+
670
+ | Resource | URL |
671
+ | --- | --- |
672
+ | **Blog post** | [Hugging Face Blog — Android Skill Router](https://huggingface.co/blog/build-small-hackathon/android-skill-router) |
673
+ | **Live demo** | [android-skill-router on Hugging Face Spaces](https://huggingface.co/spaces/build-small-hackathon/android-skill-router) |
674
+ | **Demo video** | [YouTube Short](https://youtube.com/shorts/IQRHf7HfTDA) |
675
+ | **Pocket Automator** | [GitHub — Android recorder & replay](https://github.com/kriyanshii/pocket-automator) |
676
+ | **Social post** | [Twitter/X](https://x.com/kriyanshii/status/2066587828839141634) |
677
+
678
+ ### Quick start
679
+
680
+ ```bash
681
+ git clone https://github.com/kriyanshii/android-dataset.git
682
+ cd android-dataset
683
+
684
+ # Generate intent training data
685
+ python scripts/generate_intent_dataset.py
686
+
687
+ # Train on Modal (requires modal setup)
688
+ pip install modal && modal setup
689
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
690
+
691
+ # Deploy inference API
692
+ modal deploy modal_apps/predict_api.py
693
+
694
+ # Run Gradio demo
695
+ pip install -r requirements.txt
696
+ export MODAL_PREDICT_URL="https://<your-modal-url>/predict"
697
+ python app.py
698
+ ```
699
+
700
+ ### Project layout
701
+
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
715
+ pocket_benchmark.py # Benchmark metrics and reports
716
+ modal_apps/
717
+ train_modal.py # QLoRA fine-tuning on Modal GPU
718
+ predict_api.py # FastAPI inference endpoint
719
+ evaluate_intent_modal.py # GPU evaluation
720
+ evaluate_pocket_benchmark_modal.py
721
+ scripts/
722
+ generate_skill_dataset.py # trajectories → skills.jsonl
723
+ generate_intent_dataset.py # schemas → train_intent.jsonl
724
+ generate_pocket_benchmark.py
725
+ trajectories/ # Pocket Automator exports (15 skills)
726
+ ```
727
+
728
+ ---
729
+
730
+ ## TL;DR
731
+
732
+ **Android Skill Router** shows that personal phone automation doesn't require a 70B agent in the cloud.
733
+
734
+ 1. **Record** UI flows once on your Android device with Pocket Automator
735
+ 2. **Fine-tune** a 3B model to understand how you actually talk (slang, typos, and all)
736
+ 3. **Route** to deterministic trajectories — no hallucinated taps
737
+ 4. **Replay** through accessibility APIs on real hardware
738
+
739
+ Classify → route → replay. Small model, real hardware, backyard-scale AI that actually does something useful.
740
+
741
+ ---
742
+
743
+ *Apache 2.0. Base model weights subject to [Qwen license](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct).*
README.md CHANGED
@@ -14,28 +14,38 @@ tags:
14
  - build-small-hackathon
15
  - track:backyard
16
  - sponsor:modal
 
 
17
  ---
18
 
19
  # Android Skill Router
20
 
21
- **Build Small Hackathon — Backyard AI.**
22
 
23
- Turn everyday phone requests into structured Android automation skills. A fine-tuned **Qwen2.5-3B-Instruct** model (≤4B params) classifies natural language into one of 15 skills alarms, Spotify, WhatsApp, Slack, Uber, and more then loads the matching **UI automation trajectory** recorded on a real Android device. Classifier demo for the **[Pocket Automator](https://github.com/kriyanshii/pocket-automator)** stack.
24
-
25
- - **Demo video:** [YouTube Short](https://youtube.com/shorts/IQRHf7HfTDA?si=9kEoZKRus6FzHYbB)
26
- - **Android recorder:** [Pocket Automator](https://github.com/kriyanshii/pocket-automator)
27
- - **Social post:** [Twitter Post](https://x.com/kriyanshii/status/2066587828839141634?s=20)
28
- - **Space:** [android-skill-router](https://huggingface.co/spaces/build-small-hackathon/android-skill-router)
29
-
30
- ## The idea
31
-
32
- Voice assistants route to cloud APIs. This project shows a **small, local-first classifier** that maps messy human phrasing to deterministic Android UI replay plans — useful for on-device agents, accessibility tooling, or personal automation without shipping prompts to a giant model farm.
33
 
34
  ```
35
  "play my workout playlist" → spotify_play_playlist → trajectories/spotify_play_playlist.json
36
  ```
37
 
38
- Each trajectory is a step-by-step UI tree capture (app package, taps, screens) ready for an accessibility or agent runtime.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  ## Recording trajectories
41
 
@@ -52,13 +62,6 @@ UI traces in `trajectories/` were captured with **[Pocket Automator](https://git
52
  | **Recorder** | [Pocket Automator](https://github.com/kriyanshii/pocket-automator) — Android accessibility capture & replay |
53
  | **Data** | 15 Android trajectories → `data/skills.jsonl` → ~510 prompt variations in `data/train.jsonl` |
54
 
55
- ## Hackathon entry
56
-
57
- - **track:backyard** — practical personal automation on hardware you own.
58
- - **sponsor:modal** — training, evaluation, and production inference all run on Modal.
59
- - **achievement:tinytitan** — entire stack runs on a 3B-parameter model.
60
- - **achievement:agent** — classify → route → load multi-step UI plan.
61
-
62
  ## Quick start (local dev)
63
 
64
  ```bash
@@ -89,7 +92,7 @@ The `/predict` endpoint returns structured intents:
89
  1. Create a **Gradio Space** inside the [build-small-hackathon](https://huggingface.co/build-small-hackathon) org.
90
  2. Upload this repo (exclude `trained_model/` — inference stays on Modal).
91
  3. Add a Space secret: `MODAL_PREDICT_URL` = your deployed Modal `/predict` base URL.
92
- 4. Fill in demo video + social links above before the deadline.
93
 
94
  ## Project layout
95
 
 
14
  - build-small-hackathon
15
  - track:backyard
16
  - sponsor:modal
17
+ - achievement:tinytitan
18
+ - achievement:agent
19
  ---
20
 
21
  # Android Skill Router
22
 
23
+ **Build Small Hackathon — Backyard AI track · Modal sponsor**
24
 
25
+ You say *"text mom on whatsapp i'm on my way"* — a voice assistant might web-search or shrug. Android Skill Router closes that gap with a **3B-parameter intent classifier** that maps messy phone language to structured `{skill, parameters}` JSON, then loads a **pre-recorded UI trajectory** captured on a real Android device. It is the classifier layer of the **[Pocket Automator](https://github.com/kriyanshii/pocket-automator)** stack: record a flow once on your phone, route to it forever with a tiny model.
 
 
 
 
 
 
 
 
 
26
 
27
  ```
28
  "play my workout playlist" → spotify_play_playlist → trajectories/spotify_play_playlist.json
29
  ```
30
 
31
+ **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.
32
+
33
+ **Submission links**
34
+
35
+ - **Blog post:** [Hugging Face Blog — Android Skill Router](https://huggingface.co/blog/build-small-hackathon/android-skill-router)
36
+ - **Demo video:** [YouTube Short](https://youtube.com/shorts/IQRHf7HfTDA)
37
+ - **Social post:** [Twitter/X](https://x.com/kriyanshii/status/2066587828839141634)
38
+ - **Live Space:** [android-skill-router](https://huggingface.co/spaces/build-small-hackathon/android-skill-router)
39
+ - **Android recorder:** [Pocket Automator](https://github.com/kriyanshii/pocket-automator)
40
+
41
+ ## Hackathon tags
42
+
43
+ | Tag | Why |
44
+ | --- | --- |
45
+ | `track:backyard` | Personal automation on hardware you own |
46
+ | `sponsor:modal` | Training, evaluation, and inference on Modal |
47
+ | `achievement:tinytitan` | Full stack on Qwen2.5-3B (≤4B params) |
48
+ | `achievement:agent` | Classify → route → load multi-step UI plan |
49
 
50
  ## Recording trajectories
51
 
 
62
  | **Recorder** | [Pocket Automator](https://github.com/kriyanshii/pocket-automator) — Android accessibility capture & replay |
63
  | **Data** | 15 Android trajectories → `data/skills.jsonl` → ~510 prompt variations in `data/train.jsonl` |
64
 
 
 
 
 
 
 
 
65
  ## Quick start (local dev)
66
 
67
  ```bash
 
92
  1. Create a **Gradio Space** inside the [build-small-hackathon](https://huggingface.co/build-small-hackathon) org.
93
  2. Upload this repo (exclude `trained_model/` — inference stays on Modal).
94
  3. Add a Space secret: `MODAL_PREDICT_URL` = your deployed Modal `/predict` base URL.
95
+ 4. Link the demo video and social post in the README (see **Submission links** above).
96
 
97
  ## Project layout
98
 
data/train.jsonl CHANGED
@@ -17,6 +17,7 @@
17
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "alarm me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
18
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set daily alarm 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
19
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
 
20
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set alarm tomorrow morning 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
21
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "alarm for 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
22
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create early alarm 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
@@ -27,7 +28,6 @@
27
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "remind me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
28
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set an alarm"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
29
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me up at 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
30
- {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create a morning alarm"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
31
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create calender Event for tomorrow 4 pm stating meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
32
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create calendar event for tomorrow 4 pm meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
33
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "add a meeting tomorrow at 4 pm meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
@@ -450,8 +450,9 @@
450
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set bluetooth to on"}, {"role": "assistant", "content": "{\"skill\":\"bluetooth_enable\"}"}]}
451
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "play my workout playlist"}, {"role": "assistant", "content": "{\"skill\":\"spotify_play_playlist\"}"}]}
452
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "turn bluetooth on"}, {"role": "assistant", "content": "{\"skill\":\"bluetooth_enable\"}"}]}
453
- {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me up tomorrow morning"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
454
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "send ri a message on whatsapp"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
 
 
455
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "book an uber to the airport"}, {"role": "assistant", "content": "{\"skill\":\"uber_request_ride\"}"}]}
456
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "find parag shah in my contacts"}, {"role": "assistant", "content": "{\"skill\":\"contacts_search\"}"}]}
457
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "search parag shah on linkedin"}, {"role": "assistant", "content": "{\"skill\":\"linkedin_search_person\"}"}]}
@@ -492,4 +493,5 @@
492
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "email boss saying i'll be late"}, {"role": "assistant", "content": "{\"skill\":\"gmail_send_email\"}"}]}
493
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "message boss on whatsapp running late"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
494
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "text team on whatsapp meeting moved"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
 
495
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "switch to announcements in slack"}, {"role": "assistant", "content": "{\"skill\":\"slack_open_channel\"}"}]}
 
17
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "alarm me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
18
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set daily alarm 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
19
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
20
+ {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me up tomorrow morning"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
21
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set alarm tomorrow morning 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
22
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "alarm for 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
23
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create early alarm 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
 
28
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "remind me at 7 am tomorrow"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
29
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set an alarm"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
30
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "wake me up at 7 am"}, {"role": "assistant", "content": "{\"skill\":\"create_alarm\"}"}]}
 
31
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create calender Event for tomorrow 4 pm stating meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
32
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "create calendar event for tomorrow 4 pm meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
33
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "add a meeting tomorrow at 4 pm meeting with kriyanshi"}, {"role": "assistant", "content": "{\"skill\":\"calendar_create_event\"}"}]}
 
450
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "set bluetooth to on"}, {"role": "assistant", "content": "{\"skill\":\"bluetooth_enable\"}"}]}
451
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "play my workout playlist"}, {"role": "assistant", "content": "{\"skill\":\"spotify_play_playlist\"}"}]}
452
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "turn bluetooth on"}, {"role": "assistant", "content": "{\"skill\":\"bluetooth_enable\"}"}]}
 
453
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "send ri a message on whatsapp"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
454
+ {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "send ri a message"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
455
+ {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "send alex a message"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
456
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "book an uber to the airport"}, {"role": "assistant", "content": "{\"skill\":\"uber_request_ride\"}"}]}
457
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "find parag shah in my contacts"}, {"role": "assistant", "content": "{\"skill\":\"contacts_search\"}"}]}
458
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "search parag shah on linkedin"}, {"role": "assistant", "content": "{\"skill\":\"linkedin_search_person\"}"}]}
 
493
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "email boss saying i'll be late"}, {"role": "assistant", "content": "{\"skill\":\"gmail_send_email\"}"}]}
494
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "message boss on whatsapp running late"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
495
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "text team on whatsapp meeting moved"}, {"role": "assistant", "content": "{\"skill\":\"whatsapp_send_message\"}"}]}
496
+ {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "send ri an email"}, {"role": "assistant", "content": "{\"skill\":\"gmail_send_email\"}"}]}
497
  {"messages": [{"role": "system", "content": "You classify Android automation requests into exactly one skill. Reply with JSON only: {\"skill\": \"<skill_name>\"}. Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) to pick the correct skill."}, {"role": "user", "content": "switch to announcements in slack"}, {"role": "assistant", "content": "{\"skill\":\"slack_open_channel\"}"}]}
modal_apps/evaluate_modal.py CHANGED
@@ -236,9 +236,15 @@ def main() -> None:
236
  f"Local eval prompts not found: {eval_path.resolve()}"
237
  )
238
 
 
 
 
 
 
 
239
  print(f"Uploading {eval_path} to dataset volume...")
240
  with dataset_volume.batch_upload() as batch:
241
- batch.put_file(str(eval_path), "eval_prompts.json")
242
 
243
  print("Launching evaluation on Modal GPU...")
244
  evaluate.remote()
 
236
  f"Local eval prompts not found: {eval_path.resolve()}"
237
  )
238
 
239
+ remote_name = "eval_prompts.json"
240
+ try:
241
+ dataset_volume.remove_file(remote_name)
242
+ except Exception:
243
+ pass # file may not exist yet on the volume
244
+
245
  print(f"Uploading {eval_path} to dataset volume...")
246
  with dataset_volume.batch_upload() as batch:
247
+ batch.put_file(str(eval_path), remote_name)
248
 
249
  print("Launching evaluation on Modal GPU...")
250
  evaluate.remote()
scripts/generate_training_data.py CHANGED
@@ -21,6 +21,8 @@ MUST_INCLUDE_PROMPTS: dict[str, str] = {
21
  "turn bluetooth on": "bluetooth_enable",
22
  "wake me up tomorrow morning": "create_alarm",
23
  "send ri a message on whatsapp": "whatsapp_send_message",
 
 
24
  "open the engineering channel in slack": "slack_open_channel",
25
  "pause spotify": "spotify_pause",
26
  "book an uber to the airport": "uber_request_ride",
@@ -106,6 +108,7 @@ def generate_create_alarm(task: str) -> list[str]:
106
  f"alarm me at {time_str} {when_str}",
107
  f"set daily alarm {time_str}",
108
  f"wake me at {time_str} {when_str}",
 
109
  f"set alarm {when_str} morning {time_str}",
110
  f"alarm for {time_str} {when_str}",
111
  f"create early alarm {time_str} {when_str}",
@@ -842,6 +845,10 @@ def generate_contrastive_examples() -> list[dict]:
842
  _record("whatsapp_send_message", "message boss on whatsapp running late"),
843
  _record("whatsapp_send_message", "text team on whatsapp meeting moved"),
844
  _record("whatsapp_send_message", "send ri a message on whatsapp"),
 
 
 
 
845
  _record("slack_open_channel", "open the engineering channel in slack"),
846
  _record("slack_open_channel", "go to data contributors channel on slack"),
847
  _record("slack_open_channel", "switch to announcements in slack"),
 
21
  "turn bluetooth on": "bluetooth_enable",
22
  "wake me up tomorrow morning": "create_alarm",
23
  "send ri a message on whatsapp": "whatsapp_send_message",
24
+ "send ri a message": "whatsapp_send_message",
25
+ "send alex a message": "whatsapp_send_message",
26
  "open the engineering channel in slack": "slack_open_channel",
27
  "pause spotify": "spotify_pause",
28
  "book an uber to the airport": "uber_request_ride",
 
108
  f"alarm me at {time_str} {when_str}",
109
  f"set daily alarm {time_str}",
110
  f"wake me at {time_str} {when_str}",
111
+ f"wake me up tomorrow morning",
112
  f"set alarm {when_str} morning {time_str}",
113
  f"alarm for {time_str} {when_str}",
114
  f"create early alarm {time_str} {when_str}",
 
845
  _record("whatsapp_send_message", "message boss on whatsapp running late"),
846
  _record("whatsapp_send_message", "text team on whatsapp meeting moved"),
847
  _record("whatsapp_send_message", "send ri a message on whatsapp"),
848
+ _record("whatsapp_send_message", "send ri a message"),
849
+ _record("whatsapp_send_message", "send alex a message"),
850
+ _record("gmail_send_email", "send ri an email"),
851
+ _record("create_alarm", "wake me up tomorrow morning"),
852
  _record("slack_open_channel", "open the engineering channel in slack"),
853
  _record("slack_open_channel", "go to data contributors channel on slack"),
854
  _record("slack_open_channel", "switch to announcements in slack"),