SDKs & CLI

TypeScript SDK

The official TypeScript and JavaScript library for Wity. You give it some text and a few questions, and it gives back a typed answer for each question, with a probability for every option.

The SDK handles the parts of calling an API that are easy to get wrong:

  • It sends your API key, and keeps it out of logs and error messages.
  • It retries requests that failed for a passing reason, like a busy server.
  • It turns API errors into error classes you can check with instanceof.
  • It types every answer from your questions, so your editor knows the option ids of a choice.

Install#

npm install wity-sdk
  • Needs Node.js 20 or newer. It runs on your server, not in the browser (see Security).
  • It has no other dependencies.
  • It works with both import and require: const { WityClient } = require("wity-sdk");

API key#

Get an API key from the keys page. Save it in your environment variables as WITY_API_KEY, then pass it to the client:

import { WityClient } from "wity-sdk";
const client = new WityClient({ apiKey: process.env.WITY_API_KEY });

You can pass any key as apiKey, for example one from a secret manager. If you leave apiKey out, the client reads WITY_API_KEY by itself.

Your first request#

This example reads a customer's message and asks Wity three questions about it: which team should handle it, whether it's urgent, and how much it hurts the customer. All three are answered in one request.

import { WityClient, choice, noul, score } from "wity-sdk";
const client = new WityClient({ apiKey: process.env.WITY_API_KEY });
// The text you want Wity to judge.
const ticket = "I was charged twice for my subscription this month. Please refund one of them today.";
const res = await client.systemOne({
state: ticket,
questions: {
// Pick one of these teams.
team: choice("Which team should handle this ticket?", {
billing: "Payments, charges and refunds",
technical: "Bugs, errors and outages",
other: "Anything else",
}),
// Yes or no.
urgent: noul("Does the customer ask for something to happen soon?", {
true: "They ask for fast action",
false: "No time pressure",
}),
// A level on a scale, from lowest to highest.
severity: score("How much is this hurting the customer?", ["Not at all", "A little", "A lot"]),
},
});
console.log(res.answers.team.choice); // billing
console.log(res.answers.urgent.noul); // 0.99989...
console.log(res.answers.severity.score); // 1.97322...

The examples on this page use await at the top level, which works in ES modules (.mjs files, or "type": "module" in package.json). Elsewhere, put the code inside an async function.

Every call to systemOne has this syntax. Words in <angle brackets> are placeholders for your own values:

const res = await client.systemOne({
state: <the text to judge>, // required: a string, object or array
questions: { <name>: <question>, ... }, // required: one or more questions
reasoning: <"auto", "off" or "always">, // optional. Default: "auto"
max_latency_ms: <200 to 120000>, // optional: a time limit in ms
image: <data URL>, // optional: one image
});

The sections below go through each part step by step.

1. Create a client

WityClient holds your key and settings. Create one when your app starts and use it for every request. There's nothing to close.

2. Pass the state

state is what you want Wity to judge: an email, a support ticket, a log line, a product review. It can be a string, or an object or array, which Wity reads as JSON. It can be up to 32,000 characters long.

state: "<some text>" // a string
state: { <field>: <value>, ... } // an object, read as JSON
state: [<item>, ...] // an array, read as JSON

3. Write the questions

questions is an object. Each key is a name you choose, like team. You use that name later to find the answer.

questions: {
<name>: <question>, // a name you choose, then a question built with choice, noul or score
<name>: <question>, // add as many questions as you need
}

Each value is a question, built with one of three helpers:

choice
pick one
Wity picks exactly one option from a list you write. Each option has an id (billing) and a description ("Payments, charges and refunds"). Wity reads the description to understand the option, and answers with the id.
noul
yes or no
A yes-or-no question. The true and false descriptions are optional. They say what yes and no mean in your case, which helps when the question alone is vague.
score
a scale
Wity places the text on a scale you write, from the lowest level to the highest. The levels are numbered from 0. You can have 2 to 10 levels.

Each helper takes the question first. Write it the way you'd ask a colleague. See Writing good questions for tips.

4. Read the answers

systemOne sends the request and returns a promise of the response. res.answers files each answer under the name you gave its question:

const { team, urgent, severity } = res.answers;
team.choice; // "billing": the option with the highest probability
team.probabilities; // { billing: 0.99993..., technical: 0.00003..., other: 0.00003... }
urgent.noul; // 0.99989...: the probability that the answer is yes
severity.score; // 1.97322...: the expected level. 0 is "Not at all" and 2 is "A lot",
// so this is very close to "A lot"

Each answer has the type that matches its question. TypeScript knows team.choice can only be "billing", "technical" or "other", so a typo in an option id is caught before your code runs.

Acting on an answer#

