Skip to content

Proposer

proposer

Async reflective mutation proposer for GEPA evolution.

This module provides the AsyncReflectiveMutationProposer class that generates text mutations via LLM reflection. It takes a component's current text and component feedback containing performance data, then uses async LLM calls to propose improved text.

Terminology
  • component: An evolvable unit with a name and text (like a gear in a machine)
  • component_text: The current text content of a component being evolved
  • trial: One performance record containing:
    • input: What was given to the system
    • output: What the system produced
    • feedback: Critic evaluation (score, feedback_text, feedback_*)
    • trajectory: Execution record (tool calls, state, events)
  • trials: Collection of trial records for reflection
  • proposed_component_text: The improved text for the same component
ATTRIBUTE DESCRIPTION
AsyncReflectiveMutationProposer

Main proposer class that generates text mutations via LLM reflection.

TYPE: class

ReflectionFn

Async callable signature for reflection functions: (component_text, trials, component_name) -> (proposed_text, reasoning).

TYPE: type alias

ReflectiveDataset

Mapping of component names to trial sequences.

TYPE: type alias

ProposalResult

Dictionary of proposed mutations or None.

TYPE: type alias

Examples:

Basic proposer usage with ADK reflection:

from gepa_adk.engine import (
    AsyncReflectiveMutationProposer,
    create_adk_reflection_fn,
)

reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
result = await proposer.propose(
    candidate={"instruction": "Be helpful"},
    reflective_dataset={"instruction": [trials]},
    components_to_update=["instruction"],
)
See Also
Note

This module requires an ADK reflection function for proposing mutations. Use create_adk_reflection_fn() from gepa_adk.engine.adk_reflection to create a reflection function from an ADK LlmAgent.

ReflectionFn module-attribute

ReflectionFn = Callable[
    [str, list[dict[str, Any]], str],
    Awaitable[tuple[str, str | None]],
]

Async callable for reflection.

(component_text: str, trials: list[dict], component_name: str)

-> tuple[str, str | None]

Takes current component text, trials, and component name. Returns a tuple of (proposed_component_text, reasoning). The reasoning is None when the model does not provide thought/reasoning output.

AsyncReflectiveMutationProposer

Generates text mutations via LLM reflection.

This proposer takes a candidate's current component texts and feedback data, then uses an ADK reflection function to generate improved versions. It handles empty datasets gracefully by returning None without making LLM calls.

Terminology
  • component: Evolvable unit with name + text (the "gear" being tuned)
  • component_text: The text content of a component
  • trial: One record {input, output, feedback, trajectory}
  • trials: Collection of trial records for reflection
  • proposed_component_text: The improved text for the same component
ATTRIBUTE DESCRIPTION
adk_reflection_fn

ADK reflection function for proposing mutations. Created via create_adk_reflection_fn().

TYPE: ReflectionFn

Examples:

Standard usage with ADK reflection agent:

from gepa_adk.engine import create_adk_reflection_fn

reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
result = await proposer.propose(
    candidate={"instruction": "Be helpful"},
    reflective_dataset={"instruction": [trials]},
    components_to_update=["instruction"],
)
Note

ADK-based reflection via adk_reflection_fn is the only supported approach. Use create_adk_reflection_fn() from gepa_adk.engine.adk_reflection to create the reflection function.

Source code in src/gepa_adk/engine/proposer.py
class AsyncReflectiveMutationProposer:
    """Generates text mutations via LLM reflection.

    This proposer takes a candidate's current component texts and feedback
    data, then uses an ADK reflection function to generate improved versions.
    It handles empty datasets gracefully by returning None without making
    LLM calls.

    Terminology:
        - component: Evolvable unit with name + text (the "gear" being tuned)
        - component_text: The text content of a component
        - trial: One record {input, output, feedback, trajectory}
        - trials: Collection of trial records for reflection
        - proposed_component_text: The improved text for the same component

    Attributes:
        adk_reflection_fn (ReflectionFn): ADK reflection function for proposing
            mutations. Created via `create_adk_reflection_fn()`.

    Examples:
        Standard usage with ADK reflection agent:

        ```python
        from gepa_adk.engine import create_adk_reflection_fn

        reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
        proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
        result = await proposer.propose(
            candidate={"instruction": "Be helpful"},
            reflective_dataset={"instruction": [trials]},
            components_to_update=["instruction"],
        )
        ```

    Note:
        ADK-based reflection via `adk_reflection_fn` is the only supported
        approach. Use `create_adk_reflection_fn()` from
        `gepa_adk.engine.adk_reflection` to create the reflection function.
    """

    def __init__(
        self,
        adk_reflection_fn: ReflectionFn,
    ) -> None:
        """Initialize the mutation proposer.

        Args:
            adk_reflection_fn: Async callable for ADK-based reflection.
                Takes (component_text, trials, component_name) and returns
                (proposed_text, reasoning) tuple. Create with
                `create_adk_reflection_fn()` from
                `gepa_adk.engine.adk_reflection`.

        Raises:
            ValueError: If adk_reflection_fn is None.

        Examples:
            ```python
            from gepa_adk.engine import create_adk_reflection_fn

            reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
            proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
            ```

        Note:
            Configuration validation happens immediately to fail fast rather
            than waiting until the first propose() call. After each
            ``propose()`` call, ``self.last_reasoning`` holds the most
            recent non-None reasoning string (or None).
        """
        if adk_reflection_fn is None:
            raise ValueError(
                "adk_reflection_fn is required. Use create_adk_reflection_fn() "
                "from gepa_adk.engine.adk_reflection to create one."
            )

        self.adk_reflection_fn = adk_reflection_fn
        self.last_reasoning: str | None = None

        # Log proposer initialization
        logger.info("proposer_initialized", reflection_method="adk")

    async def propose(
        self,
        candidate: dict[str, str],
        reflective_dataset: ReflectiveDataset,
        components_to_update: list[str],
    ) -> ProposalResult:
        """Propose mutated component text via LLM reflection.

        Args:
            candidate (dict[str, str]): Current candidate component texts.
                Keys are component names, values are component text.
                Example: {"instruction": "Be helpful and concise"}
            reflective_dataset (ReflectiveDataset): Trials per component name.
                Each trial contains input, output, feedback, and optional
                trajectory.
                Example: {"instruction": [{
                    "input": "Hello",
                    "output": "Hi there!",
                    "feedback": {"score": 0.75, "feedback_text": "Could be more formal"},
                    "trajectory": {...}
                }]}
            components_to_update (list[str]): Component names to generate
                proposals for. Components missing from the candidate or the
                reflective dataset (or with empty trials) are silently
                skipped. Example: ["instruction"]

        Returns:
            ProposalResult: Dictionary mapping component names to proposed
                component text, or None if the reflective dataset is empty
                or has no entries for the requested components.

        Raises:
            EvolutionError: If ADK reflection returns a non-string or empty
                response, or if the reflection function raises an unexpected
                exception (wrapped in EvolutionError).

        Examples:
            ```python
            result = await proposer.propose(
                candidate={"instruction": "Be helpful"},
                reflective_dataset={
                    "instruction": [
                        {
                            "input": "I am the King",
                            "output": "Hey!",
                            "feedback": {"score": 0.3, "feedback_text": "Too casual"},
                            "trajectory": {...},
                        }
                    ]
                },
                components_to_update=["instruction"],
            )
            # result: {"instruction": "Greet users formally..."}
            ```

        Note:
            Calls the reflection function directly with
            ``(component_text, trials, component_name)``. The function
            returns ``(proposed_text, reasoning)``; reasoning is stored
            in ``self.last_reasoning`` (last non-None value wins). Output
            validation ensures that empty or non-string LLM responses
            raise EvolutionError rather than breaking the evolution loop
            silently.
        """
        # Reset reasoning at start of each propose() call
        self.last_reasoning = None

        # Early return for empty dataset (no LLM calls)
        if not reflective_dataset:
            return None

        proposals = {}

        for component in components_to_update:
            # Skip if component not in reflective_dataset or has empty feedback
            if component not in reflective_dataset:
                continue

            trials: list[dict[str, Any]] = [
                dict(t) for t in reflective_dataset[component]
            ]
            if not trials:
                continue

            # Skip component not in candidate
            if component not in candidate:
                continue

            component_text = candidate[component]

            logger.debug(
                "proposer.reflection_path",
                method="adk",
                component=component,
            )

            # Call reflection function directly with 3-param signature
            try:
                proposed_component_text, reasoning = await self.adk_reflection_fn(
                    component_text, trials, component
                )

                # Store the last non-None reasoning
                if reasoning is not None:
                    self.last_reasoning = reasoning

                # Validate response is non-empty string
                if not isinstance(proposed_component_text, str):
                    raise EvolutionError(
                        "Reflection agent must return a string, got "
                        f"{type(proposed_component_text).__name__}."
                    )

                if not proposed_component_text.strip():
                    raise EvolutionError(
                        "Reflection agent returned empty string. "
                        "Expected non-empty string with proposed component text."
                    )

                proposals[component] = proposed_component_text.strip()
            except EvolutionError:
                # Re-raise EvolutionError as-is
                raise
            except Exception as e:
                # Wrap other exceptions in EvolutionError
                raise EvolutionError(
                    f"Reflection agent raised exception: {type(e).__name__}: {str(e)}"
                ) from e

        # Return None if no valid proposals generated
        if not proposals:
            return None

        return proposals

