Skip to main content
The adapter system is GEPA’s extensibility layer. Adapters bridge the gap between your custom systems (prompt templates, DSPy programs, RAG pipelines, agent architectures) and GEPA’s optimization engine.

What is an Adapter?

An adapter is a protocol-conforming object that tells GEPA:
  1. How to evaluate a candidate on your system
  2. How to extract feedback (Actionable Side Information) from evaluations
  3. Optionally, how to propose improvements using domain-specific logic
Adapters are not required for simple use cases. Use optimize_anything directly for quick optimizations. Adapters shine when you need custom evaluation logic or want to optimize complex systems like DSPy programs or RAG pipelines.

The GEPAAdapter Protocol

All adapters implement the GEPAAdapter protocol:
from gepa.core.adapter import GEPAAdapter

class MyAdapter(GEPAAdapter[DataInst, Trajectory, RolloutOutput]):
    def evaluate(
        self,
        batch: list[DataInst],
        candidate: dict[str, str],
        capture_traces: bool = False,
    ) -> EvaluationBatch[Trajectory, RolloutOutput]:
        """Run the candidate on a batch of data and return scores."""
        ...
    
    def make_reflective_dataset(
        self,
        candidate: dict[str, str],
        eval_batch: EvaluationBatch[Trajectory, RolloutOutput],
        components_to_update: list[str],
    ) -> dict[str, list[dict[str, Any]]]:
        """Extract feedback for each component from evaluation results."""
        ...
    
    # Optional: custom proposal logic
    propose_new_texts: ProposalFn | None = None

Type Parameters

1

DataInst

Your task-specific input data format.
# Simple format
DataInst = dict  # {"input": str, "expected": str}

# Structured format
class QADataInst(TypedDict):
    question: str
    context: str
    answer: str
2

Trajectory

Execution trace capturing intermediate states. Used for detailed feedback.
class MyTrajectory(TypedDict):
    steps: list[str]
    reasoning: str
    error: str | None
3

RolloutOutput

Final output from running your system.
class MyOutput(TypedDict):
    prediction: str
    confidence: float

Core Methods

evaluate()

Runs your system with the candidate parameters on a batch of examples.
def evaluate(
    self,
    batch: list[DataInst],
    candidate: dict[str, str],
    capture_traces: bool = False,
) -> EvaluationBatch[Trajectory, RolloutOutput]:
    outputs = []
    scores = []
    trajectories = [] if capture_traces else None
    
    for example in batch:
        # Build your system with candidate parameters
        system = self.build_system(candidate)
        
        # Run it
        result = system.run(example)
        
        # Compute score (higher is better)
        score = self.compute_score(result, example)
        
        outputs.append(result)
        scores.append(score)
        
        if capture_traces:
            trajectory = self.extract_trajectory(result)
            trajectories.append(trajectory)
    
    return EvaluationBatch(
        outputs=outputs,
        scores=scores,
        trajectories=trajectories,
    )
Never raise exceptions for individual example failures. Return a valid EvaluationBatch with failure scores (e.g., 0.0). Reserve exceptions for systemic failures (missing model, misconfiguration).

make_reflective_dataset()

Converts evaluation results into structured feedback for the reflection LLM.
def make_reflective_dataset(
    self,
    candidate: dict[str, str],
    eval_batch: EvaluationBatch,
    components_to_update: list[str],
) -> dict[str, list[dict[str, Any]]]:
    reflective_data = {}
    
    for component in components_to_update:
        examples = []
        
        for traj in eval_batch.trajectories:
            example = {
                "Inputs": self.format_inputs(traj),
                "Generated Outputs": self.format_outputs(traj),
                "Feedback": self.generate_feedback(traj),
            }
            examples.append(example)
        
        reflective_data[component] = examples
    
    return reflective_data
Recommended structure:
{
    "component_name": [
        {
            "Inputs": {"question": "What is ML?"},
            "Generated Outputs": {"answer": "ML is..."},
            "Feedback": "Correct! Include more details about..."
        },
        # More examples
    ]
}

Built-in Adapters

GEPA provides several ready-to-use adapters:

DefaultAdapter

Simple adapter for optimizing system prompts.
from gepa.adapters.default_adapter import DefaultAdapter

adapter = DefaultAdapter(
    model="openai/gpt-4o-mini",
    evaluator=my_evaluator,
)
Use case: Single-prompt optimization with straightforward evaluation.

DspyAdapter

Optimizes DSPy program instructions.
from gepa.adapters.dspy_adapter import DspyAdapter

adapter = DspyAdapter(
    student_module=my_dspy_program,
    metric_fn=my_metric,
    feedback_map={"predictor_name": feedback_fn},
)
Use case: DSPy programs with multiple predictors. See DSPy Integration.

GenericRAGAdapter

Optimizes RAG pipeline prompts.
from gepa.adapters.generic_rag_adapter import GenericRAGAdapter

adapter = GenericRAGAdapter(
    vector_store=my_vector_store,
    llm_model=my_llm,
    embedding_model="openai/text-embedding-3-small",
    rag_config={"top_k": 5, "retrieval_strategy": "similarity"},
)
Use case: RAG systems with customizable retrieval and generation prompts.

