← Back to Docs

SIGNATURE 202507141942 geminiResponse

Documentation for SIGNATURE_202507141942_geminiResponse from the Dspex repository.

Of course. I’ve analyzed your Elixir and Python codebases, and I completely agree with your assessment. The current implementation in dspy_bridge.py defaults to a question -> answer string signature, which completely discards the richness of your excellent Elixir DSPex.Signature DSL. This is the “hard-coded python crap” you’re referring to, and it’s the central bottleneck.

You’ve built a fantastic, robust foundation on both the Elixir and Python sides. The problem is the “last mile” of translation. We need to replace the hard-coded part with a dynamic, meta-programming approach where your Elixir signature definition instructs the Python side on how to build the dspy.Signature at runtime.

Here are my ideas for a more robust approach, moving from a static bridge to a truly dynamic one.


Idea 1: The Meta-Programming Bridge (The Core Concept)

The fundamental shift is to treat the Elixir signature not just as data, but as a blueprint for generating Python code constructs on the fly. Your dspy_bridge.py should become a “signature factory.”

Current Flaw in dspy_bridge.py:

The _create_program_instance function currently does this:

# ...
input_str = ', '.join(inputs) if inputs else 'question'
output_str = ', '.join(outputs) if outputs else 'answer'
string_signature = f"{input_str} -> {output_str}"
program = dspy.Predict(string_signature)

This is the problem. It flattens your structured signature into a simple string, losing all context.

Proposed Solution:

We will enhance the Python bridge to dynamically build a dspy.Signature class using the detailed information from Elixir.

Idea 2: Enhance the Elixir-to-Python Signature Payload

First, let’s ensure the Elixir side sends a rich, descriptive payload. Your DSPex.Adapters.TypeConverter and python_port.ex should be responsible for creating this.

dspex/adapters/type_converter.ex:

When converting a signature for the :python target, it should produce a more detailed map.

# In DSPex.Adapters.TypeConverter

def convert_signature_to_format(signature_module, :python, _opts) do
  signature = signature_module.__signature__()
  %{
    "name" => to_string(signature.module),
    "description" => get_module_doc(signature.module), # Helper to get @moduledoc
    "inputs" => convert_fields_to_python_format(signature.inputs),
    "outputs" => convert_fields_to_python_format(signature.outputs)
  }
end

defp convert_fields_to_python_format(fields) do
  Enum.map(fields, fn {name, type, constraints} ->
    %{
      "name" => to_string(name),
      # You already have type conversion, which is great
      "type" => elixir_to_dspy_type(type),
      "description" => Keyword.get(constraints, :description, "")
    }
  end)
end

This creates a self-contained “recipe” for the Python side to follow.

Idea 3: The Python-Side Dynamic dspy.Signature Generator

This is the heart of the solution. We’ll replace the flawed logic in dspy_bridge.py with a proper signature factory.

New implementation in dspy_bridge.py:

import dspy

# ... existing code ...

