Packages

An idiomatic Elixir client for Jev decision APIs

Current section

Files

Jump to
jev_elixir README.md
Raw

README.md

# Jev for Elixir

`jev_elixir` is a small, idiomatic interface for making typed decisions with
Jev. It exposes application-level functions such as `Jev.ask?/2` and
`Jev.analyze/3`, while provider adapters handle the external API wire format.

The first release supports TypeSafe's System One API and OpenRouter's Decisions
API. Both providers support all three System One question types: Noul, Choice,
and Score.

## Installation

Add the package to your dependencies:

```elixir
def deps do
  [
    {:jev_elixir, "~> 0.1.0"}
  ]
end
```

Configure the provider and API key at runtime:

```elixir
# config/runtime.exs
import Config

config :jev_elixir,
  provider: :typesafe,
  api_key: System.fetch_env!("TYPESAFE_API_KEY")
```

Runtime configuration keeps the API key out of compiled releases. A call can
override `:provider`, `:api_key`, `:model`, `:endpoint`, or `:timeout` through
its options when needed.

### OpenRouter

OpenRouter serves Jev through its dedicated Decisions API. Configure it with
an OpenRouter key:

```elixir
# config/runtime.exs
import Config

config :jev_elixir,
  provider: :openrouter,
  api_key: System.fetch_env!("OPENROUTER_API_KEY")
```

The adapter sends requests to
`https://openrouter.ai/api/alpha/decisions` using `typesafe/jev-1.13` by
default. The Decisions API is currently an alpha OpenRouter API, so its wire
contract may change. Override the model globally or for one call:

```elixir
config :jev_elixir,
  provider: :openrouter,
  api_key: System.fetch_env!("OPENROUTER_API_KEY"),
  endpoint: "https://openrouter.ai/api/alpha/decisions",
  model: "typesafe/jev-1.13"

Jev.ask?("Is this spam?",
  state: message,
  provider: :openrouter,
  api_key: System.fetch_env!("OPENROUTER_API_KEY"),
  model: "typesafe/jev-1.13",
  threshold: 0.95
)
```

Jev is a decisions model on OpenRouter. The adapter intentionally uses the
Decisions endpoint rather than `/chat/completions`.

## Boolean decisions

`ask?/2` returns a real boolean, which makes it straightforward to match in a
`case` expression or use in a Phoenix controller:

```elixir
def create(conn, %{"comment" => params}) do
  case Jev.ask?("Is this spam?", state: params["body"], threshold: 0.95) do
    true ->
      conn
      |> put_status(:unprocessable_entity)
      |> json(%{error: "spam"})

    false ->
      create_comment(conn, params)
  end
end
```

The threshold is the minimum probability of "yes" required for `ask?/2` to
return `true`. It defaults to `0.5`.

Use `ask/2` when the answer and its probability are both useful:

```elixir
Jev.ask("Does this message require urgent attention?", state: message)
#=> {:yes, 0.91}
```

## Choice and Score

Choice selects one supplied option. The second tuple value is the probability
TypeSafe assigned to the selected option:

```elixir
Jev.choose(
  "Which team should handle this ticket?",
  [:billing, :sales, :technical],
  state: ticket.body
)
#=> {:technical, 0.94}
```

Options can also be a map when descriptions make the categories clearer:

```elixir
Jev.choose(
  "Which team should handle this ticket?",
  %{
    billing: "Payments, invoices, and refunds",
    sales: "Pricing and purchase questions",
    technical: "Bugs, outages, and integrations"
  },
  state: ticket.body
)
```

Score evaluates ordered levels and returns TypeSafe's weighted score and
confidence:

```elixir
Jev.score(
  "How urgent is this ticket?",
  ["Can wait", "Needs attention today", "Immediate response"],
  state: ticket.body
)
#=> {1.82, 0.88}
```

## Analyze several questions in one request

`analyze/3` sends every question with the same state in one provider request:

```elixir
Jev.analyze(ticket.body,
  spam: {:boolean, "Is this spam?"},
  urgent: {:noul, "Does this require urgent attention?"},
  department: {:choice, [:billing, :sales, :technical]},
  sentiment: {:choice, [:positive, :neutral, :negative]},
  urgency: {:score, ["low", "medium", "high"]}
)
#=> %{
#=>   spam: {:no, 0.98},
#=>   urgent: {:yes, 0.91},
#=>   department: {:technical, 0.94},
#=>   sentiment: {:negative, 0.87},
#=>   urgency: {1.82, 0.88}
#=> }
```

`:boolean` is the friendly Jev name for TypeSafe's Noul question. `:noul` is
also accepted. Boolean questions may include TypeSafe criteria:

```elixir
{:noul, "Does this request a refund?",
 %{true: "The customer explicitly asks for money back", false: "No refund requested"}}
```

Choice and Score specifications without instructions derive a short
instruction from their question ID. For production decisions, explicit
instructions usually produce a clearer contract:

