Jev library


Requires the Analytica Enterprise edition or better

The Jev library lets an Analytica model ask TypeSafe's Jev model questions about text, and get back probabilities.

Download:

Save them in the same folder, named exactly as shown. Your browser may suggest names with underscores, such as Jev_library.ana; rename them, because the test model looks for Jev library.ana.

Requires Analytica 7.2 or later. Using Jev also needs a Vercel AI Gateway API key (see Setup).

What Jev does for a model

Most Analytica models start from numbers: costs, counts, probabilities. Much of the evidence behind those numbers arrives as text: customer emails, incident reports, inspection notes, contract clauses, news items, survey comments. Jev turns that text into numbers your model can use.

You give Jev a piece of text (the state) and one or more well-defined questions. It answers each question with a probability, not with prose. For example, given

Please refund the duplicate charge. This is urgent.

you can ask:

Question Kind Example answer
Is a refund requested? yes/no (Noul) 0.99, the probability of yes
Is this about billing, a technical problem, or something else? category (Choice) billing, with probabilities 0.93 / 0.04 / 0.03
How urgent is it: not urgent, somewhat urgent, or urgent? ordered scale (Score) probabilities 0.02 / 0.18 / 0.80 for the three levels

Because the answers are probabilities, they slot into a model the way any other uncertain quantity does. You can compute expected costs, set decision thresholds, send borderline cases to a person, or make them a chance variable.

Jev is a service run by TypeSafe (docs.typesafe.ai). This library sends requests to it through the Vercel AI Gateway. Each call goes over the internet, costs a small amount (about $0.00002 for a short message with three questions), and sends your text to Vercel and TypeSafe.

Setup

  1. Analytica 7.2 or later, Developer edition or better. The library stores its API key in an Analytica Secret object, which was introduced in 7.2.
  2. Add the library to your model. Download Jev library.ana, then use File > Add Module… and choose it, preferably as a linked library so that later fixes reach your model.
  3. Get a Vercel AI Gateway API key, from the AI Gateway section of your Vercel dashboard.
  4. Put the key in the JEV_API_KEY environment variable. On Windows, open Edit environment variables for your account from the Start menu, add JEV_API_KEY, and paste the key as its value. Restart Analytica afterward, because a running program doesn't see a new environment variable.

The library never stores, displays or sends the key itself. When a call is made, Analytica substitutes the key into the request header, and only for requests to https://ai-gateway.vercel.sh. If the variable isn't set, any call stops with an evaluation error, and nothing is sent. Don't put the key in a Definition or in the text you send.

Your first question

Create a variable and give it this Definition:

Local request := JevRequest(
    'Please refund the duplicate charge.',
    JevQuestions(
        refund: JevNoul('Is a refund requested?')
    )
);
Local response := request -> JevCall();
response -> answers -> refund -> noul

When you evaluate it, the result is a single number, such as 0.99: Jev's probability that the message asks for a refund.

Here is what each part does:

  • JevRequest(state, questions) bundles the text with the questions to ask about it. Building it doesn't contact anything.
  • JevQuestions(refund: …) lists the questions. refund is a name you make up. It isn't a variable in your model; it is the label you use to find this question's answer in the response.
  • JevNoul('…') defines a yes/no question.
  • request -> JevCall() sends the request and returns a JevResponse. JevResponse is a struct: a value with named fields, which you read with ->.
  • response -> answers -> refund -> noul reads the response's answers, then the answer to the refund question, then its probability.

To see everything Jev returned, end the Definition with response instead. The result window then shows the JevResponse struct, and you can click into its fields.

Keep the call in its own variable

Analytica caches results, so viewing a result again doesn't call Jev again. It does call again whenever the variable is recomputed, for example after you change something it depends on. So in a real model, give the call its own variable and let other variables read from it:

Variable Support_response := JevRequest(Message_text, Support_questions) -> JevCall()
Variable P_refund         := Support_response -> answers -> refund -> noul
Variable Category         := Support_response -> answers -> category -> choice

You can then edit and recompute P_refund or Category as often as you like without making new calls. In this guide, Variable X := … is shorthand for a variable named X with that Definition. Message_text and Support_questions stand for variables of your own.

The three kinds of question

Noul: yes/no

JevNoul('Does the customer threaten to cancel?')

