File size: 6,230 Bytes
51da1ee
cbe9a94
51da1ee
cbe9a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4c4ce7
cbe9a94
 
 
 
 
 
 
51da1ee
 
 
cbe9a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acda592
cbe9a94
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Synthetic command-intent dataset for the router specialist (seeded, no download).

    python -m oev.convert_commands
"""
import json
import os
import random

INTENTS = ["music", "search", "note", "calculator", "app", "unknown"]

INSTRUCTIONS_INTENT = ("What does the user want the computer to do? "
                       "unknown covers partial commands, bare triggers and chat.")

APPS = ["spotify", "chrome", "notepad", "calculator", "vs code", "discord",
        "steam", "explorer", "settings", "terminal"]

SONGS = ["blinding lights", "bohemian rhapsody", "take five", "gimme shelter",
         "fly me to the moon", "dream on", "landslide", "redbone",
         "electric feel", "ain't no mountain high enough"]

ARTISTS = ["the weeknd", "queen", "dave brubeck", "nina simone", "daft punk",
           "fleetwood mac", "tame impala", "miles davis", "chaka khan"]

QUERIES = ["rust async programming", "latest nvidia drivers", "weather tomorrow",
           "best noise cancelling headphones", "python deque vs list",
           "how to fold a fitted sheet", "distance to the moon",
           "cheap flights to lisbon", "markdown table syntax", "is pluto a planet"]

TASKS = ["finish the benchmark writeup", "call the dentist", "water the plants",
         "back up the laptop", "renew the domain", "ship the release notes"]

ITEMS = ["oat milk", "printer paper", "a new mouse", "birthday candles", "coffee beans"]
DAYS = ["saturday", "monday", "friday", "tomorrow"]
PEOPLE = ["sam", "priya", "marco", "lena"]

CHAT = ["hello", "hey there", "good morning", "how are you", "thanks a lot",
        "what time is it", "tell me a joke", "never mind", "actually stop"]

MUSIC_T = ["play {song} on spotify", "open spotify and play {song}",
           "put on {artist}", "play some {artist} songs", "queue up {song}",
           "play {song} by {artist}", "start {artist} radio"]
SEARCH_T = ["search the web for {query}", "search youtube for {query}",
            "google {query}", "look up {query}", "find {query} on the web"]
NOTE_T = ["write in my notes that I need to {task}", "take a note: {task}",
          "remind me to {task}", "note that I have to {task}"]
CALC_T = ["calculate {a} times {b}", "what is {a} plus {b}", "what is {a} minus {b}",
          "calculate {a} divided by {b}", "{a} times {b}"]
APP_T = ["open {app}", "launch {app}", "start {app}", "switch to {app}",
         "open up {app}"]


def _fill(templates, rng, **pools):
    t = rng.choice(templates)
    return t.format(**{k: rng.choice(v) for k, v in pools.items()})


def _full_command(rng):
    kind = rng.choice(["music", "search", "note", "calculator", "app"])
    if kind == "music":
        return _fill(MUSIC_T, rng, song=SONGS, artist=ARTISTS), "music"
    if kind == "search":
        return _fill(SEARCH_T, rng, query=QUERIES), "search"
    if kind == "note":
        task = rng.choice(TASKS) + (" " + rng.choice(["today", "this week", "by friday"])
                                     if rng.random() < 0.4 else "")
        return rng.choice(NOTE_T).format(task=task), "note"
    if kind == "calculator":
        a, b = rng.randint(2, 999), rng.randint(2, 99)
        return _fill(CALC_T, rng, a=[a], b=[b]), "calculator"
    return _fill(APP_T, rng, app=APPS), "app"


def _partial(full, rng):
    words = full.split()
    cut = rng.randint(1, max(1, len(words) - 2))
    return " ".join(words[:cut])


def _case(i, state, intent, complete, prefix):
    return {
        "id": f"{prefix}-{i:06d}",
        "domain": "commands",
        "state": state,
        "questions": [
            {"name": "intent", "type": "choice", "instructions": INSTRUCTIONS_INTENT,
             "options": INTENTS, "answer": intent},
            {"name": "complete", "type": "noul", "options": ["no", "yes"],
             "instructions": "Is this a finished command, or is the user still speaking?",
             "answer": "yes" if complete else "no"},
        ],
    }


def generate(n, seed, prefix):
    # balanced: equal intent quotas; the full/partial/trigger/chat mix inside
    # each class teaches the finished-vs-still-speaking split. repeats are
    # re-randomized rather than dropped so no class is starved by dedupe.
    rng = random.Random(seed)
    # one slot per case, cycling intent classes so every class gets n/len(INTENTS)
    slots = [INTENTS[i % len(INTENTS)] for i in range(n)]
    rng.shuffle(slots)
    out = []
    for state_slot in slots:
        r = rng.random()
        if state_slot == "unknown":
            # unknown = partials, bare triggers, chat (never full commands)
            if r < 0.55:
                full, _ = _full_command(rng)
                state = _partial(full, rng)
            elif r < 0.8:
                state = rng.choice(["open", "play", "search", "write", "calculate",
                                    "can you", "hey", "um", "launch"])
            else:
                state = rng.choice(CHAT)
            out.append(_case(len(out), state, "unknown", rng.random() < 0.3, prefix))
            continue
        # a real intent: 60% full command, 25% partial, 15% bare trigger of that intent
        full, _ = _full_command(rng)
        # force the template family to match the slot
        for _ in range(50):
            full, kind = _full_command(rng)
            if kind == state_slot:
                break
        if r < 0.6:
            out.append(_case(len(out), full, state_slot, True, prefix))
        elif r < 0.85:
            out.append(_case(len(out), _partial(full, rng), state_slot, False, prefix))
        else:
            trigger = full.split()[0]
            out.append(_case(len(out), trigger, state_slot, False, prefix))
    return out


def main(out_dir="data/commands"):
    os.makedirs(out_dir, exist_ok=True)
    sizes = {"train": 8000, "valid": 1000, "test": 1000}
    seeds = {"train": 11, "valid": 22, "test": 33}
    for split, n in sizes.items():
        path = os.path.join(out_dir, f"{split}.jsonl")
        with open(path, "w", encoding="utf-8") as f:
            f.writelines(json.dumps(c) + "\n" for c in generate(n, seeds[split], split[:2]))
        print(f"wrote {n} cases to {path} (intents: {', '.join(INTENTS)})")


if __name__ == "__main__":
    main()