Docs·Get started

Concepts

How XERJ Reader answers, when it declines, and what the numbers in a response mean. One example for each idea.

The examples on this page use the shipment email from the inbox example. Confidences in the examples are for illustration.

Answers are spans #

XERJ Reader does not write an answer. It marks where the answer starts and ends in your document, and the text is copied from there.

JSON
{"type": "span", "text": "48,200 EUR", "start": 533, "end": 543, "confidence": 0.98}

This holds for every span answer: text == document[start:end]. The model has no way to produce text that is not in your document, so it cannot invent a value.

Three things follow:

Abstention and min_confidence #

Every answer comes with a confidence between 0 and 1. min_confidence is the bar an answer must clear:

Yes/No answers follow the same rule when the bar is above 0.5. See Yes and No for lower bars.

The bar is yours to set, per request. It must be between 0 and 1 (not 0, not 1). The default is 0.5, the tuned default.

The trade-off, plainly #

Two numbers describe how answers behave on your documents:

A higher bar gives fewer answers, and more of them are right. A lower bar gives more answers, and more of them are wrong. You cannot have the most of both, so you choose.

Here is the same email, the same three questions, and three bars:

Question Example confidence Bar 0.2 Bar 0.5 (default) Bar 0.9
How much is invoice INV-20931? 0.98 48,200 EUR 48,200 EUR 48,200 EUR
Do both shipments leave Rotterdam on the same day? 0.88 No No none
Who is the CEO of Acme Retail? 0.21, best guess Tom Baker Tom Baker none none

How to choose your bar #

  1. Collect 50 to 100 real questions on your own documents, with the right answers. Include some questions whose answer is not in the document.
  2. Run them at a few bars, for example 0.3, 0.5, 0.7 and 0.9.
  3. For each bar, count the answers given and the wrong answers.
  4. Pick the lowest bar whose wrong answers you can accept.
  5. Keep the questions. Run them again when your documents change.

As a rule of thumb: raise the bar when a program acts on the answer with no person in the loop, such as paying an invoice. Lower it when a person reviews every answer anyway.

The confidence is a score to compare with a bar, not a promise. A 0.95 answer can still be wrong, which is one more reason to show start and end to the person who checks it.

Best guess #

When the answer is none, best_guess holds the closest candidate the model found, if there was one:

JSON
{
  "answer": {"type": "none", "text": null, "start": null, "end": null, "confidence": 0.21},
  "best_guess": {"text": "Tom Baker", "start": 62, "end": 71, "confidence": 0.21}
}

When there is no candidate at all, best_guess is null. For none answers, answer.confidence is the confidence of the best guess.

Yes and No #

For yes/no questions, such as “Is…”, “Do…”, “Has…”, the answer can be yes or no:

JSON
{"type": "no", "text": "No", "start": 496, "end": 507, "confidence": 0.88}

In /v1/extract, a field with "type": "boolean" turns Yes and No into true and false.

Offsets are characters #

start is the position of the first character of the answer. end is the position just after the last one. Both count characters from the start of the document, starting at 0.

Characters are Unicode code points, the way Python indexes strings. In Python, document[start:end] is the answer, always.

JavaScript strings index UTF-16 units. For most text this is the same. It differs after characters outside the Basic Multilingual Plane, such as most emoji, which take two UTF-16 units:

Python
doc = "Paid 👍 48,200 EUR"
doc[7:17]   # '48,200 EUR'  (the API returns start 7, end 17)
JavaScript
const doc = "Paid 👍 48,200 EUR";
doc.slice(7, 17);   // " 48,200 EU"  wrong: the emoji counts as 2 in JavaScript

Convert the offsets before you slice:

JavaScript
// Code-point offsets from the API -> JavaScript string indexes.
function toIndex(text, offset) {
  let i = 0;
  for (let n = 0; n < offset && i < text.length; n++) {
    i += text.codePointAt(i) > 0xffff ? 2 : 1;
  }
  return i;
}

doc.slice(toIndex(doc, 7), toIndex(doc, 17));   // "48,200 EUR"

The JavaScript SDK exports codePointSlice(text, start, end), which slices the way the API counts.

Offsets always refer to the exact text you sent. A line break \n is one character and \r\n is two. If you clean the text before sending it, keep the cleaned version for highlighting. After /v1/convert, offsets refer to the converted text.

Tokens #

You pay per token. A token is a piece of a word. In English, one token is about 4 characters on average. Other languages, tables and code often use more tokens per character.

Example: the shipment email in the inbox example has 888 characters, so one question about it is about 230 input tokens. At $0.01 per 1M input tokens, 1,000 such questions cost about $0.0023.

Every response has the exact counts:

JSON
"usage": {"input_tokens": 230, "output_tokens": 3, "total_tokens": 233}

Your Usage page and GET /v1/usage add them up. Pricing has a worked bill.

Long documents #

One request reads up to 1,000,000 characters. The whole document is read for every question, so ten questions about a 100-page contract are ten reads of the contract.

To keep long documents fast and cheap: