# `DSPex`
[🔗](https://github.com/nshkrdotcom/dspex/blob/v0.11.0/lib/dspex.ex#L1)

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.

# `attr`

Get an attribute from a Python object reference.

# `attr!`

Bang version of attr/3.

# `bytes`

Encode binary data as Python bytes.

# `call`

Call any DSPy function or class.

## Examples

    {:ok, result} = DSPex.call("dspy", "Predict", ["question -> answer"])
    {:ok, result} = DSPex.call("dspy.teleprompt", "BootstrapFewShot", [], metric: metric)

# `call!`

Bang version - raises on error, returns value directly.

# `chain_of_thought`

Create a ChainOfThought module.

## Examples

    {:ok, cot} = DSPex.chain_of_thought("question -> answer")

# `chain_of_thought!`

Bang version of chain_of_thought/2.

# `configure`

Configure DSPy global settings.

## Examples

    :ok = DSPex.configure(lm: lm)
    :ok = DSPex.configure(lm: lm, rm: retriever)

# `configure!`

Bang version of configure/1 - raises on error.

# `get`

Get a module attribute.

# `get!`

Bang version of get/2.

# `lm`

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)

# `lm!`

Bang version of lm/2 - raises on error.

# `method`

Call a method on a Python object reference.

# `method!`

Bang version of method/4.

# `predict`

Create a Predict module.

## Examples

    {:ok, predict} = DSPex.predict("question -> answer")
    {:ok, predict} = DSPex.predict("context, question -> answer")

# `predict!`

Bang version of predict/2.

# `ref?`

Check if a value is a Python object reference.

# `run`

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)

# `set_attr`

Set an attribute on a Python object reference.

# `timeout_ms`

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))
    )

# `timeout_profile`

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))
    )

# `with_timeout`

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)
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
