mirror of
https://github.com/hanzo-js/kai.git
synced 2026-10-30 15:04:57 +00:00
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
// A home assistant: a spoken request becomes one command from a closed set; risky commands wait for a spoken yes.
|
|
import { Kai, choice } from "@hanzo/kai";
|
|
|
|
// Every command the house accepts, as device.action, and two ways out.
|
|
const COMMANDS = {
|
|
"lights.on": "turn the lights on",
|
|
"lights.off": "turn the lights off",
|
|
"lights.up": "make the lights brighter",
|
|
"lights.down": "dim the lights",
|
|
"lights.status": "say whether the lights are on",
|
|
"thermostat.up": "make it warmer",
|
|
"thermostat.down": "make it cooler",
|
|
"thermostat.status": "say what the heating is set to",
|
|
"front_door.lock": "lock the front door",
|
|
"front_door.unlock": "unlock the front door",
|
|
"front_door.status": "say whether the front door is locked",
|
|
"garage.open": "open the garage door",
|
|
"garage.close": "close the garage door",
|
|
"garage.status": "say whether the garage door is open",
|
|
"oven.on": "turn the oven on or preheat it",
|
|
"oven.off": "turn the oven off",
|
|
"oven.status": "say whether the oven is on",
|
|
"alarm.arm": "turn the security alarm on",
|
|
"alarm.disarm": "turn the security alarm off",
|
|
"alarm.status": "say whether the alarm is on",
|
|
"speaker.on": "play music",
|
|
"speaker.off": "stop the music",
|
|
"speaker.up": "turn the music up",
|
|
"speaker.down": "turn the music down",
|
|
several: "two or more of these commands in one request",
|
|
chat: "small talk, or something the house cannot do",
|
|
} as const;
|
|
type Command = keyof typeof COMMANDS;
|
|
|
|
const questions = { command: choice("Which command does `heard` ask for?", COMMANDS) };
|
|
|
|
// The house's rules, in code: which commands wait for a spoken yes, and how sure Kai must be.
|
|
const RISKY = new Set<Command>(["front_door.unlock", "garage.open", "alarm.disarm", "oven.on"]);
|
|
const SURE = 0.5;
|
|
const RISKY_SURE = 0.9;
|
|
|
|
function plan(a: { choice: Command; confidence: number }): string {
|
|
if (a.confidence < SURE) return "ask again";
|
|
if (a.choice === "several") return "one at a time";
|
|
if (a.choice === "chat") return "chat";
|
|
if (a.choice.endsWith(".status")) return `answer ${a.choice}`;
|
|
if (RISKY.has(a.choice)) return a.confidence >= RISKY_SURE ? `confirm ${a.choice}` : "ask again";
|
|
return `do ${a.choice}`;
|
|
}
|
|
|
|
// Spoken requests as a recognizer hands them over, and the plan their author expects.
|
|
const HEARD: [string, string][] = [
|
|
["turn off the lights in the living room", "do lights.off"],
|
|
["unlock the front door", "confirm front_door.unlock"],
|
|
["is the garage open", "answer garage.status"],
|
|
["make it a bit warmer in here", "do thermostat.up"],
|
|
["open the garage and turn the alarm off", "one at a time"],
|
|
["preheat the oven to four hundred", "confirm oven.on"],
|
|
["arm the alarm we're heading out", "do alarm.arm"],
|
|
["tell me a joke", "chat"],
|
|
["uh can you do the the door thing", "ask again"],
|
|
];
|
|
|
|
async function main() {
|
|
// Each 429 waits out its Retry-After, up to 10 times: a run of calls can meet the rate limit.
|
|
const kai = new Kai({ retry: { maxRetries: 10 } });
|
|
let match = 0;
|
|
console.log("heard".padEnd(42) + "command".padEnd(19) + "p conf plan");
|
|
for (const [heard, want] of HEARD) {
|
|
const a = (await kai.decide({ state: { heard }, questions })).answers.command;
|
|
const got = plan(a);
|
|
match += +(got === want);
|
|
console.log(heard.padEnd(42) + a.choice.padEnd(19) + a.probabilities[a.choice].toFixed(2).padEnd(6) +
|
|
a.confidence.toFixed(2).padEnd(6) + got + (got === want ? "" : ` (want: ${want})`));
|
|
}
|
|
console.log(`${match} of ${HEARD.length} plans as expected. Nothing ran: "do" and "confirm" go to the house controller.`);
|
|
}
|
|
|
|
main();
|