Current section
Files
Jump to
Current section
Files
lib/snakebridge_generated/dspy/better_together.ex
# Generated by SnakeBridge v0.16.0 - DO NOT EDIT MANUALLY
# Regenerate with: mix compile
# Library: dspy 3.2.0
# Python module: dspy
# Python class: BetterTogether
defmodule Dspy.BetterTogether do
@moduledoc """
A meta-optimizer that combines prompt and weight optimization in configurable sequences.
BetterTogether is a meta-optimizer proposed in the paper
[Fine-Tuning and Prompt Optimization: Two Great Steps that Work Better Together](https://arxiv.org/abs/2407.10930).
It combines prompt optimization and weight optimization (fine-tuning) by applying them in a
configurable sequence, allowing a student program to iteratively improve both its prompts and
model parameters.
The core insight is that prompt and weight optimization can complement each other: prompt
optimization can potentially discover effective task decompositions and reasoning strategies,
while weight optimization can specialize the model to execute these patterns more efficiently.
Using these approaches together in sequences (e.g., prompt optimization then weight optimization)
may allow each to build on the improvements made by the other. Empirically, this approach often
outperforms either strategy alone, even with state-of-the-art optimizers. For example, a
[Databricks case study](https://www.databricks.com/blog/building-state-art-enterprise-agents-90x-cheaper-automated-prompt-optimization)
shows that combining BetterTogether with GEPA and fine-tuning outperforms either approach
alone.
The optimizer is initialized with a metric and custom optimizers. For example, you can combine
GEPA for prompt optimization with BootstrapFinetune for weight optimization:
``BetterTogether(metric=metric, p=GEPA(...), w=BootstrapFinetune(...))``. The ``compile()``
method takes a student program, trainset, and strategy string where strategy keys correspond
to the optimizer names from initialization. It executes each optimizer in the specified sequence.
When a validation set is provided, the best performing program is returned; otherwise, the
latest program is returned. Note: Weight optimizers like BootstrapFinetune require student
programs to have LMs explicitly set (not relying on global dspy.settings.lm), and BetterTogether
mirrors this requirement for simplicity. Therefore we call ``set_lm`` before compiling.
```python
>>> from dspy.teleprompt import GEPA, BootstrapFinetune
>>>
>>> # Combine GEPA for prompt optimization with BootstrapFinetune for weight optimization
>>> optimizer = BetterTogether(
... metric=metric,
... p=GEPA(metric=metric, auto="medium"),
... w=BootstrapFinetune(metric=metric)
... )
>>>
>>> student.set_lm(lm)
>>> compiled = optimizer.compile(
... student,
... trainset=trainset,
... valset=valset,
... strategy="p -> w"
... )
```
You can pass optimizer-specific arguments to each optimizer's ``compile()`` method using
``optimizer_compile_args``. This allows you to customize each optimizer's behavior:
```python
>>> from dspy.teleprompt import MIPROv2
>>>
>>> # Use MIPROv2 for prompt optimization with custom parameters
>>> optimizer = BetterTogether(
... metric=metric,
... p=MIPROv2(metric=metric),
... w=BootstrapFinetune(metric=metric)
... )
>>>
>>> student.set_lm(lm)
>>> compiled = optimizer.compile(
... student,
... trainset=trainset,
... valset=valset,
... strategy="p -> w",
... optimizer_compile_args={
... "p": {"num_trials": 10, "max_bootstrapped_demos": 8}, # Configure MIPROv2's compile arguments
... }
... )
```
Since BetterTogether is a meta-optimizer that can run arbitrary optimizers in sequence, any
sequence of optimizers can be combined together. The optimizer names used in the strategy string
correspond to the keyword arguments specified in the constructor. For example, different prompt
optimizers can be alternated multiple times (though note this is just an illustration of
BetterTogether's flexibility, not a recommended configuration):
```python
>>> from dspy.teleprompt import MIPROv2, GEPA
>>>
>>> # Chain two optimizers three times: MIPROv2 -> GEPA -> MIPROv2
>>> optimizer = BetterTogether(
... metric=metric,
... mipro=MIPROv2(metric=metric, auto="light"),
... gepa=GEPA(metric=metric, auto="light")
... )
>>>
>>> student.set_lm(lm)
>>> compiled = optimizer.compile(
... student,
... trainset=trainset,
... valset=valset,
... strategy="mipro -> gepa -> mipro"
... )
```
"""
def __snakebridge_python_name__, do: "dspy"
def __snakebridge_python_class__, do: "BetterTogether"
def __snakebridge_library__, do: "dspy"
@opaque t :: SnakeBridge.Ref.t()
@doc """
Initialize BetterTogether with a metric and custom optimizers.
## Parameters
- `metric` - Evaluation metric function for scoring programs. Should accept ``(example, prediction, trace=None)`` and return a numeric score (higher is better). This metric is used to evaluate candidate programs during optimization and is passed to the default optimizers if no custom optimizers are provided. **optimizers: Custom optimizers as keyword arguments, where keys become the optimizer names used in the strategy string. For example, ``p=GEPA(...), w=BootstrapFinetune(...)`` makes 'p' and 'w' available for use in strategies like ``"p -> w"``. If not provided, defaults to ``p=BootstrapFewShotWithRandomSearch(metric=metric)`` and ``w=BootstrapFinetune(metric=metric)``. Any DSPy Teleprompter can be used.
## Examples
iex> # Use custom optimizers
iex> from dspy.teleprompt import GEPA, BootstrapFinetune
iex> optimizer = BetterTogether(
...> metric=metric,
...> p=GEPA(metric=metric, auto="medium"),
...> w=BootstrapFinetune(metric=metric)
...> )
iex>
iex> # Use default optimizers
iex> optimizer = BetterTogether(metric=metric)
"""
@spec new(term(), keyword()) :: {:ok, SnakeBridge.Ref.t()} | {:error, Snakepit.Error.t()}
def new(metric, opts \\ []) do
SnakeBridge.Runtime.call_class(__MODULE__, :__init__, [metric], opts)
end
@doc """
Add a candidate program to the list.
## Parameters
- `candidate_programs` (list(term()))
- `student` (term())
- `strategy` (String.t())
- `score` (float())
## Returns
- `nil`
"""
@spec _add_candidate(SnakeBridge.Ref.t(), list(term()), term(), String.t(), float(), keyword()) ::
{:ok, nil} | {:error, Snakepit.Error.t()}
def _add_candidate(ref, candidate_programs, student, strategy, score, opts \\ []) do
SnakeBridge.Runtime.call_method(
ref,
:_add_candidate,
[candidate_programs, student, strategy, score],
opts
)
end
@doc """
Python method `BetterTogether._evaluate_on_valset`.
## Parameters
- `program` (term())
- `valset` (term())
- `rng` (term())
- `num_threads` (term())
- `effective_max_errors` (term())
- `provide_traceback` (term())
## Returns
- `term()`
"""
@spec _evaluate_on_valset(
SnakeBridge.Ref.t(),
term(),
term(),
term(),
term(),
term(),
term(),
keyword()
) :: {:ok, term()} | {:error, Snakepit.Error.t()}
def _evaluate_on_valset(
ref,
program,
valset,
rng,
num_threads,
effective_max_errors,
provide_traceback,
opts \\ []
) do
SnakeBridge.Runtime.call_method(
ref,
:_evaluate_on_valset,
[program, valset, rng, num_threads, effective_max_errors, provide_traceback],
opts
)
end
@doc """
Check if model names changed after optimization (e.g., fine-tuning).
## Parameters
- `student` (term())
- `pred_lms_before` (list(term()))
## Returns
- `boolean()`
"""
@spec _models_changed(SnakeBridge.Ref.t(), term(), list(term()), keyword()) ::
{:ok, boolean()} | {:error, Snakepit.Error.t()}
def _models_changed(ref, student, pred_lms_before, opts \\ []) do
SnakeBridge.Runtime.call_method(ref, :_models_changed, [student, pred_lms_before], opts)
end
@doc """
Python method `BetterTogether._prepare_optimizer_compile_args`.
## Parameters
- `optimizer_compile_args` (term())
- `teacher` (term())
## Returns
- `%{optional(String.t()) => %{optional(String.t()) => term()}}`
"""
@spec _prepare_optimizer_compile_args(SnakeBridge.Ref.t(), term(), term(), keyword()) ::
{:ok, %{optional(String.t()) => %{optional(String.t()) => term()}}}
| {:error, Snakepit.Error.t()}
def _prepare_optimizer_compile_args(ref, optimizer_compile_args, teacher, opts \\ []) do
SnakeBridge.Runtime.call_method(
ref,
:_prepare_optimizer_compile_args,
[optimizer_compile_args, teacher],
opts
)
end
@doc """
Python method `BetterTogether._prepare_strategy`.
## Parameters
- `strategy` (String.t())
## Returns
- `list(String.t())`
"""
@spec _prepare_strategy(SnakeBridge.Ref.t(), String.t(), keyword()) ::
{:ok, list(String.t())} | {:error, Snakepit.Error.t()}
def _prepare_strategy(ref, strategy, opts \\ []) do
SnakeBridge.Runtime.call_method(ref, :_prepare_strategy, [strategy], opts)
end
@doc """
Python method `BetterTogether._prepare_student_and_teacher`.
## Parameters
- `student` (term())
- `teacher` (term())
## Returns
- `{term(), term()}`
"""
@spec _prepare_student_and_teacher(SnakeBridge.Ref.t(), term(), term(), keyword()) ::
{:ok, {term(), term()}} | {:error, Snakepit.Error.t()}
def _prepare_student_and_teacher(ref, student, teacher, opts \\ []) do
SnakeBridge.Runtime.call_method(ref, :_prepare_student_and_teacher, [student, teacher], opts)
end
@doc """
Python method `BetterTogether._prepare_trainset_and_valset`.
## Parameters
- `trainset` (list(term()))
- `valset` (term())
- `valset_ratio` (float())
## Returns
- `{list(term()), term()}`
"""
@spec _prepare_trainset_and_valset(
SnakeBridge.Ref.t(),
list(term()),
term(),
float(),
keyword()
) :: {:ok, {list(term()), term()}} | {:error, Snakepit.Error.t()}
def _prepare_trainset_and_valset(ref, trainset, valset, valset_ratio, opts \\ []) do
SnakeBridge.Runtime.call_method(
ref,
:_prepare_trainset_and_valset,
[trainset, valset, valset_ratio],
opts
)
end
@doc """
Run optimizer, evaluate result, and record candidate program.
## Returns
Returns `student`. Optimized student program
## Parameters
- `optimizer` (term())
- `student` (term())
- `teacher` (term())
- `trainset` (list(term()))
- `valset` (term())
- `compile_args` (%{optional(String.t()) => term()})
- `candidate_programs` (list(term()))
- `current_strategy` (String.t())
- `rng` (term())
- `num_threads` (term())
- `effective_max_errors` (term())
- `provide_traceback` (term())
"""
@spec _run_and_evaluate_step(
SnakeBridge.Ref.t(),
term(),
term(),
term(),
list(term()),
term(),
%{optional(String.t()) => term()},
list(term()),
String.t(),
term(),
term(),
term(),
term(),
keyword()
) :: {:ok, {term(), term(), boolean(), boolean()}} | {:error, Snakepit.Error.t()}
def _run_and_evaluate_step(
ref,
optimizer,
student,
teacher,
trainset,
valset,
compile_args,
candidate_programs,
current_strategy,
rng,
num_threads,
effective_max_errors,
provide_traceback,
opts \\ []
) do
SnakeBridge.Runtime.call_method(
ref,
:_run_and_evaluate_step,
[
optimizer,
student,
teacher,
trainset,
valset,
compile_args,
candidate_programs,
current_strategy,
rng,
num_threads,
effective_max_errors,
provide_traceback
],
opts
)
end
@doc """
Python method `BetterTogether._run_strategies`.
## Parameters
- `student` (term())
- `trainset` (list(term()))
- `teacher` (term())
- `valset` (term())
- `num_threads` (term())
- `effective_max_errors` (term())
- `provide_traceback` (term())
- `seed` (term())
- `parsed_strategy` (list(String.t()))
- `shuffle_trainset_between_steps` (boolean())
- `optimizer_args` (%{optional(String.t()) => %{optional(String.t()) => term()}})
## Returns
- `term()`
"""
@spec _run_strategies(
SnakeBridge.Ref.t(),
term(),
list(term()),
term(),
term(),
term(),
term(),
term(),
term(),
list(String.t()),
boolean(),
%{optional(String.t()) => %{optional(String.t()) => term()}},
keyword()
) :: {:ok, term()} | {:error, Snakepit.Error.t()}
def _run_strategies(
ref,
student,
trainset,
teacher,
valset,
num_threads,
effective_max_errors,
provide_traceback,
seed,
parsed_strategy,
shuffle_trainset_between_steps,
optimizer_args,
opts \\ []
) do
SnakeBridge.Runtime.call_method(
ref,
:_run_strategies,
[
student,
trainset,
teacher,
valset,
num_threads,
effective_max_errors,
provide_traceback,
seed,
parsed_strategy,
shuffle_trainset_between_steps,
optimizer_args
],
opts
)
end
@doc """
Python method `BetterTogether._validate_compile_args`.
## Parameters
- `optimizer` (term())
- `optimizer_key` (String.t())
- `compile_args` (%{optional(String.t()) => term()})
## Returns
- `nil`
"""
@spec _validate_compile_args(
SnakeBridge.Ref.t(),
term(),
String.t(),
%{optional(String.t()) => term()},
keyword()
) :: {:ok, nil} | {:error, Snakepit.Error.t()}
def _validate_compile_args(ref, optimizer, optimizer_key, compile_args, opts \\ []) do
SnakeBridge.Runtime.call_method(
ref,
:_validate_compile_args,
[optimizer, optimizer_key, compile_args],
opts
)
end
@doc """
Compile and optimize a student program using a sequence of optimization strategies.
Executes the optimizers specified in the strategy string sequentially, evaluating each
intermediate result and returning the best performing program.
## Parameters
- `student` - DSPy program to optimize. All predictors must have language models assigned. program.set_lm(lm) can be used to assign a language model to all modules of a program.
- `trainset` - Training examples for optimization. Each optimizer receives the full trainset (or a shuffled version if ``shuffle_trainset_between_steps=True``).
- `teacher` - Optional teacher module(s) for bootstrapping. Can be a single module or list. Passed to optimizers.
- `valset` - Validation set for evaluating optimization steps. If not provided, a portion of trainset is held out (controlled by ``valset_ratio``). If both ``valset`` and ``valset_ratio`` are None/0, no validation occurs and the latest program is returned.
- `num_threads` - Number of parallel evaluation threads. Default is None, which means sequential evaluation.
- `max_errors` - Maximum errors to tolerate during evaluation. Defaults to ``dspy.settings.max_errors``.
- `provide_traceback` - Whether to show detailed tracebacks for evaluation errors.
- `seed` - Random seed for reproducibility. Controls trainset shuffling and evaluation sampling.
- `valset_ratio` - Fraction of trainset to hold out as validation (range [0, 1)). For example, 0.1 holds out 10%. Set to 0 to skip validation. Default is 0.1.
- `shuffle_trainset_between_steps` - Whether to shuffle trainset before each optimization step. Helps prevent overfitting to example ordering. Default is True.
- `strategy` - Sequence of optimizers to apply, separated by ``" -> "``. Each element must be a key from the optimizers provided in ``__init__``. For example, ``"p -> w -> p"`` applies prompt optimization, then weight optimization, then prompt optimization again. Default is ``"p -> w -> p"``.
- `optimizer_compile_args` - Optional dict mapping optimizer keys to their ``compile()`` arguments. If trainset, valset, or teacher are provided in the dict for a specific optimizer, they override the defaults from BetterTogether's compile method. For example: ``{"p": {"num_trials": 10}, "w": {"trainset": custom_trainset}}``. This is useful to override the default compile arguments for specific optimizers. The ``student`` argument cannot be included in optimizer_compile_args; BetterTogether's compile method manages the student reference for all optimizers.
## Raises
- `ArgumentError` - If trainset is empty, valset_ratio not in [0, 1), strategy is empty or contains invalid optimizer keys, or optimizer_compile_args contains invalid arguments.
- `ArgumentError` - If optimizer_compile_args contains a 'student' key (not allowed).
## Examples
iex> optimizer = BetterTogether(
...> metric=metric,
...> p=GEPA(metric=metric),
...> w=BootstrapFinetune(metric=metric)
...> )
iex> student.set_lm(lm)
iex> compiled = optimizer.compile(
...> student,
...> trainset=trainset,
...> valset=valset,
...> strategy="p -> w"
...> )
iex> print(f"Best score: {compiled.candidate_programs[0]['score']}")
## Returns
- `term()`
"""
@spec compile(SnakeBridge.Ref.t(), term(), keyword()) ::
{:ok, term()} | {:error, Snakepit.Error.t()}
def compile(ref, student, opts \\ []) do
kw_keys = opts |> Keyword.keys() |> Enum.map(&to_string/1)
missing_kw = ["trainset"] |> Enum.reject(&(&1 in kw_keys))
if missing_kw != [] do
raise ArgumentError,
"Missing required keyword-only arguments: " <> Enum.join(missing_kw, ", ")
end
SnakeBridge.Runtime.call_method(ref, :compile, [student], opts)
end
@doc """
Get the parameters of the teleprompter.
## Returns
- `%{optional(String.t()) => term()}`
"""
@spec get_params(SnakeBridge.Ref.t(), keyword()) ::
{:ok, %{optional(String.t()) => term()}} | {:error, Snakepit.Error.t()}
def get_params(ref, opts \\ []) do
SnakeBridge.Runtime.call_method(ref, :get_params, [], opts)
end
@spec strat_sep(SnakeBridge.Ref.t()) :: {:ok, term()} | {:error, Snakepit.Error.t()}
def strat_sep(ref) do
SnakeBridge.Runtime.get_attr(ref, :STRAT_SEP)
end
end