# 3. Quickstart

Goal: from nothing to a working GEPA optimization in about 10 minutes. Everything here is
copy‑paste‑ready. If a concept is unfamiliar, see [How GEPA works](02-how-gepa-works.md) or the
[Glossary](05-glossary.md).

---

## Prerequisites

| You need | How to check | If missing |
|---|---|---|
| Python 3.10+ | `python --version` | Install from <https://python.org> |
| pip | `pip --version` | Comes with Python |
| An LLM API key | `echo $env:OPENAI_API_KEY` (PowerShell) / `echo $OPENAI_API_KEY` (bash) | Get one from your provider |

GEPA talks to models through **LiteLLM**, so it works with OpenAI, Anthropic, Google, local models
via Ollama, and more. The examples below use OpenAI model names like `openai/gpt-4.1-mini`; swap in
whatever you have access to (e.g. `anthropic/claude-haiku-4-5`).

Set your key once per shell session:

```powershell
# PowerShell (Windows)
$env:OPENAI_API_KEY = "sk-..."
```

```bash
# bash / zsh (macOS, Linux)
export OPENAI_API_KEY="sk-..."
```

---

## Step 1 — Install

```bash
pip install gepa
```

Verify it imported correctly:

```bash
python -c "import gepa; print('GEPA ready')"
```

Expected output:

```
GEPA ready
```

> Want every optional adapter (RAG, LangChain, confidence, …)? Install `pip install "gepa[full]"`.
> The core `pip install gepa` is all you need for this page.

---

## Step 2 — Your first optimization (the built‑in dataset)

This is the smallest end‑to‑end run. It optimizes a system prompt for AIME competition math using a
dataset that ships with GEPA. Save as `first_run.py`:

```python
import gepa

# A small math dataset bundled with GEPA (questions + correct answers).
trainset, valset, _ = gepa.examples.aime.init_dataset()

# The text we want GEPA to improve.
seed_prompt = {
    "system_prompt": "You are a helpful assistant. Answer the question. "
                     "Put your final answer in the format '### <answer>'"
}

result = gepa.optimize(
    seed_candidate=seed_prompt,
    trainset=trainset,
    valset=valset,
    task_lm="openai/gpt-4.1-mini",   # the model being optimized (cheap is fine)
    reflection_lm="openai/gpt-5",     # the strong model that rewrites the prompt
    max_metric_calls=150,             # budget — raise for better results
    display_progress_bar=True,
)

print("\n=== BEST PROMPT FOUND ===")
print(result.best_candidate["system_prompt"])
print("\nBest validation score:", result.val_aggregate_scores[result.best_idx])
print("Metric calls used:", result.total_metric_calls)
```

