brussle
v1.9.0
Published
Brussle SDK for TypeScript. Brussle is the database for AI columns: define a column, keep it current as records change, record outcomes, and read the calibrated number with its 95% interval.
Maintainers
Readme
brussle (TypeScript)
Brussle calibrates AI decisions on the outcomes that you record, and judges each one again when a document it read changes. A decision, a judgment, is an AI model's typed answer about a document, and it can read other documents. For example, Which open invoice does this payment settle? reads the payment beside its customer's open invoices. When an invoice changes, Brussle judges the payments that read it again. Each answer says whether it is current, and records the judgment version and engine version behind it. From the outcomes that you record, Brussle fits a correction to each answer's number, and uses it only when it does better than the engine on held-out outcomes. If an answer reads only its own document, a column in your own database may be enough.
This package is the TypeScript client for the Brussle API.
import { Client } from "brussle";
const db = new Client({ apiKey: process.env.BRUSSLE_API_KEY! });
const ns = db.namespace("acme/prod/tenant_123");
// A judgment on unmatched payments that reads the open invoices in the same block.
await ns.judgments.create({
from_starter: "payments.settles",
paths: {
kind: "attributes.kind",
payment_kind: "payment",
invoice_kind: "invoice",
status: "attributes.status",
unmatched: "unmatched",
open: "open",
block: "attributes.block",
payment_amount: "state.amount",
reference: "state.reference",
invoice_number: "state.number",
invoice_amount: "state.amount",
},
engine: { name: "jev", version: "current" },
freshness: { policy: "on_change" },
confirm: true,
});
// An open invoice, and a payment in the same block.
await ns.write({
upsert: [
{ id: "inv_1002", attributes: { kind: "invoice", status: "open", block: "northwind-gbp" }, state: { number: "INV-1002", amount: 1200 } },
{ id: "pay_1", attributes: { kind: "payment", status: "unmatched", block: "northwind-gbp" }, state: { amount: 1200, reference: "Northwind INV-1002" } },
],
});
// A change to the block's open invoices judges its payments again. Query the payments that wait for a person,
// with a current answer only.
const { rows } = await ns.query({
filters: ["answers.settles.thresholds.unmatched", "Eq", true],
answers: "fresh_only",
include: { answers: ["settles"] },
});The quickstart runs this example step by step. Relations lists what else a judgment can read.
The example uses Jev current. A judgment can name any engine version that db.engines.list() lists as active, with its limits: today jev current and gpt-6-luna current. Both are unpinned, so each answer records the epoch of the model behind it. They differ in two ways that you choose by: only gpt-6-luna reads images, and each row kept on gpt-6-luna counts 5 times its size class on the bill, against 2 times on jev. With your own key for a provider, a judgment can instead name one of its models as <provider>/<model id> at current, once db.engines.list() lists that provider as active; none is yet. Store the key with db.credentials.set("openai", { key, namespace }). Leave engine out and Brussle picks the namespace's default_engine, else your key's default model, else the included engine. See engines. The API also names pplx-decider, which has no active version yet, so a judgment that names it is refused with engine_version_unavailable.
A document keeps its images in images, beside attributes and state. Each name holds one image or a list of images. A write sends a PNG, JPEG or WebP as a data URL, and the document then holds a reference to the stored image:
await ns.write({
upsert: [{
id: "claim_1",
attributes: { kind: "claim" },
state: { description: "Rear door dented." },
images: { photo: "data:image/jpeg;base64,/9j/4AAQ..." },
}],
});
// images.photo is now { digest: "sha256:e0db...", type: "image/jpeg", width: 1440, height: 960 }
const { bytes, type } = await ns.image("sha256:e0db...");To keep an image when you write the document again, send the reference you read. ns.image returns the stored bytes, which are exactly what an engine that reads images reads. A judgment reads an image when its context recipe names it as images.<name>, on an engine whose limits in db.engines.list() have images. Any other engine gets the image's digest in its place, and the create's warnings say so. This judgment reads the photo on gpt-6-luna:
await ns.judgments.create({
name: "damage_matches",
type: "bool",
applies_to: { "attributes.kind": "claim" },
question: "Does the photo show damage that matches the description?",
context: { fields: ["state.description", "images.photo"] },
engine: { name: "gpt-6-luna", version: "current" },
freshness: { policy: "on_change" },
});ns.eraseImage(digest) erases an image: its bytes are deleted, and documents and evaluations keep its reference, marked erased: true. It returns { digest, erased_at }, and erasing again returns the same. Reading the image then throws image_erased (410), and a write that sends its bytes again is refused with image_erased.
See Images.
To load many documents, importDocuments takes any iterable or async iterable and writes it in batches, 4 at a time, with retries:
const { documents, failures } = await ns.importDocuments(records(), { concurrency: 4 });How the client works:
- Methods mirror the API's routes, in camelCase.
- Every route that changes something takes
{ idempotencyKey }as its last argument. A call that would not be safe to repeat without one, such as a create, gets a key the SDK generates when you pass none, the same on each retry of that call. - Calls are retried after network errors, 502, 503 and 504, and after a 429 or 503 with a
Retry-Afterof at most 60 s, 2 times by default (maxRetries). A 500 is retried only for reads, writes and the other calls that are safe to repeat without a key. A call that would create a namespace past the organization's 1,000 a minute gets a 429 withRetry-Afteruntil the next minute, so it is retried; raisemaxRetriesto create many namespaces at once. A 413too_large, such as a write past a namespace's size or attribute-name limit, is never retried. - A 429 past a rate limit says which in
details.rate_limit:per_key, orper_orgfor all your organization's API keys together. The per-organization limit counts every process and key, so a retry can meet another 429 because of your other clients. See limits. ns.judgments.create,updateandbackfillcan take more than 20 s. The API then answers with a job, and the call waits for it: it checks the job every 1 to 10 s, or after the API'sRetry-After, for up towaitTimeoutMs(10 minutes by default). It then returns or throws what the call would have. After the timeout it throwsJobWaitTimeoutError, whosejobyou can pass todb.jobs.wait. With{ wait: false }the call returns{ accepted: false, result }or{ accepted: true, job }.- Request and response bodies keep the API's field names, so JSON from the docs pastes in. One exception:
ns.outcomes.appendtakes the array of outcomes, not the whole request body. - Answers and definitions are discriminated unions on
type. Filters are typed tuples. - Errors throw
ApiErrorwith the API'scodeanddetails. Thecodeisunknownfor an error response that is not the API's JSON, for example a proxy's error page, and for a success whose body is not JSON (forns.image, not a PNG, JPEG or WebP). - A call that your organization's plan does not include throws
plan_required(402).details.required_plannames the plan that includes it.
See the SDK reference.
The source, its issues and the changelog are at github.com/brussle/sdk.
