Stage 1 Prompt 1: Core Signature System Implementation
OBJECTIVE
Implement the foundational signature system for DSPy-Ash integration that enables native signature syntax signature question: :string -> answer: :string
with compile-time processing, type validation, and JSON schema generation. This system forms the core abstraction for DSPy program definitions in Elixir.
COMPLETE IMPLEMENTATION CONTEXT
SIGNATURE INNOVATION ARCHITECTURE REFERENCE
From the Signature Innovation Documents (1100-1102 series), the vision is to eliminate traditional DSPy Python signature syntax in favor of native Elixir syntax:
Traditional DSPy Python Signature:
class QA(dspy.Signature):
"""Answer questions with short factoid answers."""
question = dspy.InputField()
answer = dspy.OutputField(desc="often between 1 and 5 words")
Native Elixir Signature Syntax (Target):
defmodule QA do
use DSPex.Signature
signature question: :string -> answer: :string
end
COMPLETE ASH DSPY INTEGRATION ARCHITECTURE
From DSPEX_INTEGRATION_ARCHITECTURE.md:
Core Integration Philosophy:
- Ash framework serves as domain modeling infrastructure for ML operations
- DSPy operations mapped to Ash resources with proper lifecycle management
- Native signature syntax that compiles to both Ash resources and DSPy programs
- Adapter pattern enabling Python port initially, native Elixir implementation later
- Production-ready patterns: GraphQL APIs, background jobs, monitoring
Technical Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Ash-DSPy Integration │
├─────────────────────────────────────────────────────────────┤
│ Native Signatures → Ash Resources → DSPy Programs │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐│
│ │ Signature DSL │ │ Ash Resources │ │ Adapters ││
│ │ - Native syntax │ │ - Domain model │ │ - Python ││
│ │ - Type parsing │ │ - Actions │ │ - Future ││
│ │ - Validation │ │ - Relationships │ │ Native ││
│ └─────────────────┘ └─────────────────┘ └──────────────┘│
└─────────────────────────────────────────────────────────────┘
ASH RESOURCE PATTERNS FROM DOCUMENTATION
From ashDocs/documentation/tutorials/get-started.md:
Basic Resource Structure:
defmodule Helpdesk.Support.Ticket do
use Ash.Resource, domain: Helpdesk.Support
actions do
defaults [:read]
create :open do
accept [:subject]
end
update :close do
accept []
validate attribute_does_not_equal(:status, :closed) do
message "Ticket is already closed"
end
change set_attribute(:status, :closed)
end
end
attributes do
uuid_primary_key :id
attribute :subject, :string do
allow_nil? false
public? true
end
attribute :status, :atom do
constraints [one_of: [:open, :closed]]
default :open
allow_nil? false
end
end
end
From ashDocs/documentation/topics/actions/manual-actions.md:
Manual Action Patterns:
create :special_create do
manual MyApp.DoCreate
end
defmodule MyApp.DoCreate do
use Ash.Resource.ManualCreate
def create(changeset, _, _) do
record = create_the_record(changeset)
{:ok, record}
end
end
Manual Read Patterns:
read :action_name do
manual MyApp.ManualRead
end
defmodule MyApp.ManualRead do
use Ash.Resource.ManualRead
def read(ash_query, ecto_query, _opts, _context) do
{:ok, query_results} | {:error, error}
end
end
EXDANTIC VALIDATION INTEGRATION
From ../../exdantic/README.md analysis:
ExDantic Core Features:
- Pydantic-like validation for Elixir
- Runtime schema creation and validation
- TypeAdapter pattern for custom types
- JSON schema generation
- Field validation with constraints
Integration Points for Signature System:
- Use ExDantic for field-level validation
- Leverage TypeAdapter for ML-specific types
- JSON schema generation for OpenAI/provider compatibility
- Runtime validation of signature inputs/outputs
STAGE 1 FOUNDATION IMPLEMENTATION DETAILS
From STAGE_1_FOUNDATION_IMPLEMENTATION.md:
Project Structure:
lib/
├── dspex/
│ ├── signature/
│ │ ├── signature.ex # Core signature behavior
│ │ ├── compiler.ex # Compile-time signature processing
│ │ └── type_parser.ex # Type system parsing
│ ├── adapters/
│ │ ├── adapter.ex # Adapter behavior
│ │ └── python_port.ex # Python port implementation
│ ├── python_bridge/
│ │ ├── bridge.ex # GenServer for Python communication
│ │ └── protocol.ex # Wire protocol
│ └── ml/
│ ├── domain.ex # Ash domain
│ ├── signature.ex # Signature resource
│ └── program.ex # Program resource
Core Implementation Requirements:
- Signature Behavior with DSL macro for native syntax
- Compile-time AST processing and code generation
- Type parser supporting basic and ML-specific types
- Runtime validation for inputs/outputs
- JSON schema generation for provider compatibility
- Integration with Ash resource lifecycle
COMPLETE TYPE SYSTEM SPECIFICATION
Basic Types:
:string
- Text data, questions, responses:integer
- Numeric values, counts, indices:float
- Decimal numbers, probabilities, scores:boolean
- Binary flags, yes/no responses:atom
- Enumerated values, status indicators:any
- Unconstrained values, debugging:map
- Structured data, complex inputs
ML-Specific Types:
:embedding
- Vector embeddings for semantic search:probability
- Values constrained 0.0-1.0:confidence_score
- Model confidence metrics:reasoning_chain
- Step-by-step reasoning traces
Composite Types:
{:list, inner_type}
- Arrays of values{:dict, key_type, value_type}
- Key-value mappings{:union, [type1, type2, ...]}
- One of multiple types
SIGNATURE DSL MACRO SYSTEM
DSL Requirements:
defmodule DSPex.Signature do
defmacro __using__(_opts) do
quote do
import DSPex.Signature.DSL
Module.register_attribute(__MODULE__, :signature_ast, accumulate: false)
Module.register_attribute(__MODULE__, :signature_compiled, accumulate: false)
@before_compile DSPex.Signature.Compiler
end
end
defmodule DSL do
defmacro signature(signature_ast) do
quote do
@signature_ast unquote(Macro.escape(signature_ast))
end
end
end
end
AST Parsing Patterns:
# Handle: a: type -> b: type
{inputs, [do: outputs]} when is_list(inputs) ->
{parse_fields(inputs), parse_fields([outputs])}
# Handle: a: type, b: type -> c: type, d: type
{:->, _, [inputs, outputs]} ->
input_list = if is_list(inputs), do: inputs, else: [inputs]
output_list = if is_list(outputs), do: outputs, else: [outputs]
{parse_fields(input_list), parse_fields(output_list)}
COMPILE-TIME CODE GENERATION
Generated Functions:
def __signature__, do: @signature_compiled
def input_fields, do: @signature_compiled.inputs
def output_fields, do: @signature_compiled.outputs
def validate_inputs(data) do
DSPex.Signature.Validator.validate_fields(data, input_fields())
end
def validate_outputs(data) do
DSPex.Signature.Validator.validate_fields(data, output_fields())
end
def to_json_schema(provider \\ :openai) do
DSPex.Signature.JsonSchema.generate(__signature__, provider)
end
VALIDATION SYSTEM ARCHITECTURE
Field Validation Logic:
def validate_fields(data, fields) when is_map(data) do
results = Enum.map(fields, fn {name, type, _constraints} ->
case Map.get(data, name) do
nil -> {:error, "Missing field: #{name}"}
value -> validate_type(value, type)
end
end)
case Enum.find(results, &match?({:error, _}, &1)) do
nil ->
validated = Enum.zip(fields, results)
|> Enum.map(fn {{name, _, _}, {:ok, value}} -> {name, value} end)
|> Map.new()
{:ok, validated}
error -> error
end
end
Type Validation Implementation:
defp validate_type(value, :string) when is_binary(value), do: {:ok, value}
defp validate_type(value, :integer) when is_integer(value), do: {:ok, value}
defp validate_type(value, :float) when is_float(value), do: {:ok, value}
defp validate_type(value, :boolean) when is_boolean(value), do: {:ok, value}
defp validate_type(value, :any), do: {:ok, value}
defp validate_type(value, {:list, inner_type}) when is_list(value) do
case validate_list_items(value, inner_type, []) do
{:ok, validated_items} -> {:ok, validated_items}
error -> error
end
end
JSON SCHEMA GENERATION FOR PROVIDER COMPATIBILITY
OpenAI Function Calling Schema:
defmodule DSPex.Signature.JsonSchema do
def generate(signature, :openai) do
%{
type: "object",
properties: generate_properties(signature.inputs ++ signature.outputs),
required: get_required_fields(signature.inputs ++ signature.outputs)
}
end
defp generate_properties(fields) do
fields
|> Enum.map(fn {name, type, constraints} ->
{name, type_to_json_schema(type, constraints)}
end)
|> Map.new()
end
defp type_to_json_schema(:string, _), do: %{type: "string"}
defp type_to_json_schema(:integer, _), do: %{type: "integer"}
defp type_to_json_schema(:float, _), do: %{type: "number"}
defp type_to_json_schema(:boolean, _), do: %{type: "boolean"}
defp type_to_json_schema({:list, inner}, _) do
%{type: "array", items: type_to_json_schema(inner, [])}
end
end
INTEGRATION WITH ASH RESOURCE LIFECYCLE
Resource Generation from Signature:
defmodule DSPex.ML.Signature do
use Ash.Resource,
domain: DSPex.ML.Domain,
data_layer: AshPostgres.DataLayer
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false
attribute :module, :string, allow_nil?: false
attribute :inputs, {:array, :map}, default: []
attribute :outputs, {:array, :map}, default: []
timestamps()
end
actions do
defaults [:read, :create, :update, :destroy]
action :from_module, :struct do
argument :signature_module, :atom, allow_nil?: false
run fn input, _context ->
module = input.arguments.signature_module
signature = module.__signature__()
{:ok, %{
name: to_string(module),
module: to_string(module),
inputs: signature.inputs,
outputs: signature.outputs
}}
end
end
end
end
COMPLETE TESTING PATTERNS
Comprehensive Test Suite:
defmodule Stage1FoundationTest do
use ExUnit.Case
defmodule TestSignature do
use DSPex.Signature
signature question: :string -> answer: :string
end
defmodule ComplexSignature do
use DSPex.Signature
signature query: :string, context: {:list, :string} ->
answer: :string, confidence: :float, reasoning: {:list, :string}
end
test "basic signature compilation" do
signature = TestSignature.__signature__()
assert signature.inputs == [{:question, :string, []}]
assert signature.outputs == [{:answer, :string, []}]
end
test "complex signature compilation" do
signature = ComplexSignature.__signature__()
assert signature.inputs == [
{:query, :string, []},
{:context, {:list, :string}, []}
]
assert signature.outputs == [
{:answer, :string, []},
{:confidence, :float, []},
{:reasoning, {:list, :string}, []}
]
end
test "input validation success" do
{:ok, validated} = TestSignature.validate_inputs(%{question: "test"})
assert validated.question == "test"
end
test "input validation failure - missing field" do
{:error, reason} = TestSignature.validate_inputs(%{})
assert reason =~ "Missing field: question"
end
test "input validation failure - wrong type" do
{:error, reason} = TestSignature.validate_inputs(%{question: 123})
assert reason =~ "Expected :string"
end
test "JSON schema generation" do
schema = TestSignature.to_json_schema(:openai)
assert schema.type == "object"
assert schema.properties.question.type == "string"
assert schema.properties.answer.type == "string"
assert "question" in schema.required
assert "answer" in schema.required
end
end
ERROR HANDLING AND DEBUGGING
Comprehensive Error Messages:
defp compile_signature(ast, module) do
case parse_signature_ast(ast) do
{:ok, {inputs, outputs}} ->
generate_signature_code(inputs, outputs, module)
{:error, reason} ->
raise """
Invalid signature syntax in #{module}: #{reason}
Expected syntax examples:
signature question: :string -> answer: :string
signature query: :string, context: :string -> answer: :string, confidence: :float
Received: #{inspect(ast)}
"""
end
end
CONFIGURATION AND ENVIRONMENT SETUP
Application Configuration:
# config/config.exs
import Config
config :dspex, :adapter, DSPex.Adapters.PythonPort
config :dspex, DSPex.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "dspex_dev",
pool_size: 10
config :dspex,
ecto_repos: [DSPex.Repo]
Application Supervision:
defmodule DSPex.Application do
use Application
def start(_type, _args) do
children = [
# Signature registry for runtime lookup
{Registry, keys: :unique, name: DSPex.SignatureRegistry},
# Ash resources if using Postgres
{AshPostgres.Repo, Application.get_env(:dspex, DSPex.Repo)}
]
opts = [strategy: :one_for_one, name: DSPex.Supervisor]
Supervisor.start_link(children, opts)
end
end
IMPLEMENTATION TASK
Based on the complete context above, implement the core signature system with the following specific requirements:
FILE STRUCTURE TO CREATE:
lib/dspex/signature/
├── signature.ex # Core behavior and DSL
├── compiler.ex # Compile-time processing
├── type_parser.ex # Type system parser
├── validator.ex # Runtime validation
└── json_schema.ex # Provider schema generation
SPECIFIC IMPLEMENTATION REQUIREMENTS:
Signature Behavior (
lib/dspex/signature/signature.ex
):- Implement
__using__
macro with proper module attributes - Create DSL module with
signature
macro for native syntax - Set up compile-time hooks with
@before_compile
- Implement
Signature Compiler (
lib/dspex/signature/compiler.ex
):- Implement
__before_compile__
callback - Parse signature AST for various syntax patterns
- Generate signature metadata and validation functions
- Handle error cases with helpful messages
- Implement
Type Parser (
lib/dspex/signature/type_parser.ex
):- Support all basic types (:string, :integer, :float, :boolean, :atom, :any, :map)
- Support ML-specific types (:embedding, :probability, :confidence_score, :reasoning_chain)
- Support composite types ({:list, inner}, {:dict, key, value}, {:union, types})
- Provide clear error messages for unsupported types
Runtime Validator (
lib/dspex/signature/validator.ex
):- Implement field validation with proper error handling
- Support nested validation for composite types
- Return validated data or descriptive errors
- Handle edge cases (nil values, type mismatches)
JSON Schema Generator (
lib/dspex/signature/json_schema.ex
):- Generate OpenAI-compatible function calling schemas
- Support all signature types with proper JSON Schema mappings
- Handle required fields and optional constraints
- Extensible for other providers (Anthropic, etc.)
QUALITY REQUIREMENTS:
- Comprehensive Documentation: Every module, function, and macro must have detailed @moduledoc and @doc
- Error Handling: All error paths must provide helpful, actionable error messages
- Type Safety: Use proper typespecs for all public functions
- Testing: Include comprehensive test cases covering all syntax variations and error conditions
- Performance: Compile-time processing should be efficient, runtime validation minimal overhead
- Extensibility: Design for easy addition of new types and providers
INTEGRATION POINTS:
- Must integrate cleanly with Ash resource lifecycle
- Should support ExDantic validation patterns where applicable
- Must be compatible with the adapter pattern for Python bridge
- Should enable JSON schema generation for multiple providers
- Must support the native signature syntax exactly as specified
SUCCESS CRITERIA:
- Native signature syntax compiles successfully:
signature question: :string -> answer: :string
- Complex signatures work:
signature query: :string, context: {:list, :string} -> answer: :string, confidence: :float
- Runtime validation catches type errors and missing fields
- JSON schema generation produces valid OpenAI function calling schemas
- All test cases pass with comprehensive coverage
- Error messages are helpful and actionable
- Performance is acceptable for compile-time and runtime operations
This signature system forms the foundation for the entire DSPy-Ash integration. It must be robust, well-tested, and extensible to support the advanced features planned for later stages.