__init__

__init__(adk_reflection_fn: ReflectionFn) -> None

Initialize the mutation proposer.

PARAMETER DESCRIPTION
adk_reflection_fn

Async callable for ADK-based reflection. Takes (component_text, trials, component_name) and returns (proposed_text, reasoning) tuple. Create with create_adk_reflection_fn() from gepa_adk.engine.adk_reflection.

TYPE: ReflectionFn

RAISES DESCRIPTION
ValueError

If adk_reflection_fn is None.

Examples:

from gepa_adk.engine import create_adk_reflection_fn

reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
Note

Configuration validation happens immediately to fail fast rather than waiting until the first propose() call. After each propose() call, self.last_reasoning holds the most recent non-None reasoning string (or None).

Source code in src/gepa_adk/engine/proposer.py
def __init__(
    self,
    adk_reflection_fn: ReflectionFn,
) -> None:
    """Initialize the mutation proposer.

    Args:
        adk_reflection_fn: Async callable for ADK-based reflection.
            Takes (component_text, trials, component_name) and returns
            (proposed_text, reasoning) tuple. Create with
            `create_adk_reflection_fn()` from
            `gepa_adk.engine.adk_reflection`.

    Raises:
        ValueError: If adk_reflection_fn is None.

    Examples:
        ```python
        from gepa_adk.engine import create_adk_reflection_fn

        reflection_fn = create_adk_reflection_fn(reflection_agent, executor)
        proposer = AsyncReflectiveMutationProposer(adk_reflection_fn=reflection_fn)
        ```

    Note:
        Configuration validation happens immediately to fail fast rather
        than waiting until the first propose() call. After each
        ``propose()`` call, ``self.last_reasoning`` holds the most
        recent non-None reasoning string (or None).
    """
    if adk_reflection_fn is None:
        raise ValueError(
            "adk_reflection_fn is required. Use create_adk_reflection_fn() "
            "from gepa_adk.engine.adk_reflection to create one."
        )

    self.adk_reflection_fn = adk_reflection_fn
    self.last_reasoning: str | None = None

    # Log proposer initialization
    logger.info("proposer_initialized", reflection_method="adk")

propose async

propose(
    candidate: dict[str, str],
    reflective_dataset: ReflectiveDataset,
    components_to_update: list[str],
) -> ProposalResult

Propose mutated component text via LLM reflection.

PARAMETER DESCRIPTION
candidate

Current candidate component texts. Keys are component names, values are component text. Example: {"instruction": "Be helpful and concise"}

TYPE: dict[str, str]

reflective_dataset

Trials per component name. Each trial contains input, output, feedback, and optional trajectory. Example: {"instruction": [{ "input": "Hello", "output": "Hi there!", "feedback": {"score": 0.75, "feedback_text": "Could be more formal"}, "trajectory": {...} }]}

TYPE: ReflectiveDataset

components_to_update

Component names to generate proposals for. Components missing from the candidate or the reflective dataset (or with empty trials) are silently skipped. Example: ["instruction"]

TYPE: list[str]

RETURNS DESCRIPTION
ProposalResult

Dictionary mapping component names to proposed component text, or None if the reflective dataset is empty or has no entries for the requested components.

TYPE: ProposalResult

RAISES DESCRIPTION
EvolutionError

If ADK reflection returns a non-string or empty response, or if the reflection function raises an unexpected exception (wrapped in EvolutionError).

Examples:

