Update hackathon submission docs with blog link and README tags.
Browse filesAdd HF blog URL to README and BLOG, plus track/badge tags and submission links for Build Small.
- BLOG.md +743 -0
- README.md +23 -20
- data/train.jsonl +4 -2
- modal_apps/evaluate_modal.py +7 -1
- scripts/generate_training_data.py +7 -0
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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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),
|
| 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"),
|