The answer's noul field is the probability that the answer is yes, from 0 to 1. It isn't a degree or intensity. A value of 0.5 means "can't tell", not "half cancelled". Phrase the question so that yes is the case you care about.

When the boundary is subtle, add criteria spelling out what counts as yes and what counts as no:

JevNoul(
    'Does the customer threaten to cancel?',
    JevNoulCriteria(
        'The customer says they will cancel or leave unless something changes.',
        'No cancellation is mentioned, or it is mentioned only hypothetically.'
    )
)

TypeSafe suggests 0.7 as a starting threshold for treating an answer as yes and 0.3 for no, with values in between sent for human review. Tune those thresholds on your own cases.

Choice: one of several categories

JevChoice(
    'Which department should handle this message?',
    JevChoiceCriteria(
        billing:   'Charges, invoices, refunds and payment methods',
        technical: 'Problems using the product',
        sales:     'Questions about buying or upgrading',
        other:     'Anything else'
    )
)

The answer has these fields:

Field Meaning
choice the most likely option name, for example 'billing'
probabilities the probability of each option, summing to 1, for example probabilities -> technical
confidence 0 to 1: how concentrated the probability is on the top option (see Good practice and caveats)

Options can be 2 to 255 names, each with a description. Include an other option unless the categories are truly exhaustive. If an option name isn't a valid identifier, for example one containing spaces, see JevChoiceCriteria in the reference.

Score: a position on an ordered scale

JevScore(
    'How severe is the reported incident?',
    ['Cosmetic issue', 'Workaround exists', 'Service unavailable']
)

List 2 to 10 levels, from lowest to highest. The levels are numbered from 0, so these three are levels 0, 1 and 2. That differs from Analytica's usual numbering from 1. The answer has these fields:

Field Meaning
probabilities the probability of each level, keyed '0', '1', '2', …
score the expected level, Σ level × probability. It ranges from 0 to the top level number, so a score of 1.43 means "between Workaround exists and Service unavailable, nearer the former". Divide by (number of levels − 1) to rescale it to 0–1.
legend each level number's description, as you supplied it
confidence 0 to 1: how concentrated the probability is on one level

Because the level keys are numbers, read them with the function form of ->:

`->`(Support_response -> answers -> severity -> probabilities, '2')

The easier route is usually JevProbabilityArray (Using the answers in a model).

Asking several questions at once

Put any mix of questions into one JevQuestions. They are all answered in one call, each independently, using the same text. Adding questions barely changes the response time.

Variable Support_questions := JevQuestions(
    refund:   JevNoul('Is a refund requested?'),
    category: JevChoice('Which department should handle this?',
                  JevChoiceCriteria(billing: 'Charges and refunds', technical: 'Product problems', other: 'Anything else')),
    urgency:  JevScore('How urgent is the message?', ['Not urgent', 'Somewhat urgent', 'Urgent'])
)

TypeSafe recommends many small, specific questions over one complicated one. Ask about each factor separately, then combine the answers with your own formula in Analytica, where the weights are visible and easy to change.

Using the answers in a model

Probabilities as an Analytica array

JevProbabilityArray(answer) returns an answer's probabilities as an array over a local index named Outcome, ready to graph or compute with:

JevProbabilityArray(Support_response -> answers -> urgency)
Question kind Outcome index
Noul 'true', 'false'
Choice the option names
Score 0, 1, 2, …

Some common patterns

The expected cost of handling a message, given a cost per category, where Category_cost is indexed by your own Dept index:

Index Dept := ['billing', 'technical', 'other']
Variable P_dept := For d := Dept Do `->`(Support_response -> answers -> category -> probabilities, d)
Variable Expected_cost := Sum(P_dept * Category_cost, Dept)

Routing with a review band:

Local p := Support_response -> answers -> refund -> noul;
If p >= 0.7 Then 'Refund queue' Else If p <= 0.3 Then 'Normal queue' Else 'Human review'

Treating the answer as uncertainty in a probabilistic model:

Bernoulli(Support_response -> answers -> refund -> noul)
ChanceDist(P_dept, Dept)

Many texts at once

The usual use is a whole list of texts: a column of comments, a set of reports. Put the texts in an index, or in an array over an index, and pass the array where the library expects one text. Analytica's array abstraction then makes one request, and one call, per text:

