← Back to 20250617

140 using elixact

Documentation for 140_using_elixact from the Ds ex repository.

Of course. This is a fantastic and insightful question. After analyzing both elixact and your dspex port, the answer is a definitive yes.

elixact is not just a good fit; it is an almost perfect, idiomatic Elixir equivalent to Pydantic for the specific ways DSPy uses it. Adopting it would be a significant architectural improvement for dspex, making it more robust, declarative, and closer to the design philosophy of the original DSPy.

Let’s break down why, using your excellent Pydantic analysis as a guide.

At a Glance: Pydantic vs. Elixact for DSPEx

Pydantic Feature (in DSPy)Elixact EquivalentFit for DSPEx
Signature System (BaseModel)use Elixact with schema do ... endExcellent. The core DSL is a direct match.
Field Definition & Validationfield :name, :type with constraintsExcellent. min_length, format, etc. are all there.
JSON Schema GenerationElixact.JsonSchema.from_schema/1Excellent. This is crucial for structured outputs.
Custom Typesuse Elixact.TypeExcellent. Allows for creating reusable, validated types.
Structured Error ReportingElixact.ValidationError and Elixact.ErrorExcellent. Provides structured errors with paths and codes.
Runtime Type Creation(No direct equivalent, but Elixir macros can suffice)Good. This is a language-level difference. See discussion below.

Detailed Feature-by-Feature Mapping

Here is a point-by-point mapping of your Pydantic analysis to the features available in elixact.

1. Core Framework Architecture: Signature System

  • Pydantic: dspy.Signature inherits from pydantic.BaseModel.
  • Elixact Equivalent: Your DSPEx.Signature module, which currently parses a string, could be completely refactored to use Elixact. Instead of use DSPEx.Signature, "question -> answer", a developer would write:
    defmodule QASignature do
      use Elixact
    
      schema "A signature for question answering" do
        field :question, :string, description: "The question to answer."
        field :answer, :string, description: "The answer to the question."
      end
    end
    
    This approach is more declarative, type-safe, and provides better editor support than string parsing. Elixact’s validate/1 and validate!/1 functions would then handle the validation logic.

2. Custom Types System

  • Pydantic: BaseType inherits from pydantic.BaseModel.
  • Elixact Equivalent: The use Elixact.Type behaviour (elixact/type.ex) is designed for exactly this. It allows you to define a module that acts as a custom, reusable type with its own validation and JSON schema definition.

3-6. Specific Custom Types (Image, Audio, History, etc.)

  • Pydantic: Custom classes inheriting from BaseModel.
  • Elixact Equivalent: These would be implemented as modules using use Elixact.Type. For example:
    defmodule DSPEx.Types.Audio do
      use Elixact.Type
    
      def type_definition, do: # ... define underlying types
      def json_schema, do: # ... define json schema for the audio type
      def validate(value), do: # ... custom validation logic
    end
    

7. Field Definition System

  • Pydantic: InputField/OutputField built on pydantic.Field().
  • Elixact Equivalent: The field macro inside a schema block serves this purpose perfectly. Metadata like description, example, and default are supported, and constraints (min_length, gt, choices) are defined in the do block of the field.

8. Structured Output Generation

  • Pydantic: model_json_schema() is used to generate schemas for models like OpenAI.
  • Elixact Equivalent: Elixact.JsonSchema.from_schema/1. This is a standout feature of elixact. Your DSPEx.Adapters.InstructorLiteGemini adapter currently builds JSON schemas manually. It could be refactored to take an Elixact schema module and call Elixact.JsonSchema.from_schema(MySchema) to generate the schema automatically, making it far more robust and declarative.

9 & 17. Configuration Classes (Teleprompter, etc.)

  • Pydantic: BaseModel is used for structured configuration.
  • Elixact Equivalent: A perfect use case for elixact. You can define a schema for your teleprompter or client configurations and use validate/1 to ensure they are correct at startup.

10. Model State Management

  • Pydantic: model_dump() and model_validate().
  • Elixact Equivalent: Elixact’s validate/1 function returns a validated map, which is the Elixir equivalent of model_dump. Loading is done by passing a map to validate/1. JSON serialization is handled by standard Elixir libraries like Jason, which is idiomatic.

11. Example and Prediction Classes

  • Pydantic: Pydantic models are used for Prediction and Example.
  • Elixact Equivalent: Your DSPEx.Example is currently a simple struct. It could be redefined as an Elixact schema, giving you automatic validation and clear separation of inputs/outputs based on the schema definition, rather than a input_keys set.

12. Runtime Type Creation

  • Pydantic: pydantic.create_model() is a powerful runtime feature.
  • Elixact Equivalent: This is the most significant difference. Elixir, as a compiled language, favors compile-time metaprogramming (macros) over runtime metaprogramming. Elixact does not have a runtime create_schema function.
    • However: For the primary use case in DSPy (generating a response model for structured output), you don’t need runtime creation. You already have the signature defined. You would simply pass the existing Elixact schema module to the adapter.
    • If truly dynamic schema generation were needed, you would use Elixir macros to generate the Elixact module at compile time, which is the Elixir way of solving this problem.

13 & 14. Schema Manipulation & Type Adaptation

  • Pydantic: Manipulates schema dictionaries and uses TypeAdapter.
  • Elixact Equivalent: Elixact.JsonSchema.from_schema/1 returns a standard Elixir map, which is trivial to manipulate. Elixact.Types.coerce/2 and the custom type system handle type adaptation.

15. Adapter System

  • Pydantic: Used for validation and serialization in adapters.
  • Elixact Equivalent: This is a key integration point. As mentioned, DSPEx.Adapters.InstructorLiteGemini is the perfect candidate. It could be changed to accept an Elixact schema module, making the adapter more generic and powerful.

16. Error Handling

  • Pydantic: ValidationError.
  • Elixact Equivalent: Elixact.Error and Elixact.ValidationError are a direct match, providing structured errors with path, code, and message.

18-20. Utilities, Data Transformation, and Settings

  • Pydantic: Provides various utility functions.
  • Elixact Equivalent: The Elixact.Types, Elixact.Validator, and Elixact.Schema modules provide the necessary building blocks and functionality, fulfilling these roles perfectly.

How to Integrate Elixact into DSPEx

  1. Refactor DSPEx.Signature: This is the most important step. The use DSPEx.Signature, "input -> output" macro is clever, but fragile. It should be deprecated in favor of a new paradigm:

    defmodule MySignature do
      # Instead of `use DSPEx.Signature`, you'd just `use Elixact`
      use Elixact
      # And define fields in a schema block
      schema do
        field :question, :string
        field :answer, :string
      end
    end
    
    # The DSPEx.Program would then work with this module directly.
    # DSPEx.Predict.new(MySignature, :gemini)
    

    This makes signatures first-class modules that are far more expressive and robust.

  2. Refactor DSPEx.PredictStructured and Adapters: The DSPEx.Adapters.InstructorLiteGemini adapter is doing manual work that elixact is designed to automate. It should be changed to accept an Elixact schema and use Elixact.JsonSchema.from_schema/1 to generate the json_schema for InstructorLite.

  3. Refactor DSPEx.Example: Consider redefining the Example struct as an Elixact schema. This would give you powerful validation capabilities for your training/testing data for free.

Final Verdict

Using elixact as a Pydantic-like foundation for dspex is not only possible but highly recommended. It aligns perfectly with the architectural patterns of DSPy while using idiomatic Elixir features like macros and behaviours. It would elevate dspex from a direct port to a truly robust and well-designed Elixir library.