# 4. Recipes

Task‑oriented how‑tos. Each recipe is a self‑contained answer to "how do I use GEPA for ___?" Pick
the one that matches your situation. All code is accurate to the current API
(`src/gepa/api.py`, `src/gepa/core/adapter.py`).

---

## Recipe A — Optimize a prompt, with feedback that actually helps

**When:** you have a single‑turn prompt and a way to check answers.

The biggest lever you have is **feedback quality**. Replace the default substring check with your own
evaluator that returns a *sentence* explaining each failure. That sentence is the ASI the reflection
LLM learns from.

```python
import gepa
from gepa.adapters.default_adapter.default_adapter import DefaultAdapter, EvaluationResult

def my_evaluator(data, response) -> EvaluationResult:
    expected = data["answer"]
    correct = expected.lower() in response.lower()
    if correct:
        return EvaluationResult(score=1.0, feedback=f"Correct — found '{expected}'.")
    # Rich, actionable feedback — THIS is what makes GEPA improve:
    return EvaluationResult(
        score=0.0,
        feedback=(f"Incorrect. Expected '{expected}'. The response said: "
                  f"'{response[:200]}'. Likely cause: it didn't state the answer "
                  f"in the required format. Restate the final answer explicitly."),
    )

adapter = DefaultAdapter(model="openai/gpt-4o-mini", evaluator=my_evaluator)

result = gepa.optimize(
    seed_candidate={"system_prompt": "Answer the question."},
    trainset=trainset,                       # list of {"input","additional_context","answer"}
    adapter=adapter,                          # note: when you pass an adapter, omit task_lm
    reflection_lm="openai/gpt-4o",
    max_metric_calls=100,
)
```

> **Rule:** pass *either* `task_lm` (uses the default adapter) *or* `adapter` (your configured one) —
> not both. GEPA enforces this.

---

## Recipe B — Optimize code, a config, or any text artifact

**When:** the thing you're tuning isn't a prompt — it's a Python function, a YAML config, a policy,
an SVG, anything expressible as text and scorable by running it.

Use `optimize_anything`. Your evaluator runs the artifact and returns a score; `oa.log(...)` calls
inside it become ASI.

```python
import gepa.optimize_anything as oa
from gepa.optimize_anything import optimize_anything, GEPAConfig, EngineConfig

def evaluate(candidate: str) -> float:
    # candidate is, e.g., the source of a Python function. Run it safely and measure.
    try:
        result = run_and_measure(candidate)        # <-- your harness
        oa.log(f"Runtime: {result.ms} ms")
        oa.log(f"Correct on {result.passed}/{result.total} checks")
        if result.error:
            oa.log(f"Error: {result.error}")        # errors are the richest ASI
        return result.score
    except Exception as e:
        oa.log(f"Crashed: {e}")
        return 0.0

result = optimize_anything(
    seed_candidate="def solve(x):\n    return x  # naive starting point",
    evaluator=evaluate,
    objective="Make solve() pass all checks while minimizing runtime.",
    background="Inputs are integer arrays up to 10^6 elements.",   # optional domain hints
    config=GEPAConfig(engine=EngineConfig(max_metric_calls=300)),
)
print(result.best_candidate)
```

GEPA ships utilities for safe code execution (`src/gepa/utils/code_execution.py`) and capturing
stdout/stderr (`src/gepa/utils/stdio_capture.py`) if your artifact is runnable code. See the
`examples/` folder (`blackbox`, `arc_agi`, `adrs/cloudcast`) for full, real programs.

> **Runnable, beginner-friendly version of this recipe:**
> [`examples/program_synthesis_from_examples/`](../../examples/program_synthesis_from_examples/) has
> GEPA synthesize a Python function from labeled examples with a **100% local scorer** (no task LLM).
> It includes an offline `--smoke` self-test that runs with **no API key**, so you can confirm your
> setup before spending any budget. This is the recommended first example to actually run.

---

## Recipe C — Plug GEPA into your *own* system (write an adapter)

**When:** your system is more than one prompt — a multi‑step agent, a RAG pipeline, a tool‑using
loop — and you want GEPA to optimize text *inside* it while seeing your system's real traces.

An **adapter** is the glue between GEPA and your system. You implement two methods (a third is
optional). This is the full contract, lightly annotated:

```python
from gepa.core.adapter import EvaluationBatch, GEPAAdapter

class MyAdapter(GEPAAdapter):
    def evaluate(self, batch, candidate, capture_traces=False):
        """
        Run your system (configured with `candidate`'s text) on each input in `batch`.
        Return per-example outputs and scores. When capture_traces=True, also return a
        per-example 'trajectory' — whatever your make_reflective_dataset needs later.
        """
        outputs, scores, trajectories = [], [], ([] if capture_traces else None)
        for ex in batch:
            system = build_my_system(candidate)     # instantiate with the candidate's text
            out = system.run(ex["input"])
            score, feedback = my_metric(out, ex)    # higher is better
            outputs.append(out)
            scores.append(score)
            if trajectories is not None:   # i.e. capture_traces was True
                trajectories.append({"input": ex, "output": out, "feedback": feedback})
        return EvaluationBatch(outputs=outputs, scores=scores, trajectories=trajectories)

    def make_reflective_dataset(self, candidate, eval_batch, components_to_update):
        """
        Turn captured trajectories into clean records the reflection LLM reads.
        Return {component_name: [ {"Inputs":..., "Generated Outputs":..., "Feedback":...}, ... ]}.
        """
        records = []
        for traj in eval_batch.trajectories:
            records.append({
                "Inputs": traj["input"]["input"],
                "Generated Outputs": traj["output"],
                "Feedback": traj["feedback"],     # your sentence(s) of diagnosis = the ASI
            })
        return {components_to_update[0]: records}
```

