How GEPA Candidate JSON Becomes a DSPy Program
High-level flow
Inside GEPA (the gepa library), candidates are plain dicts:
candidate: dict[str, str] mapping component names → text blobs.
- For normal predictors:
{"predictor_name": "new instruction text", ...}.
- For tool-optimized ReAct modules: special entries like
{"tool_module:<extract_predictor_name>": "<JSON string with instructions + tools>"}.
In DSPy's GEPA wrapper (dspy/teleprompt/gepa/gepa.py):
Seed candidate for GEPA is built from the original student via _build_seed_candidate.
GEPA runs and returns a GEPAResult with candidates and best_candidate.
DSPy then post-processes the best candidate:
new_prog = adapter.build_program(gepa_result.best_candidate)
If track_stats=True, it also calls DspyGEPAResult.from_gepa_result, which maps all raw candidates to actual DSPy programs via adapter.build_program(c).
So: Result.best_candidate is turned into a DSPy program by DspyAdapter.build_program as a final post-processing step.
What build_program actually does
In dspy/teleprompt/gepa/gepa_utils.py:
- Clone original student:
new_prog = self.student.deepcopy().
- Predictor text:
- All non-
tool_module keys become predictor_candidates[name] = instruction_string.
- For tool-optimized entries (
tool_module:...), the JSON is parsed; any string-valued entries keyed by predictor name are merged into predictor_candidates.
- Signatures:
- For each predictor in
new_prog.named_predictors(), it calls
pred.signature = pred.signature.with_instructions(...).
- This only swaps the instructions field; the rest of the signature (input/output fields, types, and any stored metadata) is preserved.
- Tools (when
enable_tool_optimization=True):
_update_tool_descriptions finds all Tool objects in the module tree and updates:
tool.desc
tool.args[arg_name]["description"] when provided.
After build_program, you have a normal DSPy Module with full signatures, any pre-existing demos, and updated instructions/tool descriptions.
What build_program actually does
In dspy/teleprompt/gepa/gepa_utils.py:
def build_program(self, candidate: dict[str, str]):
new_prog = self.student.deepcopy()
# 1) Start with plain string instructions
predictor_candidates = {k: v for k, v in candidate.items() if not k.startswith(TOOL_MODULE_PREFIX)}
tool_candidates = {}
if self.enable_tool_optimization:
for key, value in candidate.items():
if not key.startswith(TOOL_MODULE_PREFIX):
continue
config = json.loads(value)
for pred_name, instruction in config.items():
if isinstance(instruction, str):
predictor_candidates[pred_name] = instruction
tool_candidates.update(config.get("tools", {}))
# 2) Update predictor instructions
for name, pred in new_prog.named_predictors():
if name in predictor_candidates:
pred.signature = pred.signature.with_instructions(predictor_candidates[name])
# 3) Update tool descriptions (ReAct tool config)
if tool_candidates:
self._update_tool_descriptions(new_prog, tool_candidates)
return new_prog
How this becomes "DSPy-style JSON" and how program.load fits in
The saving/loading mechanism is generic DSPy, not GEPA-specific:
Module.save(path) (from BaseModule.save) persists state (parameters, including instructions and demos) to:
path.json or path.pkl when save_program=False.
Module.save(path, save_program=True) persists a full program (architecture + state) as:
- Directory
path/ containing program.pkl and metadata.json.
For state-only JSON:
- You create a base program with the same architecture, then call
program.load("state.json") to load its saved instructions/demos/etc.
For full-program directories:
- You use
dspy.utils.saving.load("dir", allow_pickle=True) to get back the exact module.
teleprompt/utils.save_candidate_program uses program.save(...) to dump candidate programs, and later code calls trial_program.load(trial["program_path"]) to restore them.
In the GEPA integration, nothing special is added on top of this: once new_prog = adapter.build_program(...) exists, you can call .save() yourself, and later load() behaves exactly like with any other DSPy program.
Where do DEMOS come from?
Short answer: GEPA does not create demos at all. It preserves whatever demos the student already has; it only evolves text instructions (and optionally tool descriptions).
Details:
GEPA's adapter builds a reflective dataset for the instruction proposer (make_reflective_dataset) using trajectories. These ReflectiveExample entries are temporary supervision fed into the reflection LLM (via InstructionProposalSignature or ToolProposer) to generate new instructions; they are not stored as .demos on the final program.
The only place demos are systematically created in this codebase is in the few-shot / bootstrap utilities (e.g. create_n_fewshot_demo_sets in dspy/teleprompt/utils.py), which is separate from GEPA.
Therefore:
- If your
student was produced by something like BootstrapFewShot (or already had .demos set), those demos are carried through because build_program deep-copies the student and only touches signature.instructions and tools.
- GEPA's optimization itself does not synthesize or modify demos; it only uses reflective datasets internally to improve instructions.
Summary
- GEPA candidate JSON = dict mapping component names to text (or JSON for tool modules).
- At the end,
gepa_result.best_candidate is turned into a DSPy Module via DspyAdapter.build_program, which:
- Clones the original student,
- Rewrites
signature.instructions per predictor,
- Optionally updates tool descriptions.
- To get a DSPy-style JSON file, you then call
new_prog.save("program.json") or new_prog.save("prog_dir", save_program=True). Later you restore with either program.load("program.json") (state-only) or dspy.load("prog_dir", allow_pickle=True) (full program).
- DEMOS:
- Are not created by GEPA.
- Whatever demos were present on the input
student remain on the optimized program.
- GEPA's internal reflective datasets are separate and not persisted as demos.
1---2name: how-gepa-candidate-json-becomes-a-dspy-program3description: So: Result.best_candidate is turned into a DSPy program by DspyAdapter.build_program as a final post-processing step.4---5# How GEPA Candidate JSON Becomes a DSPy Program67## High-level flow89- **Inside GEPA (the `gepa` library)**, candidates are plain dicts:10 - `candidate: dict[str, str]` mapping **component names** → **text blobs**.11 - For normal predictors: `{"predictor_name": "new instruction text", ...}`.12 - For tool-optimized ReAct modules: special entries like 13 `{"tool_module:<extract_predictor_name>": "<JSON string with instructions + tools>"}`.1415- **In DSPy's GEPA wrapper** (`dspy/teleprompt/gepa/gepa.py`):16 - Seed candidate for GEPA is built from the original `student` via `_build_seed_candidate`.17 - GEPA runs and returns a `GEPAResult` with `candidates` and `best_candidate`.18 - DSPy then **post-processes** the best candidate:1920 ```python21 new_prog = adapter.build_program(gepa_result.best_candidate)22 ```2324 - If `track_stats=True`, it also calls `DspyGEPAResult.from_gepa_result`, which maps **all** raw candidates to actual DSPy programs via `adapter.build_program(c)`.2526So: **`Result.best_candidate` is turned into a DSPy program by `DspyAdapter.build_program` as a final post-processing step.**2728---2930## What `build_program` actually does3132In `dspy/teleprompt/gepa/gepa_utils.py`:3334- **Clone original student**: `new_prog = self.student.deepcopy()`.35- **Predictor text**:36 - All non-`tool_module` keys become `predictor_candidates[name] = instruction_string`.37 - For tool-optimized entries (`tool_module:...`), the JSON is parsed; any string-valued entries keyed by predictor name are merged into `predictor_candidates`.38- **Signatures**:39 - For each predictor in `new_prog.named_predictors()`, it calls 40 `pred.signature = pred.signature.with_instructions(...)`.41 - This **only swaps the instructions field**; the rest of the signature (input/output fields, types, and any stored metadata) is preserved.42- **Tools** (when `enable_tool_optimization=True`):43 - `_update_tool_descriptions` finds all `Tool` objects in the module tree and updates:44 - `tool.desc`45 - `tool.args[arg_name]["description"]` when provided.4647After `build_program`, you have a normal **DSPy `Module`** with full signatures, any pre-existing demos, and updated instructions/tool descriptions.4849### What build_program actually does50In `dspy/teleprompt/gepa/gepa_utils.py`:51```python52def build_program(self, candidate: dict[str, str]):53 new_prog = self.student.deepcopy()5455 # 1) Start with plain string instructions56 predictor_candidates = {k: v for k, v in candidate.items() if not k.startswith(TOOL_MODULE_PREFIX)}5758 tool_candidates = {}59 if self.enable_tool_optimization:60 for key, value in candidate.items():61 if not key.startswith(TOOL_MODULE_PREFIX):62 continue6364 config = json.loads(value)6566 for pred_name, instruction in config.items():67 if isinstance(instruction, str):68 predictor_candidates[pred_name] = instruction6970 tool_candidates.update(config.get("tools", {}))7172 # 2) Update predictor instructions73 for name, pred in new_prog.named_predictors():74 if name in predictor_candidates:75 pred.signature = pred.signature.with_instructions(predictor_candidates[name])7677 # 3) Update tool descriptions (ReAct tool config)78 if tool_candidates:79 self._update_tool_descriptions(new_prog, tool_candidates)8081 return new_prog82```8384---8586## How this becomes "DSPy-style JSON" and how `program.load` fits in8788The **saving/loading** mechanism is generic DSPy, not GEPA-specific:8990- `Module.save(path)` (from `BaseModule.save`) persists **state** (parameters, including instructions and demos) to:91 - `path.json` or `path.pkl` when `save_program=False`.92- `Module.save(path, save_program=True)` persists a **full program** (architecture + state) as:93 - Directory `path/` containing `program.pkl` and `metadata.json`.9495- **For state-only JSON**:96 - You create a base program with the same architecture, then call `program.load("state.json")` to load its saved instructions/demos/etc.97- **For full-program directories**:98 - You use `dspy.utils.saving.load("dir", allow_pickle=True)` to get back the exact module.99- `teleprompt/utils.save_candidate_program` uses `program.save(...)` to dump candidate programs, and later code calls `trial_program.load(trial["program_path"])` to restore them.100101In the GEPA integration, **nothing special is added** on top of this: once `new_prog = adapter.build_program(...)` exists, you can call `.save()` yourself, and later `load()` behaves exactly like with any other DSPy program.102103---104105## Where do DEMOS come from?106107**Short answer:** GEPA **does not create demos at all.** It preserves whatever demos the `student` already has; it only evolves text instructions (and optionally tool descriptions).108109Details:110111- GEPA's adapter builds a **reflective dataset** for the instruction proposer (`make_reflective_dataset`) using trajectories. These `ReflectiveExample` entries are **temporary supervision** fed into the reflection LLM (via `InstructionProposalSignature` or `ToolProposer`) to generate new instructions; they are **not stored as `.demos`** on the final program.112113- The **only place demos are systematically created** in this codebase is in the few-shot / bootstrap utilities (e.g. `create_n_fewshot_demo_sets` in `dspy/teleprompt/utils.py`), which is separate from GEPA.114115**Therefore:**116117- If your `student` was produced by something like `BootstrapFewShot` (or already had `.demos` set), those demos are **carried through** because `build_program` deep-copies the student and only touches `signature.instructions` and tools.118- GEPA's optimization itself **does not synthesize or modify demos**; it only uses reflective datasets internally to improve instructions.119120---121122## Summary123124- **GEPA candidate JSON** = dict mapping component names to text (or JSON for tool modules).125- At the end, **`gepa_result.best_candidate` is turned into a DSPy `Module`** via `DspyAdapter.build_program`, which:126 - Clones the original student,127 - Rewrites `signature.instructions` per predictor,128 - Optionally updates tool descriptions.129- To get a **DSPy-style JSON file**, you then call `new_prog.save("program.json")` or `new_prog.save("prog_dir", save_program=True)`. Later you restore with either `program.load("program.json")` (state-only) or `dspy.load("prog_dir", allow_pickle=True)` (full program).130- **DEMOS**:131 - Are **not created by GEPA**.132 - Whatever demos were present on the input `student` remain on the optimized program.133 - GEPA's internal reflective datasets are separate and not persisted as demos.