← Back to 20250617

EXLLM ELIXACT integration

Documentation for EXLLM_ELIXACT_integration from the Ds ex repository.

2. Integration with ex_llm

Fit: This is a perfect complement. ex_llm is a comprehensive framework for communicating with LLMs, while elixact is a library for defining and validating the data you exchange. They can be used together as distinct layers without modification to either library.

How it Would Work (The “Clean Layer” Approach):

You don’t need to change ex_llm at all. You use ex_llm as your LLM “engine” and elixact as your data validation layer.

Here is a typical workflow for your DSPy port:

  1. Define a Signature/Schema: Use elixact to define your desired output structure. This can be a compile-time module or a runtime EnhancedSchema.

    defmodule MyOutput do
      use Elixact, define_struct: true
      schema do
        field :answer, :string, description: "The final answer."
        field :confidence, :integer, constraints: [gt: 0, lt: 100]
        computed_field :assessment, :string, :assess_confidence
      end
    
      def assess_confidence(input), do: {:ok, if(input.confidence > 80, do: "high", else: "low")}
    end
    
  2. Generate JSON Schema: Use elixact to generate the JSON schema for the provider.

    # Use the enhanced resolver for provider-specific optimizations
    json_schema = Elixact.JsonSchema.EnhancedResolver.resolve_enhanced(
      MyOutput,
      optimize_for_provider: :openai
    )
    
  3. Make the LLM Call with ex_llm: Use ex_llm to make the API call, passing the generated JSON schema to the provider to ensure it returns structured data. (Note: This assumes the provider supports it, which many in ex_llm do).

    # This is a hypothetical way to use ex_llm with a JSON schema constraint
    # The actual implementation might vary based on the provider adapter
    options = [
      model: "openai/gpt-4o",
      response_format: %{type: "json_object", schema: json_schema}
    ]
    {:ok, response} = ExLLM.chat(:openai, messages, options)
    
  4. Validate the Response with elixact: Take the raw text content from the ex_llm response and validate it using elixact.

    # response.content contains the JSON string from the LLM
    json_data = Jason.decode!(response.content)
    case Elixact.EnhancedValidator.validate(MyOutput, json_data) do
      {:ok, %MyOutput{} = result} ->
        # Success! You have a validated struct with computed fields.
        IO.inspect(result)
    
      {:error, errors} ->
        # Handle validation errors, perhaps by retrying the ex_llm call
        # with an error message, similar to the instructor pattern.
        IO.inspect(errors)
    end
    

Synergy and Benefits:

  • Separation of Concerns: ex_llm handles all the complex infrastructure (API clients, retries, caching, observability, local models), while elixact handles the data modeling and validation logic. This is a very clean and robust architecture.
  • Full ex_llm Feature Set: You get access to all of ex_llm’s features—Bumblebee integration, circuit breakers, cost tracking—while still using your powerful, custom validation layer.
  • No Modifications Needed: This approach requires no changes to either library. You are composing them together as they are.

Conclusion for ex_llm: This is the most powerful and scalable approach for your DSPy port. It leverages the best of both worlds: ex_llm as a best-in-class LLM interaction framework and elixact as a best-in-class Pydantic-style data validation library.

Final Recommendation for Your DSPy Port

For your DSPy port, the ex_llm + elixact combination is the clear winner.

A DSPy-like framework needs two main components:

  1. An LM (Language Model) client: This is the piece that sends prompts to an LLM and gets back a response. ex_llm is a superb, feature-complete implementation of this.
  2. A Signature and Validation system: This is the piece that defines the expected inputs and outputs and validates them. elixact, with its Pydantic-like runtime capabilities, is the perfect Elixir equivalent for this part of DSPy.

Your framework would sit on top, orchestrating the interaction between these two powerful libraries. This layered approach is clean, maintainable, and gives you immense power out of the box.