Skip to content

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.

src/main.zig
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.

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:

Terminal window
release=https://github.com/botassembly/thinkthen/releases/\
download/v0.2.0
zig 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
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.

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:

src/main.zig
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.

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.

Yes or no about one message
src/main.zig
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.

The one choice that fits
src/main.zig
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.

Only the messages that answer yes
src/main.zig
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
src/main.zig
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
src/main.zig
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
src/main.zig
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
src/main.zig
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.

Each name in the text and its kind
src/main.zig
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.

Names linked by the relations you allow
src/main.zig
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.

Several questions for each message
src/main.zig
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.

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.