Current section
Files
Jump to
Current section
Files
examples/03_custom_adapter.exs
#!/usr/bin/env elixir
Code.require_file("support/live_cli.exs", __DIR__)
defmodule CustomSentimentAdapter do
@behaviour GEPA.Adapter
defstruct [:llm]
def new(opts), do: %__MODULE__{llm: Keyword.fetch!(opts, :llm)}
@impl true
def evaluate(%__MODULE__{} = adapter, batch, candidate, capture_traces) do
results =
Enum.map(batch, fn example ->
prompt = build_prompt(candidate["instruction"], example.text)
{:ok, response} = GEPA.LLM.complete(adapter.llm, prompt)
predicted = extract_sentiment(response)
correct? = predicted == example.sentiment
%{
output: %{predicted: predicted, response: response},
score: if(correct?, do: 1.0, else: 0.0),
trace: %{
prompt: prompt,
response: response,
expected: example.sentiment,
component: "instruction"
}
}
end)
{:ok,
%GEPA.EvaluationBatch{
outputs: Enum.map(results, & &1.output),
scores: Enum.map(results, & &1.score),
trajectories: if(capture_traces, do: Enum.map(results, & &1.trace))
}}
end
@impl true
def make_reflective_dataset(_adapter, _candidate, eval_batch, components_to_update) do
dataset =
for component <- components_to_update, into: %{} do
items =
eval_batch.trajectories
|> Enum.zip(eval_batch.scores)
|> Enum.map(fn {trace, score} ->
status = if score > 0.5, do: "Correct", else: "Wrong"
%{
"Inputs" => %{"prompt" => trace.prompt},
"Generated Outputs" => trace.response,
"Feedback" =>
"#{status}. Expected #{trace.expected}, got #{extract_sentiment(trace.response)} using #{component}."
}
end)
{component, items}
end
{:ok, dataset}
end
defp build_prompt(instruction, text) do
"""
#{instruction}
Text:
#{text}
Classify the sentiment as exactly one of: positive, negative, neutral.
"""
end
defp extract_sentiment(response) do
response = String.downcase(response)
cond do
String.contains?(response, "positive") -> "positive"
String.contains?(response, "negative") -> "negative"
true -> "neutral"
end
end
end
example = [
name: "GEPA Custom Sentiment Adapter Live Example",
script: "examples/03_custom_adapter.exs",
summary: "Runs a custom live sentiment adapter over your JSONL text/sentiment data.",
required: [:train_jsonl, :val_jsonl]
]
config = LiveCLI.parse_or_halt(System.argv(), example)
estimated_calls = max(config.max_metric_calls * 2, 1)
IO.puts(
LiveCLI.cost_warning(
example[:name],
config.adapter,
config.provider,
estimated_calls
)
)
seed_candidate = %{
"instruction" => "Classify the sentiment of the provided text."
}
adapter = CustomSentimentAdapter.new(llm: config.client)
IO.puts("""
GEPA Custom Sentiment Adapter Live Example
==========================================
Adapter/provider: #{config.adapter}/#{config.provider}
Model: #{config.model || "(provider default)"}
Training rows: #{length(config.trainset)}
Validation rows: #{length(config.valset)}
Max metric calls: #{config.max_metric_calls}
Reflection minibatch size: #{config.minibatch_size}
""")
{:ok, result} =
GEPA.optimize(
seed_candidate: seed_candidate,
trainset: config.trainset,
valset: config.valset,
adapter: adapter,
max_metric_calls: config.max_metric_calls,
reflection_llm: config.client,
reflection_minibatch_size: config.minibatch_size,
structured_output: config.structured_output?
)
IO.puts("""
Optimization Complete
=====================
Best validation score: #{Float.round(GEPA.Result.best_score(result), 4)}
Iterations: #{result.i}
Total evaluations: #{result.total_num_evals}
Best instruction:
#{GEPA.Result.best_candidate(result)["instruction"]}
""")