gepa 0.1.0

GEPA: Reflective Prompt Evolution — a Rust implementation of the genetic-Pareto prompt optimizer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! Instruction proposal signature implementing the GEPA meta-prompt (Appendix C).
//!
//! Provides the meta-prompt template and helpers for rendering and extracting
//! improved instructions from an LM response.
//!
//! Mirrors `gepa.strategies.instruction_proposal.InstructionProposalSignature`.
use std::fmt::Write as _;

// ---------------------------------------------------------------------------
// Meta-prompt (exact text from Appendix C of the GEPA paper)
// ---------------------------------------------------------------------------

/// The exact meta-prompt template from Appendix C of the GEPA paper.
///
/// Replace `<curr_param>` with the current instruction text and `<side_info>`
/// with the formatted reflective dataset.
pub const META_PROMPT_TEMPLATE: &str = "\
I provided an assistant with the following instructions to perform a task for me:
```
<curr_param>
```

The following are examples of different task inputs provided to the assistant \
along with the assistant's response for each of them, and some feedback on how \
the assistant's response could be better:
```
<side_info>
```

Your task is to write a new instruction for the assistant.

Read the inputs carefully and identify the input format and infer detailed task \
description about the task I wish to solve with the assistant.

Read all the assistant responses and the corresponding feedback. Identify all \
niche and domain specific factual information about the task and include it in \
the instruction, as a lot of it may not be available to the assistant in the \
future. The assistant may have utilized a generalizable strategy to solve the \
task, if so, include that in the instruction as well.

Provide the new instructions within ``` blocks.";

/// Meta-prompt template for **code** components.
///
/// Used when `ComponentKind::Code` is set for the component being mutated.
/// Focuses on targeted code edits, preserving working patterns, and
/// understanding error traces.
pub const CODE_META_PROMPT_TEMPLATE: &str = "\
I have a program with the following code for the <component_name> component:
```<language>
<curr_param>
```

The following are results from running this code, including metrics and any errors:
```
<side_info>
```

Your task is to propose an improved version of this code.

Analyze the metrics and error traces carefully. Identify:
1. What is working well (preserve these patterns)
2. What is underperforming or failing (target these for improvement)
3. Any numerical instabilities, resource constraints, or performance bottlenecks

Make targeted changes rather than rewriting from scratch. Explain your reasoning \
briefly, then provide the complete improved code within ``` blocks.";

/// Meta-prompt template for **config** components.
///
/// Used when `ComponentKind::Config` is set for the component being mutated.
/// Focuses on hyperparameter tuning with awareness of constraints and
/// prior results.
pub const CONFIG_META_PROMPT_TEMPLATE: &str = "\
I have a system configured with the following parameters:
```
<curr_param>
```

<constraints>

The following are results from running with this configuration:
```
<side_info>
```

Your task is to propose an improved configuration.

Analyze the metrics carefully. Consider:
1. Which parameters most likely influence the observed metrics
2. Whether to make small incremental changes or larger exploratory jumps
3. The trade-offs between different objectives (if multiple metrics shown)