class DSPyBridge:
    # ...

    def _create_signature_class(self, signature_def: Dict[str, Any]) -> type:
        """
        Dynamically builds a dspy.Signature class from a detailed definition.
        """
        class_name = signature_def.get('name', 'DynamicSignature').split('.')[-1]
        docstring = signature_def.get('description', 'A dynamically generated DSPy signature.')

        # Use a dictionary to build class attributes
        attrs = {
            '__doc__': docstring
        }

        # Dynamically create InputField and OutputField attributes
        for field_def in signature_def.get('inputs', []):
            field_name = field_def.get('name')
            if field_name:
                attrs[field_name] = dspy.InputField(
                    desc=field_def.get('description', '')
                )

        for field_def in signature_def.get('outputs', []):
            field_name = field_def.get('name')
            if field_name:
                attrs[field_name] = dspy.OutputField(
                    desc=field_def.get('description', '')
                )

        # Use type() to create the class dynamically
        DynamicSignature = type(class_name, (dspy.Signature,), attrs)
        return DynamicSignature

    def create_program(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """
        Creates a new DSPy program using a dynamically generated signature.
        """
        program_id = args.get('id')
        signature_def = args.get('signature', {})
        session_id = args.get('session_id') # For pool mode

        if not program_id or not signature_def:
            raise ValueError("Program 'id' and 'signature' are required.")

        # 1. Generate the signature class
        try:
            DynamicSignatureClass = self._create_signature_class(signature_def)
        except Exception as e:
            debug_log(f"Error creating dynamic signature class: {e}")
            raise ValueError(f"Failed to create signature class: {e}")

        # 2. Instantiate a dspy.Predict module with it
        program = dspy.Predict(DynamicSignatureClass)

        # 3. Store the program and its definition
        program_info = {
            'program': program,
            'signature_class': DynamicSignatureClass,
            'signature_def': signature_def, # Keep the original definition for reference
            'created_at': time.time(),
            'executions': 0
        }

        if self.mode == "pool-worker":
            if session_id not in self.session_programs:
                self.session_programs[session_id] = {}
            self.session_programs[session_id][program_id] = program_info
        else:
            self.programs[program_id] = program_info

        return {
            'program_id': program_id,
            'status': 'created',
            'signature': signature_def
        }

Idea 4: Dynamic Execution and I/O Handling

With a dynamic signature, the execute_program handler must also become dynamic. It should use the stored signature definition to correctly pass inputs and parse outputs.

Updated execute_program in dspy_bridge.py:

    def execute_program(self, args: Dict[str, Any]) -> Dict[str, Any]:
        program_id = args.get('program_id')
        inputs = args.get('inputs', {})
        session_id = args.get('session_id')

        # ... (logic to get program_info based on mode) ...
        program_info = self._get_program_info(program_id, session_id)
        program = program_info['program']
        signature_def = program_info['signature_def']
        
        # Ensure the configured LM is active
        self._ensure_lm_configured(session_id)

        # Dynamically pass inputs using keyword arguments
        # This is the key: **inputs unpacks the dict into named arguments
        result = program(**inputs)
        
        # Dynamically extract outputs based on the signature definition
        output_fields = [field['name'] for field in signature_def.get('outputs', [])]
        outputs = {}
        for field_name in output_fields:
            if hasattr(result, field_name):
                outputs[field_name] = getattr(result, field_name)
            else:
                # Fallback for safety, but this indicates a potential issue
                outputs[field_name] = f"Field '{field_name}' not found in prediction."
        
        program_info['executions'] += 1
        
        return outputs

A Coherent Implementation Plan

Here is a phased approach to implement this robust system:

  1. Phase 1: Python Bridge Refactor (The Core)

    • Implement the _create_signature_class method in dspy_bridge.py as described in Idea 3.
    • Update the create_program handler to use it.
    • Update the execute_program handler to use **inputs and getattr for dynamic I/O as in Idea 4.
    • Manually test this by sending a detailed JSON “recipe” to the Python process from a simple test script to ensure it works before touching Elixir.
  2. Phase 2: Elixir Adapter Enhancement

    • Update DSPex.Adapters.TypeConverter and python_port.ex to serialize the full signature definition (including descriptions) as proposed in Idea 2.
    • Ensure that the Elixir DSPex.Signature DSL can capture field descriptions, e.g., signature question: :string, description: "The user's question" -> .... This might require a small update to DSPex.Signature.Compiler.
  3. Phase 3: End-to-End Testing & Validation

    • Create a new test suite in Elixir that defines a complex signature (e.g., multiple inputs/outputs, different names).
    • Write a test that:
      1. Calls create_program with this signature.
      2. Calls get_program_info and asserts the signature was stored correctly on the Python side.
      3. Calls execute_program with valid inputs.
      4. Asserts that the returned map has the correct output field names and types.
  4. Phase 4: Cleanup and Documentation

    • Remove the old, flawed logic from the Python bridge.
    • Update docs/SIGNATURE/2025*.md to reflect this new, robust architecture, marking the old plan as obsolete and replaced by this superior dynamic approach.

This plan directly addresses your “hatred of hard-coded python crap” by making the Python side a generic, dynamic engine driven entirely by the specifications defined in your elegant Elixir DSL. It’s a much more scalable and maintainable architecture.