First Optimization¶
This example optimizes the instructions of a small support-routing subject. It uses a deterministic reflection callable so it can be understood without a second model provider.
1. Define examples¶
from pydantic_gepa import Example
train = [
Example(name="refund", inputs="Refund order 123", expected_output="refund"),
Example(name="shipping", inputs="Where is my parcel?", expected_output="shipping"),
]
validation = [
Example(name="refund-validation", inputs="I need my money back", expected_output="refund"),
Example(name="shipping-validation", inputs="Tracking has not updated", expected_output="shipping"),
]
Training and validation are explicit. Validation should represent the behavior you want to generalize to, not simply repeat training examples.
2. Define the subject¶
active_instructions = "Route every request as other."
def run_subject(text: str) -> str:
lowered = text.lower()
if "refund" in active_instructions.lower() and (
"refund" in lowered or "money back" in lowered
):
return "refund"
if "shipping" in active_instructions.lower() and (
"parcel" in lowered or "tracking" in lowered
):
return "shipping"
return "other"
In a real application, this function usually calls a Pydantic AI agent. The optimizer only requires an input-to-output callable.
3. Bind a candidate component¶
Pydantic AI agents can use AgentInstructionsInjection. For this plain callable
we use a typed candidate context:
from pydantic_gepa import CandidateContext, Component, DerivedValueInjection
instructions = Component(
name="instructions",
initial_text="Route every request as other.",
kind="instructions",
)
instruction_context = CandidateContext[str]("instructions", instructions.initial_text)
def subject(text: str) -> str:
global active_instructions
active_instructions = instruction_context.require()
return run_subject(text)
injection = DerivedValueInjection(
component="instructions",
context=instruction_context,
required_components=("instructions",),
derive_value=lambda candidate: candidate["instructions"],
)
An injection is active only while an example is evaluated. Context-local values prevent one candidate from becoming permanent application state.
4. Optimize¶
from pydantic_gepa import CallableReflectionModel, optimize
from pydantic_gepa.configuration import BudgetConfig, GEPAConfig, ReflectionConfig
config = GEPAConfig(
budget=BudgetConfig(max_metric_calls=12),
reflection=ReflectionConfig(
model=CallableReflectionModel(
lambda _prompt: "Route refund requests as refund and delivery questions as shipping."
)
),
)
result = optimize(
train=train,
validation=validation,
task=subject,
score=lambda ctx: float(ctx.output == ctx.expected_output),
components=[instructions],
injections=[injection],
config=config,
)
5. Inspect the evidence¶
print(result.best_candidate.values["instructions"])
print(result.best_score)
print(result.validation_scores)
for candidate in result.candidate_history:
print(candidate.candidate_id, candidate.parent_ids, candidate.score)
The result preserves candidate values, parent-child lineage, objective and validation scores, budget use, Pareto information when available, and artifact references. See Results and lineage.
The repository's runnable version is examples/basic.py.