OptimizeAnythingAdapter

Internal adapter powering optimize_anything(). Wraps simple evaluator functions.
# Used automatically by optimize_anything()
result = optimize_anything(
    seed_candidate="...",
    evaluator=my_evaluator,
    ...
)

Using Adapters with optimize()

The gepa.optimize() function provides a streamlined API for adapter-based optimization:
from gepa import optimize

result = optimize(
    seed_candidate={"prompt": "Initial prompt"},
    trainset=train_data,
    valset=val_data,
    adapter=my_adapter,
    reflection_lm="openai/gpt-4o",
    max_metric_calls=100,
)

API Comparison

Featureoptimize_anything()optimize()
Use caseSimple evaluatorCustom adapter
Candidate formatstr or dictdict[str, str]
EvaluatorSimple functionAdapter protocol
FlexibilityQuick prototypingFull control

Adapter Best Practices

1

Type Your Data

Use TypedDict or dataclasses for clarity:
from typing import TypedDict

class MyDataInst(TypedDict):
    input: str
    expected_output: str
    metadata: dict
2

Handle Failures Gracefully

Return failure scores instead of raising:
def evaluate(self, batch, candidate, capture_traces=False):
    scores = []
    for example in batch:
        try:
            result = self.run_system(example, candidate)
            score = self.compute_score(result)
        except Exception as e:
            # Log error but don't raise
            score = 0.0
        scores.append(score)
    return EvaluationBatch(outputs=..., scores=scores, ...)
3

Provide Rich Feedback

The more context in your reflective dataset, the better:
feedback = {
    "Inputs": {"question": example.question},
    "Generated Outputs": {"answer": result.answer},
    "Feedback": (
        f"Expected: {example.expected}\n"
        f"Got: {result.answer}\n"
        f"Error: {result.error if result.error else 'None'}\n"
        f"Suggestion: Check the reasoning in step 3."
    ),
}
4

Use Multi-Objective Scores

Track multiple metrics:
return EvaluationBatch(
    outputs=outputs,
    scores=scores,  # Primary score
    objective_scores=[  # Additional metrics
        {"accuracy": 0.9, "latency": 0.1, "cost": 0.8}
        for _ in batch
    ],
)

Example: Custom Adapter

Here’s a minimal adapter for a custom system:
from gepa.core.adapter import GEPAAdapter, EvaluationBatch
from typing import TypedDict

class MyData(TypedDict):
    input: str
    expected: str

class MyTrace(TypedDict):
    steps: list[str]
    final_output: str

class MyOutput(TypedDict):
    result: str

class MyAdapter(GEPAAdapter[MyData, MyTrace, MyOutput]):
    def __init__(self, my_system):
        self.system = my_system
    
    def evaluate(self, batch, candidate, capture_traces=False):
        outputs = []
        scores = []
        traces = [] if capture_traces else None
        
        # Update system with candidate parameters
        self.system.update_config(candidate)
        
        for example in batch:
            # Run system
            result = self.system.process(example["input"])
            
            # Score result
            score = 1.0 if result == example["expected"] else 0.0
            
            outputs.append({"result": result})
            scores.append(score)
            
            if capture_traces:
                traces.append({
                    "steps": self.system.get_steps(),
                    "final_output": result,
                })
        
        return EvaluationBatch(
            outputs=outputs,
            scores=scores,
            trajectories=traces,
        )
    
    def make_reflective_dataset(self, candidate, eval_batch, components):
        data = {}
        
        for component in components:
            examples = []
            for trace in eval_batch.trajectories:
                examples.append({
                    "Inputs": trace["steps"][0],
                    "Generated Outputs": trace["final_output"],
                    "Feedback": "Analyze the steps and improve logic.",
                })
            data[component] = examples
        
        return data

# Use it
adapter = MyAdapter(my_system)
result = optimize(
    seed_candidate={"config": "initial config"},
    trainset=train_data,
    valset=val_data,
    adapter=adapter,
    max_metric_calls=50,
)

Advanced Features

Custom Proposal Logic

Override the default proposal mechanism:
class MyAdapter(GEPAAdapter[...]):
    def propose_new_texts(
        self,
        candidate: dict[str, str],
        reflective_dataset: dict[str, list[dict]],
        components_to_update: list[str],
    ) -> dict[str, str]:
        # Your custom logic here
        new_texts = {}
        for comp in components_to_update:
            feedback = reflective_dataset[comp]
            new_texts[comp] = self.my_custom_proposer(feedback)
        return new_texts

Batched Evaluation

Leverage parallelization in your adapter:
def evaluate(self, batch, candidate, capture_traces=False):
    # Batch inference for efficiency
    results = self.system.batch_process(
        [ex["input"] for ex in batch],
        config=candidate,
    )
    
    scores = [self.score(r, ex["expected"]) 
              for r, ex in zip(results, batch)]
    
    return EvaluationBatch(...)

Next Steps

Custom Adapters

Step-by-step guide to building adapters

DSPy Integration

Deep dive into the DSPy adapter

Evaluation Metrics

Design effective scoring functions

optimize_anything

Simple API without adapters

Build docs developers (and LLMs) love