Then run it:

```python
result = gepa.optimize(
    seed_candidate={"agent_instructions": "..."},   # your named components
    trainset=trainset,
    adapter=MyAdapter(),
    reflection_lm="openai/gpt-4o",
    max_metric_calls=200,
)
```

**Five rules the adapter must follow** (from `core/adapter.py`):

1. **Higher scores are better.** GEPA *sums* scores on the minibatch (acceptance) and *averages* them
   over the valset (tracking). Keep your metric on a consistent scale.
2. **Never crash on a single bad example.** Return a fallback score (e.g. `0.0`) and put the error in
   the trajectory's feedback — that failure is exactly what reflection needs to read.
3. **`len(outputs) == len(scores) == len(batch)`,** and when `capture_traces=True`,
   `len(trajectories) == len(batch)` too.
4. **Don't mutate** the incoming `batch` or `candidate` in place.
5. **The feedback you put in trajectories is your ASI.** Invest in it — it's the whole signal.

Full guide: `docs/docs/guides/adapters.md`.

---

## Recipe D — Use a ready‑made adapter (don't reinvent the glue)

GEPA ships adapters for common systems. Check here before writing your own:

| Your system | Adapter | Install |
|---|---|---|
| Single‑turn prompt | `DefaultAdapter` | core |
| Classification where confidence matters | `ConfidenceAdapter` | `pip install "gepa[confidence]"` |
| A full DSPy program (signatures, modules, control flow) | DSPy Full Program adapter | with DSPy |
| RAG over a vector store (Chroma, Weaviate, Qdrant, Pinecone, LanceDB, Milvus) | `GenericRAGAdapter` | per store |
| An MCP server's tool descriptions & system prompt | `MCPAdapter` | `gepa[mcp]` |
| A LangChain / LangGraph pipeline | `LangChainAdapter` | `pip install "gepa[langchain]"` |
| A terminal‑use agent (Terminus) | `TerminalBenchAdapter` | adapter extra |
| Math word problems | `AnyMathsAdapter` | core |

Each lives under `src/gepa/adapters/` with an example in `src/gepa/examples/` or `examples/`. They're
also the best reference if you end up writing your own.

---

## Recipe E — Control the run: budget, stopping, tracking, resuming

```python
from gepa import TimeoutStopCondition, NoImprovementStopper

result = gepa.optimize(
    seed_candidate=seed,
    trainset=trainset,
    valset=valset,
    task_lm="openai/gpt-4o-mini",
    reflection_lm="openai/gpt-4o",

    # --- Budget / stopping (combine as many as you like) ---
    max_metric_calls=200,
    stop_callbacks=[
        TimeoutStopCondition(timeout_seconds=3600),
        NoImprovementStopper(max_iterations_without_improvement=10),
    ],

    # --- Search behavior ---
    candidate_selection_strategy="pareto",   # default; the champions-board logic
    use_merge=True,                          # breed specialists (off by default)

    # --- Observability ---
    display_progress_bar=True,
    use_wandb=True,                          # or use_mlflow=True
    run_dir="./gepa_runs/exp1",              # save state to disk
)
```

**Resuming:** if you pass a `run_dir` and the run is interrupted, calling `optimize` again with the
*same* `run_dir` resumes from the last saved checkpoint. To stop a long run gracefully, create a file
named `gepa.stop` inside `run_dir`.

**Cost tracking:** see `docs/docs/guides/cost-tracking.md` and `max_reflection_cost`.

---

## Recipe F — Make optimization cheaper and more reproducible

- **Cache evaluations.** `cache_evaluation=True` skips re‑scoring identical (candidate, example)
  pairs — saves metric calls when candidates overlap.
- **Reproducibility.** Set `seed=0` (the default). Same seed + same data + same models → same run.
- **Smaller minibatch / valset for a dry run.** During wiring‑up, use a tiny `valset` and
  `max_metric_calls=20` to confirm correctness before spending real budget.
- **Right‑size your models.** `task_lm` can be small and cheap; spend on a strong `reflection_lm`,
  because the rewrite quality is what moves the score.

---

## Which recipe am I?

```
Is your artifact a single prompt?
  └─ yes → Recipe A (with rich feedback)
  └─ no, it's code/config/SVG/etc. with no dataset
            → Recipe B (optimize_anything)
  └─ no, it's a multi-step system (agent / RAG / pipeline)
            → is there a ready-made adapter (Recipe D table)?
                 └─ yes → Recipe D
                 └─ no  → Recipe C (write an adapter)
```

Next: the [Glossary](05-glossary.md) defines every term, or revisit the [interactive guide](index.html).