The probabilities let your code decide when to act on its own and when to ask a person. A common pattern: act when the top option is very likely, and send everything else to a person.

const { team, urgent } = res.answers;
if (team.probabilities[team.choice] >= 0.9) {
await assignToTeam(team.choice, ticket); // Wity is sure: route it automatically
} else {
await sendToReview(ticket); // not sure enough: a person decides
}
if (urgent.noul >= 0.8) {
await markUrgent(ticket);
}

Probabilities add up to 1, and a higher number means Wity is more sure. The 0.9 above is only an example. Pick thresholds that suit your data. See Evaluating on your data and Probabilities & confidence.

Question types#

A closer look at each helper, with what its answer contains. The numbers in these examples are illustrations.

choice: pick one option

choice("<question>", {
<id>: "<description>", // one line per option
<id>: "<description>",
})
const question = choice("What is this email about?", {
invoice: "A bill or payment request",
meeting: "Planning or moving a meeting",
spam: "Unwanted advertising or a scam",
});
const res = await client.systemOne({ state: email, questions: { topic: question } });
const topic = res.answers.topic;
topic.choice; // "invoice" (typed as "invoice" | "meeting" | "spam")
topic.probabilities; // { invoice: 0.94, meeting: 0.04, spam: 0.02 }
topic.probabilities.spam; // the probability of any option, not only the winner
choice
string
The id of the option with the highest probability.
probabilities
Record<id, number>
A probability for every option, by id. They add up to 1.
confidence
number
How concentrated the probabilities are, from 0 to 1. It's 1 when one option has all the probability, and 0 when it's spread evenly. Use it to sort answers for review. For decisions, use probabilities.

Option ids written as numbers ({ 1: "Ground" }) come back as strings ("1"), and are typed that way.

noul: yes or no

// without descriptions
noul("<question>")
// with descriptions of yes and no (give both)
noul("<question>", { true: "<what yes means>", false: "<what no means>" })
const question = noul("Is this message trying to steal a password?", {
true: "It asks for a password, a login or a code", // optional: what yes means here
false: "It doesn't ask for any login details", // optional: what no means here
});
const res = await client.systemOne({ state: email, questions: { phishing: question } });
res.answers.phishing.noul; // 0.97: the probability of yes. The probability of no is 1 - 0.97
noul
number
The probability that the answer is yes, from 0 to 1. To turn it into a yes or a no, compare it with a threshold you choose, like noul >= 0.8.

score: a level on a scale

score("<question>", [
"<lowest level>", "<next level>", ..., "<highest level>", // 2 to 10 levels, lowest first
])
const question = score("How angry is the customer?", [
"Calm", // level 0: the lowest
"Annoyed", // level 1
"Angry", // level 2: the highest
]);
const res = await client.systemOne({ state: ticket, questions: { anger: question } });
const anger = res.answers.anger;
anger.score; // 1.42: the expected level, between "Annoyed" (1) and "Angry" (2)
anger.probabilities; // { "0": 0.07, "1": 0.44, "2": 0.49 }: one probability per level
anger.legend; // { "0": "Calm", "1": "Annoyed", "2": "Angry" }
// The single most likely level, as text:
const [best] = Object.entries(anger.probabilities).sort((a, b) => b[1] - a[1])[0];
anger.legend[best]; // "Angry"
score
number
The expected level: each level number weighted by its probability. It can fall between two levels, so 1.42 means "between level 1 and level 2".
probabilities
Record<string, number>
A probability for every level, keyed by level number as a string ("0", "1", …).
legend
Record<string, string>
Maps each level number back to the text you wrote for it.
confidence
number
How concentrated the probabilities are, as for choice.

The levels can come from a variable, like a list loaded from a database.

Limits

Wity checks the size of every request (text length, number of options and more). If a limit is broken, the SDK throws a BadRequestError that names it, and the call isn't billed. See Errors, limits & billing.

Reasoning#

Most questions are clear, and Wity answers them in one quick pass, in about a tenth of a second. Some are harder: two options are almost equally likely, or the question asks how likely something is. For those, Wity first thinks step by step, which usually takes 1 to 4 seconds. Thinking isn't billed.

You choose when Wity may think with reasoning. Pass it to systemOne, next to state and questions. It applies to every question in the request.

reasoning: "auto" // think only when the question needs it (the default)
reasoning: "off" // never think
reasoning: "always" // think before every answer
"auto"
default
Think only when the question needs it.
"off"
fastest
Never think. Use it when speed matters more than the hard cases.
"always"
slowest
Think before every answer.
const res = await client.systemOne({
state: ticket,
questions,
reasoning: "off", // "auto" (the default), "off" or "always"
max_latency_ms: 3000, // stop thinking after 3 seconds and answer with what it has
});