Index Ticket := 1..50
Variable Ticket_text := … { 50 messages, indexed by Ticket }
Variable Ticket_requests := JevRequest(Ticket_text, Support_questions)
Variable Ticket_responses := Ticket_requests -> JevCall(allowIncomplete: True)
Variable P_refund := Ticket_responses -> answers -> refund -> noul        { indexed by Ticket }
Variable Ticket_category := Ticket_responses -> answers -> category -> choice
Variable Total_cost := Sum(Ticket_responses -> cost, Ticket)

Points to watch with arrays:

  • Every element is a separate, billed call, and the calls are made one after another. Fifty texts means fifty calls. Recomputing Ticket_responses makes all of them again, so keep it upstream of anything you edit often. To freeze a finished batch, copy its results into a table or save them to a file.
  • Use allowIncomplete: True for batches. By default, a question Jev declines to answer raises an error (When things go wrong). In an array, one such error stops the whole evaluation, and all the calls already made are still billed. With allowIncomplete: True, that text's response just lists the question in unanswered, and its fields are Null.
  • Build JevStates one at a time. Array abstraction works for a text state, but JevState(...) given arrays builds a single state containing the whole arrays. To send several facts about each item, loop:
    Variable Ticket_state := For t := Ticket Do JevState(message: Ticket_text[Ticket = t], customer_tier: Tier[Ticket = t])
    Check one element with Ticket_requests[Ticket = 1] -> JevJSON() before calling.
  • To get probabilities over an array of responses, use your own index. Pass the index with the outcome names, as P_dept does above, rather than JevProbabilityArray. JevProbabilityArray builds a separate local Outcome index for each answer, so it fails when it is given an array of answers. For a Score, convert the level number to text, as here with Index Level := 0..2:
    For k := Level Do `->`(r -> answers -> urgency -> probabilities, k & '')

When things go wrong

JevCall raises an evaluation error rather than returning a made-up answer:

Message begins Meaning What to do
an error about the secret JEV_API_KEY the environment variable isn't visible to Analytica set it, then restart Analytica (Setup)
Jev connection failed: no network connection, or the request timed out check the connection, or allow more time: request -> JevCall(120000) (milliseconds; the default is 60000)
Jev HTTP 401 or 403 the key was rejected check the key's value
Jev HTTP with another code the gateway or service refused the request the message includes the service's explanation
Jev returned no usable answer for question(s): urgency (refusal) Jev declined to answer that question, or left it out reword the question, or pass allowIncomplete: True to keep the other answers; the call was still billed

JevCall never retries automatically, because a retry could be billed twice. A timeout doesn't prove the service didn't process (and charge for) the request.

Checking a request before sending it (optional)

request -> JevJSON() returns the exact JSON text that JevCall would send. It doesn't contact Vercel, and it doesn't need the API key, so you can use it before setup is finished, or to check a large batch's requests without paying for them:

JevRequest('Please refund the duplicate charge.', JevQuestions(refund: JevNoul('Is a refund requested?'))) -> JevJSON()

The result is the text

{"state": "Please refund the duplicate charge.", "model": "typesafe-ai\/jev", "questions": {"refund": {"instructions": "Is a refund requested?", "criteria": null, "type": "noul"}}}