Provide the complete improved configuration within ``` blocks. \
Change only the values, not the format or parameter names.";

// ---------------------------------------------------------------------------
// Prompt rendering
// ---------------------------------------------------------------------------

/// Render the instruction-proposal meta-prompt.
///
/// Replaces `<curr_param>` with `current_instruction` and `<side_info>` with
/// `reflective_dataset` in [`META_PROMPT_TEMPLATE`].
///
/// Returns the rendered prompt string.
pub fn render_prompt(current_instruction: &str, reflective_dataset: &str) -> String {
    META_PROMPT_TEMPLATE
        .replace("<curr_param>", current_instruction)
        .replace("<side_info>", reflective_dataset)
}

/// Render the code-mutation meta-prompt.
///
/// Replaces `<curr_param>`, `<side_info>`, `<component_name>`, and `<language>`
/// in [`CODE_META_PROMPT_TEMPLATE`].
pub fn render_code_prompt(
    current_code: &str,
    reflective_dataset: &str,
    component_name: &str,
    language: &str,
) -> String {
    CODE_META_PROMPT_TEMPLATE
        .replace("<curr_param>", current_code)
        .replace("<side_info>", reflective_dataset)
        .replace("<component_name>", component_name)
        .replace("<language>", language)
}

/// Render the config-mutation meta-prompt.
///
/// Replaces `<curr_param>`, `<side_info>`, and `<constraints>` in
/// [`CONFIG_META_PROMPT_TEMPLATE`].
pub fn render_config_prompt(
    current_config: &str,
    reflective_dataset: &str,
    constraints: Option<&str>,
) -> String {
    let constraints_block = match constraints {
        Some(c) => format!("Parameter constraints:\n{c}\n"),
        None => String::new(),
    };
    CONFIG_META_PROMPT_TEMPLATE
        .replace("<curr_param>", current_config)
        .replace("<side_info>", reflective_dataset)
        .replace("<constraints>", &constraints_block)
}

/// Render using a custom template.
///
/// The template must contain both `<curr_param>` and `<side_info>` placeholders.
///
/// # Errors
/// Returns an error message when either placeholder is absent.
pub fn render_prompt_with_template(
    template: &str,
    current_instruction: &str,
    reflective_dataset: &str,
) -> Result<String, String> {
    validate_prompt_template(template)?;
    Ok(template
        .replace("<curr_param>", current_instruction)
        .replace("<side_info>", reflective_dataset))
}

/// Check that `template` contains both required placeholders.
///
/// # Errors
/// Returns a descriptive error when either placeholder is missing.
pub fn validate_prompt_template(template: &str) -> Result<(), String> {
    let missing: Vec<&str> = ["<curr_param>", "<side_info>"]
        .into_iter()
        .filter(|p| !template.contains(p))
        .collect();
    if missing.is_empty() {
        Ok(())
    } else {
        Err(format!(
            "Missing placeholder(s) in prompt template: {}",
            missing.join(", ")
        ))
    }
}

// ---------------------------------------------------------------------------
// Output extraction
// ---------------------------------------------------------------------------

/// Extract the proposed instruction from the LM response.
///
/// Searches for the first and last triple-backtick delimiters (` ``` `) in
/// `lm_response` and returns the text between them, stripping any optional
/// language specifier immediately after the opening fence.
///
/// Handles incomplete / malformed responses gracefully:
/// - Only an opening fence  → strips the fence and returns the rest.
/// - Only a closing fence   → strips the fence and returns the rest.
/// - No fences at all       → returns the stripped raw output.
///
/// Returns `None` when `lm_response` is empty or contains only whitespace.
pub fn extract_output(lm_response: &str) -> Option<String> {
    let trimmed = lm_response.trim();
    if trimmed.is_empty() {
        return None;
    }

    let fence = "```";
    let start_pos = trimmed.find(fence);
    let end_pos = trimmed.rfind(fence);

    match (start_pos, end_pos) {
        (Some(s), Some(e)) if s != e => {
            // Both fences found at different positions.
            let after_open = &trimmed[s + fence.len()..e];
            // Strip optional language specifier (e.g., "python\n").
            let content = strip_lang_specifier(after_open);
            Some(content.trim().to_string())
        }
        (Some(_s), _) => {
            // Only an opening fence (or open == close), strip it and return the rest.
            let stripped = if let Some(after) = trimmed.strip_prefix(fence) {
                // Strip the opening fence and optional lang specifier.
                strip_lang_specifier(after)
            } else {
                // Fence not at start; just strip it.
                trimmed.replacen(fence, "", 1)
            };
            Some(stripped.trim().to_string())
        }
        (None, Some(_)) => {
            // Only a closing fence.
            let stripped = trimmed.trim_end_matches(fence);
            Some(stripped.trim().to_string())
        }
        (None, None) => Some(trimmed.to_string()),
    }
}

/// Strip an optional language specifier (e.g., `python\n`) from the start of
/// a string.  Returns the string unchanged when no specifier is present.
fn strip_lang_specifier(s: &str) -> String {
    // A lang specifier is a run of non-whitespace chars followed by a newline.
    let mut chars = s.chars().peekable();
    let mut specifier_end = 0;
    for ch in &mut chars {
        if ch == '\n' {
            specifier_end += 1; // consume the newline
            break;
        }
        if ch.is_whitespace() {
            // Whitespace before newline means no lang specifier.
            specifier_end = 0;
            break;
        }
        specifier_end += ch.len_utf8();
    }
    if specifier_end > 0 {
        s[specifier_end..].to_string()
    } else {
        s.to_string()
    }
}

// ---------------------------------------------------------------------------
// Reflective dataset formatting
// ---------------------------------------------------------------------------

/// Format a slice of JSON sample records as a markdown string for use as
/// `<side_info>` in the meta-prompt.
///
/// Each record is rendered as a top-level markdown section numbered from 1.
/// Values are recursively rendered as nested headers.
pub fn format_samples_as_markdown(samples: &[serde_json::Value]) -> String {
    samples
        .iter()
        .enumerate()
        .map(|(i, sample)| convert_sample_to_markdown(sample, i + 1))
        .collect::<Vec<_>>()
        .join("\n\n")
}

