kriyanshi Cursor commited on
Commit
40a90bb
·
1 Parent(s): 81b01a7

Ship v2 intent extraction with API, demo UI, eval, and benchmark suite.

Browse files

Add intent training pipeline, parameter-aware inference, and Pocket Automator
benchmark tooling. Exclude generated train_intent.jsonl from git (HF 10MB limit).

Co-authored-by: Cursor <cursoragent@cursor.com>

.gitignore CHANGED
@@ -13,6 +13,9 @@ trained_model/
13
  result.json
14
  *.log
15
 
 
 
 
16
  # OS / editor
17
  .DS_Store
18
  .idea/
 
13
  result.json
14
  *.log
15
 
16
+ # Training data (generated locally; uploaded to Modal volume — too large for HF Space)
17
+ data/train_intent.jsonl
18
+
19
  # OS / editor
20
  .DS_Store
21
  .idea/
README.md CHANGED
@@ -24,7 +24,7 @@ Turn everyday phone requests into structured Android automation skills. A fine-t
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:** _add your post link here_
28
  - **Space:** [android-skill-router](https://huggingface.co/spaces/build-small-hackathon/android-skill-router)
29
 
30
  ## The idea
@@ -47,7 +47,7 @@ UI traces in `trajectories/` were captured with **[Pocket Automator](https://git
47
  | --- | --- |
48
  | **Base model** | [Qwen/Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) |
49
  | **Fine-tune** | 4-bit QLoRA + SFT with [Unsloth](https://github.com/unslothai/unsloth) on Modal (`modal_apps/train_modal.py`) |
50
- | **Inference** | Modal GPU API (`modal_apps/predict_api.py`) — keeps the Gradio Space lightweight |
51
  | **Demo UI** | Gradio (`app.py`) |
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` |
@@ -62,10 +62,11 @@ UI traces in `trajectories/` were captured with **[Pocket Automator](https://git
62
  ## Quick start (local dev)
63
 
64
  ```bash
65
- # 1. Train on Modal (uploads data/train.jsonl, saves adapter to volume)
66
  pip install modal
67
  modal setup
68
- modal run modal_apps/train_modal.py
 
69
 
70
  # 2. Deploy inference API
71
  modal deploy modal_apps/predict_api.py
@@ -77,6 +78,12 @@ export MODAL_PREDICT_URL="https://<workspace>--android-skill-predict-api-skillpr
77
  python app.py
78
  ```
79
 
 
 
 
 
 
 
80
  ### Hugging Face Space setup
81
 
82
  1. Create a **Gradio Space** inside the [build-small-hackathon](https://huggingface.co/build-small-hackathon) org.
@@ -129,6 +136,39 @@ python scripts/generate_skill_dataset.py # trajectories → data/skills.jsonl
129
  python scripts/generate_training_data.py # data/skills.jsonl → data/train.jsonl
130
  ```
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  ## License
133
 
134
  Apache 2.0. Base model weights subject to [Qwen license](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct).
 
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
 
47
  | --- | --- |
48
  | **Base model** | [Qwen/Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) |
49
  | **Fine-tune** | 4-bit QLoRA + SFT with [Unsloth](https://github.com/unslothai/unsloth) on Modal (`modal_apps/train_modal.py`) |
50
+ | **Inference** | Modal GPU API (`modal_apps/predict_api.py`) — returns skill + parameters |
51
  | **Demo UI** | Gradio (`app.py`) |
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` |
 
62
  ## Quick start (local dev)
63
 
64
  ```bash
65
+ # 1. Train intent model on Modal (uploads data/train_intent.jsonl, saves adapter to volume)
66
  pip install modal
67
  modal setup
68
+ python scripts/generate_intent_dataset.py
69
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
70
 
71
  # 2. Deploy inference API
72
  modal deploy modal_apps/predict_api.py
 
78
  python app.py
79
  ```
80
 
81
+ The `/predict` endpoint returns structured intents:
82
+
83
+ ```json
84
+ {"skill": "whatsapp_send_message", "parameters": {"contact": "ri", "message": "see you soon"}}
85
+ ```
86
+
87
  ### Hugging Face Space setup
88
 
89
  1. Create a **Gradio Space** inside the [build-small-hackathon](https://huggingface.co/build-small-hackathon) org.
 
136
  python scripts/generate_training_data.py # data/skills.jsonl → data/train.jsonl
137
  ```
138
 
139
+ ## V2: Intent extraction
140
+
141
+ V1 maps prompts to a skill label only. V2 extracts structured intents:
142
+
143
+ "text mom on whatsapp i'm on my way"
144
+ → {"skill": "whatsapp_send_message", "parameters": {"contact": "mom", "message": "i'm on my way"}}
145
+
146
+ The Gradio demo and Modal `/predict` API both return skill + parameters.
147
+
148
+ ### Data
149
+ - `data/skill_schemas.json` — parameter definitions per skill
150
+ - `data/train_intent.jsonl` — ~15k synthetic SFT examples (generated locally via script; gitignored — upload to Modal for training)
151
+ - `data/eval_intent_prompts.json` — held-out intent eval set
152
+ - `data/pocket_benchmark_prompts.json` — 200 real-world messy prompts
153
+
154
+ ### Train & evaluate
155
+ ```bash
156
+ python scripts/generate_intent_dataset.py
157
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
158
+ modal run modal_apps/evaluate_intent_modal.py
159
+ modal run modal_apps/evaluate_pocket_benchmark_modal.py
160
+ ```
161
+
162
+ ### Benchmark results (Pocket Automator, 200 prompts)
163
+
164
+ | Metric | Score |
165
+ |--------|-------|
166
+ | Skill accuracy | 99.0% |
167
+ | Parameter accuracy | 86.0% |
168
+ | Exact JSON match | 85.5% |
169
+
170
+ Trajectory parameterization (slot-filling at replay time) is planned next.
171
+
172
  ## License
173
 
174
  Apache 2.0. Base model weights subject to [Qwen license](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct).
app.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  Android Skill Router — Gradio demo for the Build Small hackathon.
3
 
4
- Natural language in → fine-tuned Qwen2.5-3B skill label out → Android UI trajectory.
5
 
6
  Inference runs on Modal (deploy modal_apps/predict_api.py). Set MODAL_PREDICT_URL in Space secrets.
7
  """
@@ -22,6 +22,21 @@ from src.skill_utils import resolve_skill
22
 
23
  MODAL_PREDICT_URL = os.environ.get("MODAL_PREDICT_URL", "").rstrip("/")
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  EXAMPLE_PROMPTS = [
26
  "play my workout playlist",
27
  "turn bluetooth on",
@@ -66,19 +81,20 @@ IDLE_RESULT_HTML = """
66
  <div class="result-card result-card--idle">
67
  <div class="result-card-header">Automation Result</div>
68
  <p class="result-placeholder">
69
- Describe what you want your phone to do — the replay engine will detect the skill
70
- and surface the Android trajectory.
71
  </p>
72
  <div class="result-metrics result-metrics--empty">
73
  <div class="metric-tile"><span class="metric-label">Detected Skill</span><span class="metric-value muted">—</span></div>
 
74
  <div class="metric-tile"><span class="metric-label">Predicted Confidence</span><span class="metric-value muted">—</span></div>
75
- <div class="metric-tile"><span class="metric-label">Android Trajectory</span><span class="metric-value muted">—</span></div>
76
  </div>
77
  </div>
78
  """
79
 
80
 
81
- def _predict_skill(prompt: str) -> str:
82
  if not MODAL_PREDICT_URL:
83
  raise gr.Error(
84
  "MODAL_PREDICT_URL is not set. Deploy modal_apps/predict_api.py on Modal and add the "
@@ -104,23 +120,49 @@ def _predict_skill(prompt: str) -> str:
104
  raise gr.Error(
105
  f"Could not route prompt to a known skill (model returned {raw_skill!r})."
106
  )
107
- return skill
 
 
 
 
 
108
 
109
 
110
- def _demo_confidence_percent(prompt: str, skill: str) -> float:
111
- """Deterministic demo confidence for the hackathon UI (API returns skill only)."""
112
- digest = hashlib.sha256(f"{prompt.strip().lower()}|{skill}".encode()).hexdigest()
 
 
 
113
  return 94.0 + (int(digest[:4], 16) % 56) / 10.0
114
 
115
 
116
- def _build_result_card(skill: str, prompt: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  label = SKILL_LABELS.get(skill, skill.replace("_", " ").title())
118
- confidence = _demo_confidence_percent(prompt, skill)
 
119
  data = load_trajectory(skill)
120
  steps = data.get("steps", [])
121
  task = html.escape(str(data.get("task", "—")))
122
  app_pkg = html.escape(str(data.get("app", "—")))
123
  trajectory_file = html.escape(Path(SKILL_TO_TRAJECTORY[skill]).name)
 
124
 
125
  return f"""
126
  <div class="result-card">
@@ -141,6 +183,10 @@ def _build_result_card(skill: str, prompt: str) -> str:
141
  </div>
142
  <span class="metric-sub">Greedy decode · fine-tuned Qwen2.5-3B</span>
143
  </div>
 
 
 
 
144
  <div class="metric-tile metric-tile--wide">
145
  <span class="metric-label">Associated Android Trajectory</span>
146
  <span class="metric-value">{task}</span>
@@ -168,12 +214,13 @@ def _build_skills_catalog_card() -> str:
168
  """
169
 
170
 
171
- def _trajectory_summary(skill: str) -> tuple[str, str, str]:
172
  data = load_trajectory(skill)
173
  steps = data.get("steps", [])
174
  task = data.get("task", "—")
175
  app_pkg = data.get("app", "—")
176
  trajectory_file = Path(SKILL_TO_TRAJECTORY[skill]).name
 
177
 
178
  summary = (
179
  f"### {SKILL_LABELS.get(skill, skill)}\n\n"
@@ -183,9 +230,13 @@ def _trajectory_summary(skill: str) -> tuple[str, str, str]:
183
  f"**Steps** · {len(steps)} UI snapshots \n"
184
  f"**File** · `{trajectory_file}`"
185
  )
 
 
 
186
 
187
  preview = {
188
  "skill": skill,
 
189
  "task": task,
190
  "app": app_pkg,
191
  "steps": len(steps),
@@ -199,10 +250,12 @@ def route_prompt(prompt: str) -> tuple[str, gr.update, str]:
199
  if not prompt:
200
  raise gr.Error("Enter a prompt to classify.")
201
 
202
- skill = _predict_skill(prompt)
203
- _, preview_json, trajectory_json = _trajectory_summary(skill)
 
 
204
  return (
205
- _build_result_card(skill, prompt),
206
  gr.update(value=preview_json, visible=True),
207
  trajectory_json,
208
  )
@@ -645,6 +698,34 @@ footer { display: none !important; }
645
  min-width: 3.5rem;
646
  text-align: right;
647
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  .skills-catalog {
649
  list-style: none;
650
  margin: 0;
@@ -879,7 +960,7 @@ with gr.Blocks(
879
  <span class="pipeline-arrow">↓</span>
880
  <span class="pipeline-step highlight">Qwen 2.5 3B</span>
881
  <span class="pipeline-arrow">↓</span>
882
- <span class="pipeline-step">Skill Detection</span>
883
  <span class="pipeline-arrow">↓</span>
884
  <span class="pipeline-step">Replay Engine</span>
885
  <span class="pipeline-arrow">↓</span>
@@ -905,7 +986,7 @@ with gr.Blocks(
905
  show_label=False,
906
  )
907
  with gr.Row(elem_classes=["btn-row"]):
908
- submit = gr.Button("Run skill →", variant="primary", scale=2)
909
  skills_btn = gr.Button("Browse skills", variant="secondary", scale=1)
910
  with gr.Group(elem_classes=["examples-wrap"]):
911
  gr.Examples(
@@ -922,7 +1003,7 @@ with gr.Blocks(
922
  show_label=False,
923
  )
924
  preview_json = gr.Code(
925
- label="Routing metadata",
926
  language="json",
927
  elem_classes=["code-block"],
928
  visible=False,
 
1
  """
2
  Android Skill Router — Gradio demo for the Build Small hackathon.
3
 
4
+ Natural language in → fine-tuned Qwen2.5-3B intent out → Android UI trajectory.
5
 
6
  Inference runs on Modal (deploy modal_apps/predict_api.py). Set MODAL_PREDICT_URL in Space secrets.
7
  """
 
22
 
23
  MODAL_PREDICT_URL = os.environ.get("MODAL_PREDICT_URL", "").rstrip("/")
24
 
25
+ PARAMETER_LABELS = {
26
+ "contact": "Contact",
27
+ "message": "Message",
28
+ "recipient": "Recipient",
29
+ "time": "Time",
30
+ "day": "Day",
31
+ "title": "Title",
32
+ "date": "Date",
33
+ "channel": "Channel",
34
+ "playlist": "Playlist",
35
+ "query": "Query",
36
+ "destination": "Destination",
37
+ "name": "Name",
38
+ }
39
+
40
  EXAMPLE_PROMPTS = [
41
  "play my workout playlist",
42
  "turn bluetooth on",
 
81
  <div class="result-card result-card--idle">
82
  <div class="result-card-header">Automation Result</div>
83
  <p class="result-placeholder">
84
+ Describe what you want your phone to do — the model will extract the skill and parameters,
85
+ then surface the matching Android trajectory.
86
  </p>
87
  <div class="result-metrics result-metrics--empty">
88
  <div class="metric-tile"><span class="metric-label">Detected Skill</span><span class="metric-value muted">—</span></div>
89
+ <div class="metric-tile"><span class="metric-label">Extracted Parameters</span><span class="metric-value muted">—</span></div>
90
  <div class="metric-tile"><span class="metric-label">Predicted Confidence</span><span class="metric-value muted">—</span></div>
91
+ <div class="metric-tile metric-tile--wide"><span class="metric-label">Android Trajectory</span><span class="metric-value muted">—</span></div>
92
  </div>
93
  </div>
94
  """
95
 
96
 
97
+ def _predict_intent(prompt: str) -> dict:
98
  if not MODAL_PREDICT_URL:
99
  raise gr.Error(
100
  "MODAL_PREDICT_URL is not set. Deploy modal_apps/predict_api.py on Modal and add the "
 
120
  raise gr.Error(
121
  f"Could not route prompt to a known skill (model returned {raw_skill!r})."
122
  )
123
+
124
+ parameters = payload.get("parameters", {})
125
+ if not isinstance(parameters, dict):
126
+ parameters = {}
127
+
128
+ return {"skill": skill, "parameters": parameters}
129
 
130
 
131
+ def _demo_confidence_percent(prompt: str, skill: str, parameters: dict | None = None) -> float:
132
+ """Deterministic demo confidence for the hackathon UI."""
133
+ param_blob = json.dumps(parameters or {}, sort_keys=True, separators=(",", ":"))
134
+ digest = hashlib.sha256(
135
+ f"{prompt.strip().lower()}|{skill}|{param_blob}".encode()
136
+ ).hexdigest()
137
  return 94.0 + (int(digest[:4], 16) % 56) / 10.0
138
 
139
 
140
+ def _format_parameters_html(parameters: dict) -> str:
141
+ if not parameters:
142
+ return '<span class="metric-value muted">None required</span>'
143
+
144
+ rows = []
145
+ for key, value in parameters.items():
146
+ label = PARAMETER_LABELS.get(key, key.replace("_", " ").title())
147
+ rows.append(
148
+ f'<div class="param-row">'
149
+ f'<span class="param-key">{html.escape(label)}</span>'
150
+ f'<span class="param-value">{html.escape(str(value))}</span>'
151
+ f"</div>"
152
+ )
153
+ return "".join(rows)
154
+
155
+
156
+ def _build_result_card(skill: str, prompt: str, parameters: dict | None = None) -> str:
157
  label = SKILL_LABELS.get(skill, skill.replace("_", " ").title())
158
+ params = parameters or {}
159
+ confidence = _demo_confidence_percent(prompt, skill, params)
160
  data = load_trajectory(skill)
161
  steps = data.get("steps", [])
162
  task = html.escape(str(data.get("task", "—")))
163
  app_pkg = html.escape(str(data.get("app", "—")))
164
  trajectory_file = html.escape(Path(SKILL_TO_TRAJECTORY[skill]).name)
165
+ params_html = _format_parameters_html(params)
166
 
167
  return f"""
168
  <div class="result-card">
 
183
  </div>
184
  <span class="metric-sub">Greedy decode · fine-tuned Qwen2.5-3B</span>
185
  </div>
186
+ <div class="metric-tile metric-tile--wide">
187
+ <span class="metric-label">Extracted Parameters</span>
188
+ <div class="param-list">{params_html}</div>
189
+ </div>
190
  <div class="metric-tile metric-tile--wide">
191
  <span class="metric-label">Associated Android Trajectory</span>
192
  <span class="metric-value">{task}</span>
 
214
  """
215
 
216
 
217
+ def _trajectory_summary(skill: str, parameters: dict | None = None) -> tuple[str, str, str]:
218
  data = load_trajectory(skill)
219
  steps = data.get("steps", [])
220
  task = data.get("task", "—")
221
  app_pkg = data.get("app", "—")
222
  trajectory_file = Path(SKILL_TO_TRAJECTORY[skill]).name
223
+ params = parameters or {}
224
 
225
  summary = (
226
  f"### {SKILL_LABELS.get(skill, skill)}\n\n"
 
230
  f"**Steps** · {len(steps)} UI snapshots \n"
231
  f"**File** · `{trajectory_file}`"
232
  )
233
+ if params:
234
+ param_lines = " \n".join(f"**{k}** · {v}" for k, v in params.items())
235
+ summary += f"\n\n**Extracted parameters** \n{param_lines}"
236
 
237
  preview = {
238
  "skill": skill,
239
+ "parameters": params,
240
  "task": task,
241
  "app": app_pkg,
242
  "steps": len(steps),
 
250
  if not prompt:
251
  raise gr.Error("Enter a prompt to classify.")
252
 
253
+ intent = _predict_intent(prompt)
254
+ skill = intent["skill"]
255
+ parameters = intent.get("parameters", {})
256
+ _, preview_json, trajectory_json = _trajectory_summary(skill, parameters)
257
  return (
258
+ _build_result_card(skill, prompt, parameters),
259
  gr.update(value=preview_json, visible=True),
260
  trajectory_json,
261
  )
 
698
  min-width: 3.5rem;
699
  text-align: right;
700
  }
701
+ .param-list {
702
+ display: flex;
703
+ flex-direction: column;
704
+ gap: 0.45rem;
705
+ }
706
+ .param-row {
707
+ display: flex;
708
+ flex-wrap: wrap;
709
+ align-items: baseline;
710
+ gap: 0.35rem 0.65rem;
711
+ background: rgba(255,255,255,0.7);
712
+ border: 1px solid rgba(0,45,98,0.08);
713
+ border-radius: 10px;
714
+ padding: 0.45rem 0.65rem;
715
+ }
716
+ .param-key {
717
+ font-size: 0.68rem;
718
+ font-weight: 700;
719
+ letter-spacing: 0.1em;
720
+ text-transform: uppercase;
721
+ color: var(--magenta);
722
+ }
723
+ .param-value {
724
+ font-size: 0.92rem;
725
+ font-weight: 600;
726
+ color: var(--navy-deep);
727
+ line-height: 1.4;
728
+ }
729
  .skills-catalog {
730
  list-style: none;
731
  margin: 0;
 
960
  <span class="pipeline-arrow">↓</span>
961
  <span class="pipeline-step highlight">Qwen 2.5 3B</span>
962
  <span class="pipeline-arrow">↓</span>
963
+ <span class="pipeline-step">Intent Extraction</span>
964
  <span class="pipeline-arrow">↓</span>
965
  <span class="pipeline-step">Replay Engine</span>
966
  <span class="pipeline-arrow">↓</span>
 
986
  show_label=False,
987
  )
988
  with gr.Row(elem_classes=["btn-row"]):
989
+ submit = gr.Button("Run intent →", variant="primary", scale=2)
990
  skills_btn = gr.Button("Browse skills", variant="secondary", scale=1)
991
  with gr.Group(elem_classes=["examples-wrap"]):
992
  gr.Examples(
 
1003
  show_label=False,
1004
  )
1005
  preview_json = gr.Code(
1006
+ label="Intent + routing metadata",
1007
  language="json",
1008
  elem_classes=["code-block"],
1009
  visible=False,
data/eval_intent_prompts.json ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "prompt": "alarm tomorrow 5:30 am",
4
+ "expected": {
5
+ "skill": "create_alarm",
6
+ "parameters": {
7
+ "time": "5:30 am",
8
+ "day": "tomorrow"
9
+ }
10
+ }
11
+ },
12
+ {
13
+ "prompt": "schedule 9 pm alarm tonight",
14
+ "expected": {
15
+ "skill": "create_alarm",
16
+ "parameters": {
17
+ "time": "9 pm",
18
+ "day": "tonight"
19
+ }
20
+ }
21
+ },
22
+ {
23
+ "prompt": "wake me up at 8:30 am next friday",
24
+ "expected": {
25
+ "skill": "create_alarm",
26
+ "parameters": {
27
+ "time": "8:30 am",
28
+ "day": "next friday"
29
+ }
30
+ }
31
+ },
32
+ {
33
+ "prompt": "wake up at noon tomorrow",
34
+ "expected": {
35
+ "skill": "create_alarm",
36
+ "parameters": {
37
+ "time": "noon",
38
+ "day": "tomorrow"
39
+ }
40
+ }
41
+ },
42
+ {
43
+ "prompt": "put alarm for 6:45 am saturday",
44
+ "expected": {
45
+ "skill": "create_alarm",
46
+ "parameters": {
47
+ "time": "6:45 am",
48
+ "day": "saturday"
49
+ }
50
+ }
51
+ },
52
+ {
53
+ "prompt": "i need a 7:15 am alarm wednesday",
54
+ "expected": {
55
+ "skill": "create_alarm",
56
+ "parameters": {
57
+ "time": "7:15 am",
58
+ "day": "wednesday"
59
+ }
60
+ }
61
+ },
62
+ {
63
+ "prompt": "add flight to calendar sunday 8 am",
64
+ "expected": {
65
+ "skill": "calendar_create_event",
66
+ "parameters": {
67
+ "title": "flight",
68
+ "date": "sunday",
69
+ "time": "8 am"
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "prompt": "schedule code review thursday 2 pm",
75
+ "expected": {
76
+ "skill": "calendar_create_event",
77
+ "parameters": {
78
+ "title": "code review",
79
+ "date": "thursday",
80
+ "time": "2 pm"
81
+ }
82
+ }
83
+ },
84
+ {
85
+ "prompt": "create event hackathon demo monday 4 pm",
86
+ "expected": {
87
+ "skill": "calendar_create_event",
88
+ "parameters": {
89
+ "title": "hackathon demo",
90
+ "date": "monday",
91
+ "time": "4 pm"
92
+ }
93
+ }
94
+ },
95
+ {
96
+ "prompt": "book dentist appointment next friday 3 pm",
97
+ "expected": {
98
+ "skill": "calendar_create_event",
99
+ "parameters": {
100
+ "title": "dentist appointment",
101
+ "date": "next friday",
102
+ "time": "3 pm"
103
+ }
104
+ }
105
+ },
106
+ {
107
+ "prompt": "put lunch with priya on calendar wednesday noon",
108
+ "expected": {
109
+ "skill": "calendar_create_event",
110
+ "parameters": {
111
+ "title": "lunch with priya",
112
+ "date": "wednesday",
113
+ "time": "noon"
114
+ }
115
+ }
116
+ },
117
+ {
118
+ "prompt": "add team standup to my calendar tuesday 10 am",
119
+ "expected": {
120
+ "skill": "calendar_create_event",
121
+ "parameters": {
122
+ "title": "team standup",
123
+ "date": "tuesday",
124
+ "time": "10 am"
125
+ }
126
+ }
127
+ },
128
+ {
129
+ "prompt": "activate my wifi connection",
130
+ "expected": {
131
+ "skill": "wifi_enable",
132
+ "parameters": {}
133
+ }
134
+ },
135
+ {
136
+ "prompt": "switch wlan on",
137
+ "expected": {
138
+ "skill": "wifi_enable",
139
+ "parameters": {}
140
+ }
141
+ },
142
+ {
143
+ "prompt": "i need wifi enabled",
144
+ "expected": {
145
+ "skill": "wifi_enable",
146
+ "parameters": {}
147
+ }
148
+ },
149
+ {
150
+ "prompt": "turn bluetooth on",
151
+ "expected": {
152
+ "skill": "bluetooth_enable",
153
+ "parameters": {}
154
+ }
155
+ },
156
+ {
157
+ "prompt": "activate bluetooth radio",
158
+ "expected": {
159
+ "skill": "bluetooth_enable",
160
+ "parameters": {}
161
+ }
162
+ },
163
+ {
164
+ "prompt": "whatsapp sarah saying thanks",
165
+ "expected": {
166
+ "skill": "whatsapp_send_message",
167
+ "parameters": {
168
+ "contact": "sarah",
169
+ "message": "thanks"
170
+ }
171
+ }
172
+ },
173
+ {
174
+ "prompt": "message alex on whatsapp be there in 5",
175
+ "expected": {
176
+ "skill": "whatsapp_send_message",
177
+ "parameters": {
178
+ "contact": "alex",
179
+ "message": "be there in 5"
180
+ }
181
+ }
182
+ },
183
+ {
184
+ "prompt": "send ri a message saying i'll be late",
185
+ "expected": {
186
+ "skill": "whatsapp_send_message",
187
+ "parameters": {
188
+ "contact": "ri",
189
+ "message": "i'll be late"
190
+ }
191
+ }
192
+ },
193
+ {
194
+ "prompt": "ping priya on whatsapp with hello",
195
+ "expected": {
196
+ "skill": "whatsapp_send_message",
197
+ "parameters": {
198
+ "contact": "priya",
199
+ "message": "hello"
200
+ }
201
+ }
202
+ },
203
+ {
204
+ "prompt": "tell biraj on whatsapp see you tonight",
205
+ "expected": {
206
+ "skill": "whatsapp_send_message",
207
+ "parameters": {
208
+ "contact": "biraj",
209
+ "message": "see you tonight"
210
+ }
211
+ }
212
+ },
213
+ {
214
+ "prompt": "text mom on whatsapp i'm on my way",
215
+ "expected": {
216
+ "skill": "whatsapp_send_message",
217
+ "parameters": {
218
+ "contact": "mom",
219
+ "message": "i'm on my way"
220
+ }
221
+ }
222
+ },
223
+ {
224
+ "prompt": "launch camera and snap a pic",
225
+ "expected": {
226
+ "skill": "camera_take_photo",
227
+ "parameters": {}
228
+ }
229
+ },
230
+ {
231
+ "prompt": "capture a photo with the camera",
232
+ "expected": {
233
+ "skill": "camera_take_photo",
234
+ "parameters": {}
235
+ }
236
+ },
237
+ {
238
+ "prompt": "show me data contributors on slack",
239
+ "expected": {
240
+ "skill": "slack_open_channel",
241
+ "parameters": {
242
+ "channel": "data contributors"
243
+ }
244
+ }
245
+ },
246
+ {
247
+ "prompt": "stop the music on spotify",
248
+ "expected": {
249
+ "skill": "spotify_pause",
250
+ "parameters": {}
251
+ }
252
+ },
253
+ {
254
+ "prompt": "halt the spotify player",
255
+ "expected": {
256
+ "skill": "spotify_pause",
257
+ "parameters": {}
258
+ }
259
+ },
260
+ {
261
+ "prompt": "hold spotify for now",
262
+ "expected": {
263
+ "skill": "spotify_pause",
264
+ "parameters": {}
265
+ }
266
+ },
267
+ {
268
+ "prompt": "start focus playlist spotify",
269
+ "expected": {
270
+ "skill": "spotify_play_playlist",
271
+ "parameters": {
272
+ "playlist": "focus"
273
+ }
274
+ }
275
+ },
276
+ {
277
+ "prompt": "start my chill playlist on spotify",
278
+ "expected": {
279
+ "skill": "spotify_play_playlist",
280
+ "parameters": {
281
+ "playlist": "chill"
282
+ }
283
+ }
284
+ },
285
+ {
286
+ "prompt": "put on my liked songs spotify",
287
+ "expected": {
288
+ "skill": "spotify_play_playlist",
289
+ "parameters": {
290
+ "playlist": "liked songs"
291
+ }
292
+ }
293
+ },
294
+ {
295
+ "prompt": "call uber to convention center",
296
+ "expected": {
297
+ "skill": "uber_request_ride",
298
+ "parameters": {
299
+ "destination": "convention center"
300
+ }
301
+ }
302
+ },
303
+ {
304
+ "prompt": "request a ride to central station on uber",
305
+ "expected": {
306
+ "skill": "uber_request_ride",
307
+ "parameters": {
308
+ "destination": "central station"
309
+ }
310
+ }
311
+ },
312
+ {
313
+ "prompt": "need a ride to city hospital via uber",
314
+ "expected": {
315
+ "skill": "uber_request_ride",
316
+ "parameters": {
317
+ "destination": "city hospital"
318
+ }
319
+ }
320
+ },
321
+ {
322
+ "prompt": "linkedin search nina patel",
323
+ "expected": {
324
+ "skill": "linkedin_search_person",
325
+ "parameters": {
326
+ "name": "nina patel"
327
+ }
328
+ }
329
+ },
330
+ {
331
+ "prompt": "play reggaeton after searching spotify",
332
+ "expected": {
333
+ "skill": "spotify_search_play",
334
+ "parameters": {
335
+ "query": "reggaeton"
336
+ }
337
+ }
338
+ },
339
+ {
340
+ "prompt": "find ambient music on spotify and play",
341
+ "expected": {
342
+ "skill": "spotify_search_play",
343
+ "parameters": {
344
+ "query": "ambient"
345
+ }
346
+ }
347
+ },
348
+ {
349
+ "prompt": "search lo fi beats on spotify and play them",
350
+ "expected": {
351
+ "skill": "spotify_search_play",
352
+ "parameters": {
353
+ "query": "lo fi beats"
354
+ }
355
+ }
356
+ },
357
+ {
358
+ "prompt": "look up john smith in contacts",
359
+ "expected": {
360
+ "skill": "contacts_search",
361
+ "parameters": {
362
+ "contact": "john smith"
363
+ }
364
+ }
365
+ },
366
+ {
367
+ "prompt": "search contacts for dad",
368
+ "expected": {
369
+ "skill": "contacts_search",
370
+ "parameters": {
371
+ "contact": "dad"
372
+ }
373
+ }
374
+ },
375
+ {
376
+ "prompt": "send gmail to sarah@gmail.com subject meeting notes",
377
+ "expected": {
378
+ "skill": "gmail_send_email",
379
+ "parameters": {
380
+ "recipient": "sarah@gmail.com",
381
+ "message": "meeting notes"
382
+ }
383
+ }
384
+ },
385
+ {
386
+ "prompt": "gmail team@work.com weekly report",
387
+ "expected": {
388
+ "skill": "gmail_send_email",
389
+ "parameters": {
390
+ "recipient": "team@work.com",
391
+ "message": "weekly report"
392
+ }
393
+ }
394
+ },
395
+ {
396
+ "prompt": "send email to professor asking about assignment",
397
+ "expected": {
398
+ "skill": "gmail_send_email",
399
+ "parameters": {
400
+ "recipient": "professor",
401
+ "message": "asking about assignment"
402
+ }
403
+ }
404
+ },
405
+ {
406
+ "prompt": "write mail to hr@company.com vacation request",
407
+ "expected": {
408
+ "skill": "gmail_send_email",
409
+ "parameters": {
410
+ "recipient": "hr@company.com",
411
+ "message": "vacation request"
412
+ }
413
+ }
414
+ },
415
+ {
416
+ "prompt": "compose email to client proposal attached",
417
+ "expected": {
418
+ "skill": "gmail_send_email",
419
+ "parameters": {
420
+ "recipient": "client",
421
+ "message": "proposal attached"
422
+ }
423
+ }
424
+ }
425
+ ]
data/pocket_benchmark_prompts.json ADDED
@@ -0,0 +1,2975 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "pa-001",
4
+ "prompt": "yo set an alrm for like 5:45 tmrw morning pls",
5
+ "expected": {
6
+ "skill": "create_alarm",
7
+ "parameters": {
8
+ "time": "5:45",
9
+ "day": "tmrw morning"
10
+ }
11
+ },
12
+ "domain": "alarms",
13
+ "styles": [
14
+ "slang",
15
+ "typo",
16
+ "conversational"
17
+ ]
18
+ },
19
+ {
20
+ "id": "pa-002",
21
+ "prompt": "need to b up at 6ish on monday ngl",
22
+ "expected": {
23
+ "skill": "create_alarm",
24
+ "parameters": {
25
+ "time": "6ish",
26
+ "day": "monday"
27
+ }
28
+ },
29
+ "domain": "alarms",
30
+ "styles": [
31
+ "slang",
32
+ "incomplete",
33
+ "conversational"
34
+ ]
35
+ },
36
+ {
37
+ "id": "pa-003",
38
+ "prompt": "6am alarm pls",
39
+ "expected": {
40
+ "skill": "create_alarm",
41
+ "parameters": {
42
+ "time": "6am"
43
+ }
44
+ },
45
+ "domain": "alarms",
46
+ "styles": [
47
+ "incomplete"
48
+ ]
49
+ },
50
+ {
51
+ "id": "pa-004",
52
+ "prompt": "wake me 7:20 wednesday thx",
53
+ "expected": {
54
+ "skill": "create_alarm",
55
+ "parameters": {
56
+ "time": "7:20",
57
+ "day": "wednesday"
58
+ }
59
+ },
60
+ "domain": "alarms",
61
+ "styles": [
62
+ "incomplete",
63
+ "conversational"
64
+ ]
65
+ },
66
+ {
67
+ "id": "pa-005",
68
+ "prompt": "uh can u do a 10:15 pm thing tonight",
69
+ "expected": {
70
+ "skill": "create_alarm",
71
+ "parameters": {
72
+ "time": "10:15 pm",
73
+ "day": "tonight"
74
+ }
75
+ },
76
+ "domain": "alarms",
77
+ "styles": [
78
+ "conversational",
79
+ "incomplete"
80
+ ]
81
+ },
82
+ {
83
+ "id": "pa-006",
84
+ "prompt": "tomorow 4:30 wake up plz",
85
+ "expected": {
86
+ "skill": "create_alarm",
87
+ "parameters": {
88
+ "time": "4:30",
89
+ "day": "tomorow"
90
+ }
91
+ },
92
+ "domain": "alarms",
93
+ "styles": [
94
+ "typo",
95
+ "incomplete"
96
+ ]
97
+ },
98
+ {
99
+ "id": "pa-007",
100
+ "prompt": "deadass need alarm sunday noon",
101
+ "expected": {
102
+ "skill": "create_alarm",
103
+ "parameters": {
104
+ "time": "noon",
105
+ "day": "sunday"
106
+ }
107
+ },
108
+ "domain": "alarms",
109
+ "styles": [
110
+ "slang",
111
+ "incomplete"
112
+ ]
113
+ },
114
+ {
115
+ "id": "pa-008",
116
+ "prompt": "ok wait actually make it 8:05 am next tuesday instead",
117
+ "expected": {
118
+ "skill": "create_alarm",
119
+ "parameters": {
120
+ "time": "8:05 am",
121
+ "day": "next tuesday"
122
+ }
123
+ },
124
+ "domain": "alarms",
125
+ "styles": [
126
+ "conversational"
127
+ ]
128
+ },
129
+ {
130
+ "id": "pa-009",
131
+ "prompt": "4:50 alarm for sat pls",
132
+ "expected": {
133
+ "skill": "create_alarm",
134
+ "parameters": {
135
+ "time": "4:50",
136
+ "day": "sat"
137
+ }
138
+ },
139
+ "domain": "alarms",
140
+ "styles": [
141
+ "slang",
142
+ "incomplete"
143
+ ]
144
+ },
145
+ {
146
+ "id": "pa-010",
147
+ "prompt": "gonna crash early \u2014 buzz me at 9:30 pm today",
148
+ "expected": {
149
+ "skill": "create_alarm",
150
+ "parameters": {
151
+ "time": "9:30 pm",
152
+ "day": "today"
153
+ }
154
+ },
155
+ "domain": "alarms",
156
+ "styles": [
157
+ "slang",
158
+ "conversational"
159
+ ]
160
+ },
161
+ {
162
+ "id": "pa-011",
163
+ "prompt": "hey siri vibes but pocket automator: 5 am thurs",
164
+ "expected": {
165
+ "skill": "create_alarm",
166
+ "parameters": {
167
+ "time": "5 am",
168
+ "day": "thurs"
169
+ }
170
+ },
171
+ "domain": "alarms",
172
+ "styles": [
173
+ "slang",
174
+ "conversational"
175
+ ]
176
+ },
177
+ {
178
+ "id": "pa-012",
179
+ "prompt": "lmk when its 6:30 \u2014 jk just set alarm 6:30 friday",
180
+ "expected": {
181
+ "skill": "create_alarm",
182
+ "parameters": {
183
+ "time": "6:30",
184
+ "day": "friday"
185
+ }
186
+ },
187
+ "domain": "alarms",
188
+ "styles": [
189
+ "slang",
190
+ "conversational"
191
+ ]
192
+ },
193
+ {
194
+ "id": "pa-013",
195
+ "prompt": "alarm?? 7:45 am this weekend",
196
+ "expected": {
197
+ "skill": "create_alarm",
198
+ "parameters": {
199
+ "time": "7:45 am",
200
+ "day": "this weekend"
201
+ }
202
+ },
203
+ "domain": "alarms",
204
+ "styles": [
205
+ "incomplete",
206
+ "conversational"
207
+ ]
208
+ },
209
+ {
210
+ "id": "pa-014",
211
+ "prompt": "pls dont let me oversleep \u2014 6:10 tomorrow",
212
+ "expected": {
213
+ "skill": "create_alarm",
214
+ "parameters": {
215
+ "time": "6:10",
216
+ "day": "tomorrow"
217
+ }
218
+ },
219
+ "domain": "alarms",
220
+ "styles": [
221
+ "conversational"
222
+ ]
223
+ },
224
+ {
225
+ "id": "pa-015",
226
+ "prompt": "5:55am wakeup call monday pls",
227
+ "expected": {
228
+ "skill": "create_alarm",
229
+ "parameters": {
230
+ "time": "5:55am",
231
+ "day": "monday"
232
+ }
233
+ },
234
+ "domain": "alarms",
235
+ "styles": [
236
+ "incomplete"
237
+ ]
238
+ },
239
+ {
240
+ "id": "pa-016",
241
+ "prompt": "set wakey wakey 8am tmr",
242
+ "expected": {
243
+ "skill": "create_alarm",
244
+ "parameters": {
245
+ "time": "8am",
246
+ "day": "tmr"
247
+ }
248
+ },
249
+ "domain": "alarms",
250
+ "styles": [
251
+ "slang",
252
+ "incomplete"
253
+ ]
254
+ },
255
+ {
256
+ "id": "pa-017",
257
+ "prompt": "lowkey need 11:11 pm alarm tonight",
258
+ "expected": {
259
+ "skill": "create_alarm",
260
+ "parameters": {
261
+ "time": "11:11 pm",
262
+ "day": "tonight"
263
+ }
264
+ },
265
+ "domain": "alarms",
266
+ "styles": [
267
+ "slang"
268
+ ]
269
+ },
270
+ {
271
+ "id": "pa-018",
272
+ "prompt": "can i get a 7 am buzzer next monday",
273
+ "expected": {
274
+ "skill": "create_alarm",
275
+ "parameters": {
276
+ "time": "7 am",
277
+ "day": "next monday"
278
+ }
279
+ },
280
+ "domain": "alarms",
281
+ "styles": [
282
+ "conversational"
283
+ ]
284
+ },
285
+ {
286
+ "id": "pa-019",
287
+ "prompt": "alarm at half past six tomorrow morning",
288
+ "expected": {
289
+ "skill": "create_alarm",
290
+ "parameters": {
291
+ "time": "half past six",
292
+ "day": "tomorrow morning"
293
+ }
294
+ },
295
+ "domain": "alarms",
296
+ "styles": [
297
+ "conversational"
298
+ ]
299
+ },
300
+ {
301
+ "id": "pa-020",
302
+ "prompt": "6:00 alarm 4 wednesday",
303
+ "expected": {
304
+ "skill": "create_alarm",
305
+ "parameters": {
306
+ "time": "6:00",
307
+ "day": "wednesday"
308
+ }
309
+ },
310
+ "domain": "alarms",
311
+ "styles": [
312
+ "typo",
313
+ "incomplete"
314
+ ]
315
+ },
316
+ {
317
+ "id": "pa-021",
318
+ "prompt": "wake me up round 9 on sunday",
319
+ "expected": {
320
+ "skill": "create_alarm",
321
+ "parameters": {
322
+ "time": "9",
323
+ "day": "sunday"
324
+ }
325
+ },
326
+ "domain": "alarms",
327
+ "styles": [
328
+ "slang",
329
+ "incomplete"
330
+ ]
331
+ },
332
+ {
333
+ "id": "pa-022",
334
+ "prompt": "fr need 5:15 am alarm this tuesday",
335
+ "expected": {
336
+ "skill": "create_alarm",
337
+ "parameters": {
338
+ "time": "5:15 am",
339
+ "day": "this tuesday"
340
+ }
341
+ },
342
+ "domain": "alarms",
343
+ "styles": [
344
+ "slang"
345
+ ]
346
+ },
347
+ {
348
+ "id": "pa-023",
349
+ "prompt": "hit up zoe on whatsapp say im omw",
350
+ "expected": {
351
+ "skill": "whatsapp_send_message",
352
+ "parameters": {
353
+ "contact": "zoe",
354
+ "message": "im omw"
355
+ }
356
+ },
357
+ "domain": "whatsapp",
358
+ "styles": [
359
+ "slang",
360
+ "typo",
361
+ "incomplete"
362
+ ]
363
+ },
364
+ {
365
+ "id": "pa-024",
366
+ "prompt": "wa msg marcus 'running 20 min late'",
367
+ "expected": {
368
+ "skill": "whatsapp_send_message",
369
+ "parameters": {
370
+ "contact": "marcus",
371
+ "message": "running 20 min late"
372
+ }
373
+ },
374
+ "domain": "whatsapp",
375
+ "styles": [
376
+ "slang",
377
+ "incomplete"
378
+ ]
379
+ },
380
+ {
381
+ "id": "pa-025",
382
+ "prompt": "tell roomie on watsapp dishes are done",
383
+ "expected": {
384
+ "skill": "whatsapp_send_message",
385
+ "parameters": {
386
+ "contact": "roomie",
387
+ "message": "dishes are done"
388
+ }
389
+ },
390
+ "domain": "whatsapp",
391
+ "styles": [
392
+ "slang",
393
+ "typo"
394
+ ]
395
+ },
396
+ {
397
+ "id": "pa-026",
398
+ "prompt": "whastapp elena \u2014 meeting got moved to 4",
399
+ "expected": {
400
+ "skill": "whatsapp_send_message",
401
+ "parameters": {
402
+ "contact": "elena",
403
+ "message": "meeting got moved to 4"
404
+ }
405
+ },
406
+ "domain": "whatsapp",
407
+ "styles": [
408
+ "typo",
409
+ "incomplete",
410
+ "conversational"
411
+ ]
412
+ },
413
+ {
414
+ "id": "pa-027",
415
+ "prompt": "shoot kevin from work a text on whatsapp: heads up im wfh",
416
+ "expected": {
417
+ "skill": "whatsapp_send_message",
418
+ "parameters": {
419
+ "contact": "kevin from work",
420
+ "message": "heads up im wfh"
421
+ }
422
+ },
423
+ "domain": "whatsapp",
424
+ "styles": [
425
+ "slang",
426
+ "conversational"
427
+ ]
428
+ },
429
+ {
430
+ "id": "pa-028",
431
+ "prompt": "msg grandpa on whatsapp happy bday!!",
432
+ "expected": {
433
+ "skill": "whatsapp_send_message",
434
+ "parameters": {
435
+ "contact": "grandpa",
436
+ "message": "happy bday!!"
437
+ }
438
+ },
439
+ "domain": "whatsapp",
440
+ "styles": [
441
+ "slang",
442
+ "typo"
443
+ ]
444
+ },
445
+ {
446
+ "id": "pa-029",
447
+ "prompt": "yo ping sis saying im outside",
448
+ "expected": {
449
+ "skill": "whatsapp_send_message",
450
+ "parameters": {
451
+ "contact": "sis",
452
+ "message": "im outside"
453
+ }
454
+ },
455
+ "domain": "whatsapp",
456
+ "styles": [
457
+ "slang",
458
+ "incomplete"
459
+ ]
460
+ },
461
+ {
462
+ "id": "pa-030",
463
+ "prompt": "whatsap daniel 'got ur package'",
464
+ "expected": {
465
+ "skill": "whatsapp_send_message",
466
+ "parameters": {
467
+ "contact": "daniel",
468
+ "message": "got ur package"
469
+ }
470
+ },
471
+ "domain": "whatsapp",
472
+ "styles": [
473
+ "typo",
474
+ "incomplete"
475
+ ]
476
+ },
477
+ {
478
+ "id": "pa-031",
479
+ "prompt": "can u text aunt lisa on whatsapp that flight lands 6",
480
+ "expected": {
481
+ "skill": "whatsapp_send_message",
482
+ "parameters": {
483
+ "contact": "aunt lisa",
484
+ "message": "flight lands 6"
485
+ }
486
+ },
487
+ "domain": "whatsapp",
488
+ "styles": [
489
+ "conversational",
490
+ "incomplete"
491
+ ]
492
+ },
493
+ {
494
+ "id": "pa-032",
495
+ "prompt": "drop a whatsapp to coach mike \u2014 practice cancelled",
496
+ "expected": {
497
+ "skill": "whatsapp_send_message",
498
+ "parameters": {
499
+ "contact": "coach mike",
500
+ "message": "practice cancelled"
501
+ }
502
+ },
503
+ "domain": "whatsapp",
504
+ "styles": [
505
+ "slang",
506
+ "incomplete"
507
+ ]
508
+ },
509
+ {
510
+ "id": "pa-033",
511
+ "prompt": "whatsapp my landlord rent sent",
512
+ "expected": {
513
+ "skill": "whatsapp_send_message",
514
+ "parameters": {
515
+ "contact": "my landlord",
516
+ "message": "rent sent"
517
+ }
518
+ },
519
+ "domain": "whatsapp",
520
+ "styles": [
521
+ "incomplete"
522
+ ]
523
+ },
524
+ {
525
+ "id": "pa-034",
526
+ "prompt": "lemme msg hannah real quick on whatsapp: u free later?",
527
+ "expected": {
528
+ "skill": "whatsapp_send_message",
529
+ "parameters": {
530
+ "contact": "hannah",
531
+ "message": "u free later?"
532
+ }
533
+ },
534
+ "domain": "whatsapp",
535
+ "styles": [
536
+ "slang",
537
+ "conversational"
538
+ ]
539
+ },
540
+ {
541
+ "id": "pa-035",
542
+ "prompt": "send whatsapp to benji thx for covering my shift",
543
+ "expected": {
544
+ "skill": "whatsapp_send_message",
545
+ "parameters": {
546
+ "contact": "benji",
547
+ "message": "thx for covering my shift"
548
+ }
549
+ },
550
+ "domain": "whatsapp",
551
+ "styles": [
552
+ "slang",
553
+ "incomplete"
554
+ ]
555
+ },
556
+ {
557
+ "id": "pa-036",
558
+ "prompt": "text whatsapp to carpool group running late again sry",
559
+ "expected": {
560
+ "skill": "whatsapp_send_message",
561
+ "parameters": {
562
+ "contact": "carpool group",
563
+ "message": "running late again sry"
564
+ }
565
+ },
566
+ "domain": "whatsapp",
567
+ "styles": [
568
+ "slang",
569
+ "incomplete"
570
+ ]
571
+ },
572
+ {
573
+ "id": "pa-037",
574
+ "prompt": "ok whatsapp olivia saying docs r uploaded",
575
+ "expected": {
576
+ "skill": "whatsapp_send_message",
577
+ "parameters": {
578
+ "contact": "olivia",
579
+ "message": "docs r uploaded"
580
+ }
581
+ },
582
+ "domain": "whatsapp",
583
+ "styles": [
584
+ "slang",
585
+ "typo",
586
+ "conversational"
587
+ ]
588
+ },
589
+ {
590
+ "id": "pa-038",
591
+ "prompt": "wahtapp msg to tomas \u2014 keys under mat",
592
+ "expected": {
593
+ "skill": "whatsapp_send_message",
594
+ "parameters": {
595
+ "contact": "tomas",
596
+ "message": "keys under mat"
597
+ }
598
+ },
599
+ "domain": "whatsapp",
600
+ "styles": [
601
+ "typo",
602
+ "incomplete"
603
+ ]
604
+ },
605
+ {
606
+ "id": "pa-039",
607
+ "prompt": "just tell jules on whatsapp im grabbing boba",
608
+ "expected": {
609
+ "skill": "whatsapp_send_message",
610
+ "parameters": {
611
+ "contact": "jules",
612
+ "message": "im grabbing boba"
613
+ }
614
+ },
615
+ "domain": "whatsapp",
616
+ "styles": [
617
+ "slang",
618
+ "conversational"
619
+ ]
620
+ },
621
+ {
622
+ "id": "pa-040",
623
+ "prompt": "whatsapp neighbor hey can u sign for delivery",
624
+ "expected": {
625
+ "skill": "whatsapp_send_message",
626
+ "parameters": {
627
+ "contact": "neighbor",
628
+ "message": "hey can u sign for delivery"
629
+ }
630
+ },
631
+ "domain": "whatsapp",
632
+ "styles": [
633
+ "incomplete",
634
+ "conversational"
635
+ ]
636
+ },
637
+ {
638
+ "id": "pa-041",
639
+ "prompt": "msg whatsapp to dr patel appointment confirmed",
640
+ "expected": {
641
+ "skill": "whatsapp_send_message",
642
+ "parameters": {
643
+ "contact": "dr patel",
644
+ "message": "appointment confirmed"
645
+ }
646
+ },
647
+ "domain": "whatsapp",
648
+ "styles": [
649
+ "incomplete"
650
+ ]
651
+ },
652
+ {
653
+ "id": "pa-042",
654
+ "prompt": "hit my brother on whatsapp w/ 'u still coming?'",
655
+ "expected": {
656
+ "skill": "whatsapp_send_message",
657
+ "parameters": {
658
+ "contact": "my brother",
659
+ "message": "u still coming?"
660
+ }
661
+ },
662
+ "domain": "whatsapp",
663
+ "styles": [
664
+ "slang",
665
+ "incomplete"
666
+ ]
667
+ },
668
+ {
669
+ "id": "pa-043",
670
+ "prompt": "whatsapp bestie ngl im stressed af today",
671
+ "expected": {
672
+ "skill": "whatsapp_send_message",
673
+ "parameters": {
674
+ "contact": "bestie",
675
+ "message": "ngl im stressed af today"
676
+ }
677
+ },
678
+ "domain": "whatsapp",
679
+ "styles": [
680
+ "slang",
681
+ "incomplete"
682
+ ]
683
+ },
684
+ {
685
+ "id": "pa-044",
686
+ "prompt": "send whatsapp msg to felix \u2014 slide deck is ready",
687
+ "expected": {
688
+ "skill": "whatsapp_send_message",
689
+ "parameters": {
690
+ "contact": "felix",
691
+ "message": "slide deck is ready"
692
+ }
693
+ },
694
+ "domain": "whatsapp",
695
+ "styles": [
696
+ "incomplete"
697
+ ]
698
+ },
699
+ {
700
+ "id": "pa-045",
701
+ "prompt": "spotfy stop rn",
702
+ "expected": {
703
+ "skill": "spotify_pause",
704
+ "parameters": {}
705
+ },
706
+ "domain": "spotify",
707
+ "styles": [
708
+ "typo",
709
+ "slang",
710
+ "incomplete"
711
+ ]
712
+ },
713
+ {
714
+ "id": "pa-046",
715
+ "prompt": "yo pause the music",
716
+ "expected": {
717
+ "skill": "spotify_pause",
718
+ "parameters": {}
719
+ },
720
+ "domain": "spotify",
721
+ "styles": [
722
+ "slang",
723
+ "incomplete"
724
+ ]
725
+ },
726
+ {
727
+ "id": "pa-047",
728
+ "prompt": "mute spotify pls im on a call",
729
+ "expected": {
730
+ "skill": "spotify_pause",
731
+ "parameters": {}
732
+ },
733
+ "domain": "spotify",
734
+ "styles": [
735
+ "conversational"
736
+ ]
737
+ },
738
+ {
739
+ "id": "pa-048",
740
+ "prompt": "can u kill the spotify track",
741
+ "expected": {
742
+ "skill": "spotify_pause",
743
+ "parameters": {}
744
+ },
745
+ "domain": "spotify",
746
+ "styles": [
747
+ "slang",
748
+ "conversational"
749
+ ]
750
+ },
751
+ {
752
+ "id": "pa-049",
753
+ "prompt": "hold up pause spotify",
754
+ "expected": {
755
+ "skill": "spotify_pause",
756
+ "parameters": {}
757
+ },
758
+ "domain": "spotify",
759
+ "styles": [
760
+ "slang",
761
+ "incomplete"
762
+ ]
763
+ },
764
+ {
765
+ "id": "pa-050",
766
+ "prompt": "spoitfy pause rq",
767
+ "expected": {
768
+ "skill": "spotify_pause",
769
+ "parameters": {}
770
+ },
771
+ "domain": "spotify",
772
+ "styles": [
773
+ "typo",
774
+ "slang",
775
+ "incomplete"
776
+ ]
777
+ },
778
+ {
779
+ "id": "pa-051",
780
+ "prompt": "stop spotify playback thx",
781
+ "expected": {
782
+ "skill": "spotify_pause",
783
+ "parameters": {}
784
+ },
785
+ "domain": "spotify",
786
+ "styles": [
787
+ "conversational"
788
+ ]
789
+ },
790
+ {
791
+ "id": "pa-052",
792
+ "prompt": "shhh pause spotify",
793
+ "expected": {
794
+ "skill": "spotify_pause",
795
+ "parameters": {}
796
+ },
797
+ "domain": "spotify",
798
+ "styles": [
799
+ "incomplete",
800
+ "conversational"
801
+ ]
802
+ },
803
+ {
804
+ "id": "pa-053",
805
+ "prompt": "spotify off for a sec",
806
+ "expected": {
807
+ "skill": "spotify_pause",
808
+ "parameters": {}
809
+ },
810
+ "domain": "spotify",
811
+ "styles": [
812
+ "slang",
813
+ "incomplete"
814
+ ]
815
+ },
816
+ {
817
+ "id": "pa-054",
818
+ "prompt": "pause whats playing on spotify",
819
+ "expected": {
820
+ "skill": "spotify_pause",
821
+ "parameters": {}
822
+ },
823
+ "domain": "spotify",
824
+ "styles": [
825
+ "incomplete"
826
+ ]
827
+ },
828
+ {
829
+ "id": "pa-055",
830
+ "prompt": "ok stop spotify music",
831
+ "expected": {
832
+ "skill": "spotify_pause",
833
+ "parameters": {}
834
+ },
835
+ "domain": "spotify",
836
+ "styles": [
837
+ "conversational",
838
+ "incomplete"
839
+ ]
840
+ },
841
+ {
842
+ "id": "pa-056",
843
+ "prompt": "spotify halt plz",
844
+ "expected": {
845
+ "skill": "spotify_pause",
846
+ "parameters": {}
847
+ },
848
+ "domain": "spotify",
849
+ "styles": [
850
+ "incomplete"
851
+ ]
852
+ },
853
+ {
854
+ "id": "pa-057",
855
+ "prompt": "silence spotify real quick",
856
+ "expected": {
857
+ "skill": "spotify_pause",
858
+ "parameters": {}
859
+ },
860
+ "domain": "spotify",
861
+ "styles": [
862
+ "conversational"
863
+ ]
864
+ },
865
+ {
866
+ "id": "pa-058",
867
+ "prompt": "pause spotify \u2014 landlord knocking",
868
+ "expected": {
869
+ "skill": "spotify_pause",
870
+ "parameters": {}
871
+ },
872
+ "domain": "spotify",
873
+ "styles": [
874
+ "conversational"
875
+ ]
876
+ },
877
+ {
878
+ "id": "pa-059",
879
+ "prompt": "spotify stop the banger lol",
880
+ "expected": {
881
+ "skill": "spotify_pause",
882
+ "parameters": {}
883
+ },
884
+ "domain": "spotify",
885
+ "styles": [
886
+ "slang",
887
+ "conversational"
888
+ ]
889
+ },
890
+ {
891
+ "id": "pa-060",
892
+ "prompt": "cut spotify audio",
893
+ "expected": {
894
+ "skill": "spotify_pause",
895
+ "parameters": {}
896
+ },
897
+ "domain": "spotify",
898
+ "styles": [
899
+ "slang",
900
+ "incomplete"
901
+ ]
902
+ },
903
+ {
904
+ "id": "pa-061",
905
+ "prompt": "spotify pause pls headphones dying",
906
+ "expected": {
907
+ "skill": "spotify_pause",
908
+ "parameters": {}
909
+ },
910
+ "domain": "spotify",
911
+ "styles": [
912
+ "conversational"
913
+ ]
914
+ },
915
+ {
916
+ "id": "pa-062",
917
+ "prompt": "uh pause spotify",
918
+ "expected": {
919
+ "skill": "spotify_pause",
920
+ "parameters": {}
921
+ },
922
+ "domain": "spotify",
923
+ "styles": [
924
+ "conversational",
925
+ "incomplete"
926
+ ]
927
+ },
928
+ {
929
+ "id": "pa-063",
930
+ "prompt": "stop the spotify pls",
931
+ "expected": {
932
+ "skill": "spotify_pause",
933
+ "parameters": {}
934
+ },
935
+ "domain": "spotify",
936
+ "styles": [
937
+ "incomplete"
938
+ ]
939
+ },
940
+ {
941
+ "id": "pa-064",
942
+ "prompt": "spotify freeze for now pls",
943
+ "expected": {
944
+ "skill": "spotify_pause",
945
+ "parameters": {}
946
+ },
947
+ "domain": "spotify",
948
+ "styles": [
949
+ "incomplete",
950
+ "conversational"
951
+ ]
952
+ },
953
+ {
954
+ "id": "pa-065",
955
+ "prompt": "pause my spotify session",
956
+ "expected": {
957
+ "skill": "spotify_pause",
958
+ "parameters": {}
959
+ },
960
+ "domain": "spotify",
961
+ "styles": [
962
+ "incomplete"
963
+ ]
964
+ },
965
+ {
966
+ "id": "pa-066",
967
+ "prompt": "spotify quiet for now",
968
+ "expected": {
969
+ "skill": "spotify_pause",
970
+ "parameters": {}
971
+ },
972
+ "domain": "spotify",
973
+ "styles": [
974
+ "incomplete",
975
+ "conversational"
976
+ ]
977
+ },
978
+ {
979
+ "id": "pa-067",
980
+ "prompt": "put on my midnight drives mix on spotify",
981
+ "expected": {
982
+ "skill": "spotify_play_playlist",
983
+ "parameters": {
984
+ "playlist": "midnight drives mix"
985
+ }
986
+ },
987
+ "domain": "spotify",
988
+ "styles": [
989
+ "conversational"
990
+ ]
991
+ },
992
+ {
993
+ "id": "pa-068",
994
+ "prompt": "spotify play sunday scaries",
995
+ "expected": {
996
+ "skill": "spotify_play_playlist",
997
+ "parameters": {
998
+ "playlist": "sunday scaries"
999
+ }
1000
+ },
1001
+ "domain": "spotify",
1002
+ "styles": [
1003
+ "incomplete"
1004
+ ]
1005
+ },
1006
+ {
1007
+ "id": "pa-069",
1008
+ "prompt": "queue up deep focus vibes on spotify",
1009
+ "expected": {
1010
+ "skill": "spotify_play_playlist",
1011
+ "parameters": {
1012
+ "playlist": "deep focus vibes"
1013
+ }
1014
+ },
1015
+ "domain": "spotify",
1016
+ "styles": [
1017
+ "slang"
1018
+ ]
1019
+ },
1020
+ {
1021
+ "id": "pa-070",
1022
+ "prompt": "play my 2010s throwbacks spotify",
1023
+ "expected": {
1024
+ "skill": "spotify_play_playlist",
1025
+ "parameters": {
1026
+ "playlist": "2010s throwbacks"
1027
+ }
1028
+ },
1029
+ "domain": "spotify",
1030
+ "styles": [
1031
+ "incomplete"
1032
+ ]
1033
+ },
1034
+ {
1035
+ "id": "pa-071",
1036
+ "prompt": "spotfy playlist: rainy day jazz",
1037
+ "expected": {
1038
+ "skill": "spotify_play_playlist",
1039
+ "parameters": {
1040
+ "playlist": "rainy day jazz"
1041
+ }
1042
+ },
1043
+ "domain": "spotify",
1044
+ "styles": [
1045
+ "typo",
1046
+ "incomplete"
1047
+ ]
1048
+ },
1049
+ {
1050
+ "id": "pa-072",
1051
+ "prompt": "yo spin the gym beast mode playlist",
1052
+ "expected": {
1053
+ "skill": "spotify_play_playlist",
1054
+ "parameters": {
1055
+ "playlist": "gym beast mode"
1056
+ }
1057
+ },
1058
+ "domain": "spotify",
1059
+ "styles": [
1060
+ "slang",
1061
+ "incomplete"
1062
+ ]
1063
+ },
1064
+ {
1065
+ "id": "pa-073",
1066
+ "prompt": "spotify \u2014 my cooking playlist pls",
1067
+ "expected": {
1068
+ "skill": "spotify_play_playlist",
1069
+ "parameters": {
1070
+ "playlist": "cooking"
1071
+ }
1072
+ },
1073
+ "domain": "spotify",
1074
+ "styles": [
1075
+ "incomplete",
1076
+ "conversational"
1077
+ ]
1078
+ },
1079
+ {
1080
+ "id": "pa-074",
1081
+ "prompt": "start spotify playlist called roadtrip 2024",
1082
+ "expected": {
1083
+ "skill": "spotify_play_playlist",
1084
+ "parameters": {
1085
+ "playlist": "roadtrip 2024"
1086
+ }
1087
+ },
1088
+ "domain": "spotify",
1089
+ "styles": [
1090
+ "incomplete"
1091
+ ]
1092
+ },
1093
+ {
1094
+ "id": "pa-075",
1095
+ "prompt": "play hyperpop mix on spotify",
1096
+ "expected": {
1097
+ "skill": "spotify_play_playlist",
1098
+ "parameters": {
1099
+ "playlist": "hyperpop mix"
1100
+ }
1101
+ },
1102
+ "domain": "spotify",
1103
+ "styles": [
1104
+ "incomplete"
1105
+ ]
1106
+ },
1107
+ {
1108
+ "id": "pa-076",
1109
+ "prompt": "spotify my chillhop essentials thx",
1110
+ "expected": {
1111
+ "skill": "spotify_play_playlist",
1112
+ "parameters": {
1113
+ "playlist": "chillhop essentials"
1114
+ }
1115
+ },
1116
+ "domain": "spotify",
1117
+ "styles": [
1118
+ "incomplete",
1119
+ "conversational"
1120
+ ]
1121
+ },
1122
+ {
1123
+ "id": "pa-077",
1124
+ "prompt": "can u play the study grind playlist on spotify",
1125
+ "expected": {
1126
+ "skill": "spotify_play_playlist",
1127
+ "parameters": {
1128
+ "playlist": "study grind"
1129
+ }
1130
+ },
1131
+ "domain": "spotify",
1132
+ "styles": [
1133
+ "conversational"
1134
+ ]
1135
+ },
1136
+ {
1137
+ "id": "pa-078",
1138
+ "prompt": "spotify playlist sleep sounds go",
1139
+ "expected": {
1140
+ "skill": "spotify_play_playlist",
1141
+ "parameters": {
1142
+ "playlist": "sleep sounds"
1143
+ }
1144
+ },
1145
+ "domain": "spotify",
1146
+ "styles": [
1147
+ "incomplete"
1148
+ ]
1149
+ },
1150
+ {
1151
+ "id": "pa-079",
1152
+ "prompt": "throw on indie roadtrip on spotify",
1153
+ "expected": {
1154
+ "skill": "spotify_play_playlist",
1155
+ "parameters": {
1156
+ "playlist": "indie roadtrip"
1157
+ }
1158
+ },
1159
+ "domain": "spotify",
1160
+ "styles": [
1161
+ "slang",
1162
+ "incomplete"
1163
+ ]
1164
+ },
1165
+ {
1166
+ "id": "pa-080",
1167
+ "prompt": "spotify play my liked tracks playlist",
1168
+ "expected": {
1169
+ "skill": "spotify_play_playlist",
1170
+ "parameters": {
1171
+ "playlist": "liked tracks"
1172
+ }
1173
+ },
1174
+ "domain": "spotify",
1175
+ "styles": [
1176
+ "incomplete"
1177
+ ]
1178
+ },
1179
+ {
1180
+ "id": "pa-081",
1181
+ "prompt": "play spotify playlist bossa nova mornings",
1182
+ "expected": {
1183
+ "skill": "spotify_play_playlist",
1184
+ "parameters": {
1185
+ "playlist": "bossa nova mornings"
1186
+ }
1187
+ },
1188
+ "domain": "spotify",
1189
+ "styles": [
1190
+ "incomplete"
1191
+ ]
1192
+ },
1193
+ {
1194
+ "id": "pa-082",
1195
+ "prompt": "spotify \u2014 queue phonk workout",
1196
+ "expected": {
1197
+ "skill": "spotify_play_playlist",
1198
+ "parameters": {
1199
+ "playlist": "phonk workout"
1200
+ }
1201
+ },
1202
+ "domain": "spotify",
1203
+ "styles": [
1204
+ "incomplete"
1205
+ ]
1206
+ },
1207
+ {
1208
+ "id": "pa-083",
1209
+ "prompt": "put spotify on my cozy rainy day list",
1210
+ "expected": {
1211
+ "skill": "spotify_play_playlist",
1212
+ "parameters": {
1213
+ "playlist": "cozy rainy day"
1214
+ }
1215
+ },
1216
+ "domain": "spotify",
1217
+ "styles": [
1218
+ "conversational",
1219
+ "incomplete"
1220
+ ]
1221
+ },
1222
+ {
1223
+ "id": "pa-084",
1224
+ "prompt": "play the spotify playlist french cafe",
1225
+ "expected": {
1226
+ "skill": "spotify_play_playlist",
1227
+ "parameters": {
1228
+ "playlist": "french cafe"
1229
+ }
1230
+ },
1231
+ "domain": "spotify",
1232
+ "styles": [
1233
+ "incomplete"
1234
+ ]
1235
+ },
1236
+ {
1237
+ "id": "pa-085",
1238
+ "prompt": "spotify start my pregame playlist",
1239
+ "expected": {
1240
+ "skill": "spotify_play_playlist",
1241
+ "parameters": {
1242
+ "playlist": "pregame"
1243
+ }
1244
+ },
1245
+ "domain": "spotify",
1246
+ "styles": [
1247
+ "slang",
1248
+ "incomplete"
1249
+ ]
1250
+ },
1251
+ {
1252
+ "id": "pa-086",
1253
+ "prompt": "ok spotify play downtempo focus",
1254
+ "expected": {
1255
+ "skill": "spotify_play_playlist",
1256
+ "parameters": {
1257
+ "playlist": "downtempo focus"
1258
+ }
1259
+ },
1260
+ "domain": "spotify",
1261
+ "styles": [
1262
+ "conversational",
1263
+ "incomplete"
1264
+ ]
1265
+ },
1266
+ {
1267
+ "id": "pa-087",
1268
+ "prompt": "spotify my discover mix playlist pls",
1269
+ "expected": {
1270
+ "skill": "spotify_play_playlist",
1271
+ "parameters": {
1272
+ "playlist": "discover mix"
1273
+ }
1274
+ },
1275
+ "domain": "spotify",
1276
+ "styles": [
1277
+ "incomplete"
1278
+ ]
1279
+ },
1280
+ {
1281
+ "id": "pa-088",
1282
+ "prompt": "play playlist morning commute on spotify",
1283
+ "expected": {
1284
+ "skill": "spotify_play_playlist",
1285
+ "parameters": {
1286
+ "playlist": "morning commute"
1287
+ }
1288
+ },
1289
+ "domain": "spotify",
1290
+ "styles": [
1291
+ "incomplete"
1292
+ ]
1293
+ },
1294
+ {
1295
+ "id": "pa-089",
1296
+ "prompt": "spotify find bad bunny and play",
1297
+ "expected": {
1298
+ "skill": "spotify_search_play",
1299
+ "parameters": {
1300
+ "query": "bad bunny"
1301
+ }
1302
+ },
1303
+ "domain": "spotify",
1304
+ "styles": [
1305
+ "incomplete"
1306
+ ]
1307
+ },
1308
+ {
1309
+ "id": "pa-090",
1310
+ "prompt": "search spotify for lofi hip hop then play",
1311
+ "expected": {
1312
+ "skill": "spotify_search_play",
1313
+ "parameters": {
1314
+ "query": "lofi hip hop"
1315
+ }
1316
+ },
1317
+ "domain": "spotify",
1318
+ "styles": [
1319
+ "conversational"
1320
+ ]
1321
+ },
1322
+ {
1323
+ "id": "pa-091",
1324
+ "prompt": "play some arctic monkeys on spotify",
1325
+ "expected": {
1326
+ "skill": "spotify_search_play",
1327
+ "parameters": {
1328
+ "query": "arctic monkeys"
1329
+ }
1330
+ },
1331
+ "domain": "spotify",
1332
+ "styles": [
1333
+ "conversational",
1334
+ "incomplete"
1335
+ ]
1336
+ },
1337
+ {
1338
+ "id": "pa-092",
1339
+ "prompt": "spotfy search billie eilish play it",
1340
+ "expected": {
1341
+ "skill": "spotify_search_play",
1342
+ "parameters": {
1343
+ "query": "billie eilish"
1344
+ }
1345
+ },
1346
+ "domain": "spotify",
1347
+ "styles": [
1348
+ "typo",
1349
+ "incomplete"
1350
+ ]
1351
+ },
1352
+ {
1353
+ "id": "pa-093",
1354
+ "prompt": "look up sabrina carpenter on spotify n play",
1355
+ "expected": {
1356
+ "skill": "spotify_search_play",
1357
+ "parameters": {
1358
+ "query": "sabrina carpenter"
1359
+ }
1360
+ },
1361
+ "domain": "spotify",
1362
+ "styles": [
1363
+ "slang",
1364
+ "incomplete"
1365
+ ]
1366
+ },
1367
+ {
1368
+ "id": "pa-094",
1369
+ "prompt": "spotify: search neo soul and hit play",
1370
+ "expected": {
1371
+ "skill": "spotify_search_play",
1372
+ "parameters": {
1373
+ "query": "neo soul"
1374
+ }
1375
+ },
1376
+ "domain": "spotify",
1377
+ "styles": [
1378
+ "incomplete"
1379
+ ]
1380
+ },
1381
+ {
1382
+ "id": "pa-095",
1383
+ "prompt": "find tyler the creator on spotify and play",
1384
+ "expected": {
1385
+ "skill": "spotify_search_play",
1386
+ "parameters": {
1387
+ "query": "tyler the creator"
1388
+ }
1389
+ },
1390
+ "domain": "spotify",
1391
+ "styles": [
1392
+ "incomplete"
1393
+ ]
1394
+ },
1395
+ {
1396
+ "id": "pa-096",
1397
+ "prompt": "spotify play after searching radiohead",
1398
+ "expected": {
1399
+ "skill": "spotify_search_play",
1400
+ "parameters": {
1401
+ "query": "radiohead"
1402
+ }
1403
+ },
1404
+ "domain": "spotify",
1405
+ "styles": [
1406
+ "incomplete"
1407
+ ]
1408
+ },
1409
+ {
1410
+ "id": "pa-097",
1411
+ "prompt": "yo spotify search dnb and play",
1412
+ "expected": {
1413
+ "skill": "spotify_search_play",
1414
+ "parameters": {
1415
+ "query": "dnb"
1416
+ }
1417
+ },
1418
+ "domain": "spotify",
1419
+ "styles": [
1420
+ "slang",
1421
+ "incomplete"
1422
+ ]
1423
+ },
1424
+ {
1425
+ "id": "pa-098",
1426
+ "prompt": "can u find frank ocean on spotify and play it",
1427
+ "expected": {
1428
+ "skill": "spotify_search_play",
1429
+ "parameters": {
1430
+ "query": "frank ocean"
1431
+ }
1432
+ },
1433
+ "domain": "spotify",
1434
+ "styles": [
1435
+ "conversational"
1436
+ ]
1437
+ },
1438
+ {
1439
+ "id": "pa-099",
1440
+ "prompt": "spotify search classical piano play",
1441
+ "expected": {
1442
+ "skill": "spotify_search_play",
1443
+ "parameters": {
1444
+ "query": "classical piano"
1445
+ }
1446
+ },
1447
+ "domain": "spotify",
1448
+ "styles": [
1449
+ "incomplete"
1450
+ ]
1451
+ },
1452
+ {
1453
+ "id": "pa-100",
1454
+ "prompt": "spotify search play some drake tracks",
1455
+ "expected": {
1456
+ "skill": "spotify_search_play",
1457
+ "parameters": {
1458
+ "query": "drake tracks"
1459
+ }
1460
+ },
1461
+ "domain": "spotify",
1462
+ "styles": [
1463
+ "incomplete"
1464
+ ]
1465
+ },
1466
+ {
1467
+ "id": "pa-101",
1468
+ "prompt": "spotify find latin reggaeton and play",
1469
+ "expected": {
1470
+ "skill": "spotify_search_play",
1471
+ "parameters": {
1472
+ "query": "latin reggaeton"
1473
+ }
1474
+ },
1475
+ "domain": "spotify",
1476
+ "styles": [
1477
+ "incomplete"
1478
+ ]
1479
+ },
1480
+ {
1481
+ "id": "pa-102",
1482
+ "prompt": "search spotify for aphex twin then play",
1483
+ "expected": {
1484
+ "skill": "spotify_search_play",
1485
+ "parameters": {
1486
+ "query": "aphex twin"
1487
+ }
1488
+ },
1489
+ "domain": "spotify",
1490
+ "styles": [
1491
+ "incomplete"
1492
+ ]
1493
+ },
1494
+ {
1495
+ "id": "pa-103",
1496
+ "prompt": "spotify play whatever u find for synthwave",
1497
+ "expected": {
1498
+ "skill": "spotify_search_play",
1499
+ "parameters": {
1500
+ "query": "synthwave"
1501
+ }
1502
+ },
1503
+ "domain": "spotify",
1504
+ "styles": [
1505
+ "slang",
1506
+ "conversational"
1507
+ ]
1508
+ },
1509
+ {
1510
+ "id": "pa-104",
1511
+ "prompt": "find mitski on spotify play pls",
1512
+ "expected": {
1513
+ "skill": "spotify_search_play",
1514
+ "parameters": {
1515
+ "query": "mitski"
1516
+ }
1517
+ },
1518
+ "domain": "spotify",
1519
+ "styles": [
1520
+ "incomplete",
1521
+ "conversational"
1522
+ ]
1523
+ },
1524
+ {
1525
+ "id": "pa-105",
1526
+ "prompt": "soptify search play khruangbin",
1527
+ "expected": {
1528
+ "skill": "spotify_search_play",
1529
+ "parameters": {
1530
+ "query": "khruangbin"
1531
+ }
1532
+ },
1533
+ "domain": "spotify",
1534
+ "styles": [
1535
+ "typo",
1536
+ "incomplete"
1537
+ ]
1538
+ },
1539
+ {
1540
+ "id": "pa-106",
1541
+ "prompt": "look up burna boy spotify and play",
1542
+ "expected": {
1543
+ "skill": "spotify_search_play",
1544
+ "parameters": {
1545
+ "query": "burna boy"
1546
+ }
1547
+ },
1548
+ "domain": "spotify",
1549
+ "styles": [
1550
+ "incomplete"
1551
+ ]
1552
+ },
1553
+ {
1554
+ "id": "pa-107",
1555
+ "prompt": "spotify search play some afrobeats",
1556
+ "expected": {
1557
+ "skill": "spotify_search_play",
1558
+ "parameters": {
1559
+ "query": "afrobeats"
1560
+ }
1561
+ },
1562
+ "domain": "spotify",
1563
+ "styles": [
1564
+ "incomplete"
1565
+ ]
1566
+ },
1567
+ {
1568
+ "id": "pa-108",
1569
+ "prompt": "find fred again on spotify n play it",
1570
+ "expected": {
1571
+ "skill": "spotify_search_play",
1572
+ "parameters": {
1573
+ "query": "fred again"
1574
+ }
1575
+ },
1576
+ "domain": "spotify",
1577
+ "styles": [
1578
+ "slang",
1579
+ "incomplete"
1580
+ ]
1581
+ },
1582
+ {
1583
+ "id": "pa-109",
1584
+ "prompt": "spotify search play 90s r&b",
1585
+ "expected": {
1586
+ "skill": "spotify_search_play",
1587
+ "parameters": {
1588
+ "query": "90s r&b"
1589
+ }
1590
+ },
1591
+ "domain": "spotify",
1592
+ "styles": [
1593
+ "incomplete"
1594
+ ]
1595
+ },
1596
+ {
1597
+ "id": "pa-110",
1598
+ "prompt": "search spotify play tame impala",
1599
+ "expected": {
1600
+ "skill": "spotify_search_play",
1601
+ "parameters": {
1602
+ "query": "tame impala"
1603
+ }
1604
+ },
1605
+ "domain": "spotify",
1606
+ "styles": [
1607
+ "incomplete"
1608
+ ]
1609
+ },
1610
+ {
1611
+ "id": "pa-111",
1612
+ "prompt": "ubber to jfk pls",
1613
+ "expected": {
1614
+ "skill": "uber_request_ride",
1615
+ "parameters": {
1616
+ "destination": "jfk"
1617
+ }
1618
+ },
1619
+ "domain": "uber",
1620
+ "styles": [
1621
+ "typo",
1622
+ "incomplete",
1623
+ "conversational"
1624
+ ]
1625
+ },
1626
+ {
1627
+ "id": "pa-112",
1628
+ "prompt": "need a ride to 42 oak street on uber",
1629
+ "expected": {
1630
+ "skill": "uber_request_ride",
1631
+ "parameters": {
1632
+ "destination": "42 oak street"
1633
+ }
1634
+ },
1635
+ "domain": "uber",
1636
+ "styles": [
1637
+ "conversational"
1638
+ ]
1639
+ },
1640
+ {
1641
+ "id": "pa-113",
1642
+ "prompt": "hop in an uber to the vet clinic",
1643
+ "expected": {
1644
+ "skill": "uber_request_ride",
1645
+ "parameters": {
1646
+ "destination": "the vet clinic"
1647
+ }
1648
+ },
1649
+ "domain": "uber",
1650
+ "styles": [
1651
+ "slang"
1652
+ ]
1653
+ },
1654
+ {
1655
+ "id": "pa-114",
1656
+ "prompt": "ubr me to whole foods on 5th",
1657
+ "expected": {
1658
+ "skill": "uber_request_ride",
1659
+ "parameters": {
1660
+ "destination": "whole foods on 5th"
1661
+ }
1662
+ },
1663
+ "domain": "uber",
1664
+ "styles": [
1665
+ "typo",
1666
+ "slang",
1667
+ "incomplete"
1668
+ ]
1669
+ },
1670
+ {
1671
+ "id": "pa-115",
1672
+ "prompt": "get uber to coworking space downtown",
1673
+ "expected": {
1674
+ "skill": "uber_request_ride",
1675
+ "parameters": {
1676
+ "destination": "coworking space downtown"
1677
+ }
1678
+ },
1679
+ "domain": "uber",
1680
+ "styles": [
1681
+ "incomplete"
1682
+ ]
1683
+ },
1684
+ {
1685
+ "id": "pa-116",
1686
+ "prompt": "book ride to mom's house via uber",
1687
+ "expected": {
1688
+ "skill": "uber_request_ride",
1689
+ "parameters": {
1690
+ "destination": "mom's house"
1691
+ }
1692
+ },
1693
+ "domain": "uber",
1694
+ "styles": [
1695
+ "incomplete"
1696
+ ]
1697
+ },
1698
+ {
1699
+ "id": "pa-117",
1700
+ "prompt": "uber \u2014 take me to pier 39",
1701
+ "expected": {
1702
+ "skill": "uber_request_ride",
1703
+ "parameters": {
1704
+ "destination": "pier 39"
1705
+ }
1706
+ },
1707
+ "domain": "uber",
1708
+ "styles": [
1709
+ "incomplete",
1710
+ "conversational"
1711
+ ]
1712
+ },
1713
+ {
1714
+ "id": "pa-118",
1715
+ "prompt": "can u call uber to stanford campus",
1716
+ "expected": {
1717
+ "skill": "uber_request_ride",
1718
+ "parameters": {
1719
+ "destination": "stanford campus"
1720
+ }
1721
+ },
1722
+ "domain": "uber",
1723
+ "styles": [
1724
+ "conversational"
1725
+ ]
1726
+ },
1727
+ {
1728
+ "id": "pa-119",
1729
+ "prompt": "ride to airport terminal 2 uber",
1730
+ "expected": {
1731
+ "skill": "uber_request_ride",
1732
+ "parameters": {
1733
+ "destination": "airport terminal 2"
1734
+ }
1735
+ },
1736
+ "domain": "uber",
1737
+ "styles": [
1738
+ "incomplete"
1739
+ ]
1740
+ },
1741
+ {
1742
+ "id": "pa-120",
1743
+ "prompt": "uber to the sushi place on main",
1744
+ "expected": {
1745
+ "skill": "uber_request_ride",
1746
+ "parameters": {
1747
+ "destination": "the sushi place on main"
1748
+ }
1749
+ },
1750
+ "domain": "uber",
1751
+ "styles": [
1752
+ "incomplete",
1753
+ "conversational"
1754
+ ]
1755
+ },
1756
+ {
1757
+ "id": "pa-121",
1758
+ "prompt": "need uber to ikea rn",
1759
+ "expected": {
1760
+ "skill": "uber_request_ride",
1761
+ "parameters": {
1762
+ "destination": "ikea"
1763
+ }
1764
+ },
1765
+ "domain": "uber",
1766
+ "styles": [
1767
+ "slang",
1768
+ "incomplete"
1769
+ ]
1770
+ },
1771
+ {
1772
+ "id": "pa-122",
1773
+ "prompt": "request uber to brooklyn bridge park",
1774
+ "expected": {
1775
+ "skill": "uber_request_ride",
1776
+ "parameters": {
1777
+ "destination": "brooklyn bridge park"
1778
+ }
1779
+ },
1780
+ "domain": "uber",
1781
+ "styles": [
1782
+ "incomplete"
1783
+ ]
1784
+ },
1785
+ {
1786
+ "id": "pa-123",
1787
+ "prompt": "uber drop me at la guardia",
1788
+ "expected": {
1789
+ "skill": "uber_request_ride",
1790
+ "parameters": {
1791
+ "destination": "la guardia"
1792
+ }
1793
+ },
1794
+ "domain": "uber",
1795
+ "styles": [
1796
+ "slang",
1797
+ "incomplete"
1798
+ ]
1799
+ },
1800
+ {
1801
+ "id": "pa-124",
1802
+ "prompt": "get a cab on uber to dentist on maple ave",
1803
+ "expected": {
1804
+ "skill": "uber_request_ride",
1805
+ "parameters": {
1806
+ "destination": "dentist on maple ave"
1807
+ }
1808
+ },
1809
+ "domain": "uber",
1810
+ "styles": [
1811
+ "conversational"
1812
+ ]
1813
+ },
1814
+ {
1815
+ "id": "pa-125",
1816
+ "prompt": "uber to friends apartment 8b",
1817
+ "expected": {
1818
+ "skill": "uber_request_ride",
1819
+ "parameters": {
1820
+ "destination": "friends apartment 8b"
1821
+ }
1822
+ },
1823
+ "domain": "uber",
1824
+ "styles": [
1825
+ "incomplete"
1826
+ ]
1827
+ },
1828
+ {
1829
+ "id": "pa-126",
1830
+ "prompt": "yo uber to the concert venue",
1831
+ "expected": {
1832
+ "skill": "uber_request_ride",
1833
+ "parameters": {
1834
+ "destination": "the concert venue"
1835
+ }
1836
+ },
1837
+ "domain": "uber",
1838
+ "styles": [
1839
+ "slang",
1840
+ "incomplete"
1841
+ ]
1842
+ },
1843
+ {
1844
+ "id": "pa-127",
1845
+ "prompt": "book uber ride to seattle tacoma airport",
1846
+ "expected": {
1847
+ "skill": "uber_request_ride",
1848
+ "parameters": {
1849
+ "destination": "seattle tacoma airport"
1850
+ }
1851
+ },
1852
+ "domain": "uber",
1853
+ "styles": [
1854
+ "incomplete"
1855
+ ]
1856
+ },
1857
+ {
1858
+ "id": "pa-128",
1859
+ "prompt": "uber pls to union station",
1860
+ "expected": {
1861
+ "skill": "uber_request_ride",
1862
+ "parameters": {
1863
+ "destination": "union station"
1864
+ }
1865
+ },
1866
+ "domain": "uber",
1867
+ "styles": [
1868
+ "incomplete",
1869
+ "conversational"
1870
+ ]
1871
+ },
1872
+ {
1873
+ "id": "pa-129",
1874
+ "prompt": "ride share to target on broadway uber",
1875
+ "expected": {
1876
+ "skill": "uber_request_ride",
1877
+ "parameters": {
1878
+ "destination": "target on broadway"
1879
+ }
1880
+ },
1881
+ "domain": "uber",
1882
+ "styles": [
1883
+ "incomplete"
1884
+ ]
1885
+ },
1886
+ {
1887
+ "id": "pa-130",
1888
+ "prompt": "uber me over to the marriott hotel",
1889
+ "expected": {
1890
+ "skill": "uber_request_ride",
1891
+ "parameters": {
1892
+ "destination": "the marriott hotel"
1893
+ }
1894
+ },
1895
+ "domain": "uber",
1896
+ "styles": [
1897
+ "slang",
1898
+ "incomplete"
1899
+ ]
1900
+ },
1901
+ {
1902
+ "id": "pa-131",
1903
+ "prompt": "need uber to physio appointment on elm",
1904
+ "expected": {
1905
+ "skill": "uber_request_ride",
1906
+ "parameters": {
1907
+ "destination": "physio appointment on elm"
1908
+ }
1909
+ },
1910
+ "domain": "uber",
1911
+ "styles": [
1912
+ "incomplete"
1913
+ ]
1914
+ },
1915
+ {
1916
+ "id": "pa-132",
1917
+ "prompt": "call uber to the dog park",
1918
+ "expected": {
1919
+ "skill": "uber_request_ride",
1920
+ "parameters": {
1921
+ "destination": "the dog park"
1922
+ }
1923
+ },
1924
+ "domain": "uber",
1925
+ "styles": [
1926
+ "incomplete"
1927
+ ]
1928
+ },
1929
+ {
1930
+ "id": "pa-133",
1931
+ "prompt": "gmal email recruiter saying im interested in the role",
1932
+ "expected": {
1933
+ "skill": "gmail_send_email",
1934
+ "parameters": {
1935
+ "recipient": "recruiter",
1936
+ "message": "im interested in the role"
1937
+ }
1938
+ },
1939
+ "domain": "gmail",
1940
+ "styles": [
1941
+ "typo",
1942
+ "incomplete"
1943
+ ]
1944
+ },
1945
+ {
1946
+ "id": "pa-134",
1947
+ "prompt": "shoot an email via gmail to finance@acme.co \u2014 invoice overdue",
1948
+ "expected": {
1949
+ "skill": "gmail_send_email",
1950
+ "parameters": {
1951
+ "recipient": "finance@acme.co",
1952
+ "message": "invoice overdue"
1953
+ }
1954
+ },
1955
+ "domain": "gmail",
1956
+ "styles": [
1957
+ "slang",
1958
+ "conversational"
1959
+ ]
1960
+ },
1961
+ {
1962
+ "id": "pa-135",
1963
+ "prompt": "gmail landlord asking about lease renewal",
1964
+ "expected": {
1965
+ "skill": "gmail_send_email",
1966
+ "parameters": {
1967
+ "recipient": "landlord",
1968
+ "message": "asking about lease renewal"
1969
+ }
1970
+ },
1971
+ "domain": "gmail",
1972
+ "styles": [
1973
+ "incomplete"
1974
+ ]
1975
+ },
1976
+ {
1977
+ "id": "pa-136",
1978
+ "prompt": "email my manager on gmail: heads up sick day tomorrow",
1979
+ "expected": {
1980
+ "skill": "gmail_send_email",
1981
+ "parameters": {
1982
+ "recipient": "my manager",
1983
+ "message": "heads up sick day tomorrow"
1984
+ }
1985
+ },
1986
+ "domain": "gmail",
1987
+ "styles": [
1988
+ "conversational"
1989
+ ]
1990
+ },
1991
+ {
1992
+ "id": "pa-137",
1993
+ "prompt": "compose gmail to nora.park@design.io portfolio link attached",
1994
+ "expected": {
1995
+ "skill": "gmail_send_email",
1996
+ "parameters": {
1997
+ "recipient": "nora.park@design.io",
1998
+ "message": "portfolio link attached"
1999
+ }
2000
+ },
2001
+ "domain": "gmail",
2002
+ "styles": [
2003
+ "incomplete"
2004
+ ]
2005
+ },
2006
+ {
2007
+ "id": "pa-138",
2008
+ "prompt": "gmail msg to thesis advisor draft attached for review",
2009
+ "expected": {
2010
+ "skill": "gmail_send_email",
2011
+ "parameters": {
2012
+ "recipient": "thesis advisor",
2013
+ "message": "draft attached for review"
2014
+ }
2015
+ },
2016
+ "domain": "gmail",
2017
+ "styles": [
2018
+ "incomplete"
2019
+ ]
2020
+ },
2021
+ {
2022
+ "id": "pa-139",
2023
+ "prompt": "send gmail to carpool buddy running 10 late",
2024
+ "expected": {
2025
+ "skill": "gmail_send_email",
2026
+ "parameters": {
2027
+ "recipient": "carpool buddy",
2028
+ "message": "running 10 late"
2029
+ }
2030
+ },
2031
+ "domain": "gmail",
2032
+ "styles": [
2033
+ "slang",
2034
+ "incomplete"
2035
+ ]
2036
+ },
2037
+ {
2038
+ "id": "pa-140",
2039
+ "prompt": "write gmail to billing@saas.com cancel subscription pls",
2040
+ "expected": {
2041
+ "skill": "gmail_send_email",
2042
+ "parameters": {
2043
+ "recipient": "billing@saas.com",
2044
+ "message": "cancel subscription pls"
2045
+ }
2046
+ },
2047
+ "domain": "gmail",
2048
+ "styles": [
2049
+ "conversational"
2050
+ ]
2051
+ },
2052
+ {
2053
+ "id": "pa-141",
2054
+ "prompt": "gmail email mentor thx for the intro",
2055
+ "expected": {
2056
+ "skill": "gmail_send_email",
2057
+ "parameters": {
2058
+ "recipient": "mentor",
2059
+ "message": "thx for the intro"
2060
+ }
2061
+ },
2062
+ "domain": "gmail",
2063
+ "styles": [
2064
+ "slang",
2065
+ "incomplete"
2066
+ ]
2067
+ },
2068
+ {
2069
+ "id": "pa-142",
2070
+ "prompt": "ok gmail to ops-team@startup.dev deploy failed rollback done",
2071
+ "expected": {
2072
+ "skill": "gmail_send_email",
2073
+ "parameters": {
2074
+ "recipient": "ops-team@startup.dev",
2075
+ "message": "deploy failed rollback done"
2076
+ }
2077
+ },
2078
+ "domain": "gmail",
2079
+ "styles": [
2080
+ "conversational",
2081
+ "incomplete"
2082
+ ]
2083
+ },
2084
+ {
2085
+ "id": "pa-143",
2086
+ "prompt": "email thru gmail to dentist office reschedule appt",
2087
+ "expected": {
2088
+ "skill": "gmail_send_email",
2089
+ "parameters": {
2090
+ "recipient": "dentist office",
2091
+ "message": "reschedule appt"
2092
+ }
2093
+ },
2094
+ "domain": "gmail",
2095
+ "styles": [
2096
+ "incomplete"
2097
+ ]
2098
+ },
2099
+ {
2100
+ "id": "pa-144",
2101
+ "prompt": "gmail send to contractor quote approved lets proceed",
2102
+ "expected": {
2103
+ "skill": "gmail_send_email",
2104
+ "parameters": {
2105
+ "recipient": "contractor",
2106
+ "message": "quote approved lets proceed"
2107
+ }
2108
+ },
2109
+ "domain": "gmail",
2110
+ "styles": [
2111
+ "incomplete"
2112
+ ]
2113
+ },
2114
+ {
2115
+ "id": "pa-145",
2116
+ "prompt": "hey gmail my professor question about deadline extension",
2117
+ "expected": {
2118
+ "skill": "gmail_send_email",
2119
+ "parameters": {
2120
+ "recipient": "my professor",
2121
+ "message": "question about deadline extension"
2122
+ }
2123
+ },
2124
+ "domain": "gmail",
2125
+ "styles": [
2126
+ "conversational",
2127
+ "incomplete"
2128
+ ]
2129
+ },
2130
+ {
2131
+ "id": "pa-146",
2132
+ "prompt": "gmail to hr@corp.net pto request for next week",
2133
+ "expected": {
2134
+ "skill": "gmail_send_email",
2135
+ "parameters": {
2136
+ "recipient": "hr@corp.net",
2137
+ "message": "pto request for next week"
2138
+ }
2139
+ },
2140
+ "domain": "gmail",
2141
+ "styles": [
2142
+ "incomplete"
2143
+ ]
2144
+ },
2145
+ {
2146
+ "id": "pa-147",
2147
+ "prompt": "send email gmail to vendor payment sent today",
2148
+ "expected": {
2149
+ "skill": "gmail_send_email",
2150
+ "parameters": {
2151
+ "recipient": "vendor",
2152
+ "message": "payment sent today"
2153
+ }
2154
+ },
2155
+ "domain": "gmail",
2156
+ "styles": [
2157
+ "incomplete"
2158
+ ]
2159
+ },
2160
+ {
2161
+ "id": "pa-148",
2162
+ "prompt": "gmial client@studio.com mockups ready for feedback",
2163
+ "expected": {
2164
+ "skill": "gmail_send_email",
2165
+ "parameters": {
2166
+ "recipient": "client@studio.com",
2167
+ "message": "mockups ready for feedback"
2168
+ }
2169
+ },
2170
+ "domain": "gmail",
2171
+ "styles": [
2172
+ "typo",
2173
+ "incomplete"
2174
+ ]
2175
+ },
2176
+ {
2177
+ "id": "pa-149",
2178
+ "prompt": "compose email on gmail to roommate utilities split attached",
2179
+ "expected": {
2180
+ "skill": "gmail_send_email",
2181
+ "parameters": {
2182
+ "recipient": "roommate",
2183
+ "message": "utilities split attached"
2184
+ }
2185
+ },
2186
+ "domain": "gmail",
2187
+ "styles": [
2188
+ "incomplete"
2189
+ ]
2190
+ },
2191
+ {
2192
+ "id": "pa-150",
2193
+ "prompt": "gmail to travel agent pls confirm hotel booking",
2194
+ "expected": {
2195
+ "skill": "gmail_send_email",
2196
+ "parameters": {
2197
+ "recipient": "travel agent",
2198
+ "message": "pls confirm hotel booking"
2199
+ }
2200
+ },
2201
+ "domain": "gmail",
2202
+ "styles": [
2203
+ "conversational"
2204
+ ]
2205
+ },
2206
+ {
2207
+ "id": "pa-151",
2208
+ "prompt": "email gmail coach thanks for the session notes",
2209
+ "expected": {
2210
+ "skill": "gmail_send_email",
2211
+ "parameters": {
2212
+ "recipient": "coach",
2213
+ "message": "thanks for the session notes"
2214
+ }
2215
+ },
2216
+ "domain": "gmail",
2217
+ "styles": [
2218
+ "incomplete"
2219
+ ]
2220
+ },
2221
+ {
2222
+ "id": "pa-152",
2223
+ "prompt": "gmail msg to editor final chapter attached",
2224
+ "expected": {
2225
+ "skill": "gmail_send_email",
2226
+ "parameters": {
2227
+ "recipient": "editor",
2228
+ "message": "final chapter attached"
2229
+ }
2230
+ },
2231
+ "domain": "gmail",
2232
+ "styles": [
2233
+ "incomplete"
2234
+ ]
2235
+ },
2236
+ {
2237
+ "id": "pa-153",
2238
+ "prompt": "send gmail to internship lead following up on application",
2239
+ "expected": {
2240
+ "skill": "gmail_send_email",
2241
+ "parameters": {
2242
+ "recipient": "internship lead",
2243
+ "message": "following up on application"
2244
+ }
2245
+ },
2246
+ "domain": "gmail",
2247
+ "styles": [
2248
+ "incomplete"
2249
+ ]
2250
+ },
2251
+ {
2252
+ "id": "pa-154",
2253
+ "prompt": "gmail email to dad flight details attached",
2254
+ "expected": {
2255
+ "skill": "gmail_send_email",
2256
+ "parameters": {
2257
+ "recipient": "dad",
2258
+ "message": "flight details attached"
2259
+ }
2260
+ },
2261
+ "domain": "gmail",
2262
+ "styles": [
2263
+ "incomplete"
2264
+ ]
2265
+ },
2266
+ {
2267
+ "id": "pa-155",
2268
+ "prompt": "put dentist on calender next thurs 3pm",
2269
+ "expected": {
2270
+ "skill": "calendar_create_event",
2271
+ "parameters": {
2272
+ "title": "dentist",
2273
+ "date": "next thurs",
2274
+ "time": "3pm"
2275
+ }
2276
+ },
2277
+ "domain": "calendar",
2278
+ "styles": [
2279
+ "typo",
2280
+ "incomplete"
2281
+ ]
2282
+ },
2283
+ {
2284
+ "id": "pa-156",
2285
+ "prompt": "schedule 1:1 w/ manager monday 11am",
2286
+ "expected": {
2287
+ "skill": "calendar_create_event",
2288
+ "parameters": {
2289
+ "title": "1:1 w/ manager",
2290
+ "date": "monday",
2291
+ "time": "11am"
2292
+ }
2293
+ },
2294
+ "domain": "calendar",
2295
+ "styles": [
2296
+ "slang",
2297
+ "incomplete"
2298
+ ]
2299
+ },
2300
+ {
2301
+ "id": "pa-157",
2302
+ "prompt": "add calendar block focus time tuesday 9-11",
2303
+ "expected": {
2304
+ "skill": "calendar_create_event",
2305
+ "parameters": {
2306
+ "title": "focus time",
2307
+ "date": "tuesday",
2308
+ "time": "9-11"
2309
+ }
2310
+ },
2311
+ "domain": "calendar",
2312
+ "styles": [
2313
+ "incomplete"
2314
+ ]
2315
+ },
2316
+ {
2317
+ "id": "pa-158",
2318
+ "prompt": "book therapy session friday 5 pm on calendar",
2319
+ "expected": {
2320
+ "skill": "calendar_create_event",
2321
+ "parameters": {
2322
+ "title": "therapy session",
2323
+ "date": "friday",
2324
+ "time": "5 pm"
2325
+ }
2326
+ },
2327
+ "domain": "calendar",
2328
+ "styles": [
2329
+ "incomplete"
2330
+ ]
2331
+ },
2332
+ {
2333
+ "id": "pa-159",
2334
+ "prompt": "cal event: team retro wednesday 4pm",
2335
+ "expected": {
2336
+ "skill": "calendar_create_event",
2337
+ "parameters": {
2338
+ "title": "team retro",
2339
+ "date": "wednesday",
2340
+ "time": "4pm"
2341
+ }
2342
+ },
2343
+ "domain": "calendar",
2344
+ "styles": [
2345
+ "slang",
2346
+ "incomplete"
2347
+ ]
2348
+ },
2349
+ {
2350
+ "id": "pa-160",
2351
+ "prompt": "put parent teacher conf on calendar oct 12 6pm",
2352
+ "expected": {
2353
+ "skill": "calendar_create_event",
2354
+ "parameters": {
2355
+ "title": "parent teacher conf",
2356
+ "date": "oct 12",
2357
+ "time": "6pm"
2358
+ }
2359
+ },
2360
+ "domain": "calendar",
2361
+ "styles": [
2362
+ "incomplete"
2363
+ ]
2364
+ },
2365
+ {
2366
+ "id": "pa-161",
2367
+ "prompt": "schedule gym class saturday 8 am calendar",
2368
+ "expected": {
2369
+ "skill": "calendar_create_event",
2370
+ "parameters": {
2371
+ "title": "gym class",
2372
+ "date": "saturday",
2373
+ "time": "8 am"
2374
+ }
2375
+ },
2376
+ "domain": "calendar",
2377
+ "styles": [
2378
+ "incomplete"
2379
+ ]
2380
+ },
2381
+ {
2382
+ "id": "pa-162",
2383
+ "prompt": "add calendar entry flight to nyc sunday 7:45 am",
2384
+ "expected": {
2385
+ "skill": "calendar_create_event",
2386
+ "parameters": {
2387
+ "title": "flight to nyc",
2388
+ "date": "sunday",
2389
+ "time": "7:45 am"
2390
+ }
2391
+ },
2392
+ "domain": "calendar",
2393
+ "styles": [
2394
+ "incomplete"
2395
+ ]
2396
+ },
2397
+ {
2398
+ "id": "pa-163",
2399
+ "prompt": "calendar \u2014 lunch w/ priya tomorrow noon",
2400
+ "expected": {
2401
+ "skill": "calendar_create_event",
2402
+ "parameters": {
2403
+ "title": "lunch w/ priya",
2404
+ "date": "tomorrow",
2405
+ "time": "noon"
2406
+ }
2407
+ },
2408
+ "domain": "calendar",
2409
+ "styles": [
2410
+ "slang",
2411
+ "incomplete",
2412
+ "conversational"
2413
+ ]
2414
+ },
2415
+ {
2416
+ "id": "pa-164",
2417
+ "prompt": "set cal event sprint planning next monday 10",
2418
+ "expected": {
2419
+ "skill": "calendar_create_event",
2420
+ "parameters": {
2421
+ "title": "sprint planning",
2422
+ "date": "next monday",
2423
+ "time": "10"
2424
+ }
2425
+ },
2426
+ "domain": "calendar",
2427
+ "styles": [
2428
+ "slang",
2429
+ "incomplete"
2430
+ ]
2431
+ },
2432
+ {
2433
+ "id": "pa-165",
2434
+ "prompt": "put car inspection on calendar tuesday 2:30 pm",
2435
+ "expected": {
2436
+ "skill": "calendar_create_event",
2437
+ "parameters": {
2438
+ "title": "car inspection",
2439
+ "date": "tuesday",
2440
+ "time": "2:30 pm"
2441
+ }
2442
+ },
2443
+ "domain": "calendar",
2444
+ "styles": [
2445
+ "incomplete"
2446
+ ]
2447
+ },
2448
+ {
2449
+ "id": "pa-166",
2450
+ "prompt": "scheduel interview prep wednesday 6 pm",
2451
+ "expected": {
2452
+ "skill": "calendar_create_event",
2453
+ "parameters": {
2454
+ "title": "interview prep",
2455
+ "date": "wednesday",
2456
+ "time": "6 pm"
2457
+ }
2458
+ },
2459
+ "domain": "calendar",
2460
+ "styles": [
2461
+ "typo",
2462
+ "incomplete"
2463
+ ]
2464
+ },
2465
+ {
2466
+ "id": "pa-167",
2467
+ "prompt": "add to calendar vet appt thursday 1 pm",
2468
+ "expected": {
2469
+ "skill": "calendar_create_event",
2470
+ "parameters": {
2471
+ "title": "vet appt",
2472
+ "date": "thursday",
2473
+ "time": "1 pm"
2474
+ }
2475
+ },
2476
+ "domain": "calendar",
2477
+ "styles": [
2478
+ "incomplete"
2479
+ ]
2480
+ },
2481
+ {
2482
+ "id": "pa-168",
2483
+ "prompt": "calendar event board presentation friday 9 am",
2484
+ "expected": {
2485
+ "skill": "calendar_create_event",
2486
+ "parameters": {
2487
+ "title": "board presentation",
2488
+ "date": "friday",
2489
+ "time": "9 am"
2490
+ }
2491
+ },
2492
+ "domain": "calendar",
2493
+ "styles": [
2494
+ "incomplete"
2495
+ ]
2496
+ },
2497
+ {
2498
+ "id": "pa-169",
2499
+ "prompt": "book calendar slot date night saturday 8 pm",
2500
+ "expected": {
2501
+ "skill": "calendar_create_event",
2502
+ "parameters": {
2503
+ "title": "date night",
2504
+ "date": "saturday",
2505
+ "time": "8 pm"
2506
+ }
2507
+ },
2508
+ "domain": "calendar",
2509
+ "styles": [
2510
+ "incomplete"
2511
+ ]
2512
+ },
2513
+ {
2514
+ "id": "pa-170",
2515
+ "prompt": "put standup notes review on cal monday 9:30",
2516
+ "expected": {
2517
+ "skill": "calendar_create_event",
2518
+ "parameters": {
2519
+ "title": "standup notes review",
2520
+ "date": "monday",
2521
+ "time": "9:30"
2522
+ }
2523
+ },
2524
+ "domain": "calendar",
2525
+ "styles": [
2526
+ "slang",
2527
+ "incomplete"
2528
+ ]
2529
+ },
2530
+ {
2531
+ "id": "pa-171",
2532
+ "prompt": "schedule movers arrival next saturday 10 am",
2533
+ "expected": {
2534
+ "skill": "calendar_create_event",
2535
+ "parameters": {
2536
+ "title": "movers arrival",
2537
+ "date": "next saturday",
2538
+ "time": "10 am"
2539
+ }
2540
+ },
2541
+ "domain": "calendar",
2542
+ "styles": [
2543
+ "incomplete"
2544
+ ]
2545
+ },
2546
+ {
2547
+ "id": "pa-172",
2548
+ "prompt": "add calendar tax appointment april 5 11 am",
2549
+ "expected": {
2550
+ "skill": "calendar_create_event",
2551
+ "parameters": {
2552
+ "title": "tax appointment",
2553
+ "date": "april 5",
2554
+ "time": "11 am"
2555
+ }
2556
+ },
2557
+ "domain": "calendar",
2558
+ "styles": [
2559
+ "incomplete"
2560
+ ]
2561
+ },
2562
+ {
2563
+ "id": "pa-173",
2564
+ "prompt": "calender block deep work thursday morning 8",
2565
+ "expected": {
2566
+ "skill": "calendar_create_event",
2567
+ "parameters": {
2568
+ "title": "deep work",
2569
+ "date": "thursday morning",
2570
+ "time": "8"
2571
+ }
2572
+ },
2573
+ "domain": "calendar",
2574
+ "styles": [
2575
+ "typo",
2576
+ "incomplete"
2577
+ ]
2578
+ },
2579
+ {
2580
+ "id": "pa-174",
2581
+ "prompt": "put wedding rehearsal on calendar june 20 5 pm",
2582
+ "expected": {
2583
+ "skill": "calendar_create_event",
2584
+ "parameters": {
2585
+ "title": "wedding rehearsal",
2586
+ "date": "june 20",
2587
+ "time": "5 pm"
2588
+ }
2589
+ },
2590
+ "domain": "calendar",
2591
+ "styles": [
2592
+ "incomplete"
2593
+ ]
2594
+ },
2595
+ {
2596
+ "id": "pa-175",
2597
+ "prompt": "schedule product demo next wednesday 3 pm",
2598
+ "expected": {
2599
+ "skill": "calendar_create_event",
2600
+ "parameters": {
2601
+ "title": "product demo",
2602
+ "date": "next wednesday",
2603
+ "time": "3 pm"
2604
+ }
2605
+ },
2606
+ "domain": "calendar",
2607
+ "styles": [
2608
+ "incomplete"
2609
+ ]
2610
+ },
2611
+ {
2612
+ "id": "pa-176",
2613
+ "prompt": "add calendar coffee chat friday 10:30 am",
2614
+ "expected": {
2615
+ "skill": "calendar_create_event",
2616
+ "parameters": {
2617
+ "title": "coffee chat",
2618
+ "date": "friday",
2619
+ "time": "10:30 am"
2620
+ }
2621
+ },
2622
+ "domain": "calendar",
2623
+ "styles": [
2624
+ "incomplete"
2625
+ ]
2626
+ },
2627
+ {
2628
+ "id": "pa-177",
2629
+ "prompt": "book calendar orthodontist monday 4:15 pm",
2630
+ "expected": {
2631
+ "skill": "calendar_create_event",
2632
+ "parameters": {
2633
+ "title": "orthodontist",
2634
+ "date": "monday",
2635
+ "time": "4:15 pm"
2636
+ }
2637
+ },
2638
+ "domain": "calendar",
2639
+ "styles": [
2640
+ "incomplete"
2641
+ ]
2642
+ },
2643
+ {
2644
+ "id": "pa-178",
2645
+ "prompt": "put hackathon submission deadline on calendar sunday midnight",
2646
+ "expected": {
2647
+ "skill": "calendar_create_event",
2648
+ "parameters": {
2649
+ "title": "hackathon submission deadline",
2650
+ "date": "sunday",
2651
+ "time": "midnight"
2652
+ }
2653
+ },
2654
+ "domain": "calendar",
2655
+ "styles": [
2656
+ "incomplete"
2657
+ ]
2658
+ },
2659
+ {
2660
+ "id": "pa-179",
2661
+ "prompt": "open slack #incidents pls",
2662
+ "expected": {
2663
+ "skill": "slack_open_channel",
2664
+ "parameters": {
2665
+ "channel": "incidents"
2666
+ }
2667
+ },
2668
+ "domain": "slack",
2669
+ "styles": [
2670
+ "incomplete",
2671
+ "conversational"
2672
+ ]
2673
+ },
2674
+ {
2675
+ "id": "pa-180",
2676
+ "prompt": "jump to product-launch channel on slack",
2677
+ "expected": {
2678
+ "skill": "slack_open_channel",
2679
+ "parameters": {
2680
+ "channel": "product-launch"
2681
+ }
2682
+ },
2683
+ "domain": "slack",
2684
+ "styles": [
2685
+ "slang",
2686
+ "incomplete"
2687
+ ]
2688
+ },
2689
+ {
2690
+ "id": "pa-181",
2691
+ "prompt": "slack go to team-watercooler",
2692
+ "expected": {
2693
+ "skill": "slack_open_channel",
2694
+ "parameters": {
2695
+ "channel": "team-watercooler"
2696
+ }
2697
+ },
2698
+ "domain": "slack",
2699
+ "styles": [
2700
+ "incomplete"
2701
+ ]
2702
+ },
2703
+ {
2704
+ "id": "pa-182",
2705
+ "prompt": "pull up slack channel ios-bugs",
2706
+ "expected": {
2707
+ "skill": "slack_open_channel",
2708
+ "parameters": {
2709
+ "channel": "ios-bugs"
2710
+ }
2711
+ },
2712
+ "domain": "slack",
2713
+ "styles": [
2714
+ "slang",
2715
+ "incomplete"
2716
+ ]
2717
+ },
2718
+ {
2719
+ "id": "pa-183",
2720
+ "prompt": "switch slack to growth-experiments",
2721
+ "expected": {
2722
+ "skill": "slack_open_channel",
2723
+ "parameters": {
2724
+ "channel": "growth-experiments"
2725
+ }
2726
+ },
2727
+ "domain": "slack",
2728
+ "styles": [
2729
+ "incomplete"
2730
+ ]
2731
+ },
2732
+ {
2733
+ "id": "pa-184",
2734
+ "prompt": "show slack channel design-system",
2735
+ "expected": {
2736
+ "skill": "slack_open_channel",
2737
+ "parameters": {
2738
+ "channel": "design-system"
2739
+ }
2740
+ },
2741
+ "domain": "slack",
2742
+ "styles": [
2743
+ "incomplete"
2744
+ ]
2745
+ },
2746
+ {
2747
+ "id": "pa-185",
2748
+ "prompt": "navigate slack to platform-oncall",
2749
+ "expected": {
2750
+ "skill": "slack_open_channel",
2751
+ "parameters": {
2752
+ "channel": "platform-oncall"
2753
+ }
2754
+ },
2755
+ "domain": "slack",
2756
+ "styles": [
2757
+ "incomplete"
2758
+ ]
2759
+ },
2760
+ {
2761
+ "id": "pa-186",
2762
+ "prompt": "open the slack channel ml-research thx",
2763
+ "expected": {
2764
+ "skill": "slack_open_channel",
2765
+ "parameters": {
2766
+ "channel": "ml-research"
2767
+ }
2768
+ },
2769
+ "domain": "slack",
2770
+ "styles": [
2771
+ "conversational"
2772
+ ]
2773
+ },
2774
+ {
2775
+ "id": "pa-187",
2776
+ "prompt": "slack channel customer-escalations open it",
2777
+ "expected": {
2778
+ "skill": "slack_open_channel",
2779
+ "parameters": {
2780
+ "channel": "customer-escalations"
2781
+ }
2782
+ },
2783
+ "domain": "slack",
2784
+ "styles": [
2785
+ "incomplete"
2786
+ ]
2787
+ },
2788
+ {
2789
+ "id": "pa-188",
2790
+ "prompt": "take me to #backend-standup in slack",
2791
+ "expected": {
2792
+ "skill": "slack_open_channel",
2793
+ "parameters": {
2794
+ "channel": "backend-standup"
2795
+ }
2796
+ },
2797
+ "domain": "slack",
2798
+ "styles": [
2799
+ "incomplete"
2800
+ ]
2801
+ },
2802
+ {
2803
+ "id": "pa-189",
2804
+ "prompt": "open slack for channel release-train",
2805
+ "expected": {
2806
+ "skill": "slack_open_channel",
2807
+ "parameters": {
2808
+ "channel": "release-train"
2809
+ }
2810
+ },
2811
+ "domain": "slack",
2812
+ "styles": [
2813
+ "incomplete"
2814
+ ]
2815
+ },
2816
+ {
2817
+ "id": "pa-190",
2818
+ "prompt": "slack \u2014 show me hiring-pipeline channel",
2819
+ "expected": {
2820
+ "skill": "slack_open_channel",
2821
+ "parameters": {
2822
+ "channel": "hiring-pipeline"
2823
+ }
2824
+ },
2825
+ "domain": "slack",
2826
+ "styles": [
2827
+ "conversational",
2828
+ "incomplete"
2829
+ ]
2830
+ },
2831
+ {
2832
+ "id": "pa-191",
2833
+ "prompt": "go slack channel infra-alerts",
2834
+ "expected": {
2835
+ "skill": "slack_open_channel",
2836
+ "parameters": {
2837
+ "channel": "infra-alerts"
2838
+ }
2839
+ },
2840
+ "domain": "slack",
2841
+ "styles": [
2842
+ "incomplete"
2843
+ ]
2844
+ },
2845
+ {
2846
+ "id": "pa-192",
2847
+ "prompt": "open slak channel mobile-crash-reports",
2848
+ "expected": {
2849
+ "skill": "slack_open_channel",
2850
+ "parameters": {
2851
+ "channel": "mobile-crash-reports"
2852
+ }
2853
+ },
2854
+ "domain": "slack",
2855
+ "styles": [
2856
+ "typo",
2857
+ "incomplete"
2858
+ ]
2859
+ },
2860
+ {
2861
+ "id": "pa-193",
2862
+ "prompt": "pull slack up on qa-automation channel",
2863
+ "expected": {
2864
+ "skill": "slack_open_channel",
2865
+ "parameters": {
2866
+ "channel": "qa-automation"
2867
+ }
2868
+ },
2869
+ "domain": "slack",
2870
+ "styles": [
2871
+ "incomplete"
2872
+ ]
2873
+ },
2874
+ {
2875
+ "id": "pa-194",
2876
+ "prompt": "switch to slack channel docs-and-wiki",
2877
+ "expected": {
2878
+ "skill": "slack_open_channel",
2879
+ "parameters": {
2880
+ "channel": "docs-and-wiki"
2881
+ }
2882
+ },
2883
+ "domain": "slack",
2884
+ "styles": [
2885
+ "incomplete"
2886
+ ]
2887
+ },
2888
+ {
2889
+ "id": "pa-195",
2890
+ "prompt": "open slack growth-metrics channel rq",
2891
+ "expected": {
2892
+ "skill": "slack_open_channel",
2893
+ "parameters": {
2894
+ "channel": "growth-metrics"
2895
+ }
2896
+ },
2897
+ "domain": "slack",
2898
+ "styles": [
2899
+ "slang",
2900
+ "incomplete"
2901
+ ]
2902
+ },
2903
+ {
2904
+ "id": "pa-196",
2905
+ "prompt": "show me slack #sales-wins",
2906
+ "expected": {
2907
+ "skill": "slack_open_channel",
2908
+ "parameters": {
2909
+ "channel": "sales-wins"
2910
+ }
2911
+ },
2912
+ "domain": "slack",
2913
+ "styles": [
2914
+ "incomplete"
2915
+ ]
2916
+ },
2917
+ {
2918
+ "id": "pa-197",
2919
+ "prompt": "navigate to slack channel partner-integrations",
2920
+ "expected": {
2921
+ "skill": "slack_open_channel",
2922
+ "parameters": {
2923
+ "channel": "partner-integrations"
2924
+ }
2925
+ },
2926
+ "domain": "slack",
2927
+ "styles": [
2928
+ "incomplete"
2929
+ ]
2930
+ },
2931
+ {
2932
+ "id": "pa-198",
2933
+ "prompt": "open slack channel called observability",
2934
+ "expected": {
2935
+ "skill": "slack_open_channel",
2936
+ "parameters": {
2937
+ "channel": "observability"
2938
+ }
2939
+ },
2940
+ "domain": "slack",
2941
+ "styles": [
2942
+ "incomplete"
2943
+ ]
2944
+ },
2945
+ {
2946
+ "id": "pa-199",
2947
+ "prompt": "slack open eng-hiring channel pls",
2948
+ "expected": {
2949
+ "skill": "slack_open_channel",
2950
+ "parameters": {
2951
+ "channel": "eng-hiring"
2952
+ }
2953
+ },
2954
+ "domain": "slack",
2955
+ "styles": [
2956
+ "incomplete",
2957
+ "conversational"
2958
+ ]
2959
+ },
2960
+ {
2961
+ "id": "pa-200",
2962
+ "prompt": "jump slack to ai-playground channel",
2963
+ "expected": {
2964
+ "skill": "slack_open_channel",
2965
+ "parameters": {
2966
+ "channel": "ai-playground"
2967
+ }
2968
+ },
2969
+ "domain": "slack",
2970
+ "styles": [
2971
+ "slang",
2972
+ "incomplete"
2973
+ ]
2974
+ }
2975
+ ]
data/skill_schemas.json ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "create_alarm": {
3
+ "description": "Set an alarm at a specific time",
4
+ "parameters": {
5
+ "time": {
6
+ "type": "string",
7
+ "required": true,
8
+ "description": "Alarm time, e.g. 7 am, 6:30 pm"
9
+ },
10
+ "day": {
11
+ "type": "string",
12
+ "required": false,
13
+ "description": "Day or date, e.g. tomorrow, monday, today"
14
+ }
15
+ }
16
+ },
17
+ "calendar_create_event": {
18
+ "description": "Create a calendar event",
19
+ "parameters": {
20
+ "title": {
21
+ "type": "string",
22
+ "required": true,
23
+ "description": "Event title or description"
24
+ },
25
+ "date": {
26
+ "type": "string",
27
+ "required": true,
28
+ "description": "Event date, e.g. tomorrow, friday, next tuesday"
29
+ },
30
+ "time": {
31
+ "type": "string",
32
+ "required": true,
33
+ "description": "Event time, e.g. 4 pm, 10 am, noon"
34
+ }
35
+ }
36
+ },
37
+ "wifi_enable": {
38
+ "description": "Enable Wi-Fi on the device",
39
+ "parameters": {}
40
+ },
41
+ "bluetooth_enable": {
42
+ "description": "Turn on Bluetooth",
43
+ "parameters": {}
44
+ },
45
+ "whatsapp_send_message": {
46
+ "description": "Send a WhatsApp message to a contact",
47
+ "parameters": {
48
+ "contact": {
49
+ "type": "string",
50
+ "required": true,
51
+ "description": "Recipient name or nickname"
52
+ },
53
+ "message": {
54
+ "type": "string",
55
+ "required": true,
56
+ "description": "Message body text"
57
+ }
58
+ }
59
+ },
60
+ "camera_take_photo": {
61
+ "description": "Open the camera and take a photo",
62
+ "parameters": {}
63
+ },
64
+ "slack_open_channel": {
65
+ "description": "Open a Slack channel",
66
+ "parameters": {
67
+ "channel": {
68
+ "type": "string",
69
+ "required": true,
70
+ "description": "Slack channel name"
71
+ }
72
+ }
73
+ },
74
+ "spotify_pause": {
75
+ "description": "Pause Spotify playback",
76
+ "parameters": {}
77
+ },
78
+ "spotify_play_playlist": {
79
+ "description": "Play a Spotify playlist",
80
+ "parameters": {
81
+ "playlist": {
82
+ "type": "string",
83
+ "required": true,
84
+ "description": "Playlist name"
85
+ }
86
+ }
87
+ },
88
+ "uber_request_ride": {
89
+ "description": "Request an Uber ride to a destination",
90
+ "parameters": {
91
+ "destination": {
92
+ "type": "string",
93
+ "required": true,
94
+ "description": "Ride destination address or place name"
95
+ }
96
+ }
97
+ },
98
+ "linkedin_search_person": {
99
+ "description": "Search for a person on LinkedIn",
100
+ "parameters": {
101
+ "name": {
102
+ "type": "string",
103
+ "required": true,
104
+ "description": "Person's full or partial name"
105
+ }
106
+ }
107
+ },
108
+ "spotify_search_play": {
109
+ "description": "Search Spotify and play music",
110
+ "parameters": {
111
+ "query": {
112
+ "type": "string",
113
+ "required": true,
114
+ "description": "Music search query"
115
+ }
116
+ }
117
+ },
118
+ "youtube_search": {
119
+ "description": "Search YouTube for videos",
120
+ "parameters": {
121
+ "query": {
122
+ "type": "string",
123
+ "required": true,
124
+ "description": "YouTube search query"
125
+ }
126
+ }
127
+ },
128
+ "contacts_search": {
129
+ "description": "Search for a contact in the phone contacts app",
130
+ "parameters": {
131
+ "contact": {
132
+ "type": "string",
133
+ "required": true,
134
+ "description": "Contact name to search for"
135
+ }
136
+ }
137
+ },
138
+ "gmail_send_email": {
139
+ "description": "Send an email via Gmail",
140
+ "parameters": {
141
+ "recipient": {
142
+ "type": "string",
143
+ "required": true,
144
+ "description": "Email address or recipient name"
145
+ },
146
+ "message": {
147
+ "type": "string",
148
+ "required": true,
149
+ "description": "Email body text"
150
+ }
151
+ }
152
+ }
153
+ }
modal_apps/evaluate_intent_modal.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluate the fine-tuned intent-extraction model on Modal.
3
+
4
+ Prerequisites:
5
+ pip install modal
6
+ modal setup
7
+ python scripts/generate_intent_dataset.py
8
+ modal run modal_apps/train_modal.py # train on data/train_intent.jsonl
9
+
10
+ Run evaluation:
11
+ modal run modal_apps/evaluate_intent_modal.py
12
+
13
+ Reads eval_intent_prompts.json locally, uploads it to the dataset volume, loads the
14
+ LoRA adapter from the model volume, and reports per-prompt PASS/FAIL for skill +
15
+ parameters plus overall accuracy.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import pathlib
22
+
23
+ import modal
24
+
25
+ app = modal.App("android-intent-evaluate")
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Configuration
29
+ # ---------------------------------------------------------------------------
30
+
31
+ MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
32
+ PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
33
+ LOCAL_EVAL_PROMPTS = PROJECT_ROOT / "data" / "eval_intent_prompts.json"
34
+ REMOTE_EVAL_PROMPTS = "/data/eval_intent_prompts.json"
35
+ MODEL_DIR = pathlib.Path("/model")
36
+ ADAPTER_DIR = MODEL_DIR / "adapter"
37
+ MAX_SEQ_LENGTH = 2048
38
+ MAX_NEW_TOKENS = 128
39
+
40
+ INTENT_SYSTEM_PROMPT = (
41
+ "You extract structured Android automation intents from natural language. "
42
+ 'Reply with JSON only: {"skill": "<skill_name>", "parameters": {<extracted_fields>}}. '
43
+ "Pick exactly one skill. Extract all relevant parameters mentioned in the request "
44
+ "(contact names, messages, times, destinations, channel names, search queries, etc.). "
45
+ "Use an empty object for parameters when the skill needs none. "
46
+ "Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) "
47
+ "to pick the correct skill."
48
+ )
49
+
50
+
51
+ def build_intent_messages(user_content: str) -> list[dict[str, str]]:
52
+ return [
53
+ {"role": "system", "content": INTENT_SYSTEM_PROMPT},
54
+ {"role": "user", "content": user_content},
55
+ ]
56
+
57
+
58
+ GPU_TYPE = "A10G"
59
+ TIMEOUT_SECONDS = 30 * 60
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Volumes
63
+ # ---------------------------------------------------------------------------
64
+
65
+ dataset_volume = modal.Volume.from_name(
66
+ "android-dataset-data",
67
+ create_if_missing=True,
68
+ )
69
+ model_volume = modal.Volume.from_name(
70
+ "android-dataset-model",
71
+ create_if_missing=True,
72
+ )
73
+ model_cache_volume = modal.Volume.from_name(
74
+ "android-dataset-hf-cache",
75
+ create_if_missing=True,
76
+ )
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Container image
80
+ # ---------------------------------------------------------------------------
81
+
82
+ eval_image = (
83
+ modal.Image.debian_slim(python_version="3.11")
84
+ .pip_install_from_requirements(
85
+ str(pathlib.Path(__file__).parent / "requirements-modal.txt")
86
+ )
87
+ .env(
88
+ {
89
+ "HF_HOME": "/model_cache",
90
+ "HF_HUB_ENABLE_HF_TRANSFER": "1",
91
+ }
92
+ )
93
+ )
94
+
95
+ with eval_image.imports():
96
+ import unsloth # noqa: F401 — must import before trl/transformers/peft
97
+
98
+ import torch
99
+ from peft import PeftModel
100
+ from unsloth import FastLanguageModel
101
+ from unsloth.chat_templates import get_chat_template
102
+
103
+
104
+ def _parse_json_payload(text: str) -> dict | None:
105
+ text = text.strip()
106
+ if not text:
107
+ return None
108
+
109
+ start = text.find("{")
110
+ end = text.rfind("}")
111
+ if start == -1 or end == -1 or end <= start:
112
+ return None
113
+
114
+ try:
115
+ payload = json.loads(text[start : end + 1])
116
+ except json.JSONDecodeError:
117
+ return None
118
+
119
+ return payload if isinstance(payload, dict) else None
120
+
121
+
122
+ def extract_intent(text: str) -> dict | None:
123
+ payload = _parse_json_payload(text)
124
+ if not payload:
125
+ return None
126
+
127
+ skill = payload.get("skill")
128
+ if not isinstance(skill, str) or not skill:
129
+ return None
130
+
131
+ parameters = payload.get("parameters", {})
132
+ if parameters is None:
133
+ parameters = {}
134
+ if not isinstance(parameters, dict):
135
+ return None
136
+
137
+ return {"skill": skill, "parameters": parameters}
138
+
139
+
140
+ def normalize_param(value: str) -> str:
141
+ return " ".join(value.lower().strip().split())
142
+
143
+
144
+ def parameters_match(predicted: dict, expected: dict) -> bool:
145
+ for key, expected_value in expected.items():
146
+ predicted_value = predicted.get(key)
147
+ if predicted_value is None:
148
+ return False
149
+ if normalize_param(str(predicted_value)) != normalize_param(str(expected_value)):
150
+ return False
151
+ return True
152
+
153
+
154
+ def intent_matches(predicted: dict | None, expected: dict) -> tuple[bool, bool]:
155
+ """Return (skill_match, full_match)."""
156
+ if not predicted:
157
+ return False, False
158
+
159
+ expected_skill = expected["skill"]
160
+ expected_params = expected.get("parameters", {})
161
+
162
+ skill_match = predicted.get("skill") == expected_skill
163
+ if not skill_match:
164
+ return False, False
165
+
166
+ if not expected_params:
167
+ return True, True
168
+
169
+ params_match = parameters_match(predicted.get("parameters", {}), expected_params)
170
+ return True, params_match
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Evaluation
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ @app.function(
179
+ image=eval_image,
180
+ gpu=GPU_TYPE,
181
+ timeout=TIMEOUT_SECONDS,
182
+ volumes={
183
+ "/data": dataset_volume,
184
+ "/model": model_volume,
185
+ "/model_cache": model_cache_volume,
186
+ },
187
+ )
188
+ def evaluate() -> None:
189
+ dataset_volume.reload()
190
+ model_volume.reload()
191
+
192
+ eval_path = pathlib.Path(REMOTE_EVAL_PROMPTS)
193
+ if not eval_path.exists():
194
+ raise FileNotFoundError(
195
+ f"Eval prompts not found at {eval_path}. "
196
+ "Run `modal run modal_apps/evaluate_intent_modal.py` from the project directory."
197
+ )
198
+
199
+ if not (ADAPTER_DIR / "adapter_config.json").exists():
200
+ raise FileNotFoundError(
201
+ f"LoRA adapter not found at {ADAPTER_DIR}. "
202
+ "Run `modal run modal_apps/train_modal.py` first."
203
+ )
204
+
205
+ with eval_path.open(encoding="utf-8") as handle:
206
+ eval_prompts = json.load(handle)
207
+
208
+ print(f"Loading base model: {MODEL_NAME}")
209
+ model, tokenizer = FastLanguageModel.from_pretrained(
210
+ model_name=MODEL_NAME,
211
+ max_seq_length=MAX_SEQ_LENGTH,
212
+ dtype=None,
213
+ load_in_4bit=True,
214
+ )
215
+
216
+ print(f"Loading LoRA adapter from {ADAPTER_DIR}")
217
+ model = PeftModel.from_pretrained(model, str(ADAPTER_DIR))
218
+
219
+ tokenizer = get_chat_template(
220
+ tokenizer,
221
+ chat_template="qwen-2.5",
222
+ )
223
+
224
+ FastLanguageModel.for_inference(model)
225
+
226
+ skill_passed = 0
227
+ full_passed = 0
228
+ total = len(eval_prompts)
229
+
230
+ print(f"Running intent evaluation on {total} prompts...\n")
231
+
232
+ for case in eval_prompts:
233
+ prompt = case["prompt"]
234
+ expected = case["expected"]
235
+
236
+ messages = build_intent_messages(prompt)
237
+ inputs = tokenizer.apply_chat_template(
238
+ messages,
239
+ tokenize=True,
240
+ add_generation_prompt=True,
241
+ return_tensors="pt",
242
+ ).to("cuda")
243
+
244
+ with torch.inference_mode():
245
+ outputs = model.generate(
246
+ input_ids=inputs,
247
+ max_new_tokens=MAX_NEW_TOKENS,
248
+ use_cache=True,
249
+ do_sample=False,
250
+ )
251
+
252
+ generated = outputs[0][inputs.shape[1] :]
253
+ raw_output = tokenizer.decode(generated, skip_special_tokens=True).strip()
254
+ predicted = extract_intent(raw_output)
255
+ skill_ok, full_ok = intent_matches(predicted, expected)
256
+
257
+ if skill_ok:
258
+ skill_passed += 1
259
+ if full_ok:
260
+ full_passed += 1
261
+
262
+ print(f"Prompt: {prompt}")
263
+ print(f"Expected: {json.dumps(expected, separators=(',', ':'))}")
264
+ print(f"Predicted: {json.dumps(predicted, separators=(',', ':')) if predicted else raw_output}")
265
+ print(f"Skill: {'PASS' if skill_ok else 'FAIL'} | Full: {'PASS' if full_ok else 'FAIL'}")
266
+ print()
267
+
268
+ skill_accuracy = skill_passed / total if total else 0.0
269
+ full_accuracy = full_passed / total if total else 0.0
270
+
271
+ print("--- Summary ---")
272
+ print(f"Total: {total}")
273
+ print(f"Skill accuracy: {skill_passed}/{total} ({skill_accuracy:.1%})")
274
+ print(f"Full intent accuracy: {full_passed}/{total} ({full_accuracy:.1%})")
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Local entrypoint
279
+ # ---------------------------------------------------------------------------
280
+
281
+
282
+ @app.local_entrypoint()
283
+ def main() -> None:
284
+ eval_path = pathlib.Path(LOCAL_EVAL_PROMPTS)
285
+ if not eval_path.exists():
286
+ raise FileNotFoundError(
287
+ f"Local eval prompts not found: {eval_path.resolve()}. "
288
+ "Run `python scripts/generate_intent_dataset.py` first."
289
+ )
290
+
291
+ remote_name = "eval_intent_prompts.json"
292
+ try:
293
+ dataset_volume.remove_file(remote_name)
294
+ except Exception:
295
+ pass # file may not exist yet on the volume
296
+
297
+ print(f"Uploading {eval_path} to dataset volume...")
298
+ with dataset_volume.batch_upload() as batch:
299
+ batch.put_file(str(eval_path), remote_name)
300
+
301
+ print("Launching intent evaluation on Modal GPU...")
302
+ evaluate.remote()
modal_apps/evaluate_pocket_benchmark_modal.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluate the Pocket Automator real-world benchmark on Modal.
3
+
4
+ Prerequisites:
5
+ pip install modal
6
+ modal setup
7
+ python scripts/generate_pocket_benchmark.py
8
+ modal run modal_apps/train_modal.py
9
+
10
+ Run evaluation:
11
+ modal run modal_apps/evaluate_pocket_benchmark_modal.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import pathlib
18
+
19
+ import modal
20
+
21
+ app = modal.App("pocket-automator-benchmark")
22
+
23
+ MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
24
+ PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
25
+ LOCAL_BENCHMARK_PROMPTS = PROJECT_ROOT / "data" / "pocket_benchmark_prompts.json"
26
+ REMOTE_BENCHMARK_PROMPTS = "/data/pocket_benchmark_prompts.json"
27
+ REMOTE_RESULTS = "/data/pocket_benchmark_results.json"
28
+ REMOTE_REPORT = "/data/pocket_benchmark_report.txt"
29
+ MODEL_DIR = pathlib.Path("/model")
30
+ ADAPTER_DIR = MODEL_DIR / "adapter"
31
+ MAX_SEQ_LENGTH = 2048
32
+ MAX_NEW_TOKENS = 128
33
+
34
+ INTENT_SYSTEM_PROMPT = (
35
+ "You extract structured Android automation intents from natural language. "
36
+ 'Reply with JSON only: {"skill": "<skill_name>", "parameters": {<extracted_fields>}}. '
37
+ "Pick exactly one skill. Extract all relevant parameters mentioned in the request "
38
+ "(contact names, messages, times, destinations, channel names, search queries, etc.). "
39
+ "Use an empty object for parameters when the skill needs none. "
40
+ "Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) "
41
+ "to pick the correct skill."
42
+ )
43
+
44
+ GPU_TYPE = "A10G"
45
+ TIMEOUT_SECONDS = 45 * 60
46
+
47
+ dataset_volume = modal.Volume.from_name(
48
+ "android-dataset-data",
49
+ create_if_missing=True,
50
+ )
51
+ model_volume = modal.Volume.from_name(
52
+ "android-dataset-model",
53
+ create_if_missing=True,
54
+ )
55
+ model_cache_volume = modal.Volume.from_name(
56
+ "android-dataset-hf-cache",
57
+ create_if_missing=True,
58
+ )
59
+
60
+ eval_image = (
61
+ modal.Image.debian_slim(python_version="3.11")
62
+ .pip_install_from_requirements(
63
+ str(pathlib.Path(__file__).parent / "requirements-modal.txt")
64
+ )
65
+ .env(
66
+ {
67
+ "HF_HOME": "/model_cache",
68
+ "HF_HUB_ENABLE_HF_TRANSFER": "1",
69
+ "PYTHONPATH": "/root/src",
70
+ }
71
+ )
72
+ .add_local_dir(str(PROJECT_ROOT / "src"), remote_path="/root/src", copy=True)
73
+ )
74
+
75
+
76
+ def build_intent_messages(user_content: str) -> list[dict[str, str]]:
77
+ return [
78
+ {"role": "system", "content": INTENT_SYSTEM_PROMPT},
79
+ {"role": "user", "content": user_content},
80
+ ]
81
+
82
+
83
+ with eval_image.imports():
84
+ import unsloth # noqa: F401
85
+
86
+ import torch
87
+ from peft import PeftModel
88
+ from unsloth import FastLanguageModel
89
+ from unsloth.chat_templates import get_chat_template
90
+
91
+ from pocket_benchmark import record_result, save_benchmark_outputs
92
+ from skill_utils import extract_intent
93
+
94
+
95
+ @app.function(
96
+ image=eval_image,
97
+ gpu=GPU_TYPE,
98
+ timeout=TIMEOUT_SECONDS,
99
+ volumes={
100
+ "/data": dataset_volume,
101
+ "/model": model_volume,
102
+ "/model_cache": model_cache_volume,
103
+ },
104
+ )
105
+ def evaluate() -> None:
106
+ dataset_volume.reload()
107
+ model_volume.reload()
108
+
109
+ benchmark_path = pathlib.Path(REMOTE_BENCHMARK_PROMPTS)
110
+ if not benchmark_path.exists():
111
+ raise FileNotFoundError(
112
+ f"Benchmark prompts not found at {benchmark_path}. "
113
+ "Run `modal run modal_apps/evaluate_pocket_benchmark_modal.py` locally first."
114
+ )
115
+
116
+ if not (ADAPTER_DIR / "adapter_config.json").exists():
117
+ raise FileNotFoundError(
118
+ f"LoRA adapter not found at {ADAPTER_DIR}. "
119
+ "Run `modal run modal_apps/train_modal.py` first."
120
+ )
121
+
122
+ with benchmark_path.open(encoding="utf-8") as handle:
123
+ cases = json.load(handle)
124
+
125
+ print(f"Loading base model: {MODEL_NAME}")
126
+ model, tokenizer = FastLanguageModel.from_pretrained(
127
+ model_name=MODEL_NAME,
128
+ max_seq_length=MAX_SEQ_LENGTH,
129
+ dtype=None,
130
+ load_in_4bit=True,
131
+ )
132
+
133
+ print(f"Loading LoRA adapter from {ADAPTER_DIR}")
134
+ model = PeftModel.from_pretrained(model, str(ADAPTER_DIR))
135
+
136
+ tokenizer = get_chat_template(
137
+ tokenizer,
138
+ chat_template="qwen-2.5",
139
+ )
140
+
141
+ FastLanguageModel.for_inference(model)
142
+
143
+ results = []
144
+ total = len(cases)
145
+ print(f"Running Pocket Automator benchmark on {total} prompts...\n")
146
+
147
+ for index, case in enumerate(cases, start=1):
148
+ prompt = case["prompt"]
149
+ messages = build_intent_messages(prompt)
150
+ inputs = tokenizer.apply_chat_template(
151
+ messages,
152
+ tokenize=True,
153
+ add_generation_prompt=True,
154
+ return_tensors="pt",
155
+ ).to("cuda")
156
+
157
+ with torch.inference_mode():
158
+ outputs = model.generate(
159
+ input_ids=inputs,
160
+ max_new_tokens=MAX_NEW_TOKENS,
161
+ use_cache=True,
162
+ do_sample=False,
163
+ )
164
+
165
+ generated = outputs[0][inputs.shape[1] :]
166
+ raw_output = tokenizer.decode(generated, skip_special_tokens=True).strip()
167
+ predicted = extract_intent(raw_output)
168
+ result = record_result(case, raw_output, predicted)
169
+ results.append(result)
170
+
171
+ print(f"--- [{index}/{total}] {case.get('id', 'n/a')} ---")
172
+ print(f"Prompt: {prompt}")
173
+ print(f"Expected: {json.dumps(case['expected'], separators=(',', ':'))}")
174
+ print(
175
+ f"Predicted: {json.dumps(predicted, separators=(',', ':')) if predicted else raw_output}"
176
+ )
177
+ print(
178
+ f"Skill: {'PASS' if result['skill_correct'] else 'FAIL'} | "
179
+ f"Params: {'PASS' if result['parameter_correct'] else 'FAIL'} | "
180
+ f"Exact JSON: {'PASS' if result['exact_json_match'] else 'FAIL'}"
181
+ )
182
+ print()
183
+
184
+ metrics, report = save_benchmark_outputs(
185
+ results,
186
+ results_path=pathlib.Path(REMOTE_RESULTS),
187
+ report_path=pathlib.Path(REMOTE_REPORT),
188
+ )
189
+ dataset_volume.commit()
190
+
191
+ print("--- Pocket Automator Benchmark Summary ---")
192
+ print(report)
193
+ print(f"Saved results to {REMOTE_RESULTS}")
194
+ print(f"Saved report to {REMOTE_REPORT}")
195
+
196
+
197
+ @app.local_entrypoint()
198
+ def main() -> None:
199
+ benchmark_path = pathlib.Path(LOCAL_BENCHMARK_PROMPTS)
200
+ if not benchmark_path.exists():
201
+ raise FileNotFoundError(
202
+ f"Local benchmark prompts not found: {benchmark_path.resolve()}. "
203
+ "Run `python scripts/generate_pocket_benchmark.py` first."
204
+ )
205
+
206
+ remote_name = "pocket_benchmark_prompts.json"
207
+ try:
208
+ dataset_volume.remove_file(remote_name)
209
+ except Exception:
210
+ pass
211
+
212
+ print(f"Uploading {benchmark_path} to dataset volume...")
213
+ with dataset_volume.batch_upload() as batch:
214
+ batch.put_file(str(benchmark_path), remote_name)
215
+
216
+ print("Launching Pocket Automator benchmark on Modal GPU...")
217
+ evaluate.remote()
218
+
219
+ print(
220
+ "Benchmark complete. Results saved to Modal volume 'android-dataset-data':\n"
221
+ " - pocket_benchmark_results.json\n"
222
+ " - pocket_benchmark_report.txt"
223
+ )
modal_apps/predict_api.py CHANGED
@@ -1,11 +1,11 @@
1
  """
2
- Skill classification API on Modal (FastAPI + globally loaded QLoRA model).
3
 
4
  Prerequisites:
5
  pip install modal
6
  pip install -r modal_apps/requirements-modal.txt
7
  modal setup
8
- modal run modal_apps/train_modal.py # train and save LoRA adapter to the model volume
9
 
10
  Deploy (persistent HTTPS endpoint):
11
  modal deploy modal_apps/predict_api.py
@@ -16,9 +16,9 @@ The deploy command prints the public URL, e.g.:
16
  Test:
17
  curl -X POST https://<workspace>--android-skill-predict-api-skillpredictor-web.modal.run/predict \\
18
  -H "Content-Type: application/json" \\
19
- -d '{"prompt": "play my workout playlist"}'
20
 
21
- # -> {"skill":"spotify_play_playlist"}
22
 
23
  Develop locally (ephemeral URL, hot-reloads on file changes):
24
  modal serve modal_apps/predict_api.py
@@ -29,9 +29,7 @@ Stop a deployed app:
29
 
30
  from __future__ import annotations
31
 
32
- import json
33
  import pathlib
34
- import re
35
 
36
  import modal
37
 
@@ -46,20 +44,7 @@ PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
46
  MODEL_DIR = pathlib.Path("/model")
47
  ADAPTER_DIR = MODEL_DIR / "adapter"
48
  MAX_SEQ_LENGTH = 2048
49
-
50
- SYSTEM_PROMPT = (
51
- "You classify Android automation requests into exactly one skill. "
52
- 'Reply with JSON only: {"skill": "<skill_name>"}. '
53
- "Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) "
54
- "to pick the correct skill."
55
- )
56
-
57
-
58
- def build_classifier_messages(user_content: str) -> list[dict[str, str]]:
59
- return [
60
- {"role": "system", "content": SYSTEM_PROMPT},
61
- {"role": "user", "content": user_content},
62
- ]
63
 
64
  GPU_TYPE = "A10G"
65
  TIMEOUT_SECONDS = 10 * 60
@@ -99,30 +84,6 @@ with api_image.imports():
99
  from unsloth.chat_templates import get_chat_template
100
 
101
 
102
- def extract_skill(text: str) -> str | None:
103
- """Extract the skill name from model output JSON."""
104
- text = text.strip()
105
- if not text:
106
- return None
107
-
108
- match = re.search(r'\{[^{}]*"skill"\s*:\s*"([^"]+)"[^{}]*\}', text)
109
- if match:
110
- return match.group(1)
111
-
112
- start = text.find("{")
113
- end = text.rfind("}")
114
- if start == -1 or end == -1 or end <= start:
115
- return None
116
-
117
- try:
118
- payload = json.loads(text[start : end + 1])
119
- except json.JSONDecodeError:
120
- return None
121
-
122
- skill = payload.get("skill")
123
- return skill if isinstance(skill, str) and skill else None
124
-
125
-
126
  # ---------------------------------------------------------------------------
127
  # API (model loaded once per container via @modal.enter)
128
  # ---------------------------------------------------------------------------
@@ -169,8 +130,11 @@ class SkillPredictor:
169
  self.tokenizer = tokenizer
170
  print("Model ready.")
171
 
172
- def _generate_skill(self, prompt: str) -> str | None:
173
- messages = build_classifier_messages(prompt)
 
 
 
174
  inputs = self.tokenizer.apply_chat_template(
175
  messages,
176
  tokenize=True,
@@ -181,17 +145,26 @@ class SkillPredictor:
181
  with torch.inference_mode():
182
  outputs = self.model.generate(
183
  input_ids=inputs,
184
- max_new_tokens=64,
185
  use_cache=True,
186
  do_sample=False,
187
  )
188
 
189
  generated = outputs[0][inputs.shape[1] :]
190
  raw_output = self.tokenizer.decode(generated, skip_special_tokens=True).strip()
191
- from skill_utils import extract_skill, resolve_skill
 
 
 
 
 
 
 
 
 
 
192
 
193
- raw_skill = extract_skill(raw_output)
194
- return resolve_skill(raw_skill, prompt)
195
 
196
  @modal.asgi_app()
197
  def web(self):
@@ -221,16 +194,16 @@ class SkillPredictor:
221
  },
222
  )
223
 
224
- skill = self._generate_skill(prompt.strip())
225
- if skill is None:
226
  return JSONResponse(
227
  status_code=422,
228
  content={
229
  "error": "invalid_model_output",
230
- "message": "Model did not return valid skill JSON.",
231
  },
232
  )
233
- return JSONResponse(content={"skill": skill})
234
 
235
  return Starlette(
236
  routes=[
 
1
  """
2
+ Intent extraction API on Modal (FastAPI + globally loaded QLoRA model).
3
 
4
  Prerequisites:
5
  pip install modal
6
  pip install -r modal_apps/requirements-modal.txt
7
  modal setup
8
+ modal run modal_apps/train_modal.py --dataset train_intent.jsonl
9
 
10
  Deploy (persistent HTTPS endpoint):
11
  modal deploy modal_apps/predict_api.py
 
16
  Test:
17
  curl -X POST https://<workspace>--android-skill-predict-api-skillpredictor-web.modal.run/predict \\
18
  -H "Content-Type: application/json" \\
19
+ -d '{"prompt": "text mom on whatsapp i am on my way"}'
20
 
21
+ # -> {"skill":"whatsapp_send_message","parameters":{"contact":"mom","message":"i am on my way"}}
22
 
23
  Develop locally (ephemeral URL, hot-reloads on file changes):
24
  modal serve modal_apps/predict_api.py
 
29
 
30
  from __future__ import annotations
31
 
 
32
  import pathlib
 
33
 
34
  import modal
35
 
 
44
  MODEL_DIR = pathlib.Path("/model")
45
  ADAPTER_DIR = MODEL_DIR / "adapter"
46
  MAX_SEQ_LENGTH = 2048
47
+ MAX_NEW_TOKENS = 128
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  GPU_TYPE = "A10G"
50
  TIMEOUT_SECONDS = 10 * 60
 
84
  from unsloth.chat_templates import get_chat_template
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # ---------------------------------------------------------------------------
88
  # API (model loaded once per container via @modal.enter)
89
  # ---------------------------------------------------------------------------
 
130
  self.tokenizer = tokenizer
131
  print("Model ready.")
132
 
133
+ def _generate_intent(self, prompt: str) -> dict | None:
134
+ from classifier_prompt import build_intent_messages
135
+ from skill_utils import extract_intent, resolve_skill
136
+
137
+ messages = build_intent_messages(prompt)
138
  inputs = self.tokenizer.apply_chat_template(
139
  messages,
140
  tokenize=True,
 
145
  with torch.inference_mode():
146
  outputs = self.model.generate(
147
  input_ids=inputs,
148
+ max_new_tokens=MAX_NEW_TOKENS,
149
  use_cache=True,
150
  do_sample=False,
151
  )
152
 
153
  generated = outputs[0][inputs.shape[1] :]
154
  raw_output = self.tokenizer.decode(generated, skip_special_tokens=True).strip()
155
+ intent = extract_intent(raw_output)
156
+ if not intent:
157
+ return None
158
+
159
+ skill = resolve_skill(intent.get("skill"), prompt)
160
+ if not skill:
161
+ return None
162
+
163
+ parameters = intent.get("parameters", {})
164
+ if not isinstance(parameters, dict):
165
+ parameters = {}
166
 
167
+ return {"skill": skill, "parameters": parameters}
 
168
 
169
  @modal.asgi_app()
170
  def web(self):
 
194
  },
195
  )
196
 
197
+ intent = self._generate_intent(prompt.strip())
198
+ if intent is None:
199
  return JSONResponse(
200
  status_code=422,
201
  content={
202
  "error": "invalid_model_output",
203
+ "message": "Model did not return valid intent JSON.",
204
  },
205
  )
206
+ return JSONResponse(content=intent)
207
 
208
  return Starlette(
209
  routes=[
modal_apps/train_modal.py CHANGED
@@ -220,16 +220,27 @@ def train() -> None:
220
 
221
 
222
  @app.local_entrypoint()
223
- def main() -> None:
224
- dataset_path = pathlib.Path(LOCAL_DATASET)
 
 
 
 
 
225
  if not dataset_path.exists():
226
  raise FileNotFoundError(
227
  f"Local dataset not found: {dataset_path.resolve()}"
228
  )
229
 
 
 
 
 
 
 
230
  print(f"Uploading {dataset_path} to dataset volume...")
231
  with dataset_volume.batch_upload() as batch:
232
- batch.put_file(str(dataset_path), "train.jsonl")
233
 
234
  print("Launching training on Modal GPU...")
235
  train.remote()
 
220
 
221
 
222
  @app.local_entrypoint()
223
+ def main(dataset: str = "train_intent.jsonl") -> None:
224
+ """Upload dataset and launch training.
225
+
226
+ Args:
227
+ dataset: Filename under data/ — use train_intent.jsonl for intent extraction.
228
+ """
229
+ dataset_path = PROJECT_ROOT / "data" / dataset
230
  if not dataset_path.exists():
231
  raise FileNotFoundError(
232
  f"Local dataset not found: {dataset_path.resolve()}"
233
  )
234
 
235
+ remote_name = "train.jsonl"
236
+ try:
237
+ dataset_volume.remove_file(remote_name)
238
+ except Exception:
239
+ pass # file may not exist yet on the volume
240
+
241
  print(f"Uploading {dataset_path} to dataset volume...")
242
  with dataset_volume.batch_upload() as batch:
243
+ batch.put_file(str(dataset_path), remote_name)
244
 
245
  print("Launching training on Modal GPU...")
246
  train.remote()
scripts/generate_intent_dataset.py ADDED
@@ -0,0 +1,949 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate intent-extraction training data with skill + parameters.
3
+
4
+ Produces:
5
+ data/train_intent.jsonl — SFT examples for Unsloth (1000+ per skill)
6
+ data/eval_intent_prompts.json — held-out evaluation prompts with expected intents
7
+
8
+ Usage:
9
+ python scripts/generate_intent_dataset.py
10
+ python scripts/generate_intent_dataset.py --examples-per-skill 1200
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import random
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
22
+ sys.path.insert(0, str(PROJECT_ROOT))
23
+
24
+ from src.classifier_prompt import INTENT_SYSTEM_PROMPT # noqa: E402
25
+
26
+ SCHEMAS_FILE = PROJECT_ROOT / "data" / "skill_schemas.json"
27
+ SKILLS_FILE = PROJECT_ROOT / "data" / "skills.jsonl"
28
+ TRAIN_OUTPUT = PROJECT_ROOT / "data" / "train_intent.jsonl"
29
+ EVAL_OUTPUT = PROJECT_ROOT / "data" / "eval_intent_prompts.json"
30
+
31
+ DEFAULT_EXAMPLES_PER_SKILL = 1000
32
+ EVAL_PROMPTS_PER_SKILL = 6
33
+ RANDOM_SEED = 42
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Synthetic entity pools
37
+ # ---------------------------------------------------------------------------
38
+
39
+ CONTACTS = [
40
+ "Ri", "Biraj", "Sarah", "Alex", "Mom", "Dad", "Priya", "Maya", "Rahul",
41
+ "Neha", "Sam", "Anita", "Karan", "Boss", "John Smith", "Parag Shah",
42
+ "Arya Sheth", "Jane Doe", "Emma Wilson", "David Chen", "Lisa Park",
43
+ "Michael Brown", "Sophie Martin", "James Lee", "Nina Patel", "Tom",
44
+ "Ravi", "Sneha", "Vikram", "Ananya", "Chris", "Jordan", "Taylor",
45
+ "Morgan", "Casey", "Jamie", "Riley", "Avery", "Quinn", "Drew",
46
+ "grandma", "uncle raj", "aunt meera", "my roommate", "the landlord",
47
+ "dentist office", "doctor", "plumber", "electrician",
48
+ ]
49
+
50
+ MESSAGES = [
51
+ "I'll be late", "see you soon", "thanks", "good morning", "on my way",
52
+ "be there in 5", "running late", "call me", "what's up", "good night",
53
+ "hello", "hi there", "ok", "sounds good", "got it", "no worries",
54
+ "can we reschedule", "meeting moved to 3 pm", "i'm stuck in traffic",
55
+ "leaving now", "almost there", "happy birthday", "congrats",
56
+ "let me know when you're free", "pick up milk on the way home",
57
+ "dinner at 8", "see you tonight", "thanks for your help",
58
+ "project update attached", "weekly report is ready",
59
+ "i'll send the docs tomorrow", "running 10 minutes late",
60
+ "are you available for a call", "let's catch up this weekend",
61
+ "the package arrived", "payment sent", "invoice attached",
62
+ "reminder for tomorrow's meeting", "flight lands at 6 pm",
63
+ ]
64
+
65
+ ALARM_TIMES = [
66
+ "5 am", "5:30 am", "6 am", "6:15 am", "6:30 am", "7 am", "7:30 am",
67
+ "8 am", "8:15 am", "9 am", "10 am", "11 am", "noon", "12:30 pm",
68
+ "1 pm", "2 pm", "3 pm", "4 pm", "5 pm", "6 pm", "7 pm", "8 pm",
69
+ "9 pm", "10 pm", "11 pm",
70
+ ]
71
+
72
+ ALARM_DAYS = [
73
+ "today", "tomorrow", "tomorrow morning", "monday", "tuesday",
74
+ "wednesday", "thursday", "friday", "saturday", "sunday",
75
+ "next monday", "next tuesday", "next friday", "this weekend",
76
+ ]
77
+
78
+ CALENDAR_TITLES = [
79
+ "team standup", "dentist appointment", "lunch with alex", "project review",
80
+ "doctor visit", "interview", "gym session", "coffee with sam",
81
+ "presentation", "flight", "call with client", "dinner reservation",
82
+ "study session", "yoga class", "1 on 1 with manager", "birthday party",
83
+ "meeting with kriyanshi", "sync with team", "board meeting",
84
+ "parent teacher conference", "car service", "vet appointment",
85
+ "book club", "therapy session", "haircut", "tax appointment",
86
+ ]
87
+
88
+ CALENDAR_DATES = [
89
+ "tomorrow", "today", "monday", "tuesday", "wednesday", "thursday",
90
+ "friday", "saturday", "sunday", "next tuesday", "next friday",
91
+ "next monday", "this weekend", "next week",
92
+ ]
93
+
94
+ CALENDAR_TIMES = [
95
+ "7 am", "8 am", "9 am", "10 am", "11 am", "noon", "1 pm", "2 pm",
96
+ "3 pm", "4 pm", "5 pm", "6 pm", "7 pm", "8 pm",
97
+ ]
98
+
99
+ PLAYLISTS = [
100
+ "liked songs", "workout", "chill", "edm", "discover weekly", "daily mix",
101
+ "release radar", "party", "road trip", "morning", "focus", "jazz",
102
+ "top hits", "running", "study", "acoustic", "hip hop", "classical",
103
+ "bollywood", "lo fi", "rain sounds", "sleep", "meditation", "rock",
104
+ "indie", "pop hits", "throwback", "summer vibes", "late night",
105
+ "gym pump", "coding focus", "driving", "cooking", "cleaning",
106
+ ]
107
+
108
+ YOUTUBE_QUERIES = [
109
+ "pasta recipes", "workout videos", "python tutorials", "cat videos",
110
+ "lo fi beats", "travel vlogs", "meditation music", "guitar lessons",
111
+ "phone review", "stand up comedy", "how to bake bread", "documentary",
112
+ "asmr videos", "yoga classes", "morning routine", "news",
113
+ "diy crafts", "ghibli food", "korean street food", "home renovation",
114
+ "machine learning basics", "react tutorial", "interview tips",
115
+ "stretching routine", "origami tutorial", "watercolor painting",
116
+ "budget travel tips", "meal prep ideas", "indoor plants care",
117
+ "car maintenance", "photography tips", "skincare routine",
118
+ ]
119
+
120
+ SPOTIFY_QUERIES = [
121
+ "jazz", "lo fi beats", "taylor swift", "classical music", "workout music",
122
+ "bollywood songs", "rock music", "chill vibes", "drake", "hip hop",
123
+ "acoustic covers", "study music", "pop hits", "rain sounds", "edm",
124
+ "indie folk", "synthwave", "reggaeton", "k-pop", "country hits",
125
+ "piano covers", "ambient", "80s hits", "90s throwback", "focus beats",
126
+ ]
127
+
128
+ SLACK_CHANNELS = [
129
+ "general", "engineering", "data contributors", "random", "announcements",
130
+ "product team", "design reviews", "support tickets", "marketing",
131
+ "devops alerts", "project alpha", "team updates", "hiring", "backend team",
132
+ "daily standup", "sales", "customer success", "incidents", "on-call",
133
+ "frontend", "mobile", "qa", "releases", "watercooler", "leadership",
134
+ ]
135
+
136
+ DESTINATIONS = [
137
+ "the airport", "downtown", "123 main street", "my office", "central station",
138
+ "home", "the mall", "union square", "marina bay", "work", "the restaurant",
139
+ "the train station", "pratishtha apartment unnamed road", "grand central",
140
+ "times square", "golden gate bridge", "hotel california", "city hospital",
141
+ "university campus", "grocery store", "gym", "coffee shop on 5th ave",
142
+ "convention center", "stadium", "library", "pharmacy", "post office",
143
+ ]
144
+
145
+ LINKEDIN_NAMES = [
146
+ "arya sheth", "parag shah", "john smith", "jane doe", "priya mehta",
147
+ "david chen", "emma wilson", "michael brown", "sophie martin", "james lee",
148
+ "nina patel", "sarah johnson", "robert kim", "lisa anderson", "mark taylor",
149
+ "amanda white", "chris evans", "rachel green", "kevin hart", "olivia brown",
150
+ ]
151
+
152
+ EMAIL_RECIPIENTS = [
153
+ "boss", "team", "mom", "hr", "client", "professor", "partner",
154
+ "john@company.com", "sarah@gmail.com", "team@work.com", "hr@company.com",
155
+ "alex@gmail.com", "client@startup.io", "professor@university.edu",
156
+ "kriyanshishah06@gmail.com", "contact@gmail.com", "partner@gmail.com",
157
+ "manager@corp.com", "support@service.com", "billing@company.com",
158
+ "recruiter@jobs.com", "design@agency.com", "dev@startup.io",
159
+ ]
160
+
161
+ EMAIL_MESSAGES = [
162
+ "project update", "i'll be late", "hello", "thanks for your help",
163
+ "weekly report", "vacation request", "proposal attached", "meeting notes",
164
+ "follow up", "hello-world", "invoice attached", "contract for review",
165
+ "interview confirmation", "quarterly results", "onboarding docs",
166
+ "feedback on the design", "can we reschedule", "out of office notice",
167
+ "happy birthday", "congratulations on the promotion",
168
+ ]
169
+
170
+ NO_PARAM_PROMPTS: dict[str, list[str]] = {
171
+ "wifi_enable": [
172
+ "enable wifi", "turn on wifi", "switch on wifi", "activate wifi",
173
+ "wifi on", "turn wifi on please", "enable wifi on my phone",
174
+ "switch wifi on", "start wifi", "put wifi on", "can you turn on wifi",
175
+ "wifi enable", "turn on my wifi", "enable wireless network",
176
+ "activate wifi connection", "switch on my wifi", "wifi on now",
177
+ "please enable wifi", "turn wifi back on", "enable wifi settings",
178
+ "flip wifi on", "get wifi running", "power on wifi", "i need wifi on",
179
+ "enable wlan", "turn wlan on", "wifi should be on",
180
+ "make sure wifi is enabled", "set wifi to on", "connect to wifi",
181
+ "please turn on wifi", "wifi needs to be on", "switch wlan on now",
182
+ "turn the wifi on", "enable my wifi connection", "get wifi on",
183
+ "wifi activation please", "power up wifi", "wifi switch on",
184
+ ],
185
+ "bluetooth_enable": [
186
+ "turn on bluetooth", "enable bluetooth", "switch on bluetooth",
187
+ "activate bluetooth", "bluetooth on", "turn bluetooth on please",
188
+ "enable bluetooth on my phone", "switch bluetooth on", "start bluetooth",
189
+ "put bluetooth on", "can you turn on bluetooth", "bluetooth enable",
190
+ "turn on my bluetooth", "enable the bluetooth radio",
191
+ "activate bluetooth connection", "switch on my bluetooth",
192
+ "bluetooth on now", "please enable bluetooth", "turn bluetooth back on",
193
+ "enable bluetooth settings", "flip bluetooth on", "get bluetooth running",
194
+ "power on bluetooth", "connect bluetooth turn it on", "i need bluetooth on",
195
+ "enable bt", "turn bt on", "bluetooth should be on",
196
+ "make sure bluetooth is on", "set bluetooth to on", "turn bluetooth on now",
197
+ "bluetooth needs to be enabled", "switch bt on", "activate my bluetooth",
198
+ ],
199
+ "spotify_pause": [
200
+ "pause spotify", "stop spotify music", "pause the song on spotify",
201
+ "stop playing on spotify", "pause playback spotify", "hold the music on spotify",
202
+ "pause spotify playback", "stop spotify for now", "pause what's playing on spotify",
203
+ "mute spotify pause it", "pause my spotify music", "stop the music spotify",
204
+ "pause current track spotify", "spotify pause", "pause the spotify player",
205
+ "stop spotify song", "pause spotify please", "halt spotify music",
206
+ "pause spotify now", "stop playback on spotify", "pause the audio on spotify",
207
+ "spotify stop playing", "pause my song on spotify", "freeze spotify playback",
208
+ "pause spotify music player", "stop spotify temporarily",
209
+ "pause whatever is on spotify", "spotify pause music", "hold spotify",
210
+ "stop the spotify track", "pause music on spotify", "silence spotify",
211
+ ],
212
+ "camera_take_photo": [
213
+ "take a photo with the camera", "open camera and take a picture",
214
+ "snap a photo", "click a picture with camera", "take a picture now",
215
+ "open the camera app and shoot", "capture a photo", "take a selfie",
216
+ "open camera and snap a pic", "shoot a picture", "click a photo please",
217
+ "take photo with rear camera", "open camera take picture",
218
+ "snap a quick photo", "capture an image with camera", "take a picture of this",
219
+ "open camera and photograph", "click picture using camera",
220
+ "take a shot with camera", "launch camera and take photo",
221
+ "grab a photo with camera", "take a camera picture",
222
+ "open camera snap photo", "photograph this with camera",
223
+ "take pic with camera app", "shoot photo now",
224
+ "camera open and click picture", "take a quick picture",
225
+ "open camera capture photo", "open camera and click a picture",
226
+ ],
227
+ }
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Prompt templates (placeholders: {contact}, {message}, {time}, {day}, etc.)
231
+ # ---------------------------------------------------------------------------
232
+
233
+ ALARM_TEMPLATES = [
234
+ "create alarm for {time} {day}",
235
+ "set alarm for {time} {day}",
236
+ "wake me up at {time} {day}",
237
+ "alarm {day} {time}",
238
+ "set a {time} alarm for {day}",
239
+ "put an alarm for {time} {day}",
240
+ "schedule alarm {time} {day}",
241
+ "set wake up alarm {time} {day}",
242
+ "alarm at {time} {day}",
243
+ "set my alarm for {time} {day}",
244
+ "need alarm {day} {time}",
245
+ "{time} alarm {day} please",
246
+ "wake up alarm {day} {time}",
247
+ "alarm me at {time} {day}",
248
+ "wake me at {time} {day}",
249
+ "alarm for {time} {day}",
250
+ "schedule {time} wake up {day}",
251
+ "remind me at {time} {day}",
252
+ "wake me up at {time}",
253
+ "set an alarm for {time} {day}",
254
+ "can you set alarm {time} {day}",
255
+ "i need to wake up at {time} {day}",
256
+ "please alarm {time} {day}",
257
+ "set {day} morning alarm {time}",
258
+ ]
259
+
260
+ CALENDAR_TEMPLATES = [
261
+ "create calendar event for {date} {time} {title}",
262
+ "add a meeting {date} at {time} {title}",
263
+ "schedule {title} {date} {time}",
264
+ "put {title} on my calendar {date} at {time}",
265
+ "create event {date} {time} {title}",
266
+ "add calendar event {date} {time} {title}",
267
+ "book a meeting {date} at {time} {title}",
268
+ "create a calendar entry {date} {time} {title}",
269
+ "set up a meeting {date} {time} {title}",
270
+ "calendar meeting {date} {time} {title}",
271
+ "add {title} to calendar {date} {time}",
272
+ "schedule {title} on {date} at {time}",
273
+ "put {title} on calendar {date} {time}",
274
+ "create meeting {date} {time} {title}",
275
+ "add {title} appointment {date} {time}",
276
+ ]
277
+
278
+ WHATSAPP_TEMPLATES = [
279
+ "message {message} to {contact} on whatsapp",
280
+ "text {message} to {contact} on whatsapp",
281
+ "send {message} to {contact} on whatsapp",
282
+ "whatsapp {contact} saying {message}",
283
+ "message {contact} on whatsapp {message}",
284
+ "text {contact} {message} on whatsapp",
285
+ "send a whatsapp to {contact} saying {message}",
286
+ "whatsapp message {contact} {message}",
287
+ "tell {contact} {message} on whatsapp",
288
+ "shoot {contact} a whatsapp saying {message}",
289
+ "send {message} via whatsapp to {contact}",
290
+ "drop {contact} a whatsapp saying {message}",
291
+ "ping {contact} on whatsapp with {message}",
292
+ "write to {contact} on whatsapp {message}",
293
+ "whatsapp {contact} {message}",
294
+ "send {contact} a whatsapp text saying {message}",
295
+ "text {contact} on whatsapp saying {message}",
296
+ ]
297
+
298
+ SLACK_TEMPLATES = [
299
+ "open slack channel {channel}",
300
+ "go to {channel} channel in slack",
301
+ "open the {channel} slack channel",
302
+ "switch to {channel} in slack",
303
+ "show me {channel} channel on slack",
304
+ "navigate to {channel} on slack",
305
+ "open slack {channel}",
306
+ "pull up {channel} slack channel",
307
+ "take me to {channel} on slack",
308
+ "open channel {channel} in slack",
309
+ "slack open {channel}",
310
+ "go to slack channel {channel}",
311
+ "open the slack channel called {channel}",
312
+ "show {channel} slack",
313
+ "open the {channel} channel in slack",
314
+ "navigate to {channel} slack channel",
315
+ "switch to #{channel} in slack",
316
+ ]
317
+
318
+ PLAYLIST_TEMPLATES = [
319
+ "play my {playlist} on spotify",
320
+ "start my {playlist} playlist spotify",
321
+ "put on {playlist} spotify",
322
+ "play {playlist} on spotify",
323
+ "play {playlist} playlist from spotify",
324
+ "open {playlist} playlist spotify",
325
+ "spotify play my {playlist}",
326
+ "play music from my {playlist}",
327
+ "start playing my {playlist} spotify playlist",
328
+ "play my spotify {playlist}",
329
+ "put on my {playlist} playlist on spotify",
330
+ "start {playlist} on spotify",
331
+ "play the {playlist} playlist",
332
+ "spotify start {playlist}",
333
+ "queue up {playlist} on spotify",
334
+ ]
335
+
336
+ UBER_TEMPLATES = [
337
+ "get an uber to {destination}",
338
+ "uber to {destination}",
339
+ "book uber to {destination}",
340
+ "call an uber to {destination}",
341
+ "request uber ride to {destination}",
342
+ "uber me to {destination}",
343
+ "get a ride to {destination} on uber",
344
+ "book a cab to {destination} via uber",
345
+ "uber to {destination} now",
346
+ "need an uber to {destination}",
347
+ "schedule uber to {destination}",
348
+ "ride to {destination} using uber",
349
+ "open uber and go to {destination}",
350
+ "find uber to {destination}",
351
+ "get ride to {destination}",
352
+ "uber for {destination} please",
353
+ "book an uber to {destination}",
354
+ "search and uber for {destination}",
355
+ ]
356
+
357
+ LINKEDIN_TEMPLATES = [
358
+ "search {name} on linkedin",
359
+ "look up {name} on linkedin",
360
+ "find {name}'s linkedin profile",
361
+ "linkedin search for {name}",
362
+ "open linkedin and search {name}",
363
+ "can you find {name} on linkedin",
364
+ "search for {name} on linked in",
365
+ "look for {name} on linkedin",
366
+ "find {name} on linkedin",
367
+ "linkedin find {name}",
368
+ "search linkedin for {name}",
369
+ "pull up {name} on linkedin",
370
+ "show me {name} on linkedin",
371
+ "i need to find {name} on linkedin",
372
+ "help me search linkedin for {name}",
373
+ ]
374
+
375
+ SPOTIFY_SEARCH_TEMPLATES = [
376
+ "search {query} on spotify and play",
377
+ "find {query} on spotify and play it",
378
+ "play {query} music on spotify",
379
+ "search for {query} on spotify and start playing",
380
+ "look up {query} tracks on spotify and play",
381
+ "spotify search {query} and play",
382
+ "find and play {query} on spotify",
383
+ "search spotify for {query} music and play",
384
+ "play some {query} on spotify",
385
+ "open spotify search {query} and play",
386
+ "search {query} playlist spotify play now",
387
+ "find {query} spotify play",
388
+ "spotify play {query} after searching",
389
+ "search and play {query} tracks spotify",
390
+ "put on {query} from spotify search",
391
+ ]
392
+
393
+ YOUTUBE_TEMPLATES = [
394
+ "search {query} on youtube",
395
+ "look up {query} on youtube",
396
+ "find {query} videos on youtube",
397
+ "youtube search {query}",
398
+ "search for {query} on youtube",
399
+ "play {query} on youtube",
400
+ "show me {query} on youtube",
401
+ "open youtube and search {query}",
402
+ "find {query} on youtube",
403
+ "youtube {query}",
404
+ "look for {query} videos on youtube",
405
+ "search youtube for {query}",
406
+ "find videos about {query} on youtube",
407
+ "youtube look up {query}",
408
+ ]
409
+
410
+ CONTACTS_TEMPLATES = [
411
+ "search {contact} in contacts",
412
+ "find {contact} in contacts",
413
+ "look up {contact} in my contacts",
414
+ "search contact {contact}",
415
+ "find {contact} contact",
416
+ "search contacts for {contact}",
417
+ "look for {contact} in phone contacts",
418
+ "open contacts and search {contact}",
419
+ "find {contact}'s number in contacts",
420
+ "search my contacts for {contact}",
421
+ "contacts find {contact}",
422
+ "look up {contact} contact info",
423
+ "search my phone book for {contact}",
424
+ "contacts lookup {contact}",
425
+ "find {contact} phone number contacts",
426
+ "search address book {contact}",
427
+ "open contacts search {contact}",
428
+ "find parag shah in contacts",
429
+ ]
430
+
431
+ GMAIL_TEMPLATES = [
432
+ "send mail using gmail to {recipient} saying {message}",
433
+ "email {recipient} saying {message}",
434
+ "send gmail to {recipient} saying {message}",
435
+ "write mail to {recipient} saying {message}",
436
+ "compose an email to {recipient} {message}",
437
+ "email {recipient} {message} via gmail",
438
+ "compose gmail to {recipient} saying {message}",
439
+ "send email to {recipient} {message}",
440
+ "gmail {recipient} with {message}",
441
+ "write email to {recipient} saying {message}",
442
+ "send a gmail message {message} to {recipient}",
443
+ "email {recipient} from gmail {message}",
444
+ "compose email {message} to {recipient}",
445
+ "send {message} email to {recipient} gmail",
446
+ "shoot an email to {recipient} {message}",
447
+ "mail {recipient} with {message}",
448
+ "send gmail {message} to {recipient}",
449
+ "compose and send {message} to {recipient}",
450
+ ]
451
+
452
+ # ---------------------------------------------------------------------------
453
+ # Helpers
454
+ # ---------------------------------------------------------------------------
455
+
456
+
457
+ def load_schemas(path: Path) -> dict:
458
+ with path.open(encoding="utf-8") as handle:
459
+ return json.load(handle)
460
+
461
+
462
+ def load_skills(path: Path) -> list[dict]:
463
+ skills = []
464
+ with path.open(encoding="utf-8") as handle:
465
+ for line in handle:
466
+ line = line.strip()
467
+ if line:
468
+ skills.append(json.loads(line))
469
+ return skills
470
+
471
+
472
+ def format_intent(skill: str, parameters: dict) -> str:
473
+ return json.dumps({"skill": skill, "parameters": parameters}, separators=(",", ":"))
474
+
475
+
476
+ def make_record(prompt: str, skill: str, parameters: dict) -> dict:
477
+ return {
478
+ "messages": [
479
+ {"role": "system", "content": INTENT_SYSTEM_PROMPT},
480
+ {"role": "user", "content": prompt},
481
+ {"role": "assistant", "content": format_intent(skill, parameters)},
482
+ ]
483
+ }
484
+
485
+
486
+ def expand_combinations(
487
+ templates: list[str],
488
+ param_pools: dict[str, list[str]],
489
+ limit: int,
490
+ rng: random.Random,
491
+ ) -> list[tuple[str, dict]]:
492
+ """Sample template × parameter combinations without building the full cartesian product."""
493
+ pool_keys = list(param_pools.keys())
494
+ examples: list[tuple[str, dict]] = []
495
+ seen: set[str] = set()
496
+ attempts = 0
497
+ max_attempts = limit * 50
498
+
499
+ fillers = ["", " thanks", " asap", " please", " now"]
500
+ while len(examples) < limit and attempts < max_attempts:
501
+ attempts += 1
502
+ template = rng.choice(templates)
503
+ params = {key: rng.choice(pool) for key, pool in param_pools.items()}
504
+ try:
505
+ prompt = template.format(**params)
506
+ except KeyError:
507
+ continue
508
+ if rng.random() > 0.7:
509
+ prompt = f"{rng.choice(['please ', 'can you ', 'i need to ', ''] )}{prompt}{rng.choice(fillers)}"
510
+ key = prompt.lower().strip()
511
+ if not key or key in seen:
512
+ continue
513
+ seen.add(key)
514
+ examples.append((prompt.strip(), params))
515
+
516
+ return examples
517
+
518
+
519
+ def generate_no_param_examples(skill: str, limit: int, rng: random.Random) -> list[tuple[str, dict]]:
520
+ prompts = list(NO_PARAM_PROMPTS.get(skill, []))
521
+ rng.shuffle(prompts)
522
+
523
+ prefixes = ["please ", "can you ", "i need to ", "help me ", "quickly ", "just "]
524
+ suffixes = [" please", " now", " for me", " on my phone", " right away"]
525
+ seen: set[str] = set()
526
+ result: list[tuple[str, dict]] = []
527
+
528
+ def add(prompt: str) -> bool:
529
+ key = prompt.lower().strip()
530
+ if not key or key in seen:
531
+ return False
532
+ seen.add(key)
533
+ result.append((prompt.strip(), {}))
534
+ return True
535
+
536
+ for prompt in prompts:
537
+ add(prompt)
538
+ for prefix in prefixes:
539
+ add(f"{prefix}{prompt}")
540
+ for suffix in suffixes:
541
+ add(f"{prompt}{suffix}")
542
+ if len(result) >= limit:
543
+ return result[:limit]
544
+
545
+ fillers = ["thanks", "asap", "when you can", "if possible", "real quick"]
546
+ attempt = 0
547
+ while len(result) < limit and attempt < limit * 20:
548
+ attempt += 1
549
+ base = rng.choice(prompts)
550
+ variant = (
551
+ f"{rng.choice(prefixes)}{base}{rng.choice(suffixes)}"
552
+ f"{'' if rng.random() > 0.3 else ' ' + rng.choice(fillers)}"
553
+ )
554
+ add(variant)
555
+
556
+ return result[:limit]
557
+
558
+
559
+ def generate_skill_examples(
560
+ skill: str,
561
+ limit: int,
562
+ rng: random.Random,
563
+ ) -> list[tuple[str, dict]]:
564
+ if skill in NO_PARAM_PROMPTS:
565
+ return generate_no_param_examples(skill, limit, rng)
566
+
567
+ generators = {
568
+ "create_alarm": lambda: expand_combinations(
569
+ ALARM_TEMPLATES,
570
+ {"time": ALARM_TIMES, "day": ALARM_DAYS},
571
+ limit,
572
+ rng,
573
+ ),
574
+ "calendar_create_event": lambda: expand_combinations(
575
+ CALENDAR_TEMPLATES,
576
+ {"title": CALENDAR_TITLES, "date": CALENDAR_DATES, "time": CALENDAR_TIMES},
577
+ limit,
578
+ rng,
579
+ ),
580
+ "whatsapp_send_message": lambda: expand_combinations(
581
+ WHATSAPP_TEMPLATES,
582
+ {"contact": CONTACTS, "message": MESSAGES},
583
+ limit,
584
+ rng,
585
+ ),
586
+ "slack_open_channel": lambda: expand_combinations(
587
+ SLACK_TEMPLATES,
588
+ {"channel": SLACK_CHANNELS},
589
+ limit,
590
+ rng,
591
+ ),
592
+ "spotify_play_playlist": lambda: expand_combinations(
593
+ PLAYLIST_TEMPLATES,
594
+ {"playlist": PLAYLISTS},
595
+ limit,
596
+ rng,
597
+ ),
598
+ "uber_request_ride": lambda: expand_combinations(
599
+ UBER_TEMPLATES,
600
+ {"destination": DESTINATIONS},
601
+ limit,
602
+ rng,
603
+ ),
604
+ "linkedin_search_person": lambda: expand_combinations(
605
+ LINKEDIN_TEMPLATES,
606
+ {"name": LINKEDIN_NAMES},
607
+ limit,
608
+ rng,
609
+ ),
610
+ "spotify_search_play": lambda: expand_combinations(
611
+ SPOTIFY_SEARCH_TEMPLATES,
612
+ {"query": SPOTIFY_QUERIES},
613
+ limit,
614
+ rng,
615
+ ),
616
+ "youtube_search": lambda: expand_combinations(
617
+ YOUTUBE_TEMPLATES,
618
+ {"query": YOUTUBE_QUERIES},
619
+ limit,
620
+ rng,
621
+ ),
622
+ "contacts_search": lambda: expand_combinations(
623
+ CONTACTS_TEMPLATES,
624
+ {"contact": CONTACTS},
625
+ limit,
626
+ rng,
627
+ ),
628
+ "gmail_send_email": lambda: expand_combinations(
629
+ GMAIL_TEMPLATES,
630
+ {"recipient": EMAIL_RECIPIENTS, "message": EMAIL_MESSAGES},
631
+ limit,
632
+ rng,
633
+ ),
634
+ }
635
+
636
+ generator = generators.get(skill)
637
+ if not generator:
638
+ return []
639
+
640
+ return generator()
641
+
642
+
643
+ def generate_contrastive_examples() -> list[dict]:
644
+ """Hard negatives: same entity, different app → different skill + parameters."""
645
+ records: list[dict] = []
646
+ names = ["parag shah", "arya sheth", "john smith", "mom", "sarah"]
647
+
648
+ for name in names:
649
+ records.extend(
650
+ [
651
+ make_record(f"search {name} in contacts", "contacts_search", {"contact": name}),
652
+ make_record(f"find {name} in my contacts", "contacts_search", {"contact": name}),
653
+ make_record(f"search {name} on linkedin", "linkedin_search_person", {"name": name}),
654
+ make_record(f"find {name} on linkedin", "linkedin_search_person", {"name": name}),
655
+ make_record(f"search {name} on youtube", "youtube_search", {"query": name}),
656
+ make_record(f"look up {name} videos on youtube", "youtube_search", {"query": name}),
657
+ make_record(
658
+ f"search {name} on spotify and play",
659
+ "spotify_search_play",
660
+ {"query": name},
661
+ ),
662
+ make_record(
663
+ f"find {name} music on spotify and play it",
664
+ "spotify_search_play",
665
+ {"query": name},
666
+ ),
667
+ ]
668
+ )
669
+
670
+ records.extend(
671
+ [
672
+ make_record(
673
+ "email boss saying i'll be late",
674
+ "gmail_send_email",
675
+ {"recipient": "boss", "message": "i'll be late"},
676
+ ),
677
+ make_record(
678
+ "message boss on whatsapp running late",
679
+ "whatsapp_send_message",
680
+ {"contact": "boss", "message": "running late"},
681
+ ),
682
+ make_record(
683
+ "open the engineering channel in slack",
684
+ "slack_open_channel",
685
+ {"channel": "engineering"},
686
+ ),
687
+ make_record(
688
+ "search pasta recipes on youtube",
689
+ "youtube_search",
690
+ {"query": "pasta recipes"},
691
+ ),
692
+ make_record(
693
+ "find parag shah in contacts",
694
+ "contacts_search",
695
+ {"contact": "parag shah"},
696
+ ),
697
+ make_record(
698
+ "send ri a message on whatsapp saying see you soon",
699
+ "whatsapp_send_message",
700
+ {"contact": "ri", "message": "see you soon"},
701
+ ),
702
+ make_record(
703
+ "play my workout playlist",
704
+ "spotify_play_playlist",
705
+ {"playlist": "workout"},
706
+ ),
707
+ make_record(
708
+ "wake me up tomorrow morning",
709
+ "create_alarm",
710
+ {"time": "7 am", "day": "tomorrow morning"},
711
+ ),
712
+ ]
713
+ )
714
+
715
+ return records
716
+
717
+
718
+ def generate_eval_prompts(
719
+ train_prompts: set[str],
720
+ per_skill: int,
721
+ rng: random.Random,
722
+ ) -> list[dict]:
723
+ """Generate held-out eval prompts not present in training."""
724
+ eval_cases: list[dict] = []
725
+
726
+ eval_templates = {
727
+ "create_alarm": [
728
+ ("set a 6 am alarm for monday", {"time": "6 am", "day": "monday"}),
729
+ ("wake me up at 8:30 am next friday", {"time": "8:30 am", "day": "next friday"}),
730
+ ("alarm tomorrow 5:30 am", {"time": "5:30 am", "day": "tomorrow"}),
731
+ ("schedule 9 pm alarm tonight", {"time": "9 pm", "day": "tonight"}),
732
+ ("i need a 7:15 am alarm wednesday", {"time": "7:15 am", "day": "wednesday"}),
733
+ ("put alarm for 6:45 am saturday", {"time": "6:45 am", "day": "saturday"}),
734
+ ("wake up at noon tomorrow", {"time": "noon", "day": "tomorrow"}),
735
+ ],
736
+ "calendar_create_event": [
737
+ ("add team standup to my calendar tuesday 10 am", {"title": "team standup", "date": "tuesday", "time": "10 am"}),
738
+ ("book dentist appointment next friday 3 pm", {"title": "dentist appointment", "date": "next friday", "time": "3 pm"}),
739
+ ("put lunch with priya on calendar wednesday noon", {"title": "lunch with priya", "date": "wednesday", "time": "noon"}),
740
+ ("schedule code review thursday 2 pm", {"title": "code review", "date": "thursday", "time": "2 pm"}),
741
+ ("add flight to calendar sunday 8 am", {"title": "flight", "date": "sunday", "time": "8 am"}),
742
+ ("create event hackathon demo monday 4 pm", {"title": "hackathon demo", "date": "monday", "time": "4 pm"}),
743
+ ],
744
+ "wifi_enable": [
745
+ ("turn on wifi", {}),
746
+ ("please enable wifi on my phone", {}),
747
+ ("switch wlan on", {}),
748
+ ("activate my wifi connection", {}),
749
+ ("i need wifi enabled", {}),
750
+ ("get wifi running please", {}),
751
+ ],
752
+ "bluetooth_enable": [
753
+ ("turn bluetooth on", {}),
754
+ ("enable bluetooth please", {}),
755
+ ("switch on my bluetooth", {}),
756
+ ("bluetooth should be on", {}),
757
+ ("power on bluetooth now", {}),
758
+ ("activate bluetooth radio", {}),
759
+ ],
760
+ "whatsapp_send_message": [
761
+ ("text mom on whatsapp i'm on my way", {"contact": "mom", "message": "i'm on my way"}),
762
+ ("whatsapp sarah saying thanks", {"contact": "sarah", "message": "thanks"}),
763
+ ("message alex on whatsapp be there in 5", {"contact": "alex", "message": "be there in 5"}),
764
+ ("send ri a message saying i'll be late", {"contact": "ri", "message": "i'll be late"}),
765
+ ("tell biraj on whatsapp see you tonight", {"contact": "biraj", "message": "see you tonight"}),
766
+ ("ping priya on whatsapp with hello", {"contact": "priya", "message": "hello"}),
767
+ ],
768
+ "camera_take_photo": [
769
+ ("snap a quick photo", {}),
770
+ ("open camera and take a picture", {}),
771
+ ("capture a photo with the camera", {}),
772
+ ("take a selfie now", {}),
773
+ ("shoot a picture please", {}),
774
+ ("launch camera and snap a pic", {}),
775
+ ],
776
+ "slack_open_channel": [
777
+ ("go to general channel in slack", {"channel": "general"}),
778
+ ("open slack channel random", {"channel": "random"}),
779
+ ("navigate to design reviews slack channel", {"channel": "design reviews"}),
780
+ ("open the engineering channel in slack", {"channel": "engineering"}),
781
+ ("switch to announcements in slack", {"channel": "announcements"}),
782
+ ("show me data contributors on slack", {"channel": "data contributors"}),
783
+ ],
784
+ "spotify_pause": [
785
+ ("pause spotify playback", {}),
786
+ ("stop the music on spotify", {}),
787
+ ("hold spotify for now", {}),
788
+ ("freeze spotify playback", {}),
789
+ ("halt the spotify player", {}),
790
+ ("spotify stop playing", {}),
791
+ ],
792
+ "spotify_play_playlist": [
793
+ ("start my chill playlist on spotify", {"playlist": "chill"}),
794
+ ("play discover weekly on spotify", {"playlist": "discover weekly"}),
795
+ ("put on my liked songs spotify", {"playlist": "liked songs"}),
796
+ ("play my workout playlist", {"playlist": "workout"}),
797
+ ("queue up road trip on spotify", {"playlist": "road trip"}),
798
+ ("start focus playlist spotify", {"playlist": "focus"}),
799
+ ],
800
+ "uber_request_ride": [
801
+ ("get an uber to the airport", {"destination": "the airport"}),
802
+ ("book uber to downtown", {"destination": "downtown"}),
803
+ ("request a ride to central station on uber", {"destination": "central station"}),
804
+ ("uber me to golden gate bridge", {"destination": "golden gate bridge"}),
805
+ ("need a ride to city hospital via uber", {"destination": "city hospital"}),
806
+ ("call uber to convention center", {"destination": "convention center"}),
807
+ ],
808
+ "linkedin_search_person": [
809
+ ("look up jane doe on linkedin", {"name": "jane doe"}),
810
+ ("find parag shah's linkedin profile", {"name": "parag shah"}),
811
+ ("search linkedin for john smith", {"name": "john smith"}),
812
+ ("pull up sophie martin on linkedin", {"name": "sophie martin"}),
813
+ ("find david chen on linkedin", {"name": "david chen"}),
814
+ ("linkedin search nina patel", {"name": "nina patel"}),
815
+ ],
816
+ "spotify_search_play": [
817
+ ("find jazz on spotify and play it", {"query": "jazz"}),
818
+ ("search lo fi beats on spotify and play them", {"query": "lo fi beats"}),
819
+ ("spotify search taylor swift and play", {"query": "taylor swift"}),
820
+ ("search k-pop on spotify and play", {"query": "k-pop"}),
821
+ ("find ambient music on spotify and play", {"query": "ambient"}),
822
+ ("play reggaeton after searching spotify", {"query": "reggaeton"}),
823
+ ],
824
+ "youtube_search": [
825
+ ("search pasta recipes on youtube", {"query": "pasta recipes"}),
826
+ ("find workout videos on youtube", {"query": "workout videos"}),
827
+ ("look up python tutorials on youtube", {"query": "python tutorials"}),
828
+ ("youtube search morning routine", {"query": "morning routine"}),
829
+ ("find korean street food on youtube", {"query": "korean street food"}),
830
+ ("search machine learning basics on youtube", {"query": "machine learning basics"}),
831
+ ],
832
+ "contacts_search": [
833
+ ("find mom in my contacts", {"contact": "mom"}),
834
+ ("search contacts for dad", {"contact": "dad"}),
835
+ ("look up john smith in contacts", {"contact": "john smith"}),
836
+ ("find dentist office in contacts", {"contact": "dentist office"}),
837
+ ("search my contacts for maya", {"contact": "maya"}),
838
+ ("contacts lookup ri", {"contact": "ri"}),
839
+ ],
840
+ "gmail_send_email": [
841
+ ("email boss saying i'll be late", {"recipient": "boss", "message": "i'll be late"}),
842
+ ("send gmail to sarah@gmail.com subject meeting notes", {"recipient": "sarah@gmail.com", "message": "meeting notes"}),
843
+ ("compose email to client proposal attached", {"recipient": "client", "message": "proposal attached"}),
844
+ ("write mail to hr@company.com vacation request", {"recipient": "hr@company.com", "message": "vacation request"}),
845
+ ("send email to professor asking about assignment", {"recipient": "professor", "message": "asking about assignment"}),
846
+ ("gmail team@work.com weekly report", {"recipient": "team@work.com", "message": "weekly report"}),
847
+ ],
848
+ }
849
+
850
+ for skill, templates in eval_templates.items():
851
+ added = 0
852
+ rng.shuffle(templates)
853
+ for prompt, params in templates:
854
+ key = prompt.lower().strip()
855
+ if key in train_prompts:
856
+ continue
857
+ eval_cases.append(
858
+ {
859
+ "prompt": prompt,
860
+ "expected": {"skill": skill, "parameters": params},
861
+ }
862
+ )
863
+ added += 1
864
+ if added >= per_skill:
865
+ break
866
+
867
+ return eval_cases
868
+
869
+
870
+ def main() -> None:
871
+ parser = argparse.ArgumentParser(description="Generate intent extraction dataset.")
872
+ parser.add_argument(
873
+ "--examples-per-skill",
874
+ type=int,
875
+ default=DEFAULT_EXAMPLES_PER_SKILL,
876
+ help=f"Training examples per skill (default: {DEFAULT_EXAMPLES_PER_SKILL})",
877
+ )
878
+ parser.add_argument(
879
+ "--eval-per-skill",
880
+ type=int,
881
+ default=EVAL_PROMPTS_PER_SKILL,
882
+ help=f"Eval prompts per skill (default: {EVAL_PROMPTS_PER_SKILL})",
883
+ )
884
+ parser.add_argument(
885
+ "--seed",
886
+ type=int,
887
+ default=RANDOM_SEED,
888
+ help=f"Random seed (default: {RANDOM_SEED})",
889
+ )
890
+ args = parser.parse_args()
891
+
892
+ rng = random.Random(args.seed)
893
+ schemas = load_schemas(SCHEMAS_FILE)
894
+ skills = load_skills(SKILLS_FILE)
895
+
896
+ records: list[dict] = []
897
+ train_prompts: set[str] = set()
898
+ skill_counts: dict[str, int] = {}
899
+
900
+ for entry in skills:
901
+ skill = entry["skill"]
902
+ if skill not in schemas:
903
+ print(f"Warning: no schema for skill {skill}, skipping")
904
+ continue
905
+
906
+ examples = generate_skill_examples(skill, args.examples_per_skill, rng)
907
+ for prompt, params in examples:
908
+ key = prompt.lower().strip()
909
+ if key in train_prompts:
910
+ continue
911
+ train_prompts.add(key)
912
+ records.append(make_record(prompt, skill, params))
913
+ skill_counts[skill] = skill_counts.get(skill, 0) + 1
914
+
915
+ for record in generate_contrastive_examples():
916
+ prompt = record["messages"][1]["content"]
917
+ key = prompt.lower().strip()
918
+ if key in train_prompts:
919
+ continue
920
+ train_prompts.add(key)
921
+ records.append(record)
922
+ skill = json.loads(record["messages"][2]["content"])["skill"]
923
+ skill_counts[skill] = skill_counts.get(skill, 0) + 1
924
+
925
+ rng.shuffle(records)
926
+
927
+ with TRAIN_OUTPUT.open("w", encoding="utf-8") as handle:
928
+ for record in records:
929
+ handle.write(json.dumps(record) + "\n")
930
+
931
+ eval_prompts = generate_eval_prompts(train_prompts, args.eval_per_skill, rng)
932
+ with EVAL_OUTPUT.open("w", encoding="utf-8") as handle:
933
+ json.dump(eval_prompts, handle, indent=2)
934
+ handle.write("\n")
935
+
936
+ print(f"Wrote {len(records)} training examples to {TRAIN_OUTPUT}")
937
+ print(f"Wrote {len(eval_prompts)} eval prompts to {EVAL_OUTPUT}")
938
+ print(f"Skills: {len(skill_counts)}")
939
+ for skill, count in sorted(skill_counts.items()):
940
+ status = "OK" if count >= args.examples_per_skill else "LOW"
941
+ print(f" {skill}: {count} [{status}]")
942
+
943
+ low_skills = [s for s, c in skill_counts.items() if c < args.examples_per_skill]
944
+ if low_skills:
945
+ print(f"\nWarning: {len(low_skills)} skills below target count: {', '.join(low_skills)}")
946
+
947
+
948
+ if __name__ == "__main__":
949
+ main()
scripts/generate_pocket_benchmark.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate the Pocket Automator real-world benchmark suite.
3
+
4
+ Produces data/pocket_benchmark_prompts.json — 200 held-out prompts designed to
5
+ measure generalization with slang, typos, incomplete phrasing, and casual speech.
6
+ Prompts are filtered against train_intent.jsonl and eval_intent_prompts.json.
7
+
8
+ Usage:
9
+ python scripts/generate_pocket_benchmark.py
10
+ python scripts/generate_pocket_benchmark.py --validate-only
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
22
+ sys.path.insert(0, str(PROJECT_ROOT))
23
+
24
+ from src.pocket_benchmark import BENCHMARK_SKILLS, DOMAIN_BY_SKILL # noqa: E402
25
+
26
+ TRAIN_PATH = PROJECT_ROOT / "data" / "train_intent.jsonl"
27
+ EVAL_PATH = PROJECT_ROOT / "data" / "eval_intent_prompts.json"
28
+ OUTPUT_PATH = PROJECT_ROOT / "data" / "pocket_benchmark_prompts.json"
29
+ TARGET_COUNT = 200
30
+
31
+
32
+ def _case(
33
+ prompt: str,
34
+ skill: str,
35
+ parameters: dict,
36
+ *,
37
+ styles: list[str],
38
+ ) -> dict:
39
+ return {
40
+ "prompt": prompt,
41
+ "expected": {"skill": skill, "parameters": parameters},
42
+ "domain": DOMAIN_BY_SKILL[skill],
43
+ "styles": styles,
44
+ }
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Curated benchmark cases — phrasing intentionally unlike training templates
49
+ # ---------------------------------------------------------------------------
50
+
51
+ BENCHMARK_CASES: list[dict] = [
52
+ # --- Alarms (22) ---
53
+ _case("yo set an alrm for like 5:45 tmrw morning pls", "create_alarm", {"time": "5:45", "day": "tmrw morning"}, styles=["slang", "typo", "conversational"]),
54
+ _case("need to b up at 6ish on monday ngl", "create_alarm", {"time": "6ish", "day": "monday"}, styles=["slang", "incomplete", "conversational"]),
55
+ _case("6am alarm pls", "create_alarm", {"time": "6am"}, styles=["incomplete"]),
56
+ _case("wake me 7:20 wednesday thx", "create_alarm", {"time": "7:20", "day": "wednesday"}, styles=["incomplete", "conversational"]),
57
+ _case("uh can u do a 10:15 pm thing tonight", "create_alarm", {"time": "10:15 pm", "day": "tonight"}, styles=["conversational", "incomplete"]),
58
+ _case("tomorow 4:30 wake up plz", "create_alarm", {"time": "4:30", "day": "tomorow"}, styles=["typo", "incomplete"]),
59
+ _case("deadass need alarm sunday noon", "create_alarm", {"time": "noon", "day": "sunday"}, styles=["slang", "incomplete"]),
60
+ _case("ok wait actually make it 8:05 am next tuesday instead", "create_alarm", {"time": "8:05 am", "day": "next tuesday"}, styles=["conversational"]),
61
+ _case("4:50 alarm for sat pls", "create_alarm", {"time": "4:50", "day": "sat"}, styles=["slang", "incomplete"]),
62
+ _case("gonna crash early — buzz me at 9:30 pm today", "create_alarm", {"time": "9:30 pm", "day": "today"}, styles=["slang", "conversational"]),
63
+ _case("hey siri vibes but pocket automator: 5 am thurs", "create_alarm", {"time": "5 am", "day": "thurs"}, styles=["slang", "conversational"]),
64
+ _case("lmk when its 6:30 — jk just set alarm 6:30 friday", "create_alarm", {"time": "6:30", "day": "friday"}, styles=["slang", "conversational"]),
65
+ _case("alarm?? 7:45 am this weekend", "create_alarm", {"time": "7:45 am", "day": "this weekend"}, styles=["incomplete", "conversational"]),
66
+ _case("pls dont let me oversleep — 6:10 tomorrow", "create_alarm", {"time": "6:10", "day": "tomorrow"}, styles=["conversational"]),
67
+ _case("5:55am wakeup call monday pls", "create_alarm", {"time": "5:55am", "day": "monday"}, styles=["incomplete"]),
68
+ _case("set wakey wakey 8am tmr", "create_alarm", {"time": "8am", "day": "tmr"}, styles=["slang", "incomplete"]),
69
+ _case("lowkey need 11:11 pm alarm tonight", "create_alarm", {"time": "11:11 pm", "day": "tonight"}, styles=["slang"]),
70
+ _case("can i get a 7 am buzzer next monday", "create_alarm", {"time": "7 am", "day": "next monday"}, styles=["conversational"]),
71
+ _case("alarm at half past six tomorrow morning", "create_alarm", {"time": "half past six", "day": "tomorrow morning"}, styles=["conversational"]),
72
+ _case("6:00 alarm 4 wednesday", "create_alarm", {"time": "6:00", "day": "wednesday"}, styles=["typo", "incomplete"]),
73
+ _case("wake me up round 9 on sunday", "create_alarm", {"time": "9", "day": "sunday"}, styles=["slang", "incomplete"]),
74
+ _case("fr need 5:15 am alarm this tuesday", "create_alarm", {"time": "5:15 am", "day": "this tuesday"}, styles=["slang"]),
75
+
76
+ # --- WhatsApp (22) ---
77
+ _case("hit up zoe on whatsapp say im omw", "whatsapp_send_message", {"contact": "zoe", "message": "im omw"}, styles=["slang", "typo", "incomplete"]),
78
+ _case("wa msg marcus 'running 20 min late'", "whatsapp_send_message", {"contact": "marcus", "message": "running 20 min late"}, styles=["slang", "incomplete"]),
79
+ _case("tell roomie on watsapp dishes are done", "whatsapp_send_message", {"contact": "roomie", "message": "dishes are done"}, styles=["slang", "typo"]),
80
+ _case("whastapp elena — meeting got moved to 4", "whatsapp_send_message", {"contact": "elena", "message": "meeting got moved to 4"}, styles=["typo", "incomplete", "conversational"]),
81
+ _case("shoot kevin from work a text on whatsapp: heads up im wfh", "whatsapp_send_message", {"contact": "kevin from work", "message": "heads up im wfh"}, styles=["slang", "conversational"]),
82
+ _case("msg grandpa on whatsapp happy bday!!", "whatsapp_send_message", {"contact": "grandpa", "message": "happy bday!!"}, styles=["slang", "typo"]),
83
+ _case("yo ping sis saying im outside", "whatsapp_send_message", {"contact": "sis", "message": "im outside"}, styles=["slang", "incomplete"]),
84
+ _case("whatsap daniel 'got ur package'", "whatsapp_send_message", {"contact": "daniel", "message": "got ur package"}, styles=["typo", "incomplete"]),
85
+ _case("can u text aunt lisa on whatsapp that flight lands 6", "whatsapp_send_message", {"contact": "aunt lisa", "message": "flight lands 6"}, styles=["conversational", "incomplete"]),
86
+ _case("drop a whatsapp to coach mike — practice cancelled", "whatsapp_send_message", {"contact": "coach mike", "message": "practice cancelled"}, styles=["slang", "incomplete"]),
87
+ _case("whatsapp my landlord rent sent", "whatsapp_send_message", {"contact": "my landlord", "message": "rent sent"}, styles=["incomplete"]),
88
+ _case("lemme msg hannah real quick on whatsapp: u free later?", "whatsapp_send_message", {"contact": "hannah", "message": "u free later?"}, styles=["slang", "conversational"]),
89
+ _case("send whatsapp to benji thx for covering my shift", "whatsapp_send_message", {"contact": "benji", "message": "thx for covering my shift"}, styles=["slang", "incomplete"]),
90
+ _case("text whatsapp to carpool group running late again sry", "whatsapp_send_message", {"contact": "carpool group", "message": "running late again sry"}, styles=["slang", "incomplete"]),
91
+ _case("ok whatsapp olivia saying docs r uploaded", "whatsapp_send_message", {"contact": "olivia", "message": "docs r uploaded"}, styles=["slang", "typo", "conversational"]),
92
+ _case("wahtapp msg to tomas — keys under mat", "whatsapp_send_message", {"contact": "tomas", "message": "keys under mat"}, styles=["typo", "incomplete"]),
93
+ _case("just tell jules on whatsapp im grabbing boba", "whatsapp_send_message", {"contact": "jules", "message": "im grabbing boba"}, styles=["slang", "conversational"]),
94
+ _case("whatsapp neighbor hey can u sign for delivery", "whatsapp_send_message", {"contact": "neighbor", "message": "hey can u sign for delivery"}, styles=["incomplete", "conversational"]),
95
+ _case("msg whatsapp to dr patel appointment confirmed", "whatsapp_send_message", {"contact": "dr patel", "message": "appointment confirmed"}, styles=["incomplete"]),
96
+ _case("hit my brother on whatsapp w/ 'u still coming?'", "whatsapp_send_message", {"contact": "my brother", "message": "u still coming?"}, styles=["slang", "incomplete"]),
97
+ _case("whatsapp bestie ngl im stressed af today", "whatsapp_send_message", {"contact": "bestie", "message": "ngl im stressed af today"}, styles=["slang", "incomplete"]),
98
+ _case("send whatsapp msg to felix — slide deck is ready", "whatsapp_send_message", {"contact": "felix", "message": "slide deck is ready"}, styles=["incomplete"]),
99
+
100
+ # --- Spotify pause (22) ---
101
+ _case("spotfy stop rn", "spotify_pause", {}, styles=["typo", "slang", "incomplete"]),
102
+ _case("yo pause the music", "spotify_pause", {}, styles=["slang", "incomplete"]),
103
+ _case("mute spotify pls im on a call", "spotify_pause", {}, styles=["conversational"]),
104
+ _case("can u kill the spotify track", "spotify_pause", {}, styles=["slang", "conversational"]),
105
+ _case("hold up pause spotify", "spotify_pause", {}, styles=["slang", "incomplete"]),
106
+ _case("spoitfy pause rq", "spotify_pause", {}, styles=["typo", "slang", "incomplete"]),
107
+ _case("stop spotify playback thx", "spotify_pause", {}, styles=["conversational"]),
108
+ _case("shhh pause spotify", "spotify_pause", {}, styles=["incomplete", "conversational"]),
109
+ _case("spotify off for a sec", "spotify_pause", {}, styles=["slang", "incomplete"]),
110
+ _case("pause whats playing on spotify", "spotify_pause", {}, styles=["incomplete"]),
111
+ _case("ok stop spotify music", "spotify_pause", {}, styles=["conversational", "incomplete"]),
112
+ _case("spotify halt plz", "spotify_pause", {}, styles=["incomplete"]),
113
+ _case("silence spotify real quick", "spotify_pause", {}, styles=["conversational"]),
114
+ _case("pause spotify — landlord knocking", "spotify_pause", {}, styles=["conversational"]),
115
+ _case("spotify stop the banger lol", "spotify_pause", {}, styles=["slang", "conversational"]),
116
+ _case("cut spotify audio", "spotify_pause", {}, styles=["slang", "incomplete"]),
117
+ _case("spotify pause pls headphones dying", "spotify_pause", {}, styles=["conversational"]),
118
+ _case("uh pause spotify", "spotify_pause", {}, styles=["conversational", "incomplete"]),
119
+ _case("stop the spotify pls", "spotify_pause", {}, styles=["incomplete"]),
120
+ _case("spotify freeze for now pls", "spotify_pause", {}, styles=["incomplete", "conversational"]),
121
+ _case("pause my spotify session", "spotify_pause", {}, styles=["incomplete"]),
122
+ _case("spotify quiet for now", "spotify_pause", {}, styles=["incomplete", "conversational"]),
123
+
124
+ # --- Spotify play playlist (22) ---
125
+ _case("put on my midnight drives mix on spotify", "spotify_play_playlist", {"playlist": "midnight drives mix"}, styles=["conversational"]),
126
+ _case("spotify play sunday scaries", "spotify_play_playlist", {"playlist": "sunday scaries"}, styles=["incomplete"]),
127
+ _case("queue up deep focus vibes on spotify", "spotify_play_playlist", {"playlist": "deep focus vibes"}, styles=["slang"]),
128
+ _case("play my 2010s throwbacks spotify", "spotify_play_playlist", {"playlist": "2010s throwbacks"}, styles=["incomplete"]),
129
+ _case("spotfy playlist: rainy day jazz", "spotify_play_playlist", {"playlist": "rainy day jazz"}, styles=["typo", "incomplete"]),
130
+ _case("yo spin the gym beast mode playlist", "spotify_play_playlist", {"playlist": "gym beast mode"}, styles=["slang", "incomplete"]),
131
+ _case("spotify — my cooking playlist pls", "spotify_play_playlist", {"playlist": "cooking"}, styles=["incomplete", "conversational"]),
132
+ _case("start spotify playlist called roadtrip 2024", "spotify_play_playlist", {"playlist": "roadtrip 2024"}, styles=["incomplete"]),
133
+ _case("play hyperpop mix on spotify", "spotify_play_playlist", {"playlist": "hyperpop mix"}, styles=["incomplete"]),
134
+ _case("spotify my chillhop essentials thx", "spotify_play_playlist", {"playlist": "chillhop essentials"}, styles=["incomplete", "conversational"]),
135
+ _case("can u play the study grind playlist on spotify", "spotify_play_playlist", {"playlist": "study grind"}, styles=["conversational"]),
136
+ _case("spotify playlist sleep sounds go", "spotify_play_playlist", {"playlist": "sleep sounds"}, styles=["incomplete"]),
137
+ _case("throw on indie roadtrip on spotify", "spotify_play_playlist", {"playlist": "indie roadtrip"}, styles=["slang", "incomplete"]),
138
+ _case("spotify play my liked tracks playlist", "spotify_play_playlist", {"playlist": "liked tracks"}, styles=["incomplete"]),
139
+ _case("play spotify playlist bossa nova mornings", "spotify_play_playlist", {"playlist": "bossa nova mornings"}, styles=["incomplete"]),
140
+ _case("spotify — queue phonk workout", "spotify_play_playlist", {"playlist": "phonk workout"}, styles=["incomplete"]),
141
+ _case("put spotify on my cozy rainy day list", "spotify_play_playlist", {"playlist": "cozy rainy day"}, styles=["conversational", "incomplete"]),
142
+ _case("play the spotify playlist french cafe", "spotify_play_playlist", {"playlist": "french cafe"}, styles=["incomplete"]),
143
+ _case("spotify start my pregame playlist", "spotify_play_playlist", {"playlist": "pregame"}, styles=["slang", "incomplete"]),
144
+ _case("ok spotify play downtempo focus", "spotify_play_playlist", {"playlist": "downtempo focus"}, styles=["conversational", "incomplete"]),
145
+ _case("spotify my discover mix playlist pls", "spotify_play_playlist", {"playlist": "discover mix"}, styles=["incomplete"]),
146
+ _case("play playlist morning commute on spotify", "spotify_play_playlist", {"playlist": "morning commute"}, styles=["incomplete"]),
147
+
148
+ # --- Spotify search play (22) ---
149
+ _case("spotify find bad bunny and play", "spotify_search_play", {"query": "bad bunny"}, styles=["incomplete"]),
150
+ _case("search spotify for lofi hip hop then play", "spotify_search_play", {"query": "lofi hip hop"}, styles=["conversational"]),
151
+ _case("play some arctic monkeys on spotify", "spotify_search_play", {"query": "arctic monkeys"}, styles=["conversational", "incomplete"]),
152
+ _case("spotfy search billie eilish play it", "spotify_search_play", {"query": "billie eilish"}, styles=["typo", "incomplete"]),
153
+ _case("look up sabrina carpenter on spotify n play", "spotify_search_play", {"query": "sabrina carpenter"}, styles=["slang", "incomplete"]),
154
+ _case("spotify: search neo soul and hit play", "spotify_search_play", {"query": "neo soul"}, styles=["incomplete"]),
155
+ _case("find tyler the creator on spotify and play", "spotify_search_play", {"query": "tyler the creator"}, styles=["incomplete"]),
156
+ _case("spotify play after searching radiohead", "spotify_search_play", {"query": "radiohead"}, styles=["incomplete"]),
157
+ _case("yo spotify search dnb and play", "spotify_search_play", {"query": "dnb"}, styles=["slang", "incomplete"]),
158
+ _case("can u find frank ocean on spotify and play it", "spotify_search_play", {"query": "frank ocean"}, styles=["conversational"]),
159
+ _case("spotify search classical piano play", "spotify_search_play", {"query": "classical piano"}, styles=["incomplete"]),
160
+ _case("spotify search play some drake tracks", "spotify_search_play", {"query": "drake tracks"}, styles=["incomplete"]),
161
+ _case("spotify find latin reggaeton and play", "spotify_search_play", {"query": "latin reggaeton"}, styles=["incomplete"]),
162
+ _case("search spotify for aphex twin then play", "spotify_search_play", {"query": "aphex twin"}, styles=["incomplete"]),
163
+ _case("spotify play whatever u find for synthwave", "spotify_search_play", {"query": "synthwave"}, styles=["slang", "conversational"]),
164
+ _case("find mitski on spotify play pls", "spotify_search_play", {"query": "mitski"}, styles=["incomplete", "conversational"]),
165
+ _case("soptify search play khruangbin", "spotify_search_play", {"query": "khruangbin"}, styles=["typo", "incomplete"]),
166
+ _case("look up burna boy spotify and play", "spotify_search_play", {"query": "burna boy"}, styles=["incomplete"]),
167
+ _case("spotify search play some afrobeats", "spotify_search_play", {"query": "afrobeats"}, styles=["incomplete"]),
168
+ _case("find fred again on spotify n play it", "spotify_search_play", {"query": "fred again"}, styles=["slang", "incomplete"]),
169
+ _case("spotify search play 90s r&b", "spotify_search_play", {"query": "90s r&b"}, styles=["incomplete"]),
170
+ _case("search spotify play tame impala", "spotify_search_play", {"query": "tame impala"}, styles=["incomplete"]),
171
+
172
+ # --- Uber (22) ---
173
+ _case("ubber to jfk pls", "uber_request_ride", {"destination": "jfk"}, styles=["typo", "incomplete", "conversational"]),
174
+ _case("need a ride to 42 oak street on uber", "uber_request_ride", {"destination": "42 oak street"}, styles=["conversational"]),
175
+ _case("hop in an uber to the vet clinic", "uber_request_ride", {"destination": "the vet clinic"}, styles=["slang"]),
176
+ _case("ubr me to whole foods on 5th", "uber_request_ride", {"destination": "whole foods on 5th"}, styles=["typo", "slang", "incomplete"]),
177
+ _case("get uber to coworking space downtown", "uber_request_ride", {"destination": "coworking space downtown"}, styles=["incomplete"]),
178
+ _case("book ride to mom's house via uber", "uber_request_ride", {"destination": "mom's house"}, styles=["incomplete"]),
179
+ _case("uber — take me to pier 39", "uber_request_ride", {"destination": "pier 39"}, styles=["incomplete", "conversational"]),
180
+ _case("can u call uber to stanford campus", "uber_request_ride", {"destination": "stanford campus"}, styles=["conversational"]),
181
+ _case("ride to airport terminal 2 uber", "uber_request_ride", {"destination": "airport terminal 2"}, styles=["incomplete"]),
182
+ _case("uber to the sushi place on main", "uber_request_ride", {"destination": "the sushi place on main"}, styles=["incomplete", "conversational"]),
183
+ _case("need uber to ikea rn", "uber_request_ride", {"destination": "ikea"}, styles=["slang", "incomplete"]),
184
+ _case("request uber to brooklyn bridge park", "uber_request_ride", {"destination": "brooklyn bridge park"}, styles=["incomplete"]),
185
+ _case("uber drop me at la guardia", "uber_request_ride", {"destination": "la guardia"}, styles=["slang", "incomplete"]),
186
+ _case("get a cab on uber to dentist on maple ave", "uber_request_ride", {"destination": "dentist on maple ave"}, styles=["conversational"]),
187
+ _case("uber to friends apartment 8b", "uber_request_ride", {"destination": "friends apartment 8b"}, styles=["incomplete"]),
188
+ _case("yo uber to the concert venue", "uber_request_ride", {"destination": "the concert venue"}, styles=["slang", "incomplete"]),
189
+ _case("book uber ride to seattle tacoma airport", "uber_request_ride", {"destination": "seattle tacoma airport"}, styles=["incomplete"]),
190
+ _case("uber pls to union station", "uber_request_ride", {"destination": "union station"}, styles=["incomplete", "conversational"]),
191
+ _case("ride share to target on broadway uber", "uber_request_ride", {"destination": "target on broadway"}, styles=["incomplete"]),
192
+ _case("uber me over to the marriott hotel", "uber_request_ride", {"destination": "the marriott hotel"}, styles=["slang", "incomplete"]),
193
+ _case("need uber to physio appointment on elm", "uber_request_ride", {"destination": "physio appointment on elm"}, styles=["incomplete"]),
194
+ _case("call uber to the dog park", "uber_request_ride", {"destination": "the dog park"}, styles=["incomplete"]),
195
+
196
+ # --- Gmail (22) ---
197
+ _case("gmal email recruiter saying im interested in the role", "gmail_send_email", {"recipient": "recruiter", "message": "im interested in the role"}, styles=["typo", "incomplete"]),
198
+ _case("shoot an email via gmail to finance@acme.co — invoice overdue", "gmail_send_email", {"recipient": "finance@acme.co", "message": "invoice overdue"}, styles=["slang", "conversational"]),
199
+ _case("gmail landlord asking about lease renewal", "gmail_send_email", {"recipient": "landlord", "message": "asking about lease renewal"}, styles=["incomplete"]),
200
+ _case("email my manager on gmail: heads up sick day tomorrow", "gmail_send_email", {"recipient": "my manager", "message": "heads up sick day tomorrow"}, styles=["conversational"]),
201
+ _case("compose gmail to nora.park@design.io portfolio link attached", "gmail_send_email", {"recipient": "nora.park@design.io", "message": "portfolio link attached"}, styles=["incomplete"]),
202
+ _case("gmail msg to thesis advisor draft attached for review", "gmail_send_email", {"recipient": "thesis advisor", "message": "draft attached for review"}, styles=["incomplete"]),
203
+ _case("send gmail to carpool buddy running 10 late", "gmail_send_email", {"recipient": "carpool buddy", "message": "running 10 late"}, styles=["slang", "incomplete"]),
204
+ _case("write gmail to billing@saas.com cancel subscription pls", "gmail_send_email", {"recipient": "billing@saas.com", "message": "cancel subscription pls"}, styles=["conversational"]),
205
+ _case("gmail email mentor thx for the intro", "gmail_send_email", {"recipient": "mentor", "message": "thx for the intro"}, styles=["slang", "incomplete"]),
206
+ _case("ok gmail to ops-team@startup.dev deploy failed rollback done", "gmail_send_email", {"recipient": "ops-team@startup.dev", "message": "deploy failed rollback done"}, styles=["conversational", "incomplete"]),
207
+ _case("email thru gmail to dentist office reschedule appt", "gmail_send_email", {"recipient": "dentist office", "message": "reschedule appt"}, styles=["incomplete"]),
208
+ _case("gmail send to contractor quote approved lets proceed", "gmail_send_email", {"recipient": "contractor", "message": "quote approved lets proceed"}, styles=["incomplete"]),
209
+ _case("hey gmail my professor question about deadline extension", "gmail_send_email", {"recipient": "my professor", "message": "question about deadline extension"}, styles=["conversational", "incomplete"]),
210
+ _case("gmail to hr@corp.net pto request for next week", "gmail_send_email", {"recipient": "hr@corp.net", "message": "pto request for next week"}, styles=["incomplete"]),
211
+ _case("send email gmail to vendor payment sent today", "gmail_send_email", {"recipient": "vendor", "message": "payment sent today"}, styles=["incomplete"]),
212
+ _case("gmial client@studio.com mockups ready for feedback", "gmail_send_email", {"recipient": "client@studio.com", "message": "mockups ready for feedback"}, styles=["typo", "incomplete"]),
213
+ _case("compose email on gmail to roommate utilities split attached", "gmail_send_email", {"recipient": "roommate", "message": "utilities split attached"}, styles=["incomplete"]),
214
+ _case("gmail to travel agent pls confirm hotel booking", "gmail_send_email", {"recipient": "travel agent", "message": "pls confirm hotel booking"}, styles=["conversational"]),
215
+ _case("email gmail coach thanks for the session notes", "gmail_send_email", {"recipient": "coach", "message": "thanks for the session notes"}, styles=["incomplete"]),
216
+ _case("gmail msg to editor final chapter attached", "gmail_send_email", {"recipient": "editor", "message": "final chapter attached"}, styles=["incomplete"]),
217
+ _case("send gmail to internship lead following up on application", "gmail_send_email", {"recipient": "internship lead", "message": "following up on application"}, styles=["incomplete"]),
218
+ _case("gmail email to dad flight details attached", "gmail_send_email", {"recipient": "dad", "message": "flight details attached"}, styles=["incomplete"]),
219
+
220
+ # --- Calendar (24) ---
221
+ _case("put dentist on calender next thurs 3pm", "calendar_create_event", {"title": "dentist", "date": "next thurs", "time": "3pm"}, styles=["typo", "incomplete"]),
222
+ _case("schedule 1:1 w/ manager monday 11am", "calendar_create_event", {"title": "1:1 w/ manager", "date": "monday", "time": "11am"}, styles=["slang", "incomplete"]),
223
+ _case("add calendar block focus time tuesday 9-11", "calendar_create_event", {"title": "focus time", "date": "tuesday", "time": "9-11"}, styles=["incomplete"]),
224
+ _case("book therapy session friday 5 pm on calendar", "calendar_create_event", {"title": "therapy session", "date": "friday", "time": "5 pm"}, styles=["incomplete"]),
225
+ _case("cal event: team retro wednesday 4pm", "calendar_create_event", {"title": "team retro", "date": "wednesday", "time": "4pm"}, styles=["slang", "incomplete"]),
226
+ _case("put parent teacher conf on calendar oct 12 6pm", "calendar_create_event", {"title": "parent teacher conf", "date": "oct 12", "time": "6pm"}, styles=["incomplete"]),
227
+ _case("schedule gym class saturday 8 am calendar", "calendar_create_event", {"title": "gym class", "date": "saturday", "time": "8 am"}, styles=["incomplete"]),
228
+ _case("add calendar entry flight to nyc sunday 7:45 am", "calendar_create_event", {"title": "flight to nyc", "date": "sunday", "time": "7:45 am"}, styles=["incomplete"]),
229
+ _case("calendar — lunch w/ priya tomorrow noon", "calendar_create_event", {"title": "lunch w/ priya", "date": "tomorrow", "time": "noon"}, styles=["slang", "incomplete", "conversational"]),
230
+ _case("set cal event sprint planning next monday 10", "calendar_create_event", {"title": "sprint planning", "date": "next monday", "time": "10"}, styles=["slang", "incomplete"]),
231
+ _case("put car inspection on calendar tuesday 2:30 pm", "calendar_create_event", {"title": "car inspection", "date": "tuesday", "time": "2:30 pm"}, styles=["incomplete"]),
232
+ _case("scheduel interview prep wednesday 6 pm", "calendar_create_event", {"title": "interview prep", "date": "wednesday", "time": "6 pm"}, styles=["typo", "incomplete"]),
233
+ _case("add to calendar vet appt thursday 1 pm", "calendar_create_event", {"title": "vet appt", "date": "thursday", "time": "1 pm"}, styles=["incomplete"]),
234
+ _case("calendar event board presentation friday 9 am", "calendar_create_event", {"title": "board presentation", "date": "friday", "time": "9 am"}, styles=["incomplete"]),
235
+ _case("book calendar slot date night saturday 8 pm", "calendar_create_event", {"title": "date night", "date": "saturday", "time": "8 pm"}, styles=["incomplete"]),
236
+ _case("put standup notes review on cal monday 9:30", "calendar_create_event", {"title": "standup notes review", "date": "monday", "time": "9:30"}, styles=["slang", "incomplete"]),
237
+ _case("schedule movers arrival next saturday 10 am", "calendar_create_event", {"title": "movers arrival", "date": "next saturday", "time": "10 am"}, styles=["incomplete"]),
238
+ _case("add calendar tax appointment april 5 11 am", "calendar_create_event", {"title": "tax appointment", "date": "april 5", "time": "11 am"}, styles=["incomplete"]),
239
+ _case("calender block deep work thursday morning 8", "calendar_create_event", {"title": "deep work", "date": "thursday morning", "time": "8"}, styles=["typo", "incomplete"]),
240
+ _case("put wedding rehearsal on calendar june 20 5 pm", "calendar_create_event", {"title": "wedding rehearsal", "date": "june 20", "time": "5 pm"}, styles=["incomplete"]),
241
+ _case("schedule product demo next wednesday 3 pm", "calendar_create_event", {"title": "product demo", "date": "next wednesday", "time": "3 pm"}, styles=["incomplete"]),
242
+ _case("add calendar coffee chat friday 10:30 am", "calendar_create_event", {"title": "coffee chat", "date": "friday", "time": "10:30 am"}, styles=["incomplete"]),
243
+ _case("book calendar orthodontist monday 4:15 pm", "calendar_create_event", {"title": "orthodontist", "date": "monday", "time": "4:15 pm"}, styles=["incomplete"]),
244
+ _case("put hackathon submission deadline on calendar sunday midnight", "calendar_create_event", {"title": "hackathon submission deadline", "date": "sunday", "time": "midnight"}, styles=["incomplete"]),
245
+
246
+ # --- Slack (22) ---
247
+ _case("open slack #incidents pls", "slack_open_channel", {"channel": "incidents"}, styles=["incomplete", "conversational"]),
248
+ _case("jump to product-launch channel on slack", "slack_open_channel", {"channel": "product-launch"}, styles=["slang", "incomplete"]),
249
+ _case("slack go to team-watercooler", "slack_open_channel", {"channel": "team-watercooler"}, styles=["incomplete"]),
250
+ _case("pull up slack channel ios-bugs", "slack_open_channel", {"channel": "ios-bugs"}, styles=["slang", "incomplete"]),
251
+ _case("switch slack to growth-experiments", "slack_open_channel", {"channel": "growth-experiments"}, styles=["incomplete"]),
252
+ _case("show slack channel design-system", "slack_open_channel", {"channel": "design-system"}, styles=["incomplete"]),
253
+ _case("navigate slack to platform-oncall", "slack_open_channel", {"channel": "platform-oncall"}, styles=["incomplete"]),
254
+ _case("open the slack channel ml-research thx", "slack_open_channel", {"channel": "ml-research"}, styles=["conversational"]),
255
+ _case("slack channel customer-escalations open it", "slack_open_channel", {"channel": "customer-escalations"}, styles=["incomplete"]),
256
+ _case("take me to #backend-standup in slack", "slack_open_channel", {"channel": "backend-standup"}, styles=["incomplete"]),
257
+ _case("open slack for channel release-train", "slack_open_channel", {"channel": "release-train"}, styles=["incomplete"]),
258
+ _case("slack — show me hiring-pipeline channel", "slack_open_channel", {"channel": "hiring-pipeline"}, styles=["conversational", "incomplete"]),
259
+ _case("go slack channel infra-alerts", "slack_open_channel", {"channel": "infra-alerts"}, styles=["incomplete"]),
260
+ _case("open slak channel mobile-crash-reports", "slack_open_channel", {"channel": "mobile-crash-reports"}, styles=["typo", "incomplete"]),
261
+ _case("pull slack up on qa-automation channel", "slack_open_channel", {"channel": "qa-automation"}, styles=["incomplete"]),
262
+ _case("switch to slack channel docs-and-wiki", "slack_open_channel", {"channel": "docs-and-wiki"}, styles=["incomplete"]),
263
+ _case("open slack growth-metrics channel rq", "slack_open_channel", {"channel": "growth-metrics"}, styles=["slang", "incomplete"]),
264
+ _case("show me slack #sales-wins", "slack_open_channel", {"channel": "sales-wins"}, styles=["incomplete"]),
265
+ _case("navigate to slack channel partner-integrations", "slack_open_channel", {"channel": "partner-integrations"}, styles=["incomplete"]),
266
+ _case("open slack channel called observability", "slack_open_channel", {"channel": "observability"}, styles=["incomplete"]),
267
+ _case("slack open eng-hiring channel pls", "slack_open_channel", {"channel": "eng-hiring"}, styles=["incomplete", "conversational"]),
268
+ _case("jump slack to ai-playground channel", "slack_open_channel", {"channel": "ai-playground"}, styles=["slang", "incomplete"]),
269
+ ]
270
+
271
+
272
+ def normalize_prompt(text: str) -> str:
273
+ return " ".join(text.lower().strip().split())
274
+
275
+
276
+ def token_set(text: str) -> set[str]:
277
+ return set(re.findall(r"[a-z0-9@]+", text.lower()))
278
+
279
+
280
+ def jaccard_similarity(left: str, right: str) -> float:
281
+ left_tokens = token_set(left)
282
+ right_tokens = token_set(right)
283
+ if not left_tokens or not right_tokens:
284
+ return 0.0
285
+ return len(left_tokens & right_tokens) / len(left_tokens | right_tokens)
286
+
287
+
288
+ def load_existing_prompts() -> set[str]:
289
+ prompts: set[str] = set()
290
+
291
+ if TRAIN_PATH.exists():
292
+ with TRAIN_PATH.open(encoding="utf-8") as handle:
293
+ for line in handle:
294
+ record = json.loads(line)
295
+ for message in record["messages"]:
296
+ if message["role"] == "user":
297
+ prompts.add(normalize_prompt(message["content"]))
298
+
299
+ if EVAL_PATH.exists():
300
+ with EVAL_PATH.open(encoding="utf-8") as handle:
301
+ for case in json.load(handle):
302
+ prompts.add(normalize_prompt(case["prompt"]))
303
+
304
+ return prompts
305
+
306
+
307
+ def validate_cases(cases: list[dict], existing: set[str]) -> list[str]:
308
+ errors: list[str] = []
309
+ seen: set[str] = set()
310
+
311
+ if len(cases) != TARGET_COUNT:
312
+ errors.append(f"Expected {TARGET_COUNT} prompts, got {len(cases)}")
313
+
314
+ skill_counts: dict[str, int] = {}
315
+ style_counts: dict[str, int] = {}
316
+
317
+ for index, case in enumerate(cases, start=1):
318
+ prompt = case["prompt"]
319
+ normalized = normalize_prompt(prompt)
320
+ skill = case["expected"]["skill"]
321
+
322
+ if normalized in seen:
323
+ errors.append(f"Duplicate benchmark prompt: {prompt}")
324
+ seen.add(normalized)
325
+
326
+ if normalized in existing:
327
+ errors.append(f"Exact overlap with training/eval: {prompt}")
328
+
329
+ for existing_prompt in existing:
330
+ if jaccard_similarity(normalized, existing_prompt) >= 0.85:
331
+ errors.append(
332
+ f"High similarity ({jaccard_similarity(normalized, existing_prompt):.2f}) "
333
+ f"with existing prompt '{existing_prompt}': {prompt}"
334
+ )
335
+ break
336
+
337
+ skill_counts[skill] = skill_counts.get(skill, 0) + 1
338
+ for style in case.get("styles", []):
339
+ style_counts[style] = style_counts.get(style, 0) + 1
340
+
341
+ if skill not in BENCHMARK_SKILLS:
342
+ errors.append(f"Case {index}: unsupported skill {skill}")
343
+
344
+ for skill in BENCHMARK_SKILLS:
345
+ count = skill_counts.get(skill, 0)
346
+ if count == 0:
347
+ errors.append(f"Missing skill coverage: {skill}")
348
+
349
+ for style in ("slang", "typo", "incomplete", "conversational"):
350
+ count = style_counts.get(style, 0)
351
+ if count < 20:
352
+ errors.append(f"Insufficient '{style}' coverage: {count} (need >= 20)")
353
+
354
+ return errors
355
+
356
+
357
+ def assign_ids(cases: list[dict]) -> list[dict]:
358
+ output: list[dict] = []
359
+ for index, case in enumerate(cases, start=1):
360
+ output.append({"id": f"pa-{index:03d}", **case})
361
+ return output
362
+
363
+
364
+ def main() -> None:
365
+ parser = argparse.ArgumentParser(description="Generate Pocket Automator benchmark prompts.")
366
+ parser.add_argument(
367
+ "--validate-only",
368
+ action="store_true",
369
+ help="Validate existing benchmark file without rewriting it.",
370
+ )
371
+ args = parser.parse_args()
372
+
373
+ existing = load_existing_prompts()
374
+ cases = assign_ids(BENCHMARK_CASES)
375
+ errors = validate_cases(cases, existing)
376
+
377
+ if errors:
378
+ print("Validation failed:")
379
+ for error in errors:
380
+ print(f" - {error}")
381
+ sys.exit(1)
382
+
383
+ if args.validate_only:
384
+ print(f"Validation passed for {len(cases)} benchmark prompts.")
385
+ return
386
+
387
+ OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
388
+ with OUTPUT_PATH.open("w", encoding="utf-8") as handle:
389
+ json.dump(cases, handle, indent=2)
390
+ handle.write("\n")
391
+
392
+ skill_counts = {}
393
+ for case in cases:
394
+ skill = case["expected"]["skill"]
395
+ skill_counts[skill] = skill_counts.get(skill, 0) + 1
396
+
397
+ print(f"Wrote {len(cases)} benchmark prompts to {OUTPUT_PATH}")
398
+ print("Skill distribution:")
399
+ for skill in BENCHMARK_SKILLS:
400
+ print(f" {skill}: {skill_counts.get(skill, 0)}")
401
+
402
+
403
+ if __name__ == "__main__":
404
+ main()
src/classifier_prompt.py CHANGED
@@ -1,4 +1,4 @@
1
- """Shared system prompt for skill classification (training + inference)."""
2
 
3
  from __future__ import annotations
4
 
@@ -9,9 +9,26 @@ SYSTEM_PROMPT = (
9
  "to pick the correct skill."
10
  )
11
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  def build_classifier_messages(user_content: str) -> list[dict[str, str]]:
14
  return [
15
  {"role": "system", "content": SYSTEM_PROMPT},
16
  {"role": "user", "content": user_content},
17
  ]
 
 
 
 
 
 
 
 
1
+ """Shared system prompts for skill classification and intent extraction."""
2
 
3
  from __future__ import annotations
4
 
 
9
  "to pick the correct skill."
10
  )
11
 
12
+ INTENT_SYSTEM_PROMPT = (
13
+ "You extract structured Android automation intents from natural language. "
14
+ 'Reply with JSON only: {"skill": "<skill_name>", "parameters": {<extracted_fields>}}. '
15
+ "Pick exactly one skill. Extract all relevant parameters mentioned in the request "
16
+ "(contact names, messages, times, destinations, channel names, search queries, etc.). "
17
+ "Use an empty object for parameters when the skill needs none. "
18
+ "Use the app or action named in the request (contacts, Gmail, Slack, YouTube, etc.) "
19
+ "to pick the correct skill."
20
+ )
21
+
22
 
23
  def build_classifier_messages(user_content: str) -> list[dict[str, str]]:
24
  return [
25
  {"role": "system", "content": SYSTEM_PROMPT},
26
  {"role": "user", "content": user_content},
27
  ]
28
+
29
+
30
+ def build_intent_messages(user_content: str) -> list[dict[str, str]]:
31
+ return [
32
+ {"role": "system", "content": INTENT_SYSTEM_PROMPT},
33
+ {"role": "user", "content": user_content},
34
+ ]
src/evaluate_intent.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluate the fine-tuned intent-extraction model locally.
3
+
4
+ Run:
5
+ python -m src.evaluate_intent
6
+ python -m src.evaluate_intent --model-path ./trained_model/adapter
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+
15
+ import torch
16
+ from peft import PeftModel
17
+ from transformers import AutoModelForCausalLM, AutoTokenizer
18
+
19
+ from src.classifier_prompt import build_intent_messages
20
+ from src.paths import DATA_DIR, TRAINED_MODEL_DIR
21
+ from src.skill_utils import extract_intent, intent_matches
22
+
23
+ BASE_MODEL = "Qwen/Qwen2.5-3B-Instruct"
24
+ MAX_SEQ_LENGTH = 2048
25
+ MAX_NEW_TOKENS = 128
26
+ DEFAULT_MODEL_PATH = TRAINED_MODEL_DIR / "adapter"
27
+ FALLBACK_MODEL_PATH = TRAINED_MODEL_DIR / "merged"
28
+ EVAL_PROMPTS_PATH = DATA_DIR / "eval_intent_prompts.json"
29
+
30
+
31
+ def load_eval_prompts() -> list[dict]:
32
+ with EVAL_PROMPTS_PATH.open(encoding="utf-8") as handle:
33
+ return json.load(handle)
34
+
35
+
36
+ def pick_device() -> str:
37
+ if torch.cuda.is_available():
38
+ return "cuda"
39
+ if torch.backends.mps.is_available():
40
+ return "mps"
41
+ return "cpu"
42
+
43
+
44
+ def is_adapter_path(model_path: Path) -> bool:
45
+ return (model_path / "adapter_config.json").exists()
46
+
47
+
48
+ def is_complete_merged_model(model_path: Path) -> bool:
49
+ config_path = model_path / "config.json"
50
+ if not config_path.exists() or config_path.stat().st_size == 0:
51
+ return False
52
+
53
+ index_path = model_path / "model.safetensors.index.json"
54
+ if index_path.exists():
55
+ index = json.loads(index_path.read_text(encoding="utf-8"))
56
+ shard_names = set(index.get("weight_map", {}).values())
57
+ return all(
58
+ (model_path / shard).exists() and (model_path / shard).stat().st_size > 0
59
+ for shard in shard_names
60
+ )
61
+
62
+ single_shard = model_path / "model.safetensors"
63
+ return single_shard.exists() and single_shard.stat().st_size > 0
64
+
65
+
66
+ def resolve_model_path(path: str) -> Path:
67
+ model_path = Path(path)
68
+ if model_path.exists():
69
+ if is_adapter_path(model_path):
70
+ return model_path
71
+ if is_complete_merged_model(model_path):
72
+ return model_path
73
+ print(f"Warning: {model_path} looks incomplete; trying adapter fallback.")
74
+
75
+ adapter_path = DEFAULT_MODEL_PATH
76
+ if adapter_path.exists() and is_adapter_path(adapter_path):
77
+ print(f"Using LoRA adapter at {adapter_path}")
78
+ return adapter_path
79
+
80
+ merged_path = FALLBACK_MODEL_PATH
81
+ if merged_path.exists() and is_complete_merged_model(merged_path):
82
+ print(f"Using merged model at {merged_path}")
83
+ return merged_path
84
+
85
+ raise FileNotFoundError(
86
+ "No usable trained model found. Expected a complete merged model or "
87
+ f"LoRA adapter at {DEFAULT_MODEL_PATH}."
88
+ )
89
+
90
+
91
+ def load_model(model_path: Path, device: str):
92
+ dtype = torch.float16 if device in {"cuda", "mps"} else torch.float32
93
+
94
+ if is_adapter_path(model_path):
95
+ print(f"Loading base model: {BASE_MODEL}")
96
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
97
+ base_model = AutoModelForCausalLM.from_pretrained(
98
+ BASE_MODEL,
99
+ dtype=dtype,
100
+ low_cpu_mem_usage=True,
101
+ )
102
+ model = PeftModel.from_pretrained(base_model, str(model_path))
103
+ else:
104
+ print("Loading merged model weights")
105
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
106
+ model = AutoModelForCausalLM.from_pretrained(
107
+ model_path,
108
+ dtype=dtype,
109
+ low_cpu_mem_usage=True,
110
+ )
111
+
112
+ model.to(device)
113
+ model.eval()
114
+ return model, tokenizer
115
+
116
+
117
+ def generate_intent(model, tokenizer, prompt: str, device: str) -> str:
118
+ messages = build_intent_messages(prompt)
119
+ inputs = tokenizer.apply_chat_template(
120
+ messages,
121
+ tokenize=True,
122
+ add_generation_prompt=True,
123
+ return_tensors="pt",
124
+ ).to(device)
125
+
126
+ with torch.inference_mode():
127
+ outputs = model.generate(
128
+ input_ids=inputs,
129
+ max_new_tokens=MAX_NEW_TOKENS,
130
+ use_cache=True,
131
+ do_sample=False,
132
+ )
133
+
134
+ generated = outputs[0][inputs.shape[1] :]
135
+ return tokenizer.decode(generated, skip_special_tokens=True).strip()
136
+
137
+
138
+ def evaluate(model, tokenizer, test_prompts: list[dict], device: str) -> tuple[float, float]:
139
+ skill_correct = 0
140
+ full_correct = 0
141
+
142
+ for index, case in enumerate(test_prompts, start=1):
143
+ prompt = case["prompt"]
144
+ expected = case["expected"]
145
+ expected_skill = expected["skill"]
146
+ expected_params = expected.get("parameters", {})
147
+
148
+ raw_output = generate_intent(model, tokenizer, prompt, device)
149
+ predicted = extract_intent(raw_output)
150
+
151
+ skill_pass = predicted is not None and predicted.get("skill") == expected_skill
152
+ full_pass = intent_matches(predicted, expected_skill, expected_params)
153
+
154
+ skill_correct += int(skill_pass)
155
+ full_correct += int(full_pass)
156
+
157
+ print(f"--- [{index}/{len(test_prompts)}] ---")
158
+ print(f"Prompt: {prompt}")
159
+ print(f"Expected: {json.dumps(expected, separators=(',', ':'))}")
160
+ print(f"Predicted: {json.dumps(predicted, separators=(',', ':')) if predicted else raw_output}")
161
+ print(f"Skill: {'PASS' if skill_pass else 'FAIL'}")
162
+ print(f"Full: {'PASS' if full_pass else 'FAIL'}")
163
+ print()
164
+
165
+ skill_accuracy = skill_correct / len(test_prompts)
166
+ full_accuracy = full_correct / len(test_prompts)
167
+ print(f"Skill accuracy: {skill_correct}/{len(test_prompts)} ({skill_accuracy:.1%})")
168
+ print(f"Full intent accuracy: {full_correct}/{len(test_prompts)} ({full_accuracy:.1%})")
169
+ return skill_accuracy, full_accuracy
170
+
171
+
172
+ def main() -> None:
173
+ parser = argparse.ArgumentParser(description="Evaluate the intent extraction model.")
174
+ parser.add_argument(
175
+ "--model-path",
176
+ default=str(DEFAULT_MODEL_PATH),
177
+ help=f"Path to LoRA adapter or merged model (default: {DEFAULT_MODEL_PATH})",
178
+ )
179
+ args = parser.parse_args()
180
+
181
+ if not EVAL_PROMPTS_PATH.exists():
182
+ raise FileNotFoundError(
183
+ f"Eval prompts not found at {EVAL_PROMPTS_PATH}. "
184
+ "Run `python scripts/generate_intent_dataset.py` first."
185
+ )
186
+
187
+ test_prompts = load_eval_prompts()
188
+ device = pick_device()
189
+ model_path = resolve_model_path(args.model_path)
190
+ print(f"Device: {device}")
191
+ print(f"Loading model from {model_path.resolve()}")
192
+ model, tokenizer = load_model(model_path, device)
193
+ print(f"Running intent evaluation on {len(test_prompts)} prompts...\n")
194
+ evaluate(model, tokenizer, test_prompts, device)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()
src/evaluate_pocket_benchmark.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluate the Pocket Automator real-world benchmark locally.
3
+
4
+ Run:
5
+ python scripts/generate_pocket_benchmark.py
6
+ python -m src.evaluate_pocket_benchmark
7
+ python -m src.evaluate_pocket_benchmark --model-path ./trained_model/adapter
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import torch
17
+ from peft import PeftModel
18
+ from transformers import AutoModelForCausalLM, AutoTokenizer
19
+
20
+ from src.classifier_prompt import build_intent_messages
21
+ from src.paths import TRAINED_MODEL_DIR
22
+ from src.pocket_benchmark import (
23
+ BENCHMARK_PROMPTS_PATH,
24
+ load_benchmark_prompts,
25
+ record_result,
26
+ save_benchmark_outputs,
27
+ )
28
+ from src.skill_utils import extract_intent
29
+
30
+ BASE_MODEL = "Qwen/Qwen2.5-3B-Instruct"
31
+ MAX_SEQ_LENGTH = 2048
32
+ MAX_NEW_TOKENS = 128
33
+ DEFAULT_MODEL_PATH = TRAINED_MODEL_DIR / "adapter"
34
+ FALLBACK_MODEL_PATH = TRAINED_MODEL_DIR / "merged"
35
+
36
+
37
+ def pick_device() -> str:
38
+ if torch.cuda.is_available():
39
+ return "cuda"
40
+ if torch.backends.mps.is_available():
41
+ return "mps"
42
+ return "cpu"
43
+
44
+
45
+ def is_adapter_path(model_path: Path) -> bool:
46
+ return (model_path / "adapter_config.json").exists()
47
+
48
+
49
+ def is_complete_merged_model(model_path: Path) -> bool:
50
+ config_path = model_path / "config.json"
51
+ if not config_path.exists() or config_path.stat().st_size == 0:
52
+ return False
53
+
54
+ index_path = model_path / "model.safetensors.index.json"
55
+ if index_path.exists():
56
+ index = json.loads(index_path.read_text(encoding="utf-8"))
57
+ shard_names = set(index.get("weight_map", {}).values())
58
+ return all(
59
+ (model_path / shard).exists() and (model_path / shard).stat().st_size > 0
60
+ for shard in shard_names
61
+ )
62
+
63
+ single_shard = model_path / "model.safetensors"
64
+ return single_shard.exists() and single_shard.stat().st_size > 0
65
+
66
+
67
+ def resolve_model_path(path: str) -> Path:
68
+ model_path = Path(path)
69
+ if model_path.exists():
70
+ if is_adapter_path(model_path):
71
+ return model_path
72
+ if is_complete_merged_model(model_path):
73
+ return model_path
74
+ print(f"Warning: {model_path} looks incomplete; trying adapter fallback.")
75
+
76
+ adapter_path = DEFAULT_MODEL_PATH
77
+ if adapter_path.exists() and is_adapter_path(adapter_path):
78
+ print(f"Using LoRA adapter at {adapter_path}")
79
+ return adapter_path
80
+
81
+ merged_path = FALLBACK_MODEL_PATH
82
+ if merged_path.exists() and is_complete_merged_model(merged_path):
83
+ print(f"Using merged model at {merged_path}")
84
+ return merged_path
85
+
86
+ raise FileNotFoundError(
87
+ "No usable trained model found. Expected a complete merged model or "
88
+ f"LoRA adapter at {DEFAULT_MODEL_PATH}."
89
+ )
90
+
91
+
92
+ def load_model(model_path: Path, device: str):
93
+ dtype = torch.float16 if device in {"cuda", "mps"} else torch.float32
94
+
95
+ if is_adapter_path(model_path):
96
+ print(f"Loading base model: {BASE_MODEL}")
97
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
98
+ base_model = AutoModelForCausalLM.from_pretrained(
99
+ BASE_MODEL,
100
+ dtype=dtype,
101
+ low_cpu_mem_usage=True,
102
+ )
103
+ model = PeftModel.from_pretrained(base_model, str(model_path))
104
+ else:
105
+ print("Loading merged model weights")
106
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
107
+ model = AutoModelForCausalLM.from_pretrained(
108
+ model_path,
109
+ dtype=dtype,
110
+ low_cpu_mem_usage=True,
111
+ )
112
+
113
+ model.to(device)
114
+ model.eval()
115
+ return model, tokenizer
116
+
117
+
118
+ def generate_intent(model, tokenizer, prompt: str, device: str) -> str:
119
+ messages = build_intent_messages(prompt)
120
+ inputs = tokenizer.apply_chat_template(
121
+ messages,
122
+ tokenize=True,
123
+ add_generation_prompt=True,
124
+ return_tensors="pt",
125
+ ).to(device)
126
+
127
+ with torch.inference_mode():
128
+ outputs = model.generate(
129
+ input_ids=inputs,
130
+ max_new_tokens=MAX_NEW_TOKENS,
131
+ use_cache=True,
132
+ do_sample=False,
133
+ )
134
+
135
+ generated = outputs[0][inputs.shape[1] :]
136
+ return tokenizer.decode(generated, skip_special_tokens=True).strip()
137
+
138
+
139
+ def evaluate(model, tokenizer, cases: list[dict], device: str) -> list[dict]:
140
+ results: list[dict] = []
141
+
142
+ for index, case in enumerate(cases, start=1):
143
+ prompt = case["prompt"]
144
+ raw_output = generate_intent(model, tokenizer, prompt, device)
145
+ predicted = extract_intent(raw_output)
146
+ result = record_result(case, raw_output, predicted)
147
+ results.append(result)
148
+
149
+ print(f"--- [{index}/{len(cases)}] {case.get('id', 'n/a')} ---")
150
+ print(f"Prompt: {prompt}")
151
+ print(f"Expected: {json.dumps(case['expected'], separators=(',', ':'))}")
152
+ print(
153
+ f"Predicted: {json.dumps(predicted, separators=(',', ':')) if predicted else raw_output}"
154
+ )
155
+ print(f"Skill: {'PASS' if result['skill_correct'] else 'FAIL'}")
156
+ print(f"Params: {'PASS' if result['parameter_correct'] else 'FAIL'}")
157
+ print(f"Exact JSON:{'PASS' if result['exact_json_match'] else 'FAIL'}")
158
+ print()
159
+
160
+ return results
161
+
162
+
163
+ def main() -> None:
164
+ parser = argparse.ArgumentParser(description="Evaluate Pocket Automator benchmark.")
165
+ parser.add_argument(
166
+ "--model-path",
167
+ default=str(DEFAULT_MODEL_PATH),
168
+ help=f"Path to LoRA adapter or merged model (default: {DEFAULT_MODEL_PATH})",
169
+ )
170
+ parser.add_argument(
171
+ "--benchmark-path",
172
+ default=str(BENCHMARK_PROMPTS_PATH),
173
+ help=f"Path to benchmark prompts JSON (default: {BENCHMARK_PROMPTS_PATH})",
174
+ )
175
+ args = parser.parse_args()
176
+
177
+ benchmark_path = Path(args.benchmark_path)
178
+ if not benchmark_path.exists():
179
+ raise FileNotFoundError(
180
+ f"Benchmark prompts not found at {benchmark_path}. "
181
+ "Run `python scripts/generate_pocket_benchmark.py` first."
182
+ )
183
+
184
+ cases = load_benchmark_prompts(benchmark_path)
185
+ device = pick_device()
186
+ model_path = resolve_model_path(args.model_path)
187
+ print(f"Device: {device}")
188
+ print(f"Loading model from {model_path.resolve()}")
189
+ model, tokenizer = load_model(model_path, device)
190
+ print(f"Running Pocket Automator benchmark on {len(cases)} prompts...\n")
191
+
192
+ results = evaluate(model, tokenizer, cases, device)
193
+ metrics, report = save_benchmark_outputs(results)
194
+
195
+ print("--- Pocket Automator Benchmark Summary ---")
196
+ print(report)
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
src/pocket_benchmark.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pocket Automator benchmark utilities — metrics, confusion matrix, failure reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections import defaultdict
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ try:
11
+ from src.skill_utils import (
12
+ normalize_param,
13
+ normalize_skill,
14
+ parameters_match,
15
+ )
16
+ except ImportError:
17
+ from skill_utils import (
18
+ normalize_param,
19
+ normalize_skill,
20
+ parameters_match,
21
+ )
22
+
23
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
24
+ DATA_DIR = PROJECT_ROOT / "data"
25
+
26
+ BENCHMARK_PROMPTS_PATH = DATA_DIR / "pocket_benchmark_prompts.json"
27
+ BENCHMARK_RESULTS_PATH = DATA_DIR / "pocket_benchmark_results.json"
28
+ BENCHMARK_REPORT_PATH = DATA_DIR / "pocket_benchmark_report.txt"
29
+
30
+ BENCHMARK_SKILLS: tuple[str, ...] = (
31
+ "create_alarm",
32
+ "whatsapp_send_message",
33
+ "spotify_pause",
34
+ "spotify_play_playlist",
35
+ "spotify_search_play",
36
+ "uber_request_ride",
37
+ "gmail_send_email",
38
+ "calendar_create_event",
39
+ "slack_open_channel",
40
+ )
41
+
42
+ DOMAIN_BY_SKILL: dict[str, str] = {
43
+ "create_alarm": "alarms",
44
+ "whatsapp_send_message": "whatsapp",
45
+ "spotify_pause": "spotify",
46
+ "spotify_play_playlist": "spotify",
47
+ "spotify_search_play": "spotify",
48
+ "uber_request_ride": "uber",
49
+ "gmail_send_email": "gmail",
50
+ "calendar_create_event": "calendar",
51
+ "slack_open_channel": "slack",
52
+ }
53
+
54
+
55
+ def load_benchmark_prompts(path: Path | None = None) -> list[dict[str, Any]]:
56
+ benchmark_path = path or BENCHMARK_PROMPTS_PATH
57
+ with benchmark_path.open(encoding="utf-8") as handle:
58
+ return json.load(handle)
59
+
60
+
61
+ def canonical_intent(skill: str, parameters: dict | None = None) -> str:
62
+ params = parameters or {}
63
+ normalized = {key: normalize_param(str(value)) for key, value in sorted(params.items())}
64
+ payload = {"skill": skill, "parameters": normalized}
65
+ return json.dumps(payload, separators=(",", ":"), sort_keys=True)
66
+
67
+
68
+ def exact_intent_match(
69
+ predicted: dict | None,
70
+ expected_skill: str,
71
+ expected_parameters: dict | None = None,
72
+ ) -> bool:
73
+ if not predicted:
74
+ return False
75
+
76
+ expected_params = expected_parameters or {}
77
+ predicted_skill = normalize_skill(predicted.get("skill")) or predicted.get("skill")
78
+ if predicted_skill != expected_skill:
79
+ return False
80
+
81
+ predicted_params = predicted.get("parameters") or {}
82
+ if set(predicted_params.keys()) != set(expected_params.keys()):
83
+ return False
84
+
85
+ return parameters_match(predicted_params, expected_params)
86
+
87
+
88
+ def evaluate_prediction(
89
+ predicted: dict | None,
90
+ expected: dict[str, Any],
91
+ ) -> dict[str, bool]:
92
+ expected_skill = expected["skill"]
93
+ expected_params = expected.get("parameters", {})
94
+
95
+ skill_correct = predicted is not None and (
96
+ normalize_skill(predicted.get("skill")) or predicted.get("skill")
97
+ ) == expected_skill
98
+ parameter_correct = skill_correct and parameters_match(
99
+ predicted.get("parameters", {}),
100
+ expected_params,
101
+ )
102
+ exact_json = exact_intent_match(predicted, expected_skill, expected_params)
103
+
104
+ return {
105
+ "skill_correct": skill_correct,
106
+ "parameter_correct": parameter_correct,
107
+ "exact_json_match": exact_json,
108
+ }
109
+
110
+
111
+ def build_confusion_matrix(
112
+ results: list[dict[str, Any]],
113
+ skills: tuple[str, ...] | None = None,
114
+ ) -> dict[str, dict[str, int]]:
115
+ skill_list = list(skills or BENCHMARK_SKILLS)
116
+ matrix: dict[str, dict[str, int]] = {
117
+ expected: {predicted: 0 for predicted in skill_list + ["<invalid>"]}
118
+ for expected in skill_list
119
+ }
120
+
121
+ for result in results:
122
+ expected_skill = result["expected"]["skill"]
123
+ predicted = result.get("predicted")
124
+ if predicted and predicted.get("skill"):
125
+ predicted_skill = normalize_skill(predicted.get("skill")) or predicted.get("skill")
126
+ if predicted_skill not in matrix[expected_skill]:
127
+ predicted_skill = "<invalid>"
128
+ else:
129
+ predicted_skill = "<invalid>"
130
+
131
+ matrix[expected_skill][predicted_skill] += 1
132
+
133
+ return matrix
134
+
135
+
136
+ def summarize_metrics(results: list[dict[str, Any]]) -> dict[str, Any]:
137
+ total = len(results)
138
+ if total == 0:
139
+ return {
140
+ "total": 0,
141
+ "skill_accuracy": 0.0,
142
+ "parameter_accuracy": 0.0,
143
+ "exact_json_match_rate": 0.0,
144
+ "skill_correct": 0,
145
+ "parameter_correct": 0,
146
+ "exact_json_match": 0,
147
+ }
148
+
149
+ skill_correct = sum(1 for result in results if result["skill_correct"])
150
+ parameter_correct = sum(1 for result in results if result["parameter_correct"])
151
+ exact_json_match = sum(1 for result in results if result["exact_json_match"])
152
+
153
+ return {
154
+ "total": total,
155
+ "skill_correct": skill_correct,
156
+ "parameter_correct": parameter_correct,
157
+ "exact_json_match": exact_json_match,
158
+ "skill_accuracy": skill_correct / total,
159
+ "parameter_accuracy": parameter_correct / total,
160
+ "exact_json_match_rate": exact_json_match / total,
161
+ }
162
+
163
+
164
+ def build_failure_report(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
165
+ failures: list[dict[str, Any]] = []
166
+
167
+ for result in results:
168
+ if result["exact_json_match"]:
169
+ continue
170
+
171
+ failure_types: list[str] = []
172
+ if not result["skill_correct"]:
173
+ failure_types.append("skill")
174
+ elif not result["parameter_correct"]:
175
+ failure_types.append("parameters")
176
+ else:
177
+ failure_types.append("json_format")
178
+
179
+ failures.append(
180
+ {
181
+ "id": result.get("id"),
182
+ "prompt": result["prompt"],
183
+ "domain": result.get("domain"),
184
+ "styles": result.get("styles", []),
185
+ "expected": result["expected"],
186
+ "predicted": result.get("predicted"),
187
+ "raw_output": result.get("raw_output"),
188
+ "failure_types": failure_types,
189
+ }
190
+ )
191
+
192
+ return failures
193
+
194
+
195
+ def format_confusion_matrix(matrix: dict[str, dict[str, int]]) -> str:
196
+ predicted_labels = list(next(iter(matrix.values())).keys())
197
+ corner_label = "expected \\ predicted"
198
+ header = f"{corner_label:<28}" + "".join(f"{label:>18}" for label in predicted_labels)
199
+ lines = [header, "-" * len(header)]
200
+
201
+ for expected, counts in matrix.items():
202
+ row = f"{expected:<28}" + "".join(f"{counts[label]:>18}" for label in predicted_labels)
203
+ lines.append(row)
204
+
205
+ return "\n".join(lines)
206
+
207
+
208
+ def format_benchmark_report(
209
+ metrics: dict[str, Any],
210
+ matrix: dict[str, dict[str, int]],
211
+ failures: list[dict[str, Any]],
212
+ *,
213
+ title: str = "Pocket Automator Benchmark Report",
214
+ ) -> str:
215
+ lines = [
216
+ title,
217
+ "=" * len(title),
218
+ "",
219
+ "Metrics",
220
+ "-------",
221
+ f"Total prompts: {metrics['total']}",
222
+ f"Skill accuracy: {metrics['skill_correct']}/{metrics['total']} ({metrics['skill_accuracy']:.1%})",
223
+ f"Parameter accuracy: {metrics['parameter_correct']}/{metrics['total']} ({metrics['parameter_accuracy']:.1%})",
224
+ f"Exact JSON match: {metrics['exact_json_match']}/{metrics['total']} ({metrics['exact_json_match_rate']:.1%})",
225
+ "",
226
+ "Confusion Matrix (rows=expected, cols=predicted)",
227
+ "------------------------------------------------",
228
+ format_confusion_matrix(matrix),
229
+ "",
230
+ f"Failure Report ({len(failures)} failures)",
231
+ "--------------",
232
+ ]
233
+
234
+ if not failures:
235
+ lines.append("No failures — perfect score.")
236
+ else:
237
+ for index, failure in enumerate(failures, start=1):
238
+ lines.extend(
239
+ [
240
+ "",
241
+ f"[{index}] {failure.get('id', 'n/a')} — {', '.join(failure['failure_types'])}",
242
+ f"Prompt: {failure['prompt']}",
243
+ f"Domain: {failure.get('domain', 'n/a')}",
244
+ f"Styles: {', '.join(failure.get('styles') or [])}",
245
+ f"Expected: {json.dumps(failure['expected'], separators=(',', ':'))}",
246
+ f"Predicted: {json.dumps(failure.get('predicted'), separators=(',', ':')) if failure.get('predicted') else failure.get('raw_output', 'null')}",
247
+ ]
248
+ )
249
+
250
+ style_failures: dict[str, int] = defaultdict(int)
251
+ domain_failures: dict[str, int] = defaultdict(int)
252
+ for failure in failures:
253
+ for style in failure.get("styles") or []:
254
+ style_failures[style] += 1
255
+ domain = failure.get("domain")
256
+ if domain:
257
+ domain_failures[domain] += 1
258
+
259
+ if style_failures or domain_failures:
260
+ lines.extend(["", "Failure breakdown", "-----------------"])
261
+ if style_failures:
262
+ lines.append("By style:")
263
+ for style, count in sorted(style_failures.items()):
264
+ lines.append(f" {style}: {count}")
265
+ if domain_failures:
266
+ lines.append("By domain:")
267
+ for domain, count in sorted(domain_failures.items()):
268
+ lines.append(f" {domain}: {count}")
269
+
270
+ return "\n".join(lines) + "\n"
271
+
272
+
273
+ def record_result(
274
+ case: dict[str, Any],
275
+ raw_output: str,
276
+ predicted: dict | None,
277
+ ) -> dict[str, Any]:
278
+ expected = case["expected"]
279
+ checks = evaluate_prediction(predicted, expected)
280
+ return {
281
+ "id": case.get("id"),
282
+ "prompt": case["prompt"],
283
+ "domain": case.get("domain"),
284
+ "styles": case.get("styles", []),
285
+ "expected": expected,
286
+ "predicted": predicted,
287
+ "raw_output": raw_output,
288
+ **checks,
289
+ }
290
+
291
+
292
+ def save_benchmark_outputs(
293
+ results: list[dict[str, Any]],
294
+ *,
295
+ results_path: Path | None = None,
296
+ report_path: Path | None = None,
297
+ ) -> tuple[dict[str, Any], str]:
298
+ metrics = summarize_metrics(results)
299
+ matrix = build_confusion_matrix(results)
300
+ failures = build_failure_report(results)
301
+ report = format_benchmark_report(metrics, matrix, failures)
302
+
303
+ resolved_results_path = results_path or BENCHMARK_RESULTS_PATH
304
+ resolved_report_path = report_path or BENCHMARK_REPORT_PATH
305
+
306
+ payload = {
307
+ "metrics": metrics,
308
+ "confusion_matrix": matrix,
309
+ "failures": failures,
310
+ "results": results,
311
+ }
312
+
313
+ resolved_results_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
314
+ resolved_report_path.write_text(report, encoding="utf-8")
315
+
316
+ return metrics, report
src/skill_utils.py CHANGED
@@ -41,6 +41,7 @@ SKILL_ALIASES: dict[str, str] = {
41
  "slack_channel": "slack_open_channel",
42
  "search_contacts": "contacts_search",
43
  "contact_search": "contacts_search",
 
44
  }
45
 
46
 
 
41
  "slack_channel": "slack_open_channel",
42
  "search_contacts": "contacts_search",
43
  "contact_search": "contacts_search",
44
+ "spotify_create_playlist": "spotify_play_playlist",
45
  }
46
 
47