Jev Nu
Nushell module for the TypeSafe System One API: typed decisions with calibrated probabilities
jev
A Nushell module for TypeSafe's System One API: typed decisions with calibrated probabilities, for code that needs to branch on a judgement.
Install
·
Reference
·
API docs
#Install
The module is plain Nushell, with no build step and no dependencies. Clone it
and point use at the module directory:
$ git clone https://github.com/cablehead/jev.nu ~/src/jev.nu $ use ~/src/jev.nu/jev $ jev ╭───┬────────────┬──────────────────────────────────────────╮ │ # │ name │ description │ ├───┼────────────┼──────────────────────────────────────────┤ │ 0 │ jev ask │ Ask a batch of questions about one state │ │ 1 │ jev choice │ Build a pick-one question │ │ 2 │ jev models │ List the models this account can send │ │ 3 │ jev noul │ Build a yes/no question │ │ 4 │ jev score │ Build a rate-it-on-a-scale question │ ╰───┴────────────┴──────────────────────────────────────────╯
Add it to config.nu to have it always loaded. Get a key from
console.typesafe.ai and export it:
$env.TYPESAFE_API_KEY = "apikey_..."
#Reference
#Ask: hello world
State goes in on the pipeline. Questions come in as a record, and the answers come back under the same keys.
$ "Help! My payouts have been failing for 3 days." | jev ask {
urgent: (jev noul "Does this convey urgency?")
} | get urgent.noul
0.95
No text, no parsing. 0.95 is the model's probability that the answer is yes.
Answers nest, so to nuon is the quickest way to see a whole one at the prompt.
#Configuration
| Variable | Meaning | Default |
|---|---|---|
TYPESAFE_API_KEY |
API key. Required. | none |
JEV_MODEL |
Default model or alias | jev-latest |
JEV_BASE_URL |
Endpoint, for testing | https://api.typesafe.ai/v1 |
#Questions
Three question types, three builders. Each returns a record and sends nothing, so you can hold them in a constant and review them in one place.
#jev noul - yes/no
Answers with the probability that the answer is yes, from 0 to 1. Near 0.5 means the model splits its bet, not that the truth is in the middle. There is no confidence on a noul: the value is already the certainty.
$ jev noul "The message reports a bug" $ jev noul "Does this convey urgency?" --yes "Explicitly time-sensitive" --no "No urgency expressed"
--yes and --no describe what each side means. They land in the request as
criteria.true and criteria.false.
#jev choice - pick one
For an answer that is one of a known set with no order between them. Map each
option to a description, or to null when the name says enough.
$ "Help! My payouts have been failing for 3 days." | jev ask {
dept: (jev choice "Which team should handle this?" {
billing: "Payments, invoicing, refunds"
technical: "Bugs, outages, integrations"
sales: null
})
} | get dept | to nuon
{type: choice, choice: billing, confidence: 0.79, probabilities: {technical: 0.14, billing: 0.86, sales: 0.0}}
Add an other option when the list might not cover every input.
#jev score - rate on a scale
For an answer that falls on a spectrum you can describe. Levels are ordered, lowest first.
$ "Help! My payouts have been failing for 3 days." | jev ask {
frustration: (jev score "How frustrated is the customer?" ["Calm" "Frustrated" "Very angry"])
} | get frustration | to nuon
{type: score, score: 1.04, confidence: 0.94, legend: {"0": Calm, "1": Frustrated, "2": "Very angry"}, probabilities: {"0": 0.0, "1": 0.96, "2": 0.04}}
The score is probability-weighted, so it lands between levels. Read it as a number and threshold it, not as an index.
#Ask everything at once
Every question in a request is evaluated against the state in parallel, and only input tokens are billed. Batching 13 questions into one call measures 12x cheaper and 10x faster than 13 calls, with the same answers. So ask everything the code might need, including questions whose answer only matters for some inputs, and ignore the ones you do not use.
$ let questions = {
dept: (jev choice "Which team should handle this?" {billing: null, technical: null, sales: null})
urgent: (jev noul "Does this convey urgency?")
frustration: (jev score "How frustrated is the customer?" ["Calm" "Frustrated" "Very angry"])
}
$ let a = "Help! My payouts have been failing for 3 days." | jev ask $questions
$ if $a.urgent.noul > 0.8 and $a.frustration.score > 1.5 { page-someone } else { queue $a.dept.choice }
A judgement that depends on several things gets split into one question per thing, combined with weights you keep in code. When the result does not match what your team would decide, change the weights, not a prompt.
#Confidence
Choice and score answers carry a confidence from 0 to 1, derived from how
peaked the probability distribution is. Gate on it by stakes: a wrong read-only
answer is recoverable, a wrong destructive one is not.
match $a.dept {
{confidence: $c} if $c < 0.5 => (route-to-human $ticket)
{choice: "billing", confidence: $c} if $c > 0.9 => (auto-refund $ticket)
{choice: $team} => (queue $team)
}
The full probabilities map is always in the answer, so you are free to compute
your own measure instead.
#Return modes
--return answers is the default. full gives the envelope, which is where the
resolved model id and the token usage live:
$ $ticket | jev ask --return full $questions | select model usage | to nuon
{model: "jev-1.13.0", usage: {input_tokens: 414, output_tokens: 70}}
An alias moves when a release ships, so log model if you have tuned thresholds
against a version.
merged adds an answers column to a record state, which is what keeps a table
a table:
$ open tickets.json | each {|t| $t | jev ask --return merged $questions }
| select id answers.dept.choice answers.dept.confidence answers.urgent.noul
╭───┬────┬─────────────────────┬─────────────────────────┬─────────────────────╮
│ # │ id │ answers.dept.choice │ answers.dept.confidence │ answers.urgent.noul │
├───┼────┼─────────────────────┼─────────────────────────┼─────────────────────┤
│ 0 │ 1 │ billing │ 1.00 │ 0.63 │
│ 1 │ 2 │ technical │ 1.00 │ 0.89 │
╰───┴────┴─────────────────────┴─────────────────────────┴─────────────────────╯
From there it is ordinary Nushell: where answers.urgent.noul > 0.8,
sort-by answers.frustration.score.
#Structured state
State can be a record or a list, not just a string. When a question is about one part of it, name that part in the instructions with a dot path in backticks.
$ {
ticket: {subject: "Duplicate charge", body: "I was charged twice for order A-104."}
order: {id: "A-104", charges: [{amount_usd: 49}, {amount_usd: 49}]}
refund_policy: "Duplicate charges are eligible for a refund."
} | jev ask {
requested: (jev noul "Does `ticket.body` request a refund?")
supported: (jev noul "Does `refund_policy` support a refund, given `order.charges`?")
}
#Reviewing a request before sending it
--payload returns the body instead of sending it. The questions and the
thresholds are the part a human needs to read, so make them easy to look at.
$ "a ticket" | jev ask --payload {urgent: (jev noul "Is this urgent?")} | to nuon
{state: "a ticket", model: "jev-latest", questions: {urgent: {type: noul, instructions: "Is this urgent?"}}}
#Models
$ jev models ╭───┬─────────────┬────────────┬───────────────────────────────────────────────╮ │ # │ name │ released │ description │ ├───┼─────────────┼────────────┼───────────────────────────────────────────────┤ │ 0 │ jev-latest │ a week ago │ The latest iteration of TypeSafe's System One │ │ │ │ │ Model: Jev │ │ 1 │ jev-preview │ a week ago │ A preview version of `jev-latest`: should be │ │ │ │ │ better in most ways │ ╰───┴─────────────┴────────────┴───────────────────────────────────────────────╯
--model completes from this list, cached in $nu.cache-dir for a day. A
versioned id like jev-1.13.0 is accepted whether or not it appears.
#Errors and retries
429 and 529 are retried with backoff, honoring retry-after when the response
carries one, three times by default. Everything else fails with the reason and
the request id:
$ $ticket | jev ask $questions
Error: nu::shell::error
x typesafe 401: Cannot authenticate with the server. Please check your API key and try again.
help: Check $env.TYPESAFE_API_KEY. Keys are issued at https://console.typesafe.ai/keys
request id: req_01a0b62941e07e429e4ae0cc556693dd
#Validation
The builders check the shape of a question before it costs a round trip, and the error points at your source:
$ jev choice "Which team?" {billing: null}
Error: nu::shell::error
x too few options
,-[entry #1:1:26]
1 │ jev choice "Which team?" {billing: null}
· ───────┬───────
· ╰── 1 of the 2 options a choice needs
╰────
help: A choice needs at least two options. For a yes/no judgment use `jev noul`.
[!NOTE] A one-level score is worth catching here, because the API accepts it and answers
score: 0.0, confidence: 1.0. That looks like a strong answer and means nothing.
Raw question records work too. The builders only add the checking.
$ $ticket | jev ask {urgent: {type: "noul", instructions: "Is this urgent?"}}
#Commands
jev noul [ instructions: any # string, record, or list --yes: string # what a yes (near 1) means --no: string # what a no (near 0) means ]: nothing -> record jev choice [ instructions: any criteria: record # option -> description, or null ]: nothing -> record jev score [ instructions: any criteria: list # ordered levels, lowest first ]: nothing -> record jev ask [ questions: record # question id -> question --model (-m): string # default $env.JEV_MODEL --return (-r): string # answers (default), full, merged --payload # return the request body, send nothing --max-retries: int # on 429 and 529, default 3 --timeout: duration # per attempt, default 2min ]: [string -> any, record -> any, list -> any] jev models []: nothing -> table
#Tests
$ nu tests/examples.nu ╭───┬────────────┬─────────────────────────────────────────────────┬──────╮ │ # │ name │ example │ ok │ ├───┼────────────┼─────────────────────────────────────────────────┼──────┤ │ 0 │ jev ask │ review the request without sending it │ true │ │ 1 │ jev choice │ route a ticket, describing two of three options │ true │ │ 2 │ jev noul │ a bare yes/no question │ true │ │ 3 │ jev noul │ spell out what each side means │ true │ │ 4 │ jev score │ rate customer frustration on three levels │ true │ ╰───┴────────────┴─────────────────────────────────────────────────┴──────╯ 5 passed, 3 skipped
Every @example that records a --result runs as a test. The builders are pure,
so their examples are the suite. Examples that reach the API record no result and
are skipped: they cost money and their answers move with the model.