mirror of
https://github.com/hanzo-js/kai.git
synced 2026-10-30 15:04:57 +00:00
130 lines
6.1 KiB
TypeScript
130 lines
6.1 KiB
TypeScript
// Route a request to a model or a tool from fixed sets. Kai selects; the host authorizes; nothing runs here.
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { gunzipSync } from "node:zlib";
|
|
import { Kai, choice, type Questions } from "@hanzo/kai";
|
|
|
|
const PIN = "https://raw.githubusercontent.com/hanzoai/benchmarks/039fbfe5fa5dcb1483407594d829fa822d78974a/decision/results";
|
|
const N = 50;
|
|
const SEED = 7;
|
|
|
|
// Models: the harness's domain question, then a table from domain to model. Unsure -> the default model.
|
|
const DOMAINS = {
|
|
code: "software engineering, programming, refactoring, architecture, debugging",
|
|
math_or_logic: "mathematics, logic puzzles, proofs, complex calculation",
|
|
writing: "creative writing, essays, emails, blog posts, copywriting",
|
|
factual_lookup: "facts, definitions, trivia, history",
|
|
data_analysis: "statistics, SQL, data manipulation, metrics",
|
|
chitchat: "casual conversation, greetings, small talk",
|
|
} as const;
|
|
type Domain = keyof typeof DOMAINS;
|
|
const domain = choice("What domain does `request` belong to?", DOMAINS);
|
|
const MODEL: Record<Domain, string> = {
|
|
code: "zen6", math_or_logic: "zen6", data_analysis: "zen5",
|
|
writing: "zen5", factual_lookup: "zen5-mini", chitchat: "zen5-mini",
|
|
};
|
|
const DEFAULT = "zen5";
|
|
const MODEL_FLOOR = 0.6;
|
|
|
|
// Tools: a closed set with a way out. Unsure -> ask the user; no tool is picked.
|
|
const TOOLS = {
|
|
search_docs: "look up how the product works in its documentation",
|
|
query_metrics: "read numbers such as signups, revenue or usage from the analytics store",
|
|
open_ticket: "file a bug report or a request for the engineering team",
|
|
send_email: "send an email on the user's behalf",
|
|
none: "answer from the conversation alone; no tool is needed",
|
|
} as const;
|
|
type Tool = keyof typeof TOOLS;
|
|
const tool = choice("Which tool should handle `request`?", TOOLS);
|
|
const TOOL_FLOOR = 0.7;
|
|
// The host's policy, not the model's: which tools run on a pick and which wait for the user.
|
|
const GRANT: Record<Tool, "run" | "confirm"> = {
|
|
search_docs: "run", query_metrics: "run", open_ticket: "run", send_email: "confirm", none: "run",
|
|
};
|
|
|
|
// Requests and the tool their author expects; "unclear" expects a question back.
|
|
const REQUESTS: [string, Tool | "unclear"][] = [
|
|
["How do I rotate my API key?", "search_docs"],
|
|
["How many new signups did we get last week?", "query_metrics"],
|
|
["The CSV export button crashes the page every time. Can someone fix it?", "open_ticket"],
|
|
["Email the March invoice to [email protected].", "send_email"],
|
|
["What is 15% of 240?", "none"],
|
|
["Can you take care of the thing from yesterday?", "unclear"],
|
|
];
|
|
|
|
async function load(file: string): Promise<any> {
|
|
const dir = join(tmpdir(), "kai-cookbook");
|
|
const path = join(dir, file.replace("/", "-"));
|
|
if (!existsSync(path)) {
|
|
const res = await fetch(`${PIN}/${file}`);
|
|
if (!res.ok) throw new Error(`${file}: HTTP ${res.status}`);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(path, Buffer.from(await res.arrayBuffer()));
|
|
}
|
|
return JSON.parse(gunzipSync(readFileSync(path)).toString());
|
|
}
|
|
|
|
// n indices of 0..size-1, shuffled by a seeded generator (mulberry32).
|
|
function sample(size: number, n: number, seed: number): number[] {
|
|
let s = seed >>> 0;
|
|
const rand = () => {
|
|
s = (s + 0x6d2b79f5) >>> 0;
|
|
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
const idx = [...Array(size).keys()];
|
|
for (let i = size - 1; i > 0; i--) {
|
|
const j = Math.floor(rand() * (i + 1));
|
|
[idx[i], idx[j]] = [idx[j], idx[i]];
|
|
}
|
|
return idx.slice(0, n);
|
|
}
|
|
|
|
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 } });
|
|
|
|
const suite: [{ request: string }, Questions, Record<string, { idx: number }>][] = (await load("states.json.gz"))["app.model_routing_domain"];
|
|
const asked = suite[0][1].domain;
|
|
if (asked.instructions !== domain.instructions || JSON.stringify(asked.criteria) !== JSON.stringify(DOMAINS))
|
|
throw new Error("app.model_routing_domain asks a different question than this script");
|
|
const labels = Object.keys(DOMAINS) as Domain[];
|
|
let right = 0, routed = 0, routedRight = 0;
|
|
const use = new Map<string, number>();
|
|
for (const i of sample(suite.length, N, SEED)) {
|
|
const [state, , gold] = suite[i];
|
|
const a = (await kai.decide({ state, questions: { domain } })).answers.domain;
|
|
const sure = a.confidence >= MODEL_FLOOR;
|
|
const model = sure ? MODEL[a.choice] : DEFAULT;
|
|
use.set(model, (use.get(model) ?? 0) + 1);
|
|
right += +(a.choice === labels[gold.domain.idx]);
|
|
routed += +sure;
|
|
routedRight += +(sure && a.choice === labels[gold.domain.idx]);
|
|
}
|
|
console.log(`app.model_routing_domain, ${N} requests, seed ${SEED}: domain accuracy ${(right / N).toFixed(2)} (${right}/${N})`);
|
|
console.log(` confidence >= ${MODEL_FLOOR}: ${routed} routed by domain (${routedRight} right), ${N - routed} to ${DEFAULT}`);
|
|
console.log(` models used: ${[...use].map(([m, n]) => `${m} ${n}`).join(", ")}`);
|
|
|
|
console.log("\nrequest".padEnd(74) + "pick".padEnd(15) + "conf outcome");
|
|
let ok = 0, stopped = 0;
|
|
for (const [request, want] of REQUESTS) {
|
|
const a = (await kai.decide({ state: { request }, questions: { tool } })).answers.tool;
|
|
const sure = a.confidence >= TOOL_FLOOR;
|
|
const outcome =
|
|
!sure ? "ask the user what they need; no tool"
|
|
: GRANT[a.choice] === "confirm" ? `hold ${a.choice} until the user confirms`
|
|
: a.choice === "none" ? "answer directly"
|
|
: `hand ${a.choice} to the host's executor`;
|
|
const right = want === "unclear" ? !sure : a.choice === want;
|
|
ok += +right;
|
|
stopped += +(!right && !sure);
|
|
console.log(request.padEnd(73) + a.choice.padEnd(15) + a.confidence.toFixed(2).padEnd(6) + outcome + (right ? "" : ` (want: ${want})`));
|
|
}
|
|
const missed = REQUESTS.length - ok;
|
|
console.log(`${ok} of ${REQUESTS.length} as expected; ${stopped} of ${missed} misses stopped at the floor, ${missed - stopped} passed it`);
|
|
}
|
|
|
|
main();
|