Current section
Files
Jump to
Current section
Files
jev_elixir
README.md
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).