Typed questions
Declare questions as a struct literal. The compiler derives a result struct with one typed field per question.
Shape of a call
client.ask(state, questions, options) takes:
state: what the questions are about. Pass a string, or any valuestd.jsoncan serialize.questions: a struct literal whose fields are built withtai.noul,tai.choiceortai.score. Each field name becomes the question id sent to the API, so ids are unique by construction.options: per-call settings, covered below.
It returns a tai.Result with the typed answers, token usage and model(), the versioned id that answered. The result owns no heap memory.
tai.noul: yes/no
The answer is an f64 from 0 (no) to 1 (yes). Use .criteria(yes, no) to say what each end of the scale means.
const r = try client.ask("I was charged twice for my March invoice.", .{
.is_billing = tai.noul("Is this about billing?"),
.is_urgent = tai.noul("Does this convey urgency?")
.criteria("Explicitly time-sensitive", "No urgency expressed"),
}, .{});
const route_to_billing = r.answers.is_billing > 0.5;
tai.choice: pick one option
Pass a struct literal that maps each option to its description. Use null for an option that needs no description. tai generates an enum from the field names, so the switch below must handle every option.
const r = try client.ask("The app crashes every time I upload a PDF.", .{
.department = tai.choice("Which team should handle this?", .{
.billing = "Payments, invoicing, refunds",
.technical = "Bugs, outages, integrations",
.sales = null,
}),
}, .{});
const department = r.answers.department;
const assignee = switch (department.choice) {
.billing => "finance-oncall",
.technical => "eng-oncall",
.sales => "account-manager",
};
const unsure = department.confidence < 0.5 or department.probability(.sales) > 0.2;
The answer is a tai.ChoiceResult of that enum:
| Field / method | Type | Meaning |
|---|---|---|
choice | enum | The highest-probability option |
probabilities | std.EnumArray(E, f64) | Every option’s probability |
confidence | f64 | How certain the model is (Confidence) |
probability(option) | f64 | Shorthand for probabilities.get(option) |
If you already have an enum, pass the type itself. Each option is sent with a null description.
const Severity = enum { low, medium, high, critical };
const r = try client.ask("Production database is down for all customers.", .{
.severity = tai.choice("How severe is this incident?", Severity),
}, .{});
const severity: Severity = r.answers.severity.choice;
A choice must have between 1 and 255 options, and the compiler enforces that.
tai.score: rate against levels
Pass a tuple of ordered level descriptions (2 to 10 levels), and level() returns an index:
const by_index = try client.ask(message, .{
.frustration = tai.score("How frustrated is the customer?", .{ "Calm", "Frustrated", "Very angry" }),
}, .{});
const level: usize = by_index.answers.frustration.level(); // 0, 1 or 2
Or name the levels with a struct literal, and level() returns an enum:
const by_name = try client.ask(message, .{
.frustration = tai.score("How frustrated is the customer?", .{
.calm = "Calm",
.frustrated = "Frustrated",
.angry = "Very angry",
}),
}, .{});
const frustration = by_name.answers.frustration;
const offer_refund = frustration.level() == .angry or frustration.probability(.angry) > 0.4;
The answer is a tai.ScoreResult:
| Field / method | Type | Meaning |
|---|---|---|
score | f64 | Probability-weighted level; can land between levels (e.g. 1.05) |
probabilities | [N]f64 | Probability of each level, by index |
confidence | f64 | How certain the model is |
level() | usize or enum | The single most probable level |
probability(level) | f64 | Probability of one level |
An enum type works here too: its tag names are sent as the level descriptions.
State and instructions
state, instructions and descriptions accept any value that std.json can serialize, so you can pass records straight in. Refer to fields by name in backticks:
const ticket = .{
.customer = .{ .plan = "pro", .months_subscribed = 18 },
.message = "I'm cancelling unless the CSV export bug is fixed this week.",
};
const r = try client.ask(ticket, .{
.churn_risk = tai.noul("Is `customer` likely to cancel based on `message`?"),
.same_issue = tai.noul(.{
.question = "Is `message` about the same issue as `previous`?",
.previous = "CSV export produces an empty file.",
}),
}, .{});
A plain string is sent as text. Everything else is serialized as JSON. Values can be comptime or runtime; only the shape (option names, number of levels) has to be known at compile time.
Reusing a set of questions
A question set is an ordinary value, so you can declare it once and ask it about many states. tai.Result(@TypeOf(set)) names the result type, and tai.Answers(@TypeOf(set)) names just the answers.
const triage = .{
.is_urgent = tai.noul("Does this convey urgency?"),
.department = tai.choice("Which team?", .{ .billing = null, .technical = null }),
};
const tickets = [_][]const u8{
"I was double charged, please refund me today.",
"Minor typo on the pricing page.",
};
var results: [tickets.len]tai.Result(@TypeOf(triage)) = undefined;
for (tickets, &results) |body, *result| {
result.* = try client.ask(body, triage, .{});
}
Per-call options
The last argument to ask is a tai.Client.CallOptions:
var diag: tai.Client.Diagnostics = .{};
defer diag.deinit();
const r = try client.ask("Refund request for order #1234.", .{
.refund = tai.noul("Is this a refund request?"),
}, .{
.model = "jev-latest", // or pin a versioned id such as the one r.model() reports
.retry = .none, // or a custom tai.RetryPolicy
.diagnostics = &diag,
});
Pin a versioned model if you’ve tuned confidence thresholds against it: the jev-latest alias moves when a new release ships.