(The \/ is JSON's standard escape for /.) You never need JevJSON to make a call; JevCall uses it internally.

Good practice and caveats

  • Write questions a careful person could answer from the text alone. Make choice options distinct, list score levels in order, and say whether implied evidence counts or only explicit statements do. The text you send is the only information Jev has.
  • Validate before relying on it. Run a few dozen cases where you know the right answer, and compare. The probabilities are designed to be calibrated, but how well they are calibrated for your texts and questions is something to measure.
  • confidence is not a probability of being right. It is computed from the answer's own probabilities. It is 1 when all the probability is on one option or level, and 0 when the probability is spread evenly. It is handy for thresholds. Noul answers have none, because the probability itself says how sure Jev is.
  • Answers are inputs, not facts. Jev reads only what you send. Treat its answers like an expert's judgment: useful, but worth checking where the stakes are high.
  • Privacy. Everything in the state is sent to Vercel and TypeSafe. The Secret protects your key, not your data. Don't send information you aren't permitted to share.
  • Reproducibility. The default model 'jev-latest' can change as TypeSafe updates it. For results that must be reproducible, name a version as the third argument, for example JevRequest(text, questions, 'jev-1.14.0'), if that version is available.

Reference

atom means the parameter takes a single value. If you pass an array, the function is applied to each element (array abstraction).

JevCall and JevJSON are methods of a request: write request -> JevCall(), not JevCall(request).

Building a request

JevRequest(state, questions, model: 'jev-latest')

  • state: the text to assess, or a structured value such as a JevState.
  • questions: a JevQuestions.
  • model: a Jev model name. 'jev-latest' is sent as typesafe-ai/jev, any other name x is sent as typesafe-ai/x, and a name containing / is sent unchanged.

Building a request makes no call.

JevState(...)

Named facts sent to Jev together, as a JSON object. Use it when the evidence isn't one block of text:

JevState(message: 'Please refund the duplicate charge.', amount: 49, currency: 'USD', prior_complaints: 2)

A field can hold a list. As a result, JevState does not array-abstract: build one state per item with For (Many texts at once).

JevQuestions(name1: question1, name2: question2, ...)

The questions to ask, each under a name you choose. Each question must be a JevNoul, JevChoice or JevScore, and there must be at least one.

JevNoul(instructions, criteria)

A yes/no question. criteria is optional: JevNoulCriteria(yes_description, no_description).

JevChoice(instructions, criteria)

A category question. criteria is a JevChoiceCriteria with 2–255 options.

JevChoiceCriteria(option1: description1, ...)

The options for a Choice. Their order doesn't matter. A description can be Null, in which case the option name alone defines it. For option names that aren't identifiers:

ParseJSON('{"Billing and refunds": "Charges, invoices, refunds", "Something else": null}', JevChoiceCriteria)

JevScore(instructions, levels)

An ordered-scale question. levels is a list of 2–10 descriptions, lowest first. They are numbered from 0.

An instructions argument, or any description, can also be a structured value, for example _Struct(task: '…', examples: '…'). It is then sent as JSON.

Making the call

request -> JevCall(timeout_ms: 60000, allowIncomplete: False)

Sends the request and returns a JevResponse.

  • timeout_ms is in milliseconds. It applies to connecting, sending and receiving.
  • With allowIncomplete: True, a refused or missing answer is reported in unanswered instead of raising an error.

Every evaluation is a billed call.

request -> JevJSON()

Returns the request's JSON text without sending it (Checking a request before sending it (optional)).

JevParseResponse(body, questions, allowIncomplete: False)

Converts saved response text, for example a body saved to a file, into a JevResponse without any network access. If you pass questions, it checks that each question was answered, as JevCall does.

Reading the response

JevResponse

Field Contents
answers one JevAnswer per question, under the names you gave the questions
cost the charge for this call in US dollars, as reported by the gateway (Null if it wasn't reported)
usage input_tokens and output_tokens
unanswered names of questions with no usable answer: empty normally, filled only with allowIncomplete: True
model the model that answered, for example typesafe-ai/jev
provider_metadata other information from the gateway, kept as is

JevAnswer

Field Noul Choice Score
type 'noul' 'choice' 'score'
noul probability of yes
choice most likely option
score expected level (0 to top level)
probabilities by option name by level, '0', '1', …
legend level descriptions, by '0', '1', …
confidence 0–1 0–1

Fields that don't apply are Null. A question Jev declined has type 'refusal' and no results.

JevProbabilityArray(answer)

Returns the answer's probabilities as an array over a local index Outcome (Using the answers in a model). It works on one answer at a time, and raises an error for a refusal.

The test model

Test of Jev library.ana contains the library's regression tests in its QA Testing module:

  • QA Offline results: 53 cases covering the constructors, the JSON sent, response parsing (including refusals, missing answers and malformed responses), cost, probability arrays and input checks. They make no calls and need no key.
  • QA Live result: one real call with a Noul, a Choice and a Score question. It checks that the answers have the right types and ranges and that the probabilities sum to 1. It costs a fraction of a cent each time it is recomputed.
  • QA Summary: both together. Viewing it can trigger the live call if that result isn't already cached.

Example models

  • Dishwasher warranty reliability. A fictional appliance maker uses Jev to read 477 technicians' warranty service notes. From them it builds failure rates by part and age, a warranty reserve, a spare-parts plan and a part-upgrade decision. It shows batch calls, counting with probabilities, checking Jev against a reviewed sample, and a pitfall with rare events. It runs without an API key, using saved answers.
Comments
Loading comments...