Zig SDK
The Zig SDK gives your Zig code the ten functions as methods on one client; each call gives back a result whose answer is a plain Zig value, such as true or false from decide, and returns error.Unsure when the answer is not sure.
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
// answer returns error.Unsure when not sure, and the message's error // when it failed. const is_refund = try (try client.decide( init.arena.allocator(), "Does the customer request a refund?", .{ .text = "Please refund my order." }, .{}, &diag, )).answer(&diag); try out.print("{}\n", .{is_refund});}Output
true
true means yes: the message asks for a refund.
Add it to your build
Section titled “Add it to your build”The SDK needs Zig 0.17.0. In a project that has a build.zig.zon, such as one zig init made, fetch the archive for your machine:
release=https://github.com/botassembly/thinkthen/releases/\download/v0.2.0zig fetch --save=thinkthen \ "$release/thinkthen-zig-0.2.0-linux-x86_64.tar.gz"Archives are published for linux-x86_64, linux-arm64 and macos-arm64; change the end of the address to match your machine. zig fetch --save adds the dependency to build.zig.zon.
This build.zig builds src/main.zig, adds the thinkthen module, installs the native library beside your program with installNative, and adds a run step:
build.zig
const std = @import("std");const thinkthen = @import("thinkthen");
pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const dependency = b.dependency("thinkthen", .{ .target = target, .optimize = optimize, }); const executable = b.addExecutable(.{ .name = "example", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); executable.root_module.addImport("thinkthen", dependency.module("thinkthen")); const installed = b.addInstallArtifact(executable, .{}); // Installs the package's native library beside the executable. try thinkthen.installNative(b, dependency, installed); b.getInstallStep().dependOn(&installed.step); const run = b.addRunArtifact(executable); run.step.dependOn(&installed.step); b.step("run", "Run the example").dependOn(&run.step);}Save the program as src/main.zig and run zig build run. The client uses the key from the Quickstart.
Handle not sure and failures
Section titled “Handle not sure and failures”Set a low bar and a high bar with tt.Threshold.band. Between them, decide is not sure. Each result's outcome is a union of .answered, .unsure and .failed, so a switch handles all three:
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const refund = try client.decide( init.arena.allocator(), "Does the customer request a refund?", .{ .text = "I was charged twice for one invoice." }, .{ .threshold = tt.Threshold.band(0.2, 0.8) }, &diag, ); switch (refund.outcome) { .answered => |answered| try out.print( "{}\n", .{answered.answer}, ), .unsure => |unsure| try out.print( "Not sure ({d}): ask a person.\n", .{unsure.probability}, ), .failed => |failure| try out.print( "{s}: {s}\n", .{ failure.name(), failure.message }, ), }}Output
Not sure (0.7): ask a person.
A double charge might be a refund request, or might not, so the answer is not sure. Send it to a person.
try result.answer(&diag) is the short way to the answer. It returns error.Unsure when not sure, with the probability in diag.probability. When ThinkThen can't judge the message, it returns that message's error, such as error.Record or error.Backend, and diag.code and diag.message() say why. Neither one can pass for false. A problem with the client or the call itself, such as a wrong argument, comes back as an error from tt.Client.init or the call, such as error.Usage, with diag filled the same way. Troubleshooting lists each error code.
The ten functions
Section titled “The ten functions”Each function reads the same support-inbox messages as the function pages. A one-message call, such as client.decide, takes an arena and gives back one result. A many-message call, such as client.filterAll, takes an allocator and an input such as .{ .texts = &messages }; read it with while (try call.next(&diag)) |item| and free it with deinit.
decide
Section titled “decide”Yes or no about one message
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
// answer returns error.Unsure when not sure, and the message's error // when it failed. const is_refund = try (try client.decide( init.arena.allocator(), "Does the customer request a refund?", .{ .text = "Please refund my order." }, .{}, &diag, )).answer(&diag); try out.print("{}\n", .{is_refund});}Output
true
Yes: the message asks for a refund. DecideOptions takes .threshold (one bar, or a low and a high bar with tt.Threshold.band), .yes_means and .no_means. client.decideAll takes many messages and gives one result each, in order. decide shows every option.
choose
Section titled “choose”The one choice that fits
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const topic = try (try client.choose( init.arena.allocator(), "What is the message about?", tt.choices(&.{ "billing", "shipping", "other" }), .{ .text = "Please refund my order." }, .{}, &diag, )).answer(&diag); try out.print("{s}\n", .{topic});}Output
billing
A refund is about billing. tt.choices builds the list at compile time. ChooseOptions takes .descriptions, from each choice to what it means, and .threshold, which can make choose not sure. choose shows every option.
filter
Section titled “filter”Only the messages that answer yes
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const messages = [_][]const u8{ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.", }; // next yields only the kept messages. // A failed message returns its error in its place. var refunds = try client.filterAll( init.gpa, "Does the customer request a refund?", .{ .texts = &messages }, .{}, &diag, ); defer refunds.deinit(); while (try refunds.next(&diag)) |refund| try out.print("{s}\n", .{refund.text});}Output
Please refund my order.
I was charged twice for one invoice.
The parcel question is left out. next gives back only the kept messages, and skips the ones it is not sure about. A message that fails returns its error in its place, with its position in diag.position, and reading can go on after it. nextRow gives every message with its outcome instead. filter shows every option.
Every label that fits
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
// Every topic that fits, which may be none. const topics = try (try client.tag( init.arena.allocator(), "Which topics appear?", tt.choices(&.{ "billing", "shipping" }), .{ .text = "I was charged twice and my parcel is late again." }, .{}, &diag, )).answer(&diag); for (topics) |topic| try out.print("{s}\n", .{topic});}Output
billing
shipping
The message is about both billing and shipping. The answer is a slice of labels, empty when no label fits. tag shows every option.
A number from 0 to 1 on your scale
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
// From 0 (routine) to 1 (urgent). const urgency = try (try client.score( init.arena.allocator(), "How urgent is the request?", tt.choices(&.{ "routine", "urgent" }), .{ .text = "Please refund my order." }, .{}, &diag, )).answer(&diag); try out.print("{d}\n", .{urgency});}Output
0.05
The levels run lowest first, so a plain refund request scores near routine. The answer is an f64. score shows every option.
Every message, highest first
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const messages = [_][]const u8{ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.", }; // Every message, most urgent first. The answer is its rank, starting at 1. var by_urgency = try client.rankAll( init.gpa, "How urgent is the request?", tt.choices(&.{ "routine", "urgent" }), .{ .texts = &messages }, .{}, &diag, ); defer by_urgency.deinit(); while (try by_urgency.next(&diag)) |ranked| { const rank = try ranked.answer(&diag); try out.print("{d} {s}\n", .{ rank, messages[@intCast(ranked.position - 1)], }); }}Output
1 I was charged twice for one invoice.
2 Where is my parcel?
3 Please refund my order.
The double charge is the most urgent. Each result's answer is its rank, a u64 starting at 1, and position is its place in your input, counting from 1. RankOptions takes .top to keep only the first few. rank shows every option.
The line that answers the question
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
// null when no line answers. const action = try (try client.find( init.arena.allocator(), "Which line states the requested action?", .{ .text = "Morgan works for Example Group.\nPlease refund my order." }, .{}, &diag, )).answer(&diag); if (action) |line| try out.print("{s}\n", .{line.text}) else try out.print("no line answers\n", .{});}Output
Please refund my order.
The second line states the action. The answer is a ?tt.Span with text, start and end, or null when no line answers. Offsets count UTF-8 bytes. find shows every option.
recognize
Section titled “recognize”Each name in the text and its kind
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const names = try (try client.recognize( init.arena.allocator(), tt.choices(&.{ "person", "organization" }), .{ .text = "Morgan works for Example Group." }, .{}, &diag, )).answer(&diag); for (names) |name| try out.print("{s}: {s}\n", .{ name.kind, name.text });}Output
person: Morgan
organization: Example Group
The answer is a slice of tt.Name, each with text, kind, start, end and probability. recognize shows every option.
relate
Section titled “relate”Names linked by the relations you allow
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const works_for = [_]tt.RelationRule{.{ .name = "works_for", .source = "person", .target = "organization", }}; const relations = try (try client.relate( init.arena.allocator(), &works_for, .{ .text = "Morgan works for Example Group.\nPlease refund my order." }, .{}, &diag, )).answer(&diag); for (relations) |relation| try out.print("{s} --{s}--> {s}\n", .{ relation.source.text, relation.relation, relation.target.text, });}Output
Morgan --works_for--> Example Group
A tt.RelationRule names the kind of name at each end and the relation between them. Each tt.Relation in the answer has a source, a target and the relation. relate shows every option.
annotate
Section titled “annotate”Several questions for each message
const std = @import("std");const tt = @import("thinkthen");
pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buffer); const out = &stdout.interface; defer out.flush() catch {}; var diag: tt.Diagnostic = .{}; var client = try tt.Client.init(.{}, &diag); defer client.deinit();
const form: tt.AnnotationQuestions = .{ .entries = &.{ .{ .name = "refund", .question = .{ .decide = .{ .question = "Does the customer request a refund?", } } }, .{ .name = "topic", .question = .{ .choose = .{ .question = "What is the message about?", .choices = tt.choices(&.{ "billing", "shipping", "other" }), } } }, .{ .name = "urgency", .question = .{ .score = .{ .question = "How urgent is the request?", .levels = tt.choices(&.{ "routine", "urgent" }), } } }, } }; const messages = [_][]const u8{ "Please refund my order.", "Where is my parcel?", "I was charged twice for one invoice.", }; var triage = try client.annotateAll( init.gpa, form, .{ .texts = &messages }, .{}, &diag, ); defer triage.deinit(); while (try triage.next(&diag)) |filled| { // Each answer can be not sure or failed on its own, and answer says so. const answers = try filled.answer(&diag); const is_refund = try answers.get(.decide, "refund").?.answer(&diag); const topic = try answers.get(.choose, "topic").?.answer(&diag); const urgency = try answers.get(.score, "urgency").?.answer(&diag); try out.print("{} {s} {d}\n", .{ is_refund, topic, urgency }); }}Output
true billing 0.05
false shipping 0.14
true billing 0.45
answers.get(.decide, "refund") reads one answer by its function and name, and gives null for a name you never added. Each answer can be not sure or fail on its own, so its answer returns error.Unsure or its error, as decide does. For one message, call client.annotate. annotate shows every option.
The full API
Section titled “The full API”Each options type takes .field, a pointer such as "/text", to judge one field of a JSON message passed as .{ .json = ... }. tt.Client.init takes tt.Settings to set the batch size and retries. A many-message call's cancel stops it from any thread. The Zig API reference comments every public name.