GEPA Optimization

Run agent-opt's GEPAOptimizer to evolve a prompt through reflection and mutation, then read out the best prompt found.

📝
TL;DR

Run agent-opt’s GEPAOptimizer on a summarization prompt: it evaluates the current prompt, reflects on the failures with a teacher model, mutates the prompt, and repeats within a fixed evaluation budget. You end with a best-scoring prompt and its score.

TimeDifficultyPackage
20 minIntermediateagent-opt + gepa
Prerequisites
  • Future AGI account → app.futureagi.com
  • API keys: FI_API_KEY and FI_SECRET_KEY (see Get your API keys)
  • An OpenAI API key (the reflection and generator models call OpenAI through LiteLLM)
  • Python 3.11

Install

pip install agent-opt gepa
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"
export OPENAI_API_KEY="your-openai-api-key"

Note

GEPAOptimizer wraps the external gepa library. Without pip install gepa, importing GEPAOptimizer raises ImportError: To use GEPAOptimizer, please install the 'gepa' library with: pip install gepa.

Tutorial

Prepare a dataset and a starting prompt

GEPA needs a dataset of inputs to score candidate prompts against. Each row is a plain dict.

dataset = [
    {
        "article": "The James Webb Space Telescope has captured stunning new images of the Pillars of Creation, revealing intricate details of gas and dust clouds where new stars are forming.",
    },
    {
        "article": "Researchers at the University of Austin have discovered a new enzyme capable of breaking down PET plastic, the material commonly found in beverage bottles, in a matter of hours.",
    },
]

initial_prompt = "Summarize this article concisely: {article}"

You should see nothing yet, this just defines the dataset and starting prompt in memory. Two rows is enough to run the tutorial; use 30+ for a result you’d trust.

Configure the Evaluator

The Evaluator scores each candidate prompt’s output using a Future AGI eval template.

from fi.opt.base import Evaluator

evaluator = Evaluator(
    eval_template="summary_quality",
    eval_model_name="turing_flash",
)

You should see nothing yet, this just constructs the evaluator. If FI_API_KEY or FI_SECRET_KEY isn’t set, construction raises ValueError naming the missing key.

Map dataset fields with BasicDataMapper

BasicDataMapper tells the optimizer which dataset column is the input and which generated field is the output to score.

from fi.opt.datamappers import BasicDataMapper

data_mapper = BasicDataMapper(
    key_map={"input": "article", "output": "generated_output"}
)

generated_output is the field name the generator writes to during a run, not a column in dataset.

You should see nothing yet, this just builds the mapper; it does no work until optimize() runs.

Configure GEPAOptimizer

GEPA needs two models: a reflection model that critiques failures and rewrites the prompt, and a generator model whose prompt is being optimized.

from fi.opt.optimizers import GEPAOptimizer

optimizer = GEPAOptimizer(
    reflection_model="gpt-4o",       # critiques failures and proposes mutations
    generator_model="gpt-4o-mini",   # the "student" model whose prompt is optimized
)

A stronger reflection model produces more useful critiques and better mutations. It’s worth spending more here than on the generator model.

You should see nothing yet, this just constructs the optimizer; no API calls happen until optimize() runs.

Run the optimization

max_metric_calls is the total evaluation budget for the whole run, including the calls spent scoring the initial prompt.

result = optimizer.optimize(
    evaluator=evaluator,
    data_mapper=data_mapper,
    dataset=dataset,
    initial_prompts=[initial_prompt],
    max_metric_calls=20,
)

Note

If your dataset has 300 rows and max_metric_calls is 200, the budget runs out scoring the starting prompt alone, with nothing left for actual mutation. Keep max_metric_calls well above your dataset size; the SDK default is 150.

This calls the reflection model to analyze failures, mutates the prompt, and scores each new candidate with the evaluator. On a 2-row dataset with a small budget it typically finishes in under a minute; the default budget of 150 takes longer.

Inspect the results

seed_score = result.history[0].average_score

print(f"Seed Score: {seed_score:.4f}")
print(f"Best Score: {result.final_score:.4f}")
print(f"Initial Prompt:\n{initial_prompt}")
print(f"Best Prompt Found:\n{result.best_generator.get_prompt_template()}")

result.history[0] is the seed candidate’s evaluation, appended before any mutation, so seed_score and result.final_score give you a before/after delta rather than a single number. Example output, illustrative:

Seed Score: 0.7200
Best Score: 0.8400
Initial Prompt:
Summarize this article concisely: {article}
Best Prompt Found:
Summarize this article in 1-2 sentences, naming the specific finding or event and its concrete outcome...

The exact prompt and score depend on your dataset and models, so don’t expect to match this run.

Troubleshooting

SymptomCauseFix
ImportError: To use GEPAOptimizer, please install the 'gepa' libraryThe gepa package isn’t installed alongside agent-optpip install gepa
TypeError: unexpected keyword argument 'max_metric_calls'max_metric_calls passed to GEPAOptimizer(...) instead of .optimize(...)Move max_metric_calls to the optimize() call
AuthenticationError on the first evaluator callFI_API_KEY or FI_SECRET_KEY wasn’t exported before the script startedExport both keys in the same shell session, then rerun
LiteLLM raises an OpenAI auth errorOPENAI_API_KEY missing; reflection_model and generator_model both route through LiteLLM to OpenAIExport OPENAI_API_KEY alongside the FI keys
ValueError: Initial prompts list cannot be emptyinitial_prompts=[] or omittedPass at least one prompt string, e.g. initial_prompts=[initial_prompt]
optimize() exhausts its budget on the first evaluation, result.final_score never changesmax_metric_calls is smaller than or close to the dataset sizeRaise max_metric_calls well above the dataset row count
optimize() runs for many minutesmax_metric_calls left at a large value (150-300) while testing, and each call round-trips the reflection and generator modelsLower max_metric_calls to 10-20 for a test run, raise it only for a real optimization pass

Once you have a GEPA baseline, Comparing Prompt Optimizers runs it alongside ProTeGi and PromptWizard on the same task.

Was this page helpful?

Questions & Discussion