DSPex is a comprehensive Elixir implementation of DSPy (Declarative Self-improving Language Programs) that provides a unified interface for working with Large Language Models. It combines high-performance native Elixir implementations with seamless Python DSPy integration through Snakepit for complex ML workflows.
DSPex now features a revolutionary bidirectional tool bridge system that provides:
- β Universal DSPy Access: Automatic discovery and exposure of all DSPy classes without manual wrappers
- β Bidirectional Tool Calling: Python DSPy code can call back to Elixir functions during reasoning
- β Real-time Schema Introspection: Automatically discovers DSPy modules, methods, and signatures
- β
Enhanced Metaprogramming: Advanced
defdsyp
macro with automatic tool registration - β Seamless Elixir Integration: Native Elixir modules that transparently call Python DSPy
- β Production-Ready gRPC Bridge: High-performance communication with proper session management
- β Zero-Config Discovery: No manual configuration needed - DSPy modules are auto-discovered
graph TB
A[Elixir Application] --> B[DSPex.Bridge]
B --> C[Universal call_dspy Tool]
C --> D[Snakepit gRPC Bridge]
D --> E[Python DSPy Adapter]
E --> F[DSPy Library]
G[Schema Discovery] --> H[discover_dspy_schema Tool]
H --> D
I[Auto-Generated Wrappers] --> J[defdsyp Macro]
J --> B
DSPex implements a sophisticated multi-layer tool system:
@tool(description="Universal DSPy function caller with introspection")
def call_dspy(module_path: str, function_name: str, args: List, kwargs: Dict)
@tool(description="Discover DSPy module schema with introspection")
def discover_dspy_schema(module_path: str = "dspy")
@tool(description="Configure DSPy with a language model")
def configure_lm(model_type: str, **kwargs)
@tool(description="Check DSPy availability and version")
def check_dspy()
# Direct DSPy interaction
DSPex.Bridge.call_dspy("dspy.Predict", "__init__", %{"signature" => "question -> answer"})
DSPex.Bridge.call_method(instance_ref, "__call__", %{"question" => "What is DSPy?"})
# Schema discovery
{:ok, schema} = DSPex.Bridge.discover_schema("dspy")
# Discovers 70+ DSPy classes automatically
# Instance management
{:ok, {session_id, instance_id}} = DSPex.Bridge.create_instance("dspy.ChainOfThought", %{"signature" => "question -> reasoning, answer"})
# Migrated to use schema bridge internally
{:ok, predictor} = DSPex.Modules.Predict.create("question -> answer")
{:ok, result} = DSPex.Modules.Predict.execute(predictor, %{"question" => "What is Elixir?"})
{:ok, cot} = DSPex.Modules.ChainOfThought.create("question -> reasoning, answer")
{:ok, result} = DSPex.Modules.ChainOfThought.execute(cot, %{"question" => "Explain quantum computing"})
- Problem Solved: Previously required manual wrapper creation for every DSPy class
- Solution: Universal
call_dspy
tool with automatic Python introspection - Result: All DSPy classes are now accessible without writing Elixir wrappers
- Discovers 70+ DSPy classes automatically: Predict, ChainOfThought, ReAct, ProgramOfThought, BootstrapFewShot, MIPRO, ColBERTv2, etc.
- Method introspection: Automatically discovers method signatures, parameters, and docstrings
- Type-aware: Understands constructor requirements and parameter types
- High-performance: Uses Snakepit v0.4.1 gRPC bridge for fast Python communication
- Session management: Proper session affinity and instance storage
- Error handling: Comprehensive error parsing and meaningful error messages
- JSON parameter handling: Automatic serialization/deserialization of complex parameters
The defdsyp
macro generates complete DSPy wrapper modules:
defmodule MyApp.CustomPredictor do
use DSPex.Bridge
defdsyp __MODULE__, "dspy.Predict", %{
execute_method: "__call__",
result_transform: &MyApp.ResultTransforms.prediction_result/1,
methods: %{
"forward" => "forward_predict",
"save" => "save_model"
}
}
end
# Generates:
# - create/2 function
# - execute/3 function
# - call/3 function (stateless)
# - Custom methods: forward_predict/2, save_model/2
User Request
β
DSPex High-Level API (Predict, ChainOfThought, etc.)
β
DSPex.Bridge (Universal Interface)
β
Snakepit gRPC Bridge (Session Management)
β
Python DSPy Adapter (Tool Registration)
β
DSPy Library (70+ Classes)
β
Language Models (Gemini, OpenAI, etc.)
- DSPex.Bridge - Universal interface with
call_dspy
,discover_schema
,create_instance
- DSPex.Modules.* - High-level Elixir modules using the schema bridge internally
- Python Adapter - 17 registered tools for DSPy interaction
- Schema Discovery - Automatic introspection of DSPy classes and methods
- Session Management - Proper instance lifecycle and session affinity
All examples now use the new gRPC system with schema bridge:
- 01_question_answering_pipeline.exs - β Real Gemini API calls, working Predict and ChainOfThought
- 02_code_generation_system.exs - β Advanced reasoning, schema discovery (7 optimizers found)
- 03_document_analysis_rag.exs - β Document analysis pipeline with real LLM responses
- 04_optimization_showcase.exs - β DSPy optimizers and advanced features
- 05_streaming_inference_pipeline.exs - β Streaming inference demonstrations
# Actual results from our system:
$ DSPex.Bridge.discover_schema("dspy")
# Found 74 DSPy classes/functions:
# - Predict (class)
# - ChainOfThought (class)
# - ReAct (class)
# - ProgramOfThought (class)
# - BootstrapFewShot (class)
# - MIPRO (class)
# - ColBERTv2 (class)
# - ... and 67 more
# Start applications
{:ok, _} = Application.ensure_all_started(:snakepit)
{:ok, _} = Application.ensure_all_started(:dspex)
# Configure Gemini
Snakepit.execute_in_session("session", "configure_lm", %{
"model_type" => "gemini",
"api_key" => System.get_env("GOOGLE_API_KEY"),
"model" => "gemini-2.5-flash-lite"
})
# Create and use DSPy modules
{:ok, predictor} = DSPex.Modules.Predict.create("question -> answer")
{:ok, result} = DSPex.Modules.Predict.execute(predictor, %{"question" => "What is Elixir?"})
# Result: "Elixir is a dynamic, functional language designed for building maintainable and scalable applications."
{:ok, cot} = DSPex.Modules.ChainOfThought.create("question -> reasoning, answer")
{:ok, result} = DSPex.Modules.ChainOfThought.execute(cot, %{"question" => "If a train travels 120 miles in 2 hours, what is its average speed?"})
# Result includes step-by-step reasoning and final answer
# Discover available DSPy modules
{:ok, schema} = DSPex.Bridge.discover_schema("dspy")
IO.puts("Found #{map_size(schema)} DSPy classes")
# Direct DSPy calls
{:ok, {session_id, instance_id}} = DSPex.Bridge.create_instance("dspy.ReAct", %{
"signature" => "task -> thought, action, observation, answer"
})
{:ok, result} = DSPex.Bridge.call_method({session_id, instance_id}, "__call__", %{
"task" => "Create a function to merge two sorted arrays"
})
# Generate a custom DSPy wrapper
defmodule MyApp.Summarizer do
use DSPex.Bridge
defdsyp __MODULE__, "dspy.ChainOfThought", %{
execute_method: "__call__",
result_transform: fn result ->
# Custom result transformation
%{"summary" => get_in(result, ["answer"])}
end
}
end
# Use the generated wrapper
{:ok, summarizer} = MyApp.Summarizer.create(%{"signature" => "document -> summary"})
{:ok, result} = MyApp.Summarizer.execute(summarizer, %{"document" => "Long document text..."})
# mix.exs
def deps do
[
{:dspex, "~> 0.2.0"}
]
end
# Install DSPy and required packages
pip install dspy-ai>=2.6.0
pip install litellm # For LLM provider support
pip install grpcio grpcio-tools # For gRPC bridge
# config/config.exs
config :snakepit,
pooling_enabled: true,
adapter_module: Snakepit.Adapters.GRPCPython,
pool_config: %{
pool_size: 4,
adapter_args: ["--adapter", "dspex_adapters.dspy_grpc.DSPyGRPCHandler"]
}
config :dspex,
default_model: "gemini-2.5-flash-lite"
# Generate documentation for all DSPy modules
{:ok, docs} = DSPex.Bridge.generate_docs("dspy")
File.write!("dspy_docs.md", docs)
# Discover specific module types
{:ok, schema} = DSPex.Bridge.discover_schema("dspy.teleprompt") # Optimizers
{:ok, schema} = DSPex.Bridge.discover_schema("dspy.retrieve") # Retrievers
# Create session-aware instances
{:ok, predictor} = DSPex.Modules.Predict.create("question -> answer",
session_id: "my_session")
# All calls use the same session
{:ok, result1} = DSPex.Modules.Predict.execute(predictor, %{"question" => "First question"})
{:ok, result2} = DSPex.Modules.Predict.execute(predictor, %{"question" => "Second question"})
# Comprehensive error parsing
{:error, error} = DSPex.Modules.Predict.create("invalid signature format")
# Returns: "Invalid signature format"
# Detailed tracebacks for debugging
{:error, error} = DSPex.Bridge.call_dspy("nonexistent.Module", "__init__", %{})
# Returns detailed Python traceback
# All examples now use real Gemini API calls
export GOOGLE_API_KEY=your-api-key
# Basic question answering with real LLM responses
mix run examples/dspy/01_question_answering_pipeline.exs
# Advanced code generation with reasoning
mix run examples/dspy/02_code_generation_system.exs
# Document analysis RAG system
mix run examples/dspy/03_document_analysis_rag.exs
# DSPy optimization showcase
mix run examples/dspy/04_optimization_showcase.exs
# Streaming inference pipeline
mix run examples/dspy/05_streaming_inference_pipeline.exs
$ mix run examples/dspy/01_question_answering_pipeline.exs
=== Question Answering Pipeline Demo ===
1. Basic Prediction
-------------------
β Created Predict instance: {"pipeline_session", "predict_344934"}
Q: What is the capital of France?
A: The capital of France is Paris.
Q: What is 2 + 2?
A: 4
Q: Who wrote Romeo and Juliet?
A: William Shakespeare wrote Romeo and Juliet.
2. Chain of Thought Reasoning
-----------------------------
β Created ChainOfThought instance: {"pipeline_session", "chainofthought_408431"}
Q: If a train travels 120 miles in 2 hours, and then 180 miles in 3 hours, what is its average speed for the entire journey?
Reasoning: To find the average speed for the entire journey, we need to calculate the total distance traveled and divide it by the total time taken.
The train travels 120 miles in the first part of the journey and 180 miles in the second part.
Total distance = Distance 1 + Distance 2 = 120 miles + 180 miles = 300 miles.
The train travels for 2 hours in the first part and 3 hours in the second part.
Total time = Time 1 + Time 2 = 2 hours + 3 hours = 5 hours.
Average speed = Total distance / Total time = 300 miles / 5 hours = 60 miles per hour.
Answer: The average speed for the entire journey is 60 miles per hour.
- Issue: Snakepit was serializing Elixir maps as JSON strings
- Solution: Automatic JSON parsing in Python adapter with DSPy-specific signature handling
- Result: Seamless parameter passing between Elixir and Python
- Sessions: Proper gRPC session management with worker affinity
- Instances: Python objects stored in
_MODULE_STORAGE
with unique IDs - Lifecycle: Automatic cleanup and session-aware instance retrieval
- Raw DSPy Results β Python Serialization β Elixir Transformation β User-Friendly Format
- Handles: completions, prediction_data, reasoning/answer pairs, error states
- Extensible: Custom result transformers via
defdsyp
configuration
# Run all tests
mix test
# Run specific test layers
mix test.fast # Mock adapter tests (~70ms)
mix test.protocol # Protocol tests
mix test.integration # Full integration tests with real DSPy
# Test schema bridge specifically
mix test test/dspex/bridge_test.exs
- gRPC Bridge Latency: ~2-5ms per call
- DSPy Module Creation: ~50-100ms (includes LLM configuration)
- Inference Calls: Depends on LLM provider (Gemini: ~500-2000ms)
- Schema Discovery: ~200ms for full DSPy introspection (70+ classes)
# Built-in session and performance monitoring
Snakepit.pool_status()
DSPex.Bridge.discover_schema("dspy") |> map_size() # Check available modules
- Universal DSPy bridge with automatic discovery
- Schema-driven code generation with
defdsyp
macro - Production gRPC integration with Snakepit v0.4.1
- All 5 examples working with real API calls
- Comprehensive error handling and debugging
- Session management and instance lifecycle
- Performance optimization and caching
- Additional DSPy module wrappers (ReAct, ProgramOfThought)
- Streaming inference support
- Advanced optimization techniques
- Distributed DSPy execution
- Model management and versioning
- Comprehensive benchmarking suite
- Visual pipeline builder
- Advanced monitoring and observability
DSPex is built with a sophisticated architecture that makes contributions straightforward:
- Python Tools: Add new
@tool
decorated methods to the Python adapter - Elixir Modules: Create new modules using
DSPex.Bridge.call_dspy
- Auto-Generated Wrappers: Use the
defdsyp
macro for custom DSPy integrations - Examples: All examples use the schema bridge system
# Development setup
git clone https://github.com/nshkrdotcom/dspex.git
cd dspex
mix deps.get
mix test
# Interactive development
iex -S mix
MIT
- DSPy - The revolutionary Python framework that inspired this implementation
- Snakepit - The high-performance Python integration bridge that makes this possible
- LiteLLM - Universal LLM API interface used for model access
- The Elixir Community - For building an amazing ecosystem that makes projects like this possible
DSPex - Bringing the power of DSPy to the Elixir ecosystem with production-ready performance and developer-friendly APIs.