dspy.ReAct module whose instructions contain every tool description, which means that rewriting the instructions rewrites the tool descriptions too. The metric runs one scenario against the candidate and scores the verdict, with a penalty for every step over the budget, where a step is one reply or one tool call, and GEPA additionally takes the judge’s reasoning as text feedback for the next candidate.
The runnable example is in the SDK repository: sdks/python/examples/agent-optimization.
What you need
- Python 3.10 or later. In the example folder,
uv syncinstalls everything. In your own project,pip install "langwatch[dspy]" "dspy[optuna]" langwatch-scenario(MIPROv2 needs theoptunaextra), and importscenariobeforedspyin every module, because in the other order dspy 3.3 fails while loading numpy. OPENAI_API_KEYfor the agent, the simulated user and the judge, andLANGWATCH_API_KEYso the optimization run and the simulations appear in LangWatch.- Five to ten scenarios. One optimization run executes each of them several times: the MIPROv2 run of the example below took 64 scenario runs, 42 minutes and $0.93, with
gpt-5-minias the agent, the simulated user and the judge, andgpt-5writing the instruction candidates.
Step 1: Wrap the agent as a DSPy module
The agent is adspy.ReAct over the tools, and a scenario adapter passes the conversation to it as dspy.History plus the last user message and returns the answer:
trajectory_to_messages turns the ReAct trajectory into tool_calls and tool messages followed by the answer, so the judge sees the tool calls and the rejected arguments as well as the reply; both helpers are in the example’s agent.py. The tool descriptions come from the docstrings: dspy.ReAct writes them into the instructions of its react predictor, and that predictor is what the optimizer rewrites.
Step 2: One example per scenario
Eachdspy.Example is one scenario: the situation, the criteria and a step budget, where a rejected tool call that the agent retries counts as an extra step.
Step 3: The program runs the scenario, the metric scores it
The optimizer calls the program on each example and then the metric on the result, so the outer program runs the scenario with the candidate agent inside it and returns the verdict, and the metric only has to score:scenario.run executes the conversation on its own thread, and GEPA reads the trace of the thread that called the program, so the example’s forward copies the DSPy trace of the scenario thread back into the calling thread.
Step 4: Run the optimizer
- GEPA
- MIPROv2
max_metric_calls=48 bought four iterations on the example: the first full pass costs six calls, and each accepted candidate costs a minibatch of three for the parent, three for the candidate and a full pass of six. GEPA’s auto="light" budget plans hundreds of metric calls, which is too many when every call is a full simulation.dspy.Evaluate(devset=trainset, metric=scenario_metric, num_threads=3), so the before and after pass rates and steps come from the same scenarios; the example’s evaluate_suite prints that table. To run the example:
Step 5: Read the result in LangWatch
- Experiments shows the optimization run as a score chart, one point per trial, with the instructions of each predictor at that step. A MIPROv2 run appears through
langwatch.dspy.init, and the GEPA example logs each candidate through a callback inoptimize_gepa.py. See DSPy visualization. - Agent Testing shows every simulation the optimizer ran, as runs of the
dspy-optimizationset. The example puts the baseline pass and the final pass in their own batches, named<run id>-baselineand<run id>-best, so they appear as two runs of six next to the larger runs from inside the optimizer, and the scripts print both names.
The MIPROv2 run in Experiments: one point per trial, the best trial marked, and the instructions of the selected step below the chart.
The dspy-optimization set in Agent Testing: the baseline and best passes of both runs as runs of six, next to the runs from inside the optimizers.
What the example runs showed
One run of each script on the six scenarios, with the agent, the simulated user and the judge ongpt-5-mini and gpt-5 as the proposer or the reflection model:
Neither optimizer saw the tool code. Both saw the rejected calls in the traces, and both wrote the accepted values into the
react instructions. MIPROv2, from the proposer’s candidates:
finish step as a tool call. The criterion now names the tools it means.
On the MIPROv2 run the angry customer passed before and failed after, and the 83% of the best trial did not repeat on the final pass, which is the non-determinism described in the Limits below.
Limits
- The optimizer changes text, meaning the instructions and the tool descriptions. A tool that needs a different schema, a missing tool or a harness cap stays a code change: see Fix tool calls.
- A scenario is not deterministic: the simulated user words the situation differently on every run, and a candidate can pass once and fail once. Evaluate the baseline and the final program twice before you compare them, and use
--repeaton the platform for the same reason. - The score counts the agent’s steps, so the cost of the simulated user and the judge is not in it.
- DSPy is Python. A TypeScript agent runs the guided loop instead: see Reduce turns.
Also check: Optimization algorithms (which optimizer to pick), Scenarios in code, DSPy visualization (how a run appears in Experiments).