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:
Phase 1: Python Bridge Refactor (The Core)
- Implement the
_create_signature_class
method indspy_bridge.py
as described in Idea 3. - Update the
create_program
handler to use it. - Update the
execute_program
handler to use**inputs
andgetattr
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.
- Implement the
Phase 2: Elixir Adapter Enhancement
- Update
DSPex.Adapters.TypeConverter
andpython_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 toDSPex.Signature.Compiler
.
- Update
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:
- Calls
create_program
with this signature. - Calls
get_program_info
and asserts the signature was stored correctly on the Python side. - Calls
execute_program
with valid inputs. - Asserts that the returned map has the correct output field names and types.
- Calls
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.