```elixir
%{
  department:
    {:choice, [:billing, :sales, :technical],
     "Which department should handle this ticket?"}
}
```

## Common use cases

### Moderate user-generated content

Block high-confidence spam while allowing the normal comment flow to remain
simple:

```elixir
case Jev.ask?("Is this comment spam?", state: comment.body, threshold: 0.95) do
  true -> {:error, :spam}
  false -> Comments.publish(comment)
end
```

Several moderation checks can share one request:

```elixir
signals =
  Jev.analyze(post.body,
    spam: {:boolean, "Is this unsolicited advertising?"},
    harassment: {:boolean, "Does this attack or intimidate a person?"},
    adult_content: {:boolean, "Does this contain explicit adult content?"}
  )

case signals do
  %{harassment: {:yes, probability}} when probability >= 0.9 ->
    Moderation.hold_for_review(post)

  %{adult_content: {:yes, probability}} when probability >= 0.9 ->
    Moderation.apply_age_gate(post)

  _ ->
    Moderation.allow(post)
end
```

### Route customer-support tickets

Use Choice to send work to the most appropriate queue:

```elixir
case Jev.choose(
       "Which team should own this customer request?",
       %{
         billing: "Charges, invoices, refunds, and payouts",
         sales: "Pricing, plans, and purchasing",
         technical: "Bugs, outages, and integrations"
       },
       state: ticket.body
     ) do
  {:billing, probability} when probability >= 0.8 -> Tickets.route(ticket, :billing)
  {:sales, probability} when probability >= 0.8 -> Tickets.route(ticket, :sales)
  {:technical, probability} when probability >= 0.8 -> Tickets.route(ticket, :technical)
  {_team, _probability} -> Tickets.route(ticket, :manual_triage)
end
```

### Prioritize an inbox or work queue

Use Score when the decision is an ordered scale rather than a category:

```elixir
case Jev.score(
       "How urgently should an agent respond?",
       ["Can wait", "Respond today", "Respond immediately"],
       state: ticket.body
     ) do
  {score, confidence} when score >= 1.5 and confidence >= 0.75 -> :high_priority
  {score, _confidence} when score >= 0.75 -> :normal_priority
  {_score, _confidence} -> :low_priority
end
```

### Send suspicious transactions to review

Jev can provide one signal in a wider risk policy. Keep deterministic limits
and account rules in application code:

```elixir
state = %{
  amount: payment.amount,
  currency: payment.currency,
  description: payment.description,
  account_age_days: account.age_days
}

case Jev.ask?(
       "Does this transaction description and account context look suspicious?",
       state: state,
       threshold: 0.9
     ) do
  true -> Payments.send_to_review(payment)
  false -> Payments.continue_policy_checks(payment)
end
```

### Qualify and route leads

Analyze intent and destination together, then keep uncertain results in a
general queue:

```elixir
result =
  Jev.analyze(inquiry,
    purchase_intent: {:boolean, "Is the sender actively evaluating a purchase?"},
    segment:
      {:choice, [:self_service, :small_business, :enterprise],
       "Which customer segment best fits this inquiry?"}
  )

case result do
  %{purchase_intent: {:yes, intent}, segment: {segment, confidence}}
  when intent >= 0.85 and confidence >= 0.8 ->
    Leads.route(inquiry, segment)

  _ ->
    Leads.route(inquiry, :nurture)
end
```

Other useful applications include email triage, marketplace listing review,
survey classification, sentiment tracking, request categorization, and
escalation detection. Choose thresholds from examples drawn from your own
application, then retain a manual-review path for uncertain decisions.

## Return values and errors

The public API normalizes provider responses:

| Question | Result |
| --- | --- |
| Boolean / Noul | `{:yes, probability}` or `{:no, probability}` |
| Choice | `{selected_option, selected_probability}` |
| Score | `{weighted_score, confidence}` |

For a `:no` result, the probability is the probability of no (`1 - noul`).
Provider, HTTP, and invalid-response failures raise `Jev.Error`. Invalid local
arguments raise `ArgumentError`. This lets `ask?/2` keep its useful boolean
contract instead of returning a truthy `{:ok, false}` tuple.

## Provider architecture

Provider modules implement `Jev.Provider.evaluate/3`. `:typesafe` resolves to
`Jev.Provider.TypeSafe`; `:openrouter` resolves to
`Jev.Provider.OpenRouter`. Both share the same question encoder and answer
normalizer. A custom adapter module can be configured directly:

```elixir
config :jev_elixir, provider: MyApp.JevProvider
```

This boundary lets future providers translate the common question DSL and
normalize their results without changing application code.

## Current scope

Version `0.1.1` provides synchronous TypeSafe and OpenRouter calls, runtime
configuration, per-request overrides, the three typed question forms, batch
analysis, and a custom provider behaviour. Future work can add non-raising API
variants, telemetry, retry policy controls, and richer response metadata.

## License

MIT. See [LICENSE](LICENSE).