Questions

Shorthands

security: "Is this a vulnerability?"                # Noul
kind:     {"Kind?", %{bug: "Broken", other: nil}}   # Choice, 2..255
severity: {"Severity?", ["Low", "Mid", "High"]}     # Score, 2..10

A string or map is a yes/no question. A map of atom labels is a choice. A list of levels is a score.

Structs

%Jev.Noul{instructions: q, criteria: %{true: "...", false: "..."}}
%Jev.Choice{instructions: q, criteria: %{label: "..."}}
%Jev.Score{instructions: q, criteria: ["...", "..."]}

Instructions and every description accept text or a JSON-encodable map.

Normalize

Jev.questions(kind: {...}, security: "...")
#=> %{kind: %Jev.Choice{}, security: %Jev.Noul{}}

Raises on an empty list, a reserved name, or bad arity.

Reserved names

confidence, probabilities, usage, model

Reply

Shape

%{
  kind: :bug,          # choice → atom
  severity: 2.4,       # score  → float
  security: 0.03,      # noul   → probability
  confidence: %{kind: 0.91, severity: 0.62},
  probabilities: %{kind: %{bug: 0.93, ...},
                   severity: %{0 => 0.1, ...}},
  usage: %{input_tokens: 812, output_tokens: 0, cost: 3.4e-5},
  model: "jev-1.13.0"
}

Matching

def handle_answer(%{security: p}, tag, s) when p > 0.5
def handle_answer(%{kind: k, confidence: %{kind: c}}, tag, s) when c > 0.85
def handle_answer(%{probabilities: %{kind: ps}}, tag, s)
def handle_answer({:error, %Jev.Error{status: 429}}, tag, s)
def handle_answer({:error, reason}, tag, s)

With stdlib

with %{kind: k, confidence: %{kind: c}} when c > 0.85 <- reply, do: act(k)
Enum.max_by(reply.probabilities.kind, &elem(&1, 1))
round(reply.severity)

Server

Define

defmodule Triage do
  use Jev.Server

  def init(arg), do: {:ok, %{}}
  def handle_call(msg, from, s), do: {:reply, {from, state, questions}, s}
  def handle_answer(reply, from, s), do: ...
end

Send to Jev

{:reply, {tag, state, questions}, s}
{:reply, {tag, state, [questions], [model: "jev-preview"]}, s}
{:reply, {tag, state, [questions], [endpoint: :laya]}, s}
{:reply, {tag, state, [questions], [backend: MyModel]}, s}

From any callback. The answer arrives at handle_answer(reply, tag, s).

Other returns

{:noreply, s}
{:noreply, s, timeout | :hibernate | {:continue, term}}
{:stop, reason, s}

Start

Jev.Server.start_link(Triage, arg, name: Triage)
children = [{Triage, arg}]

Answer a caller

GenServer.reply(from, result)

Transport

Call

Jev.HTTP.post(state, questions, opts \\ [])
#=> {:ok, reply} | {:error, %Jev.Error{} | %JSONCodec.Error{} | exception}

Options

endpoint, model, api_key, base_url, max_retries, receive_timeout, tag

Endpoints

Jev.HTTP.endpoint(endpoint: :laya)
#=> %{name: :laya, base_url: "...", api_key: nil, model: "jev-latest", usd_per_million_input: 0.0, ...}

Error

%Jev.Error{status: 422, body: %{...}, request_id: "req_...", endpoint: :typesafe}

Retries 429 and 529 with backoff before returning.

Configuration

Application

config :jev,
  api_key: "...",                     # or TYPESAFE_API_KEY
  base_url: "https://api.typesafe.ai",
  model: "jev-latest",
  max_retries: 3,
  receive_timeout: 30_000,
  usd_per_million_input: 0.042,
  req_options: []

Named endpoints

config :jev,
  endpoint: :typesafe,                # the default
  endpoints: [
    laya: [base_url: "http://localhost:8000"],
    jeff: [base_url: "...", api_key: "...", model: "...", usd_per_million_input: 0.007]
  ]

No key sent and zero cost unless given. Transport settings inherited.

Test

config :jev,
  api_key: "test",
  req_options: [plug: {Req.Test, Jev.HTTP}, retry_delay: 0]

Testing

Stub a reply

Req.Test.stub(Jev.HTTP, &Jev.Test.respond(&1, kind: :bug, security: 0.03))
Req.Test.stub(Jev.HTTP, &Jev.Test.error(&1, 529, "overloaded"))

Answers by question name, plus confidence, probabilities, model, usage.

Read the request

{request, conn} = Jev.Test.request(conn)
request["state"]

Pure

Jev.Test.body([kind: :bug], questions)   #=> wire body map
Jev.confidence(%{a: 0.75, b: 0.25})       #=> 0.5

Telemetry

Events

[:jev, :request, :start]      # system_time
[:jev, :request, :stop]       # duration, input_tokens, output_tokens, cost
[:jev, :request, :exception]  # duration
[:jev, :answer]               # confidence, probability

Metadata

backend, endpoint, model, questions, state_hash, tag, status, request_id, confidence; per answer name, type, answer.

Own backend

@behaviour Jev.Backend
def post(state, questions, opts) do
  Jev.Telemetry.span(state, questions, %{backend: __MODULE__, tag: opts[:tag]}, fn ->
    {:ok, reply}
  end)
end

config :jev, backend: MyModel makes it the default.