Current section
Files
Jump to
Current section
Files
lib/dspex.ex
defmodule DSPex do
@moduledoc """
DSPex provides a unified interface for DSPy functionality in Elixir.
This is the main entry point for all DSPex operations. Implementation details
are hidden - operations are automatically routed to native Elixir or Python
implementations based on availability and performance characteristics.
## Basic Usage
# Parse a signature
{:ok, signature} = DSPex.signature("question: str -> answer: str")
# Execute a prediction
{:ok, result} = DSPex.predict(signature, %{question: "What is DSPy?"})
# Use chain of thought
{:ok, cot_result} = DSPex.chain_of_thought(signature, %{question: "Explain quantum computing"})
## Pipelines
pipeline = DSPex.pipeline([
{:native, DSPex.Native.Signature, spec: "query -> keywords: list[str]"},
{:python, "dspy.ChainOfThought", signature: "keywords -> summary"},
{:native, DSPex.Native.Template, template: "Summary: <%= @summary %>"}
])
{:ok, result} = DSPex.run_pipeline(pipeline, %{query: "machine learning"})
"""
alias DSPex.Pipeline
# Type aliases for cleaner specs
@type signature :: DSPex.Native.Signature.t()
@type signature_compiled :: %{
signature: DSPex.Native.Signature.t(),
validator: (map() -> {:ok, map()} | {:error, term()}),
serializer: (map() -> {binary(), map()}),
compiled_at: DateTime.t()
}
# Signatures - always native for performance
@doc """
Parse a DSPy signature specification.
Signatures define the input and output schema for ML operations.
## Examples
# Simple signature
DSPex.signature("question -> answer")
# With types
DSPex.signature("question: str -> answer: str, confidence: float")
# With descriptions
DSPex.signature("question: str 'User query' -> answer: str 'Generated response'")
"""
@spec signature(String.t() | map()) :: {:ok, signature()} | {:error, term()}
defdelegate signature(spec), to: DSPex.Native.Signature, as: :parse
@doc """
Compile a signature for optimized repeated use.
"""
@spec compile_signature(binary()) :: {:ok, signature_compiled()} | {:error, term()}
defdelegate compile_signature(spec), to: DSPex.Native.Signature, as: :compile
# Module operations - routed to appropriate implementation
@doc """
Basic prediction using a signature.
## Options
* `:temperature` - LLM temperature (0.0-2.0)
* `:max_tokens` - Maximum tokens to generate
* `:model` - Specific model to use
* `:stream` - Enable streaming responses
"""
@spec predict(signature(), map(), keyword()) :: {:ok, map()} | {:error, term()}
def predict(signature, inputs, opts \\ []) do
DSPex.Modules.Predict.predict(signature, inputs, opts)
end
@doc """
Chain of Thought reasoning.
Generates step-by-step reasoning before the final answer.
"""
@spec chain_of_thought(signature(), map(), keyword()) :: {:ok, map()} | {:error, term()}
def chain_of_thought(signature, inputs, opts \\ []) do
DSPex.Modules.ChainOfThought.think(signature, inputs, opts)
end
@doc """
ReAct pattern - Reasoning + Acting with tools.
## Example
tools = [
%{name: "search", description: "Search the web", function: &search/1},
%{name: "calculate", description: "Perform calculations", function: &calc/1}
]
DSPex.react(signature, inputs, tools)
"""
@spec react(signature(), map(), list(map()), keyword()) ::
{:ok, map()} | {:error, term()}
def react(signature, inputs, tools, opts \\ []) do
DSPex.Modules.ReAct.reason_and_act(signature, inputs, tools, opts)
end
# Pipeline operations
@doc """
Create a pipeline of operations.
Pipelines can mix native and Python implementations seamlessly.
"""
@spec pipeline(list(Pipeline.step())) :: Pipeline.t()
defdelegate pipeline(steps), to: Pipeline, as: :new
@doc """
Execute a pipeline with given input.
## Options
* `:timeout` - Overall pipeline timeout (default: 30000ms)
* `:continue_on_error` - Continue execution on step failure
* `:stream` - Enable streaming for supported steps
"""
@spec run_pipeline(Pipeline.t(), map(), keyword()) :: {:ok, term()} | {:error, term()}
defdelegate run_pipeline(pipeline, input, opts \\ []), to: Pipeline, as: :run
# LLM operations
@doc """
Create a new LLM client for direct model interactions.
## Examples
# Using InstructorLite
{:ok, client} = DSPex.lm_client(
adapter: :instructor_lite,
provider: :openai,
api_key: System.get_env("OPENAI_API_KEY")
)
# Using HTTP adapter
{:ok, client} = DSPex.lm_client(
adapter: :http,
provider: :anthropic,
api_key: System.get_env("ANTHROPIC_API_KEY")
)
"""
@spec lm_client(keyword()) :: {:ok, map()} | {:error, term()}
defdelegate lm_client(opts), to: DSPex.LLM.Client, as: :new
@doc """
Generate text using an LLM client.
"""
@spec lm_generate(
%{:adapter_module => atom(), any() => any()},
binary() | list(map()),
Keyword.t()
) :: {:error, any()} | {:ok, map()}
defdelegate lm_generate(client, prompt, opts \\ []), to: DSPex.LLM.Client, as: :generate
# Utility functions
@doc """
Validate data against a signature.
"""
@spec validate(map(), signature()) :: :ok | {:error, list(String.t())}
defdelegate validate(data, signature), to: DSPex.Native.Validator
@doc """
Render a template with context.
"""
@spec render_template(String.t(), map()) :: String.t()
defdelegate render_template(template, context), to: DSPex.Native.Template, as: :render
@doc """
Check system health and status.
"""
@spec health_check() :: %{
status: :ok,
version: String.t(),
pools: map(),
native_modules: term(),
python_modules: term()
}
def health_check do
%{
status: :ok,
version: "0.1.0",
snakepit_status: snakepit_status(),
native_modules: DSPex.Native.Registry.list()
}
end
defp snakepit_status do
case Snakepit.get_stats() do
stats when is_map(stats) ->
Map.put(stats, :status, :running)
_ ->
%{status: :not_available}
end
catch
_, _ -> %{status: :error}
end
end