fn convert_sample_to_markdown(sample: &serde_json::Value, num: usize) -> String {
    let mut s = format!("# Example {num}\n");
    if let Some(obj) = sample.as_object() {
        for (key, val) in obj {
            let _ = writeln!(s, "## {key}");
            s.push_str(&render_value(val, 3));
        }
    } else {
        s.push_str(&render_value(sample, 2));
    }
    s
}

fn render_value(value: &serde_json::Value, level: usize) -> String {
    let level = level.min(6);
    let hashes = "#".repeat(level);
    match value {
        serde_json::Value::Object(map) => {
            let mut s = String::new();
            for (k, v) in map {
                let _ = writeln!(s, "{hashes} {k}");
                s.push_str(&render_value(v, level + 1));
            }
            if map.is_empty() {
                s.push('\n');
            }
            s
        }
        serde_json::Value::Array(arr) => {
            let mut s = String::new();
            for (i, item) in arr.iter().enumerate() {
                let _ = writeln!(s, "{hashes} Item {}", i + 1);
                s.push_str(&render_value(item, level + 1));
            }
            if arr.is_empty() {
                s.push('\n');
            }
            s
        }
        serde_json::Value::String(text) => format!("{text}\n\n"),
        other => format!("{other}\n\n"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_prompt_substitutes_placeholders() {
        let prompt = render_prompt("Do the task.", "Example 1:\nInput: hi");
        assert!(prompt.contains("Do the task."));
        assert!(prompt.contains("Example 1:\nInput: hi"));
        assert!(!prompt.contains("<curr_param>"));
        assert!(!prompt.contains("<side_info>"));
    }

    #[test]
    fn validate_prompt_template_accepts_valid() {
        assert!(validate_prompt_template("Instruction: <curr_param>\nData: <side_info>").is_ok());
    }

    #[test]
    fn validate_prompt_template_rejects_missing_placeholders() {
        let err = validate_prompt_template("Missing both").unwrap_err();
        assert!(err.contains("<curr_param>"));
        assert!(err.contains("<side_info>"));
    }

    #[test]
    fn validate_prompt_template_rejects_one_missing() {
        let err = validate_prompt_template("Has <curr_param> but not the other").unwrap_err();
        assert!(err.contains("<side_info>"));
        assert!(!err.contains("<curr_param>"));
    }

    #[test]
    fn extract_output_normal_fenced_block() {
        let response = "Here is the instruction:\n```\nDo the task carefully.\n```\nDone.";
        let extracted = extract_output(response).expect("should extract");
        assert_eq!(extracted, "Do the task carefully.");
    }

    #[test]
    fn extract_output_fenced_block_with_lang_spec() {
        let response = "```python\nprint('hello')\n```";
        let extracted = extract_output(response).expect("should extract");
        assert_eq!(extracted, "print('hello')");
    }

    #[test]
    fn extract_output_no_fences_returns_stripped() {
        let response = "  Just plain text.  ";
        let extracted = extract_output(response).expect("should extract");
        assert_eq!(extracted, "Just plain text.");
    }

    #[test]
    fn extract_output_empty_returns_none() {
        assert!(extract_output("").is_none());
        assert!(extract_output("   ").is_none());
    }

    #[test]
    fn extract_output_only_opening_fence() {
        let response = "```\nInstruction text here.";
        let extracted = extract_output(response).expect("should extract");
        assert_eq!(extracted, "Instruction text here.");
    }

    #[test]
    fn extract_output_only_closing_fence() {
        let response = "Instruction text here.\n```";
        let extracted = extract_output(response).expect("should extract");
        assert_eq!(extracted, "Instruction text here.");
    }

    #[test]
    fn format_samples_as_markdown_produces_numbered_sections() {
        let samples = vec![
            serde_json::json!({
                "Inputs": "What is 2+2?",
                "Generated Outputs": "5",
                "Feedback": "Incorrect. The answer is 4."
            }),
            serde_json::json!({
                "Inputs": "What is the capital of France?",
                "Generated Outputs": "London",
                "Feedback": "Wrong. Paris is the capital of France."
            }),
        ];
        let md = format_samples_as_markdown(&samples);
        assert!(md.contains("# Example 1"));
        assert!(md.contains("# Example 2"));
        assert!(md.contains("## Inputs"));
        assert!(md.contains("## Feedback"));
    }

    #[test]
    fn meta_prompt_template_contains_placeholders() {
        assert!(META_PROMPT_TEMPLATE.contains("<curr_param>"));
        assert!(META_PROMPT_TEMPLATE.contains("<side_info>"));
        // Validate the template itself.
        assert!(validate_prompt_template(META_PROMPT_TEMPLATE).is_ok());
    }
}