Current section

Files

Jump to
dspex lib dspex.ex
Raw

lib/dspex.ex

defmodule DSPex do
@moduledoc """
DSPex - DSPy for Elixir via SnakeBridge.
Minimal wrapper that provides transparent access to DSPy through SnakeBridge's
Universal FFI, alongside the generated `Dspy.*` bindings for the full API surface.
## Quick Start
DSPex.run(fn ->
# Configure LM
lm = DSPex.lm!("gemini/gemini-flash-lite-latest")
DSPex.configure!(lm: lm)
# Create predictor and run
predict = DSPex.call!("dspy", "Predict", ["question -> answer"])
result = DSPex.method!(predict, "forward", [], question: "What is 2+2?")
# Get the answer
answer = DSPex.attr!(result, "answer")
IO.puts("Answer: \#{answer}")
end)
## Timeout Configuration
DSPex leverages SnakeBridge 0.13+'s timeout architecture for LLM workloads.
By default, all DSPy calls use the `:ml_inference` profile (10 minute timeout).
### Timeout Profiles
| Profile | Timeout | Use Case |
|-----------------|----------|---------------------------------------|
| `:default` | 2 min | Standard Python calls |
| `:streaming` | 30 min | Streaming responses |
| `:ml_inference` | 10 min | LLM inference (DSPex default) |
| `:batch_job` | 1 hour | Long-running batch operations |
### Per-Call Timeout Override
Override timeout for individual calls using `__runtime__` option:
# Use a different profile
DSPex.method!(predict, "forward", [],
question: "Complex question",
__runtime__: [timeout_profile: :batch_job]
)
# Set exact timeout in milliseconds
DSPex.method!(predict, "forward", [],
question: "Quick question",
__runtime__: [timeout: 30_000] # 30 seconds
)
### Global Configuration
Configure timeouts in `config/config.exs`:
config :snakebridge,
runtime: [
library_profiles: %{"dspy" => :ml_inference},
# Or set global default:
# timeout_profile: :ml_inference
]
## Architecture
DSPex uses SnakeBridge's Universal FFI to call DSPy directly:
Elixir (DSPex.call/4)
↓
SnakeBridge.call/4
↓
Snakepit gRPC
↓
Python DSPy
↓
LLM Providers
All Python lifecycle is managed automatically by Snakepit.
"""
@doc """
Run DSPex code with automatic Python lifecycle management.
Wraps your code in `Snakepit.run_as_script/2` which:
- Starts the Python process pool
- Runs your code
- Cleans up on exit
Pass `halt: true` in opts if you need to force the BEAM to exit
(for example, when running inside wrapper scripts).
DSPex restarts Snakepit by default so it owns the runtime and can close
persistent resources (like DETS) cleanly. Pass `restart: false` to reuse an
already-started Snakepit instance.
## Example
DSPex.run(fn ->
lm = DSPex.lm!("gemini/gemini-flash-lite-latest")
DSPex.configure!(lm: lm)
# ... your DSPy code
end)
"""
def run(fun, opts \\ []) when is_function(fun, 0) do
opts = Keyword.put_new(opts, :restart, true)
Snakepit.run_as_script(
fn ->
ensure_snakebridge_started!()
fun.()
end,
opts
)
end
defp ensure_snakebridge_started! do
case Application.ensure_all_started(:snakebridge) do
{:ok, _started} ->
:ok
{:error, {:snakebridge, reason}} ->
raise "Failed to start snakebridge: #{inspect(reason)}"
{:error, reason} ->
raise "Failed to start snakebridge: #{inspect(reason)}"
end
end
# ---------------------------------------------------------------------------
# DSPy-specific helpers
# ---------------------------------------------------------------------------
@doc """
Create a DSPy language model.
## Examples
{:ok, lm} = DSPex.lm("gemini/gemini-flash-lite-latest")
{:ok, lm} = DSPex.lm("anthropic/claude-3-sonnet-20240229", temperature: 0.7)
"""
def lm(model, opts \\ []) do
SnakeBridge.call("dspy", "LM", [model], opts)
end
@doc "Bang version of lm/2 - raises on error."
def lm!(model, opts \\ []) do
SnakeBridge.call!("dspy", "LM", [model], opts)
end
@doc """
Configure DSPy global settings.
## Examples
:ok = DSPex.configure(lm: lm)
:ok = DSPex.configure(lm: lm, rm: retriever)
"""
def configure(opts \\ []) do
case SnakeBridge.call("dspy", "configure", [], opts) do
{:ok, _} -> :ok
error -> error
end
end
@doc "Bang version of configure/1 - raises on error."
def configure!(opts \\ []) do
SnakeBridge.call!("dspy", "configure", [], opts)
:ok
end
@doc """
Create a Predict module.
## Examples
{:ok, predict} = DSPex.predict("question -> answer")
{:ok, predict} = DSPex.predict("context, question -> answer")
"""
def predict(signature, opts \\ []) do
SnakeBridge.call("dspy", "Predict", [signature], opts)
end
@doc "Bang version of predict/2."
def predict!(signature, opts \\ []) do
SnakeBridge.call!("dspy", "Predict", [signature], opts)
end
@doc """
Create a ChainOfThought module.
## Examples
{:ok, cot} = DSPex.chain_of_thought("question -> answer")
"""
def chain_of_thought(signature, opts \\ []) do
SnakeBridge.call("dspy", "ChainOfThought", [signature], opts)
end
@doc "Bang version of chain_of_thought/2."
def chain_of_thought!(signature, opts \\ []) do
SnakeBridge.call!("dspy", "ChainOfThought", [signature], opts)
end
# ---------------------------------------------------------------------------
# Timeout helpers
# ---------------------------------------------------------------------------
@doc """
Add timeout configuration to options.
This is a convenience helper for adding `__runtime__` timeout options.
## Options
* `:timeout` - Exact timeout in milliseconds
* `:timeout_profile` - Use a predefined profile (`:default`, `:streaming`, `:ml_inference`, `:batch_job`)
## Examples
# Set exact timeout
opts = DSPex.with_timeout([], timeout: 60_000) # 1 minute
DSPex.method!(predict, "forward", [], Keyword.merge(opts, question: "..."))
# Use batch profile for long operations
opts = DSPex.with_timeout([question: "complex"], timeout_profile: :batch_job)
DSPex.method!(predict, "forward", [], opts)
# Inline usage
DSPex.method!(predict, "forward", [],
DSPex.with_timeout([question: "test"], timeout: 30_000)
)
"""
def with_timeout(opts, timeout_opts) when is_list(opts) and is_list(timeout_opts) do
runtime = Keyword.get(opts, :__runtime__, [])
new_runtime = Keyword.merge(runtime, timeout_opts)
Keyword.put(opts, :__runtime__, new_runtime)
end
@doc """
Timeout profile atoms for use with `__runtime__` option.
Returns a keyword list ready to merge into call options.
## Examples
DSPex.method!(predict, "forward", [],
Keyword.merge([question: "test"], DSPex.timeout_profile(:batch_job))
)
"""
def timeout_profile(profile)
when profile in [:default, :streaming, :ml_inference, :batch_job] do
[__runtime__: [timeout_profile: profile]]
end
@doc """
Create a timeout option for exact milliseconds.
Returns a keyword list ready to merge into call options.
## Examples
DSPex.method!(predict, "forward", [],
Keyword.merge([question: "test"], DSPex.timeout_ms(120_000))
)
"""
def timeout_ms(milliseconds) when is_integer(milliseconds) and milliseconds > 0 do
[__runtime__: [timeout: milliseconds]]
end
# ---------------------------------------------------------------------------
# Universal FFI pass-through (convenience re-exports)
# ---------------------------------------------------------------------------
@doc """
Call any DSPy function or class.
## Examples
{:ok, result} = DSPex.call("dspy", "Predict", ["question -> answer"])
{:ok, result} = DSPex.call("dspy.teleprompt", "BootstrapFewShot", [], metric: metric)
"""
defdelegate call(module, function, args \\ [], opts \\ []), to: SnakeBridge
@doc "Bang version - raises on error, returns value directly."
defdelegate call!(module, function, args \\ [], opts \\ []), to: SnakeBridge
@doc "Get a module attribute."
defdelegate get(module, attr), to: SnakeBridge
@doc "Bang version of get/2."
defdelegate get!(module, attr), to: SnakeBridge
@doc "Call a method on a Python object reference."
defdelegate method(ref, method, args \\ [], opts \\ []), to: SnakeBridge
@doc "Bang version of method/4."
defdelegate method!(ref, method, args \\ [], opts \\ []), to: SnakeBridge
@doc "Get an attribute from a Python object reference."
defdelegate attr(ref, attribute, opts \\ []), to: SnakeBridge
@doc "Bang version of attr/3."
defdelegate attr!(ref, attribute, opts \\ []), to: SnakeBridge
@doc "Set an attribute on a Python object reference."
defdelegate set_attr(ref, attribute, value, opts \\ []), to: SnakeBridge
@doc "Check if a value is a Python object reference."
defdelegate ref?(value), to: SnakeBridge
@doc "Encode binary data as Python bytes."
defdelegate bytes(data), to: SnakeBridge
end