README.md

<p align="center">
  <img src="assets/DSPex.svg" alt="DSPex Logo" width="200">
</p>

<p align="center">
  <strong>DSPy for Elixir via SnakeBridge</strong><br>
  Declarative LLM programming with full access to Stanford's DSPy framework
</p>

<p align="center">
  <a href="https://hex.pm/packages/dspex"><img src="https://img.shields.io/hexpm/v/dspex.svg" alt="Hex Version"></a>
  <a href="https://hex.pm/packages/dspex"><img src="https://img.shields.io/hexpm/dt/dspex.svg" alt="Hex Downloads"></a>
  <a href="https://hexdocs.pm/dspex"><img src="https://img.shields.io/badge/docs-hexdocs-blue.svg" alt="Documentation"></a>
  <a href="https://github.com/nshkrdotcom/dspex/blob/main/LICENSE"><img src="https://img.shields.io/hexpm/l/dspex.svg" alt="License"></a>
</p>

---

## Overview

DSPex brings [DSPy](https://github.com/stanfordnlp/dspy) — Stanford's framework for programming language models — to Elixir. Rather than generating wrapper code, DSPex provides a minimal, transparent interface through [SnakeBridge](https://github.com/nshkrdotcom/snakebridge)'s Universal FFI. Call any DSPy function directly from Elixir with full type safety and automatic Python lifecycle management.

**Why DSPex?**

- **Zero boilerplate** — No code generation needed, just call Python directly
- **Full DSPy access** — Signatures, Predict, ChainOfThought, optimizers, and more
- **100+ LLM providers** — OpenAI, Anthropic, Google, Ollama, and anything LiteLLM supports
- **Production-ready timeouts** — Built-in profiles for ML inference workloads
- **Elixir-native error handling** — `{:ok, result}` / `{:error, reason}` everywhere

## Installation

Add DSPex to your `mix.exs`:

```elixir
def deps do
  [
    {:dspex, "~> 0.4.0"}
  ]
end
```

Create `config/runtime.exs` for Python bridge configuration:

```elixir
import Config
SnakeBridge.ConfigHelper.configure_snakepit!()
```

Then install dependencies and set up Python:

```bash
mix deps.get
mix snakebridge.setup  # Installs dspy-ai automatically
```

## Quick Start

```elixir
DSPex.run(fn ->
  # 1. Create and configure a language model
  lm = DSPex.lm!("gemini/gemini-flash-lite-latest")
  DSPex.configure!(lm: lm)

  # 2. Create a predictor with a signature
  predict = DSPex.predict!("question -> answer")

  # 3. Run inference
  result = DSPex.method!(predict, "forward", [], question: "What is the capital of France?")
  answer = DSPex.attr!(result, "answer")

  IO.puts(answer)  # => "Paris"
end)
```

## Core Concepts

### Signatures

DSPy signatures define input/output contracts using a simple arrow syntax:

```elixir
# Single input/output
predict = DSPex.predict!("question -> answer")

# Multiple fields
predict = DSPex.predict!("context, question -> answer")

# Rich multi-field signatures
predict = DSPex.predict!("title, content -> category, keywords, sentiment")
```

### Modules

DSPex supports all DSPy modules:

```elixir
# Simple prediction
predict = DSPex.predict!("question -> answer")

# Chain-of-thought reasoning (includes intermediate steps)
cot = DSPex.chain_of_thought!("question -> answer")
result = DSPex.method!(cot, "forward", [], question: "What is 15% of 80?")
reasoning = DSPex.attr!(result, "reasoning")  # Shows step-by-step thinking
answer = DSPex.attr!(result, "answer")
```

### Language Models

Any LiteLLM-compatible provider works out of the box:

```elixir
# Google Gemini (default)
lm = DSPex.lm!("gemini/gemini-flash-lite-latest", temperature: 0.7)

# OpenAI
lm = DSPex.lm!("openai/gpt-4o-mini")

# Anthropic
lm = DSPex.lm!("anthropic/claude-3-sonnet-20240229")

# Local Ollama
lm = DSPex.lm!("ollama/llama2")
```

### Direct LM Calls

Bypass modules and call the LM directly:

```elixir
lm = DSPex.lm!("gemini/gemini-flash-lite-latest")
DSPex.configure!(lm: lm)

# Direct call with messages
messages = [%{role: "user", content: "Say hello in French"}]
response = DSPex.method!(lm, "forward", [messages])
```

## Examples

DSPex includes 18 comprehensive examples demonstrating various use cases:

Use `mix run --no-start` so DSPex owns the Snakepit lifecycle and closes the
process registry DETS cleanly (avoids repair warnings after unclean exits).

| Example | Description | Run Command |
|---------|-------------|-------------|
| `basic.exs` | Simple Q&A prediction | `mix run --no-start examples/basic.exs` |
| `chain_of_thought.exs` | Reasoning with visible steps | `mix run --no-start examples/chain_of_thought.exs` |
| `qa_with_context.exs` | Context-aware Q&A | `mix run --no-start examples/qa_with_context.exs` |
| `multi_hop_qa.exs` | Multi-hop question answering | `mix run --no-start examples/multi_hop_qa.exs` |
| `rag.exs` | Retrieval-augmented generation | `mix run --no-start examples/rag.exs` |
| `custom_signature.exs` | Signatures with instructions | `mix run --no-start examples/custom_signature.exs` |
| `multi_field.exs` | Multiple inputs/outputs | `mix run --no-start examples/multi_field.exs` |
| `classification.exs` | Sentiment analysis | `mix run --no-start examples/classification.exs` |
| `entity_extraction.exs` | Extract people, orgs, locations | `mix run --no-start examples/entity_extraction.exs` |
| `code_gen.exs` | Code generation with reasoning | `mix run --no-start examples/code_gen.exs` |
| `math_reasoning.exs` | Complex math problem solving | `mix run --no-start examples/math_reasoning.exs` |
| `summarization.exs` | Text summarization | `mix run --no-start examples/summarization.exs` |
| `translation.exs` | Multi-language translation | `mix run --no-start examples/translation.exs` |
| `custom_module.exs` | Custom module composition | `mix run --no-start examples/custom_module.exs` |
| `optimization.exs` | BootstrapFewShot optimization | `mix run --no-start examples/optimization.exs` |
| `flagship_multi_pool_gepa.exs` | Multi-pool GEPA + numpy analytics pipeline | `mix run --no-start examples/flagship_multi_pool_gepa.exs` |
| `direct_lm_call.exs` | Direct LM interaction | `mix run --no-start examples/direct_lm_call.exs` |
| `timeout_test.exs` | Timeout configuration demo | `mix run --no-start examples/timeout_test.exs` |

For the full multi-pool GEPA walkthrough, see `guides/flagship_multi_pool_gepa.md`.

## Timeout Configuration

DSPex leverages SnakeBridge's timeout architecture, designed for ML inference 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

```elixir
# Use a different profile
DSPex.method!(predict, "forward", [],
  question: "Complex analysis...",
  __runtime__: [timeout_profile: :batch_job]
)

# Set exact timeout in milliseconds
DSPex.method!(predict, "forward", [],
  question: "Quick question",
  __runtime__: [timeout: 30_000]  # 30 seconds
)

# Helper functions
opts = DSPex.with_timeout([question: "test"], timeout: 60_000)
DSPex.method!(predict, "forward", [], opts)

# Profile helper
DSPex.method!(predict, "forward", [],
  Keyword.merge([question: "test"], DSPex.timeout_profile(:batch_job))
)
```

### Global Configuration

```elixir
# config/config.exs
config :snakebridge,
  runtime: [
    library_profiles: %{"dspy" => :ml_inference}
  ]
```

## API Reference

DSPex provides a thin wrapper over SnakeBridge's Universal FFI:

### Lifecycle

| Function | Description |
|----------|-------------|
| `DSPex.run/1,2` | Wrap code in Python lifecycle management |

### DSPy Helpers

| Function | Description |
|----------|-------------|
| `DSPex.lm/1,2` | Create a DSPy language model |
| `DSPex.configure/0,1` | Configure DSPy global settings |
| `DSPex.predict/1,2` | Create a Predict module |
| `DSPex.chain_of_thought/1,2` | Create a ChainOfThought module |

### Universal FFI

| Function | Description |
|----------|-------------|
| `DSPex.call/2-4` | Call any Python function or class |
| `DSPex.method/2-4` | Call a method on a Python object |
| `DSPex.attr/2` | Get an attribute from a Python object |
| `DSPex.set_attr/3` | Set an attribute on a Python object |
| `DSPex.get/2` | Get a module attribute |
| `DSPex.ref?/1` | Check if a value is a Python object reference |
| `DSPex.bytes/1` | Encode binary data as Python bytes |

### Timeout Helpers

| Function | Description |
|----------|-------------|
| `DSPex.with_timeout/2` | Add timeout options to call opts |
| `DSPex.timeout_profile/1` | Get timeout profile opts |
| `DSPex.timeout_ms/1` | Get exact timeout opts |

All functions have `!` variants that raise on error instead of returning `{:error, reason}`.

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                    Your Elixir App                      │
├─────────────────────────────────────────────────────────┤
│                      DSPex.run/1                        │
│              (Python lifecycle wrapper)                 │
├─────────────────────────────────────────────────────────┤
│                   SnakeBridge.call/4                    │
│                   (Universal FFI)                       │
├─────────────────────────────────────────────────────────┤
│                    Snakepit gRPC                        │
│              (Python process bridge)                    │
├─────────────────────────────────────────────────────────┤
│                     Python DSPy                         │
│            (Stanford's LLM framework)                   │
├─────────────────────────────────────────────────────────┤
│                   LLM Providers                         │
│     (OpenAI, Anthropic, Google, Ollama, etc.)           │
└─────────────────────────────────────────────────────────┘
```

**Key Design Principles:**

- **Minimal wrapper** — DSPex delegates to SnakeBridge, no magic
- **No code generation** — Call Python directly at runtime
- **Automatic lifecycle** — Snakepit manages Python processes
- **Session-aware** — Maintains Python state across calls
- **Thread-safe** — gRPC bridge handles concurrency

## Requirements

- **Elixir** ~> 1.18
- **Python** 3.8+
- **API Key** — Set `GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. based on your provider

## Related Projects

- [DSPy](https://github.com/stanfordnlp/dspy) — The Python framework DSPex wraps
- [SnakeBridge](https://github.com/nshkrdotcom/snakebridge) — The Python-Elixir bridge powering DSPex
- [Snakepit](https://github.com/nshkrdotcom/snakepit) — Python process pool and gRPC server

## License

MIT License. See [LICENSE](LICENSE) for details.