Program language models in Elixir, measure what they do, and improve them from examples.
Imp brings DSPy's idea to the BEAM: instead of writing prompt strings and parsing whatever comes back, you declare what a step takes and returns, run it as an ordinary Elixir value, score it on examples, and let an optimizer make it better. The model is used where judgment is needed; everything around it stays plain, testable code.
Say your support tickets go to four squads with internal names: atlas owns money, harbor the platform, beacon identity, quill the product.
lm = Imp.req_llm("openai:gpt-5.4-mini", api_key: System.fetch_env!("OPENAI_API_KEY"))
router =
"ticket -> team: enum[atlas,harbor,beacon,quill]"
|> Imp.signature("Route the support ticket to the squad that owns it.")
|> Imp.predict(lm: lm, adapter: Imp.Adapter.JSON)
{:ok, prediction} = Imp.call(router, %{ticket: "We were charged twice this month."})
Imp.get(prediction, :team)
#=> "harbor"Imp writes the prompt from the signature and checks the answer against it, so the team is always one of the four, never free text. It is also wrong: a double charge is money, which is atlas. The model is guessing, because nothing tells it what your squad names mean.
Imp ships sixty labeled tickets for this router, split into training and test sets. Score the router on the test set, give it examples, and score it again:
data = :imp |> Application.app_dir("priv/tutorial/support_tickets.json") |> File.read!() |> Jason.decode!()
examples = fn rows ->
for %{"ticket" => t, "team" => team} <- rows,
do: Imp.example(ticket: t, team: team) |> Imp.with_inputs(:ticket)
end
metric = Imp.exact_match(:team)
Imp.evaluate(router, examples.(data["test"]), metric).score
#=> 0.25
improved =
Imp.optimize!(router, Imp.Optimizer.LabeledFewShot.new(k: 8), examples.(data["train"]))
Imp.evaluate(improved, examples.(data["test"]), metric).score
#=> 0.75The optimizer added eight solved tickets from the training set to the program.
In six runs with gpt-5.4-mini the router went from 20โ45% to 75โ85% on
tickets it never saw, for about a cent each. The improvement is data you can
read (improved.demos), save with the program, and review like any other
change. Stronger optimizers search over instructions and examples when a task
needs more.
An Imp program is a value. Test it with a scripted model instead of a provider, save the improved version without credentials, and serve it from a supervised process with bounded concurrency and timeouts. The same pieces grow into tools and agents, retrieval, multi-step programs, and a dozen optimizers, when a task needs them.
The core is stable: the Imp facade, signatures, adapters, Imp.predict,
Imp.chain_of_thought and Imp.react, evaluation, tools, telemetry and
saving. The reference groups the other optimizers, agent loops, training
integrations and runs under Experimental optimizers and advanced
workflows; those may change before 1.0.
{:imp, "~> 0.5"}Imp needs Elixir 1.19 and a C++ compiler for one dependency (erlexec).
- Getting started builds this router step by step, from the first call to running it in an application.
- Coming from DSPy maps what you already know.
- Cheatsheet has the common calls on one page.
Imp is MIT licensed.