result = await proposer.propose(
    candidate={"instruction": "Be helpful"},
    reflective_dataset={
        "instruction": [
            {
                "input": "I am the King",
                "output": "Hey!",
                "feedback": {"score": 0.3, "feedback_text": "Too casual"},
                "trajectory": {...},
            }
        ]
    },
    components_to_update=["instruction"],
)
# result: {"instruction": "Greet users formally..."}
Note

Calls the reflection function directly with (component_text, trials, component_name). The function returns (proposed_text, reasoning); reasoning is stored in self.last_reasoning (last non-None value wins). Output validation ensures that empty or non-string LLM responses raise EvolutionError rather than breaking the evolution loop silently.

Source code in src/gepa_adk/engine/proposer.py
async def propose(
    self,
    candidate: dict[str, str],
    reflective_dataset: ReflectiveDataset,
    components_to_update: list[str],
) -> ProposalResult:
    """Propose mutated component text via LLM reflection.

    Args:
        candidate (dict[str, str]): Current candidate component texts.
            Keys are component names, values are component text.
            Example: {"instruction": "Be helpful and concise"}
        reflective_dataset (ReflectiveDataset): Trials per component name.
            Each trial contains input, output, feedback, and optional
            trajectory.
            Example: {"instruction": [{
                "input": "Hello",
                "output": "Hi there!",
                "feedback": {"score": 0.75, "feedback_text": "Could be more formal"},
                "trajectory": {...}
            }]}
        components_to_update (list[str]): Component names to generate
            proposals for. Components missing from the candidate or the
            reflective dataset (or with empty trials) are silently
            skipped. Example: ["instruction"]

    Returns:
        ProposalResult: Dictionary mapping component names to proposed
            component text, or None if the reflective dataset is empty
            or has no entries for the requested components.

    Raises:
        EvolutionError: If ADK reflection returns a non-string or empty
            response, or if the reflection function raises an unexpected
            exception (wrapped in EvolutionError).

    Examples:
        ```python
        result = await proposer.propose(
            candidate={"instruction": "Be helpful"},
            reflective_dataset={
                "instruction": [
                    {
                        "input": "I am the King",
                        "output": "Hey!",
                        "feedback": {"score": 0.3, "feedback_text": "Too casual"},
                        "trajectory": {...},
                    }
                ]
            },
            components_to_update=["instruction"],
        )
        # result: {"instruction": "Greet users formally..."}
        ```

    Note:
        Calls the reflection function directly with
        ``(component_text, trials, component_name)``. The function
        returns ``(proposed_text, reasoning)``; reasoning is stored
        in ``self.last_reasoning`` (last non-None value wins). Output
        validation ensures that empty or non-string LLM responses
        raise EvolutionError rather than breaking the evolution loop
        silently.
    """
    # Reset reasoning at start of each propose() call
    self.last_reasoning = None

    # Early return for empty dataset (no LLM calls)
    if not reflective_dataset:
        return None

    proposals = {}

    for component in components_to_update:
        # Skip if component not in reflective_dataset or has empty feedback
        if component not in reflective_dataset:
            continue

        trials: list[dict[str, Any]] = [
            dict(t) for t in reflective_dataset[component]
        ]
        if not trials:
            continue

        # Skip component not in candidate
        if component not in candidate:
            continue

        component_text = candidate[component]

        logger.debug(
            "proposer.reflection_path",
            method="adk",
            component=component,
        )

        # Call reflection function directly with 3-param signature
        try:
            proposed_component_text, reasoning = await self.adk_reflection_fn(
                component_text, trials, component
            )

            # Store the last non-None reasoning
            if reasoning is not None:
                self.last_reasoning = reasoning

            # Validate response is non-empty string
            if not isinstance(proposed_component_text, str):
                raise EvolutionError(
                    "Reflection agent must return a string, got "
                    f"{type(proposed_component_text).__name__}."
                )

            if not proposed_component_text.strip():
                raise EvolutionError(
                    "Reflection agent returned empty string. "
                    "Expected non-empty string with proposed component text."
                )

            proposals[component] = proposed_component_text.strip()
        except EvolutionError:
            # Re-raise EvolutionError as-is
            raise
        except Exception as e:
            # Wrap other exceptions in EvolutionError
            raise EvolutionError(
                f"Reflection agent raised exception: {type(e).__name__}: {str(e)}"
            ) from e

    # Return None if no valid proposals generated
    if not proposals:
        return None

    return proposals