Runtime questions
When the set of questions is only known at runtime, build a tai.Request and read a tai.Response.
When to use this
The typed DSL needs question names and choice options at compile time. For questions built at runtime (labels loaded from a config file, one question per retrieved passage, speculative fan-out), use Client.systemOne instead. It maps one-to-one onto the HTTP API.
Here, labels and document come from somewhere at runtime, and arena is an allocator that outlives the call:
const questions = try arena.alloc(tai.NamedQuestion, labels.len);
for (labels, questions) |label, *q| q.* = .{
.name = label,
.question = .{ .noul = .{
.instructions = .{ .text = try std.fmt.allocPrint(arena, "Is this about {s}?", .{label}) },
} },
};
var result = try client.systemOne(.{
.state = .{ .text = document },
.questions = questions,
}, .{});
defer result.deinit();
var matches: std.ArrayList([]const u8) = .empty;
for (result.value.answers) |named| switch (named.answer) {
.noul => |n| if (n.noul > 0.5) try matches.append(arena, named.name),
else => {},
};
Requests
A tai.Request holds the state, a slice of tai.NamedQuestion, and an optional model that overrides the client’s default. Each tai.Question is a union of .noul, .choice or .score, mirroring the three API question types:
const options = try arena.alloc(tai.Choice.Option, team_names.len);
for (team_names, options) |name, *option| option.* = .{ .name = name };
const request: tai.Request = .{
.state = .{ .text = "Stripe webhooks are failing with 500s." },
.questions = &.{
.{ .name = "team", .question = .{ .choice = .{
.instructions = .{ .text = "Which team owns this?" },
.options = options,
} } },
.{ .name = "impact", .question = .{ .score = .{
.instructions = .{ .text = "How much customer impact?" },
.levels = &.{ .{ .text = "None" }, .{ .text = "Some" }, .{ .text = "Severe" } },
} } },
},
};
try tai.validate(request); // systemOne also validates before sending
tai.Content is anything the API accepts as string | object | array:
| Variant | Sent as |
|---|---|
.text | A JSON string |
.json | A std.json.Value, as is |
.raw | Pre-serialized JSON, written verbatim |
Content.fromValue(allocator, value) serializes any Zig value into a .raw.
Before anything is sent, systemOne checks the documented limits: at least one question, unique names, 1 to 255 choice options with unique names, and 2 to 10 score levels.
Responses
systemOne returns a std.json.Parsed(tai.Response); call deinit to free it. response.answers lists every answer in API order as a tai.Answer union. The noul, choice and score getters look one up by name, returning null if it’s missing or of another type:
const response = result.value;
const team = response.choice("team").?; // null if missing or not a choice
const owner = team.choice; // one of team_names
const p_payments = team.probabilityOf("payments") orelse 0;
const impact = response.score("impact").?;
const worst = impact.levels[impact.mostLikelyLevel().?].description; // "None", "Some" or "Severe"
ChoiceAnswerhaschoice,probabilities(option and probability pairs) andconfidence, plusprobabilityOf(name), andas(Enum)to convert the chosen option into one of your enums.ScoreAnswerhasscore,levels(description and probability, by index) andconfidence, plusmostLikelyLevel().- Answer types this SDK version doesn’t know about are kept as
.unknown: std.json.Value, so a new API feature won’t break your parsing.