max_latency_ms caps how long one request may take, from 200 to 120000 ms. When the time runs out, Wity stops thinking and answers with what it has. The SDK waits at least max_latency_ms plus 10 seconds before it gives up, even if timeoutMs is shorter.

Each answer says whether Wity thought:

const team = res.answers.team;
team.reasoning?.thought; // true if Wity thought before answering
team.reasoning?.reason; // why it thought, like "close_call". null if it didn't think
team.reasoning?.budget_limited; // true if max_latency_ms cut the thinking short
team.direct_probabilities; // the answer before thinking. Missing if it didn't think

With reasoning: "off", reasoning on the answer is missing. For a noul, the answer before thinking is in direct_noul. More in Reasoning & auto mode.

Images#

A request can include one image, like a photo to check. Send it as a data URL: the image type, then the file's bytes encoded as base64 text.

import { readFileSync } from "node:fs";
// Turn the image file into a data URL: its type, then its bytes in base64.
const image = "data:image/jpeg;base64," + readFileSync("parcel.jpg").toString("base64");
const res = await client.systemOne({
state: "Photo of the parcel, taken by the driver at delivery",
questions: { damaged: noul("Is the parcel damaged?") },
image,
});
  • Use image/png instead of image/jpeg for a PNG file.
  • state is still required. Use it to say what the image shows, or to add text that goes with it.
  • generate takes an image the same way. See Images.

Generate#

Questions pick from options you wrote in advance. generate writes new text instead: a value read off a document, a form field, a one-line reply. It returns one piece of text per call, with no probabilities. If the answer is one of a known set, ask a question instead: you get probabilities, and the answer is easier to check.

Free text

const reply = await client.generate({
state: ticket,
instructions: "Write the one-sentence reply the agent sends to the customer.",
max_tokens: 80, // the longest the answer may be. 1 to 512, default 128
});
reply.text; // "Thank you for bringing this to our attention; I've reviewed your account..."

instructions says what to write. max_tokens caps the length. A token is a word or a piece of a word.

JSON in a shape you define

Pass a shape to get JSON back instead of free text. The shape is a JSON Schema, the standard way to describe JSON: which fields it has, and what each one holds. Its top level must be an object.

const origin = await client.generate<{ city: string }>({
state: "Ticket scan: LISBOA (LIS) -> BERLIN BRANDENBURG (BER), 14 Oct, seat 23C.",
instructions: "Origin city name, in English",
// A JSON Schema: the answer must be an object with a "city" field
// holding 1 to 40 letters or spaces.
shape: {
type: "object",
properties: { city: { type: "string", pattern: "^[A-Za-z ]{1,40}$" } },
required: ["city"],
},
max_tokens: 40,
});
if (origin.finish_reason === "stop") {
origin.value?.city; // "Lisbon": the JSON, already parsed
} else {
// "length": Wity ran out of max_tokens. The JSON is incomplete and value is null.
await sendToReview(ticket);
}
  • finish_reason is "stop" when Wity finished. The JSON then matches your shape, and value holds it, already parsed.
  • finish_reason is "length" when Wity hit max_tokens first. text is cut off, and value is null. Raise max_tokens, or send the item to a person.
  • generate<{ city: string }> tells TypeScript what value looks like. Keep it in line with your shape: the SDK doesn't check that they match.
  • text always holds the raw output, as a string.

Heads up

Check generated text before acting on it. It can state things that aren't in state.

Many requests at once#

Every call returns a promise, so you can send several requests at the same time with Promise.all:

const tickets = [
"The site has been down for an hour and we're losing sales.",
"Could you add a dark mode some day?",
];
// Send one request per ticket, all at the same time, and wait for all of them.
const results = await Promise.all(
tickets.map((t) => client.systemOne({ state: t, questions: { urgent: noul("Is this urgent?") } })),
);
results.forEach((res, i) => console.log(res.answers.urgent.noul.toFixed(2), tickets[i]));
  • Promise.all returns the results in the same order as tickets.
  • Each key allows a limited number of requests at a time (see limits). The SDK retries when you go over, but for large batches, send them in smaller groups.

Errors#

When a request fails, the SDK throws an error. Every one of them extends WityError, so err instanceof WityError matches them all. Check for the specific ones you want to handle differently:

import { APIError, AuthenticationError, BadRequestError, RateLimitError, WityError } from "wity-sdk";
try {
const res = await client.systemOne({ state: ticket, questions });
} catch (err) {
if (err instanceof BadRequestError) {
console.error(err.message); // the request is wrong, and the message says which part. Fix it: retrying won't help
} else if (err instanceof AuthenticationError) {
// the key is missing, wrong or revoked
} else if (err instanceof RateLimitError) {
// still too many requests after the SDK's own retries. Try again later
} else if (err instanceof APIError) {
console.error(err.status, err.requestId); // any other error from the API. Quote requestId when you report it
} else if (err instanceof WityError) {
console.error(err.message); // anything else, like no connection or no key set
} else {
throw err; // not from the SDK
}
}
BadRequestError
400
The request is wrong. The message says which part. Fix it: retrying won't help.
AuthenticationError
401
The key is missing, wrong or revoked.
InsufficientBalanceError
402
The account has no credit left. Add some on the Billing page.
RateLimitError
429
Too many requests, even after the SDK's retries. Slow down and try again.
InternalServerError
5xx
A problem on Wity's side, even after the SDK's retries.
APIError
any other status
The parent of all the errors above. It has status, body, headers and requestId, the server's id for the request. Quote requestId when you report a problem.
APIConnectionError
network
The API couldn't be reached, or the connection broke while the answer was arriving.
APITimeoutError
timeout
No answer within the timeout.
APIUserAbortError
cancelled
You cancelled the call with its signal (see below).
WityError
base class
Anything else: a setup problem like a missing key, a request that can't be turned into JSON, or an answer that isn't from Wity.

Unknown fields

A field the SDK doesn't know, like a typo (max_latency instead of max_latency_ms), is still sent. That way, new API fields work without waiting for an SDK update. The SDK logs a warning that names the field, and if Wity rejects the request, the BadRequestError names it too.

Retries, timeouts and cancelling#

The SDK retries some failed requests by itself, so your code doesn't have to:

  • It retries 408, 429 and 5xx errors and network failures, up to 2 times. It waits a little longer before each retry, or as long as the API's Retry-After header asks.
  • If Retry-After asks for more than 60 seconds, the SDK throws the error straight away, so you can decide what to do.
  • It doesn't retry timeouts. The request may already have reached Wity, and you've already waited the full time.
  • If the connection breaks while the answer is arriving, Wity may already have billed the call. The retry is billed again.

You can change both settings when you create the client:

const client = new WityClient({
apiKey: process.env.WITY_API_KEY,
timeoutMs: 30_000, // give up on one attempt after 30 seconds. Default: 60000
maxRetries: 0, // don't retry failed requests. Default: 2
});

To cancel a call, pass an AbortSignal as the second argument. Cancelling also stops any retries. AbortSignal.timeout(ms) gives a signal that cancels after that time, which caps the total time across all attempts:

// give up after 20 seconds in total
await client.systemOne({ state: ticket, questions }, { signal: AbortSignal.timeout(20_000) });

Logging#

By default the SDK only logs warnings, like an unknown field in a request. When you're debugging, turn on more detail:

new WityClient({ logLevel: "debug" }); // or set WITY_LOG_LEVEL=debug
new WityClient({ logLevel: "off" }); // nothing at all
new WityClient({ logger: pino() }); // your own logger: anything with debug, info, warn and error
"warn"
default
Unknown fields in a request, and a Retry-After too long to wait for. Nothing when calls succeed.
"info"
level
Also each retry (why, and how long it waits), timeouts, connection errors and cancelled calls.
"debug"
level
Also each request: route, attempt, size in bytes, question names, status, time and request id.
"off"
level
Nothing.
  • Logs go to the console with a [wity] prefix, unless you pass your own logger.
  • Logs never contain your API key or the state text, which is often customer data.
  • Errors are always thrown, whatever the level. Logs only add detail.

Configuration#

Everything you can pass when you create a client:

apiKey
string
Your API key. If left out, read from WITY_API_KEY.
baseURL
string
The API's address. If left out, read from WITY_BASE_URL, or the production API. Only needed for testing against another server. client.baseURL shows the one in use.
timeoutMs
number
Milliseconds to wait for one attempt. Default: 60000.
maxRetries
number
How many times to retry a failed request. Default: 2.
logLevel
string
How much to log. If left out, read from WITY_LOG_LEVEL, or "warn".
logger
object
Your own logger, with debug, info, warn and error methods. Default: the console.
fetch
function
Your own fetch, for example to add a proxy. Default: the global fetch.

Security#

  • The SDK runs on your server. WityClient doesn't run in a browser page or worker.
  • Test setups that act like a browser, such as jsdom, count as a browser. Run tests that create a WityClient in a Node environment.
  • The client never shows the key when it's printed, logged or turned into JSON, and errors never include it.
  • baseURL must use https://. Plain http:// is allowed only for localhost, 127.0.0.1 and [::1].
  • The client never follows redirects, so the key only goes to the address you set.
  • An answer larger than 10 MB can't be from Wity. The client stops reading it and throws a WityError.

Questions, bugs and security problems: info@alphanimble.com. Please report security problems by email, not in public. The SDK is licensed Apache-2.0.