Packages

Sans-io Gleam client for TypeSafe AI's System One API and its Jev decision model: build the HTTP request, send it with any client, decode the typed answers.

Current section

Files

Jump to
jev_api README.md
Raw

README.md

# jev_api
A sans-io [Gleam](https://gleam.run) client for [TypeSafe AI](https://typesafe.ai)'s
System One API and its **Jev** decision model.
Jev does not generate text. You send it one piece of *state* and any number of
typed *questions*, and it answers all of them in a single call with calibrated
probabilities, a chosen option, or a score on a scale you define. That makes it
a good fit for classification, routing, rubric scoring and automated checks
inside ordinary code.
[![Package Version](https://img.shields.io/hexpm/v/jev_api)](https://hex.pm/packages/jev_api)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/jev_api/)
```sh
gleam add jev_api
```
## Sans-io
This library never touches the network. It builds a
`gleam/http/request.Request(String)` for you to send with whatever HTTP client
suits your target, and decodes the `gleam/http/response.Response(String)` you
get back. That keeps it free of target-specific code and trivial to test: the
core depends only on `gleam_stdlib`, `gleam_http` and `gleam_json`.
On Erlang, [`gleam_httpc`](https://hex.pm/packages/gleam_httpc) sends the
request:
```gleam
import gleam/httpc
import gleam/json
import jev_api
import jev_api/question
pub fn triage(api_key: String, ticket: String) {
let client = jev_api.new(api_key)
let req =
jev_api.evaluate_request(
client,
state: json.string(ticket),
questions: [
#("urgent", question.noul("Does this message express urgency?")),
#(
"team",
question.choice("Which team should handle this?", [
"billing",
"technical",
"other",
]),
),
#(
"frustration",
question.score("How frustrated is the customer?", [
"Calm",
"Frustrated",
"Very angry",
]),
),
],
)
let assert Ok(res) = httpc.send(req)
jev_api.evaluate_response(res)
}
```
On JavaScript, send the same request with
[`gleam_fetch`](https://hex.pm/packages/gleam_fetch) instead. If you are not
using `gleam_http` at all, the request's `body`, `headers`, `method` and URL
fields are plain values, and `evaluate_response` only needs a
`response.Response(status:, headers:, body:)` built from what your client
returned.
## Reading answers
`evaluate_response` returns an `Evaluation`: the concrete model that answered,
token usage, TypeSafe's request id, and one `Answer` per question under the key
you asked it with.
```gleam
import gleam/dict
import jev_api
let assert Ok(evaluation) = jev_api.evaluate_response(res)
case jev_api.answer(evaluation, "urgent") {
Ok(jev_api.NoulAnswer(probability:)) if probability >. 0.8 -> escalate()
_ -> queue()
}
case jev_api.answer(evaluation, "team") {
Ok(jev_api.ChoiceAnswer(choice:, confidence:, ..)) if confidence >. 0.7 ->
route_to(choice)
_ -> route_to_human()
}
case jev_api.answer(evaluation, "frustration") {
// `score` can fall between levels: 1.68 is "Frustrated" leaning "Very angry".
Ok(jev_api.ScoreAnswer(score:, levels:, ..)) -> record(score, levels)
_ -> Nil
}
```
| Question | Answer |
| -------- | ---------------------------------------------- |
| `Noul` | `NoulAnswer(probability)`, 0.0 to 1.0 for "yes" |
| `Choice` | `ChoiceAnswer(choice, probabilities, confidence)` |
| `Score` | `ScoreAnswer(score, levels, confidence)`, where each `Level` has its `index`, the `label` you sent, and its `probability` |
Confidence measures how peaked a distribution is, not how likely the answer is
to be correct. TypeSafe's [confidence guide](https://docs.typesafe.ai/confidence)
covers choosing thresholds.
## Questions
`jev_api/question` has constructors for the common plain-string case:
```gleam
question.noul("Is the customer asking for a human agent?")
question.noul_with_criteria(
"Has the customer contacted support about this before?",
when_true: "Mentions a prior attempt, ticket, or that they asked before",
when_false: "No sign of any previous contact",
)
question.choice("Which team should handle this?", ["billing", "technical", "other"])
question.described_choice("Which team should handle this?", [
#("billing", "Charges, invoices, refunds, subscriptions"),
#("technical", "Bugs, outages, integrations"),
#("other", "None of the above"),
])
question.score("How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"])
```
Instructions and criteria are JSON on the wire, so a structured rubric is just
a `gleam/json` value in the record:
```gleam
question.Score(
instructions: json.string("How large is this pull request?"),
levels: [
json.object([
#("summary", json.string("One change, clearly stated")),
#("signals", json.array(["A single fix or feature"], json.string)),
]),
json.object([
#("summary", json.string("Several independent changes bundled together")),
#("signals", json.array(["Two or more unrelated fixes"], json.string)),
]),
],
)
```
Structured levels come back verbatim in `Level.label` as a `Dynamic`.
## State
`state` is any JSON. TypeSafe recommends an object so each part of the context
has a descriptive name, a string for a single piece of text, or an array for a
sequence of messages or records. A request is budgeted at roughly 32,000
tokens. `null` is rejected with a `ValidationError`.
```gleam
json.object([
#("ticket", json.object([
#("subject", json.string("Duplicate charge")),
#("messages", json.array(messages, message_to_json)),
])),
#("refund_policy", json.string("Duplicate charges are eligible for a refund.")),
])
```
## Errors
`evaluate_response` and `models_response` classify failures by the API's own
error envelope first and the HTTP status second:
| Error | When |
| -------------------------------------- | ---------------------------------------------------------------- |
| `AuthenticationError(message)` | missing, invalid or unauthorised key (401/403) |
| `ValidationError(problems)` | the request body failed validation (422), one problem per field |
| `RateLimited(message, retry_after)` | 429; `retry_after` is the `retry-after` header in seconds |
| `Overloaded(message)` | 529; retry after a delay |
| `ApiError(status, error_type, message)` | any other `error_type` envelope, e.g. `api_usage_error` for an unknown model |
| `UnexpectedResponse(status, body)` | a non-success status this library does not recognise |
| `MalformedResponse(status, body, error)` | a success status whose body did not decode |
`jev_api.request_id(res)` reads TypeSafe's request id from any response, including
errors, for support requests.
## Models and configuration
```gleam
// Ask a different model. `models_request`/`models_response` list the ones
// your account can use (currently `jev-latest` and `jev-preview`).
let client = jev_api.new(api_key) |> jev_api.with_model("jev-preview")
// Go through a proxy or a mock server. A path prefix is kept.
let assert Ok(client) = jev_api.with_base_url(client, "https://proxy.internal/typesafe")
```
Not modelled yet: the undocumented `bounding_box` question type the API
advertises in its validation errors.
## Development
The toolchain is pinned in `mise.toml` (Gleam 1.18.1, Erlang/OTP 29).
```sh
gleam test # unit tests against captured API responses
TYPESAFE_API_KEY=... gleam dev # live smoke test: lists models, evaluates a ticket
gleam format src test dev
```
### Releasing
Releases are automated with [`version_bump`](https://hex.pm/packages/version_bump),
a Gleam port of semantic-release, installed as a dev dependency and configured
under `[tools.version_bump]` in `gleam.toml`. Write
[Conventional Commits](https://www.conventionalcommits.org): `fix:` cuts a
patch, `feat:` a minor, and a `BREAKING CHANGE:` footer or `!` a major. Commits
of other types release nothing.
The package stays in 0.x while the API surface settles. `initial_development =
true` gives SemVer's initial-development semantics: the first release is 0.1.0,
and a breaking change bumps the minor rather than the major, so `0.3.1` becomes
`0.4.0`. To leave 0.x, cut 1.0.0 by hand (set `version` in `gleam.toml`,
commit, tag it `v1.0.0` and `gleam publish`); version_bump then bumps from that
tag and the flag has no further effect.
Preview the next version and release notes locally:
```sh
gleam run -m version_bump -- --dry-run
```
On every push to `main`, `.github/workflows/release.yml` runs the tests, then
`gleam run -m version_bump`, which writes the version into `gleam.toml`,
commits and tags it, publishes to Hex and creates a GitHub Release. It needs
one secret, `HEX_API_KEY` (a hex.pm key with publish permission), and the
workflow's `contents: write` permission covers the push and the release.