Stage 1 Prompt 3b: Adapter Infrastructure Components
OBJECTIVE
Implement the critical infrastructure components that were identified as missing from the updated Prompt 3: TypeConverter, ErrorHandler, Factory, and comprehensive testing patterns. These components provide the robust foundation needed for production adapter operations while maintaining full integration with our 3-layer testing architecture.
CONTEXT FROM PROMPT 3 ANALYSIS
During the Prompt 3 update for 3-layer testing integration, several critical infrastructure components were removed under the assumption they were “over-engineering.” Analysis revealed these components are essential for production adapter operations:
CRITICAL MISSING COMPONENTS:
- TypeConverter (~100 lines) - Type conversion between Elixir ↔ Python ↔ JSON Schema
- ErrorHandler (~80 lines) - Standardized error handling with retry logic
- Factory (~50 lines) - Adapter lifecycle management and validation
- Comprehensive Testing (~100 lines) - Cross-adapter behavior compliance
WHY THESE ARE ESSENTIAL:
- Multi-backend support requires robust type conversion
- Production reliability demands sophisticated error handling
- Adapter lifecycle needs validation and requirement checking
- Cross-adapter compatibility requires behavior compliance testing
COMPLETE IMPLEMENTATION CONTEXT
TYPE CONVERSION SYSTEM (RESTORED AND ENHANCED)
Comprehensive Type Mapping with 3-Layer Test Support:
defmodule DSPex.Adapters.TypeConverter do
@moduledoc """
Type conversion between Elixir and adapter-specific formats.
Enhanced with test layer awareness and ML-specific types.
"""
@type_mappings %{
# Basic types
:string => %{python: "str", json_schema: "string", mock: "string"},
:integer => %{python: "int", json_schema: "integer", mock: "integer"},
:float => %{python: "float", json_schema: "number", mock: "float"},
:boolean => %{python: "bool", json_schema: "boolean", mock: "boolean"},
:atom => %{python: "str", json_schema: "string", mock: "string"},
:any => %{python: "Any", json_schema: "any", mock: "any"},
:map => %{python: "Dict", json_schema: "object", mock: "map"},
# ML-specific types
:embedding => %{python: "List[float]", json_schema: "array", mock: "embedding"},
:probability => %{python: "float", json_schema: "number", mock: "probability"},
:confidence_score => %{python: "float", json_schema: "number", mock: "confidence"},
:reasoning_chain => %{python: "List[str]", json_schema: "array", mock: "reasoning"}
}
@doc """
Convert type to target format with test layer awareness.
"""
def convert_type(type, target_format, opts \\ []) do
test_layer = Keyword.get(opts, :test_layer)
case type do
# Basic types
basic when is_atom(basic) ->
get_type_mapping(basic, target_format, test_layer)
# Composite types
{:list, inner_type} ->
inner_converted = convert_type(inner_type, target_format, opts)
case target_format do
:python -> "List[#{inner_converted}]"
:json_schema -> %{type: "array", items: inner_converted}
:mock -> {:list, inner_converted}
end
{:dict, key_type, value_type} ->
key_converted = convert_type(key_type, target_format, opts)
value_converted = convert_type(value_type, target_format, opts)
case target_format do
:python -> "Dict[#{key_converted}, #{value_converted}]"
:json_schema -> %{
type: "object",
additionalProperties: value_converted
}
:mock -> {:dict, key_converted, value_converted}
end
{:union, types} ->
converted_types = Enum.map(types, &convert_type(&1, target_format, opts))
case target_format do
:python -> "Union[#{Enum.join(converted_types, ", ")}]"
:json_schema -> %{anyOf: converted_types}
:mock -> {:union, converted_types}
end
end
end
def convert_signature_to_format(signature_module, target_format, opts \\ []) do
signature = signature_module.__signature__()
%{
inputs: convert_fields_to_format(signature.inputs, target_format, opts),
outputs: convert_fields_to_format(signature.outputs, target_format, opts)
}
end
defp convert_fields_to_format(fields, target_format, opts) do
Enum.map(fields, fn {name, type, constraints} ->
converted_type = convert_type(type, target_format, opts)
case target_format do
:python ->
%{
name: to_string(name),
type: converted_type,
description: get_field_description(constraints)
}
:json_schema ->
base_schema = %{
type: converted_type,
description: get_field_description(constraints)
}
add_json_schema_constraints(base_schema, constraints)
:mock ->
%{
name: name,
type: converted_type,
constraints: constraints
}
end
end)
end
defp get_type_mapping(type, target_format, test_layer) do
case Map.get(@type_mappings, type) do
nil -> {:error, "Unknown type: #{type}"}
mapping ->
# Use test-layer specific mapping if available
case test_layer do
:layer_1 -> Map.get(mapping, :mock, Map.get(mapping, target_format))
:layer_2 -> Map.get(mapping, target_format, to_string(type))
:layer_3 -> Map.get(mapping, target_format, to_string(type))
_ -> Map.get(mapping, target_format, to_string(type))
end
end
end
@doc """
Validate input against expected type with test layer awareness.
"""
def validate_input(value, expected_type, opts \\ []) do
test_layer = Keyword.get(opts, :test_layer, :layer_3)
case {value, expected_type, test_layer} do
# Basic type validation
{v, :string, _} when is_binary(v) -> {:ok, v}
{v, :integer, _} when is_integer(v) -> {:ok, v}
{v, :float, _} when is_float(v) -> {:ok, v}
{v, :boolean, _} when is_boolean(v) -> {:ok, v}
{v, :any, _} -> {:ok, v}
# ML-specific validation
{v, :probability, _} when is_float(v) and v >= 0.0 and v <= 1.0 ->
{:ok, v}
{v, :confidence_score, _} when is_float(v) and v >= 0.0 and v <= 1.0 ->
{:ok, v}
{v, :embedding, _} when is_list(v) ->
if Enum.all?(v, &is_float/1) do
{:ok, v}
else
{:error, "Embedding must be a list of floats"}
end
# Composite type validation
{v, {:list, inner_type}, layer} when is_list(v) ->
validate_list_items(v, inner_type, [{:test_layer, layer} | opts])
# Test layer specific relaxed validation
{v, type, :layer_1} ->
# Layer 1 (mock) accepts more flexible types
validate_mock_input(v, type)
{value, type, _} ->
{:error, "Expected #{inspect(type)}, got #{inspect(value)}"}
end
end
defp validate_list_items(list, inner_type, opts) do
case Enum.reduce_while(list, {:ok, []}, fn item, {:ok, acc} ->
case validate_input(item, inner_type, opts) do
{:ok, validated_item} -> {:cont, {:ok, acc ++ [validated_item]}}
error -> {:halt, error}
end
end) do
{:ok, validated_list} -> {:ok, validated_list}
error -> error
end
end
defp validate_mock_input(value, _type) do
# Mock adapter accepts any reasonable input for testing
{:ok, value}
end
defp get_field_description(constraints) do
Keyword.get(constraints, :description, "")
end
defp add_json_schema_constraints(schema, constraints) do
Enum.reduce(constraints, schema, fn
{:min_length, min}, acc -> Map.put(acc, :minLength, min)
{:max_length, max}, acc -> Map.put(acc, :maxLength, max)
{:min_value, min}, acc -> Map.put(acc, :minimum, min)
{:max_value, max}, acc -> Map.put(acc, :maximum, max)
{:one_of, values}, acc -> Map.put(acc, :enum, values)
_, acc -> acc
end)
end
end
ERROR HANDLING SYSTEM (RESTORED AND ENHANCED)
Comprehensive Error Handling with Test Layer Support:
defmodule DSPex.Adapters.ErrorHandler do
@moduledoc """
Standardized error handling for adapter operations with test layer awareness.
"""
defstruct [:type, :message, :context, :recoverable, :retry_after, :test_layer]
@type adapter_error :: %__MODULE__{
type: atom(),
message: String.t(),
context: map(),
recoverable: boolean(),
retry_after: pos_integer() | nil,
test_layer: atom() | nil
}
@doc """
Wrap error with context and test layer awareness.
"""
def wrap_error(error, context \\ %{}) do
test_layer = DSPex.Testing.TestMode.effective_test_mode()
case error do
{:error, :timeout} ->
%__MODULE__{
type: :timeout,
message: "Operation timed out",
context: context,
recoverable: true,
retry_after: get_retry_delay(:timeout, test_layer),
test_layer: test_layer
}
{:error, :connection_failed} ->
%__MODULE__{
type: :connection_failed,
message: "Failed to connect to adapter backend",
context: context,
recoverable: should_retry_connection?(test_layer),
retry_after: get_retry_delay(:connection_failed, test_layer),
test_layer: test_layer
}
{:error, {:validation_failed, details}} ->
%__MODULE__{
type: :validation_failed,
message: "Input validation failed: #{details}",
context: context,
recoverable: false,
retry_after: nil,
test_layer: test_layer
}
{:error, {:program_not_found, program_id}} ->
%__MODULE__{
type: :program_not_found,
message: "Program not found: #{program_id}",
context: Map.put(context, :program_id, program_id),
recoverable: false,
retry_after: nil,
test_layer: test_layer
}
{:error, {:bridge_error, bridge_details}} ->
%__MODULE__{
type: :bridge_error,
message: "Python bridge error: #{inspect(bridge_details)}",
context: Map.put(context, :bridge_details, bridge_details),
recoverable: should_retry_bridge_error?(bridge_details, test_layer),
retry_after: get_retry_delay(:bridge_error, test_layer),
test_layer: test_layer
}
{:error, reason} when is_binary(reason) ->
%__MODULE__{
type: :unknown,
message: reason,
context: context,
recoverable: false,
retry_after: nil,
test_layer: test_layer
}
other ->
%__MODULE__{
type: :unexpected,
message: "Unexpected error: #{inspect(other)}",
context: context,
recoverable: false,
retry_after: nil,
test_layer: test_layer
}
end
end
def should_retry?(%__MODULE__{recoverable: recoverable}), do: recoverable
def get_retry_delay(%__MODULE__{retry_after: delay}), do: delay
def get_error_context(%__MODULE__{context: context}), do: context
def is_test_error?(%__MODULE__{test_layer: test_layer}) do
test_layer in [:layer_1, :layer_2, :layer_3]
end
@doc """
Format error for logging with test context.
"""
def format_error(%__MODULE__{} = error) do
base_msg = "#{error.type}: #{error.message}"
case error.test_layer do
nil -> base_msg
layer -> "#{base_msg} [#{layer}]"
end
end
# Test layer specific retry delays
defp get_retry_delay(:timeout, :layer_1), do: 100 # Fast for mock tests
defp get_retry_delay(:timeout, :layer_2), do: 500 # Medium for protocol tests
defp get_retry_delay(:timeout, :layer_3), do: 5000 # Slower for integration tests
defp get_retry_delay(:timeout, _), do: 5000
defp get_retry_delay(:connection_failed, :layer_1), do: 100
defp get_retry_delay(:connection_failed, :layer_2), do: 1000
defp get_retry_delay(:connection_failed, :layer_3), do: 10000
defp get_retry_delay(:connection_failed, _), do: 10000
defp get_retry_delay(:bridge_error, test_layer) do
get_retry_delay(:connection_failed, test_layer)
end
defp get_retry_delay(_, _), do: nil
# Test layer specific retry logic
defp should_retry_connection?(:layer_1), do: false # Mock should never fail connection
defp should_retry_connection?(:layer_2), do: true # Protocol tests may need retries
defp should_retry_connection?(:layer_3), do: true # Integration tests definitely need retries
defp should_retry_connection?(_), do: true
defp should_retry_bridge_error?(details, :layer_1), do: false # No bridge in mock
defp should_retry_bridge_error?(details, :layer_2) do
# Protocol layer retries specific bridge protocol errors
case details do
%{type: :protocol_error} -> false
%{type: :timeout} -> true
_ -> false
end
end
defp should_retry_bridge_error?(details, :layer_3) do
# Full integration retries most bridge errors
case details do
%{type: :validation_error} -> false
_ -> true
end
end
defp should_retry_bridge_error?(_, _), do: true
end
FACTORY PATTERN (RESTORED AND ENHANCED)
Factory with Test Layer Integration:
defmodule DSPex.Adapters.Factory do
@moduledoc """
Factory for creating and managing adapter instances with test layer awareness.
"""
alias DSPex.Adapters.{Registry, ErrorHandler, TypeConverter}
@doc """
Create adapter with test layer specific configuration.
"""
def create_adapter(adapter_type \\ nil, opts \\ []) do
test_layer = Keyword.get(opts, :test_layer) ||
DSPex.Testing.TestMode.effective_test_mode()
resolved_adapter_type = adapter_type ||
Registry.get_adapter_for_test_layer(test_layer)
with {:ok, adapter_module} <- Registry.validate_adapter(resolved_adapter_type),
{:ok, _} <- check_adapter_requirements(adapter_module, opts),
{:ok, _} <- validate_test_layer_compatibility(adapter_module, test_layer) do
{:ok, adapter_module}
else
{:error, reason} -> {:error, ErrorHandler.wrap_error({:error, reason}, %{
adapter_type: adapter_type,
test_layer: test_layer
})}
end
end
@doc """
Execute operation with adapter, retry logic, and test layer awareness.
"""
def execute_with_adapter(adapter, operation, args, opts \\ []) do
timeout = Keyword.get(opts, :timeout, get_default_timeout())
max_retries = Keyword.get(opts, :max_retries, get_default_retries())
test_layer = Keyword.get(opts, :test_layer) ||
DSPex.Testing.TestMode.effective_test_mode()
context = %{
adapter: adapter,
operation: operation,
test_layer: test_layer
}
execute_with_retry(adapter, operation, args, max_retries, timeout, context)
end
@doc """
Execute with signature validation and type conversion.
"""
def execute_with_signature_validation(adapter, signature_module, inputs, opts \\ []) do
test_layer = Keyword.get(opts, :test_layer) ||
DSPex.Testing.TestMode.effective_test_mode()
with {:ok, validated_inputs} <- validate_inputs_for_signature(signature_module, inputs, test_layer),
{:ok, adapter_inputs} <- convert_inputs_for_adapter(adapter, signature_module, validated_inputs, test_layer) do
execute_with_adapter(adapter, :execute_program, adapter_inputs, opts)
else
{:error, reason} -> {:error, ErrorHandler.wrap_error({:error, reason}, %{
signature: signature_module,
inputs: inputs,
test_layer: test_layer
})}
end
end
defp execute_with_retry(adapter, operation, args, retries_left, timeout, context) do
case apply_with_timeout(adapter, operation, args, timeout) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
wrapped_error = ErrorHandler.wrap_error(error, context)
if retries_left > 0 and ErrorHandler.should_retry?(wrapped_error) do
delay = ErrorHandler.get_retry_delay(wrapped_error) || 1000
Process.sleep(delay)
execute_with_retry(adapter, operation, args, retries_left - 1, timeout, context)
else
{:error, wrapped_error}
end
end
end
defp apply_with_timeout(adapter, operation, args, timeout) do
task = Task.async(fn -> apply(adapter, operation, args) end)
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, result} -> result
nil -> {:error, :timeout}
end
end
defp check_adapter_requirements(adapter_module, opts) do
test_layer = Keyword.get(opts, :test_layer)
case adapter_module do
DSPex.Adapters.PythonPort ->
if test_layer == :layer_3 do
check_python_bridge_available()
else
{:ok, :test_mode_bypass}
end
DSPex.Adapters.BridgeMock ->
if test_layer == :layer_2 do
check_bridge_mock_available()
else
{:ok, :test_mode_bypass}
end
DSPex.Adapters.Mock ->
ensure_mock_started(opts)
_ ->
{:ok, :no_requirements}
end
end
defp validate_test_layer_compatibility(adapter_module, test_layer) do
if function_exported?(adapter_module, :supports_test_layer?, 1) do
case adapter_module.supports_test_layer?(test_layer) do
true -> {:ok, :compatible}
false -> {:error, "Adapter #{adapter_module} does not support test layer #{test_layer}"}
end
else
# Assume compatibility if not implemented
{:ok, :compatible}
end
end
defp validate_inputs_for_signature(signature_module, inputs, test_layer) do
signature = signature_module.__signature__()
Enum.reduce_while(signature.inputs, {:ok, %{}}, fn {field_name, field_type, _constraints}, {:ok, acc} ->
case Map.get(inputs, field_name) || Map.get(inputs, to_string(field_name)) do
nil ->
{:halt, {:error, "Missing required input: #{field_name}"}}
value ->
case TypeConverter.validate_input(value, field_type, test_layer: test_layer) do
{:ok, validated_value} ->
{:cont, {:ok, Map.put(acc, field_name, validated_value)}}
{:error, reason} ->
{:halt, {:error, "Invalid input for #{field_name}: #{reason}"}}
end
end
end)
end
defp convert_inputs_for_adapter(adapter, signature_module, inputs, test_layer) do
# Convert inputs based on adapter requirements
case adapter do
DSPex.Adapters.PythonPort ->
TypeConverter.convert_signature_to_format(signature_module, :python, test_layer: test_layer)
{:ok, inputs} # Python adapter handles conversion internally
DSPex.Adapters.BridgeMock ->
{:ok, inputs} # Protocol testing uses same format
DSPex.Adapters.Mock ->
{:ok, inputs} # Mock accepts any format
_ ->
{:ok, inputs} # Default pass-through
end
end
defp check_python_bridge_available do
case Process.whereis(DSPex.PythonBridge.Bridge) do
nil -> {:error, "Python bridge not running"}
_pid -> {:ok, :available}
end
end
defp check_bridge_mock_available do
case DSPex.Testing.BridgeMockServer.running?() do
true -> {:ok, :available}
false -> {:error, "Bridge mock server not running"}
end
end
defp ensure_mock_started(opts) do
case Process.whereis(DSPex.Adapters.Mock) do
nil -> DSPex.Adapters.Mock.start_link(opts)
_pid -> {:ok, :already_started}
end
end
# Test layer specific defaults
defp get_default_timeout do
case DSPex.Testing.TestMode.effective_test_mode() do
:layer_1 -> 1_000 # Fast for mock tests
:layer_2 -> 5_000 # Medium for protocol tests
:layer_3 -> 30_000 # Longer for integration tests
_ -> 30_000
end
end
defp get_default_retries do
case DSPex.Testing.TestMode.effective_test_mode() do
:layer_1 -> 0 # No retries for mock (should be deterministic)
:layer_2 -> 2 # Some retries for protocol tests
:layer_3 -> 3 # More retries for integration tests
_ -> 3
end
end
end
COMPREHENSIVE TESTING PATTERNS (RESTORED AND ENHANCED)
Cross-Adapter Behavior Compliance Testing:
defmodule DSPex.Adapters.BehaviorComplianceTest do
@moduledoc """
Comprehensive behavior compliance testing for all adapters across test layers.
"""
use ExUnit.Case
defmodule TestSignature do
use DSPex.Signature
signature question: :string -> answer: :string, confidence: :float
end
defmodule ComplexTestSignature do
use DSPex.Signature
signature input: :string, context: {:list, :string} ->
result: :string, reasoning: {:list, :string}, confidence: :probability
end
@adapters_by_layer %{
layer_1: DSPex.Adapters.Mock,
layer_2: DSPex.Adapters.BridgeMock,
layer_3: DSPex.Adapters.PythonPort
}
@test_layers [:layer_1, :layer_2, :layer_3]
setup do
# Reset test environment
DSPex.Testing.TestMode.set_test_mode(:mock_adapter)
# Start required services
{:ok, _} = DSPex.Adapters.Mock.start_link()
DSPex.Adapters.Mock.reset()
:ok
end
for layer <- @test_layers do
describe "#{layer} adapter behavior compliance" do
setup do
adapter = Map.get(@adapters_by_layer, unquote(layer))
# Set appropriate test mode for this layer
test_mode = case unquote(layer) do
:layer_1 -> :mock_adapter
:layer_2 -> :bridge_mock
:layer_3 -> :full_integration
end
DSPex.Testing.TestMode.set_test_mode(test_mode)
{:ok, adapter: adapter, test_layer: unquote(layer)}
end
@tag String.to_atom("#{layer}")
test "creates programs successfully", %{adapter: adapter, test_layer: test_layer} do
config = %{
id: "test_program_#{test_layer}",
signature: TestSignature,
modules: []
}
{:ok, program_id} = adapter.create_program(config)
assert program_id == "test_program_#{test_layer}"
end
@tag String.to_atom("#{layer}")
test "executes programs with valid inputs", %{adapter: adapter, test_layer: test_layer} do
# Create program first
config = %{
id: "test_program_#{test_layer}",
signature: TestSignature,
modules: []
}
{:ok, _} = adapter.create_program(config)
# Execute program
inputs = %{question: "What is 2+2?"}
{:ok, outputs} = adapter.execute_program("test_program_#{test_layer}", inputs)
assert Map.has_key?(outputs, :answer) or Map.has_key?(outputs, "answer")
assert Map.has_key?(outputs, :confidence) or Map.has_key?(outputs, "confidence")
end
@tag String.to_atom("#{layer}")
test "handles complex signatures", %{adapter: adapter, test_layer: test_layer} do
config = %{
id: "complex_program_#{test_layer}",
signature: ComplexTestSignature,
modules: []
}
{:ok, program_id} = adapter.create_program(config)
inputs = %{
input: "Analyze this text",
context: ["context1", "context2"]
}
{:ok, outputs} = adapter.execute_program(program_id, inputs)
# Verify complex output structure
assert Map.has_key?(outputs, :result) or Map.has_key?(outputs, "result")
assert Map.has_key?(outputs, :reasoning) or Map.has_key?(outputs, "reasoning")
assert Map.has_key?(outputs, :confidence) or Map.has_key?(outputs, "confidence")
end
@tag String.to_atom("#{layer}")
test "returns error for non-existent program", %{adapter: adapter} do
inputs = %{question: "test"}
{:error, _reason} = adapter.execute_program("nonexistent", inputs)
end
@tag String.to_atom("#{layer}")
test "lists programs correctly", %{adapter: adapter, test_layer: test_layer} do
# Create a few programs
for i <- 1..3 do
config = %{
id: "test_program_#{test_layer}_#{i}",
signature: TestSignature,
modules: []
}
{:ok, _} = adapter.create_program(config)
end
{:ok, programs} = adapter.list_programs()
assert length(programs) >= 3
end
@tag String.to_atom("#{layer}")
test "supports health check", %{adapter: adapter} do
if function_exported?(adapter, :health_check, 0) do
case adapter.health_check() do
:ok -> assert true
{:error, _reason} -> assert true # Error is acceptable in some test layers
end
end
end
@tag String.to_atom("#{layer}")
test "provides test capabilities", %{adapter: adapter} do
if function_exported?(adapter, :get_test_capabilities, 0) do
capabilities = adapter.get_test_capabilities()
assert is_map(capabilities)
assert Map.has_key?(capabilities, :performance)
end
end
@tag String.to_atom("#{layer}")
test "validates test layer support", %{adapter: adapter, test_layer: test_layer} do
if function_exported?(adapter, :supports_test_layer?, 1) do
assert adapter.supports_test_layer?(test_layer) == true
end
end
end
end
describe "Factory pattern compliance" do
test "creates correct adapters for test layers" do
for {layer, expected_adapter} <- @adapters_by_layer do
{:ok, adapter} = DSPex.Adapters.Factory.create_adapter(nil, test_layer: layer)
assert adapter == expected_adapter
end
end
test "validates adapter requirements" do
{:ok, _adapter} = DSPex.Adapters.Factory.create_adapter(:mock, test_layer: :layer_1)
end
test "handles execution with retry logic" do
adapter = DSPex.Adapters.Mock
# This should succeed
{:ok, result} = DSPex.Adapters.Factory.execute_with_adapter(
adapter,
:health_check,
[],
test_layer: :layer_1
)
assert result == :ok
end
end
describe "Type conversion compliance" do
test "converts basic types correctly" do
assert DSPex.Adapters.TypeConverter.convert_type(:string, :python) == "str"
assert DSPex.Adapters.TypeConverter.convert_type(:integer, :python) == "int"
assert DSPex.Adapters.TypeConverter.convert_type({:list, :string}, :python) == "List[str]"
end
test "validates inputs with test layer awareness" do
{:ok, "hello"} = DSPex.Adapters.TypeConverter.validate_input("hello", :string, test_layer: :layer_1)
{:ok, 42} = DSPex.Adapters.TypeConverter.validate_input(42, :integer, test_layer: :layer_2)
{:ok, [1, 2, 3]} = DSPex.Adapters.TypeConverter.validate_input([1, 2, 3], {:list, :integer}, test_layer: :layer_3)
end
test "rejects invalid inputs appropriately" do
{:error, _} = DSPex.Adapters.TypeConverter.validate_input(42, :string, test_layer: :layer_3)
{:error, _} = DSPex.Adapters.TypeConverter.validate_input("hello", :integer, test_layer: :layer_3)
{:error, _} = DSPex.Adapters.TypeConverter.validate_input([1, "two"], {:list, :integer}, test_layer: :layer_3)
end
test "converts signatures to different formats" do
signature_def = DSPex.Adapters.TypeConverter.convert_signature_to_format(TestSignature, :python)
assert Map.has_key?(signature_def, :inputs)
assert Map.has_key?(signature_def, :outputs)
assert length(signature_def.inputs) == 1
assert length(signature_def.outputs) == 2
end
end
describe "Error handling compliance" do
test "wraps errors with proper context" do
error = DSPex.Adapters.ErrorHandler.wrap_error({:error, :timeout}, %{context: :test})
assert error.type == :timeout
assert error.recoverable == true
assert is_integer(error.retry_after)
assert error.context.context == :test
end
test "provides test layer specific retry delays" do
# Set different test modes and verify retry delays
DSPex.Testing.TestMode.set_test_mode(:mock_adapter)
error1 = DSPex.Adapters.ErrorHandler.wrap_error({:error, :timeout})
DSPex.Testing.TestMode.set_test_mode(:full_integration)
error2 = DSPex.Adapters.ErrorHandler.wrap_error({:error, :timeout})
# Layer 1 should have shorter delays than Layer 3
assert error1.retry_after < error2.retry_after
end
test "formats errors with test context" do
error = DSPex.Adapters.ErrorHandler.wrap_error({:error, "test error"})
formatted = DSPex.Adapters.ErrorHandler.format_error(error)
assert is_binary(formatted)
assert formatted =~ "test error"
end
end
end
IMPLEMENTATION TASK
Based on the analysis showing these components are critical infrastructure (not over-engineering), implement the missing adapter infrastructure components:
FILE STRUCTURE TO CREATE:
lib/dspex/adapters/
├── type_converter.ex # Enhanced type conversion with test layer support
├── error_handler.ex # Standardized error handling with retry logic
├── factory.ex # Adapter lifecycle management and execution
└── supervisor.ex # Adapter supervision (if needed)
test/dspex/adapters/
├── behavior_compliance_test.exs # Cross-adapter behavior testing
├── type_converter_test.exs # Type conversion comprehensive tests
├── error_handler_test.exs # Error handling pattern tests
└── factory_test.exs # Factory pattern tests
SPECIFIC IMPLEMENTATION REQUIREMENTS:
- TypeConverter: ML-specific types, test layer awareness, multiple format support
- ErrorHandler: Test layer specific retry logic, comprehensive error context
- Factory: Adapter lifecycle, signature validation, test layer compatibility
- Comprehensive Testing: Cross-adapter compliance, behavior verification
INTEGRATION POINTS:
- Must integrate with 3-layer testing architecture from Prompt 3
- Should enhance existing adapter behavior without breaking changes
- Must support the multi-adapter roadmap for Stages 2-4
- Should provide production-ready error handling and type conversion
These components provide the robust infrastructure foundation that was identified as missing from the updated Prompt 3 while maintaining full integration with our proven 3-layer testing architecture.