Current section
Files
Jump to
Current section
Files
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. No code generation needed - just call Python directly.
## Quick Start
DSPex.run(fn ->
# Configure LM
lm = DSPex.lm!("openai/gpt-4o-mini")
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.7.7+'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).
## Example
DSPex.run(fn ->
lm = DSPex.lm!("openai/gpt-4o-mini")
DSPex.configure!(lm: lm)
# ... your DSPy code
end)
"""
def run(fun, opts \\ []) when is_function(fun, 0) do
Snakepit.run_as_script(fun, opts)
end
# ---------------------------------------------------------------------------
# DSPy-specific helpers
# ---------------------------------------------------------------------------
@doc """
Create a DSPy language model.
## Examples
{:ok, lm} = DSPex.lm("openai/gpt-4o-mini")
{: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), to: SnakeBridge
@doc "Bang version of attr/2."
defdelegate attr!(ref, attribute), to: SnakeBridge
@doc "Set an attribute on a Python object reference."
defdelegate set_attr(ref, attribute, value), 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