> **Where does this dataset come from?** `gepa.examples.aime.init_dataset()` downloads two public
> math‑competition datasets from **Hugging Face** the first time you call it (so it needs the
> `datasets` library and an internet connection):
> - **train + validation** — past **AIME** (American Invitational Mathematics Examination) problems
>   from [`AI-MO/aimo-validation-aime`](https://huggingface.co/datasets/AI-MO/aimo-validation-aime),
>   shuffled with a fixed seed and split in half.
> - **test** — the held‑out **AIME 2025** problems from
>   [`MathArena/aime_2025`](https://huggingface.co/datasets/MathArena/aime_2025).
>
> Each example is a dict like `{"input": "<problem text>", "answer": "### 204", ...}`. AIME answers
> are integers 0–999, which makes automatic scoring a clean exact‑match. You only need this for the
> bundled demo — for your own data you supply your own list of `{"input", "answer"}` dicts (see
> [Step 3](#step-3--optimize-your-own-prompt-no-special-dataset)).

Run it:

```bash
python first_run.py
```

You'll see a progress bar advance as GEPA spends its budget, log lines like
`Iteration 7: Found a better program on the valset with score 0.53`, and finally the evolved prompt —
typically far longer and more detailed than the one‑liner you started with.

> **Cost & time note.** This run makes up to 150 metric calls plus reflection LLM calls. With small
> models it's a few dollars and a few minutes. To do a near‑free dry run first, drop
> `max_metric_calls` to `20` — you won't get a great prompt, but you'll confirm everything is wired
> up.

---

## Step 3 — Optimize *your own* prompt (no special dataset)

The built‑in adapter (`DefaultAdapter`) handles single‑turn prompt tasks out of the box. You supply
examples as dicts with three keys: `input`, `additional_context`, and `answer`. GEPA scores an answer
as correct if `answer` appears in the model's response.

```python
import gepa

trainset = [
    {"input": "What is 2+2?",                      "additional_context": {}, "answer": "4"},
    {"input": "What is the capital of France?",    "additional_context": {}, "answer": "Paris"},
    {"input": "What color do you get mixing red and blue?", "additional_context": {}, "answer": "purple"},
    {"input": "How many legs does a spider have?", "additional_context": {}, "answer": "8"},
    # ... add 20–100+ for real use; more & more varied examples = better optimization
]

seed_prompt = {"system_prompt": "You are a helpful assistant. Answer questions concisely."}

result = gepa.optimize(
    seed_candidate=seed_prompt,
    trainset=trainset,
    task_lm="openai/gpt-4o-mini",
    reflection_lm="openai/gpt-4o",
    max_metric_calls=50,
)

print(result.best_candidate["system_prompt"])
```

> Note: when no `valset` is given, GEPA reuses the `trainset` for validation. For results that
> generalize, provide a separate `valset`.

---

## Step 4 — Optimize *anything*, not just prompts

`optimize_anything` is the general entry point. Instead of a dataset, you provide one function:
`evaluator(candidate) -> score`. Inside it, you run your system however you like and call `oa.log(...)`
to record diagnostics — those logs become the **ASI** (Actionable Side Information) that reflection
reads. This is how people optimize code, configs, agent designs, and SVGs.

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

def evaluate(candidate: str) -> float:
    """Run the candidate text through your system and score it (higher = better)."""
    result = run_my_system(candidate)          # <-- your code
    oa.log(f"Output: {result.output}")          # captured as ASI
    oa.log(f"Error: {result.error}")            # captured as ASI — feeds reflection
    return result.score

result = optimize_anything(
    seed_candidate="<your initial artifact, as a string>",
    evaluator=evaluate,
    objective="Describe in plain English what 'good' means here.",
    config=GEPAConfig(engine=EngineConfig(max_metric_calls=100)),
)

print(result.best_candidate)
```

For richer feedback, return a `(score, dict)` tuple instead of a bare float:

```python
def evaluate(candidate: str) -> tuple[float, dict]:
    result = run_my_system(candidate)
    return result.score, {"Error": result.stderr, "Output": result.stdout}
```

`optimize_anything` has three modes, chosen by whether you pass a `dataset`/`valset`:

| Mode | Args | Use it for |
|---|---|---|
| **Single‑task search** | neither | Solve one hard problem; the candidate *is* the answer (e.g. pack circles, tune one function). |
| **Multi‑task search** | `dataset` only | Solve a batch of related problems, sharing insight across them (e.g. many CUDA kernels). |
| **Generalization** | `dataset` + `valset` | Build a skill that transfers to unseen problems (e.g. a reusable prompt). |

You can even pass `seed_candidate=None` ("seedless") and let the reflection LLM write the first draft
from your `objective`. See `docs/docs/api/optimize_anything/optimize_anything.md`.

---

## Step 5 — Using GEPA inside DSPy (recommended for AI pipelines)

If you build LLM pipelines with [DSPy](https://dspy.ai/), GEPA is available as `dspy.GEPA` and
optimizes the instructions inside your program. The key is a **feedback metric** — one that returns a
score *and* a sentence of explanation:

```python
import dspy

dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))

class QAProgram(dspy.Module):
    def __init__(self):
        self.generate = dspy.ChainOfThought("question -> answer")
    def forward(self, question):
        return self.generate(question=question)

def metric_with_feedback(example, pred, trace=None):
    correct = example.answer.lower() in pred.answer.lower()
    feedback = (f"Correct! '{pred.answer}' matches '{example.answer}'." if correct
                else f"Incorrect. Expected '{example.answer}', got '{pred.answer}'. "
                     f"Reason more carefully before answering.")
    return dspy.Prediction(score=1.0 if correct else 0.0, feedback=feedback)

optimizer = dspy.GEPA(
    metric=metric_with_feedback,
    reflection_lm=dspy.LM("openai/gpt-4o"),
    auto="light",         # automatic budget
    track_stats=True,
)
optimized = optimizer.compile(QAProgram(), trainset=trainset)
print(optimized.generate.signature.instructions)
```

---

## Reading the result

Whatever entry point you use, you get a `GEPAResult`:

```python
result.best_candidate                          # ← the optimized text (your deliverable)
result.best_idx                                # index of the best candidate
result.val_aggregate_scores[result.best_idx]   # its validation score
result.candidates                              # every candidate GEPA explored
result.total_metric_calls                      # budget actually spent
result.per_val_instance_best_candidates        # the "champions board": val_id -> frontier candidates
```

---

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `AssertionError: ... requires a task LM` | No `adapter` and no `task_lm` given. | Pass `task_lm=`, or pass a custom `adapter`. |
| `reflection_lm was not provided` | The default proposer needs a reflection model. | Pass `reflection_lm="openai/gpt-4o"` (or similar). |
| `must provide ... a stopping condition` | No budget set. | Pass `max_metric_calls=...` (or a `stop_callbacks`). |
| Authentication / 401 errors | API key not set or wrong provider prefix. | Set the env var; check the `provider/model` prefix matches your key. |
| Scores never improve | Feedback is too thin (just 0/1). | Return a *sentence* explaining each failure — that's what GEPA learns from. |
| Run is too slow/expensive | Budget or models too large for a first test. | Lower `max_metric_calls`, use smaller `task_lm`. |

---

Next: [Recipes](04-recipes.md) for task‑oriented how‑tos (custom systems, RAG, agents), or the
[interactive guide](index.html) to watch the loop run.
