# Optimize agents

> Connect the Agent Optimizer to Agent Frameworks.

The Opik Agent Optimizer can optimize both simple prompts and complex agent workflows. For most use cases, you can optimize prompts directly using `ChatPrompt`. When you need multi-prompt workflows, agent orchestration, or custom execution logic, you'll use `OptimizableAgent` to create a custom agent class.

## When to use OptimizableAgent vs ChatPrompt

**Use `ChatPrompt` directly** (default approach):

* Single-prompt optimization - optimizing one prompt template
* Most common use case
* No custom execution logic needed

**Use `OptimizableAgent`** when you need:

* Multi-prompt workflows - orchestrating multiple prompts in sequence
* Agent framework integration - connecting to ADK, LangGraph, CrewAI, etc.
* Custom execution logic - special tool handling, async workflows, etc.

Optimizers work seamlessly with both approaches. The optimizer calls your agent's `invoke_agent()` method repeatedly during optimization, passing different prompt candidates to evaluate.

## Single-prompt optimization

For most optimization tasks, you can use `ChatPrompt` directly without creating a custom agent. The optimizer uses a default LiteLLM-based agent under the hood.

```python
from opik_optimizer import ChatPrompt, MetaPromptOptimizer
from opik.evaluation.metrics import LevenshteinRatio
from opik_optimizer.datasets import hotpot

dataset = hotpot(count=300)

def levenshtein_ratio(dataset_item, llm_output):
    return LevenshteinRatio().score(
        reference=dataset_item["answer"], 
        output=llm_output
    )

prompt = ChatPrompt(
    system="You are a helpful assistant.",
    user="{question}",
    model="openai/gpt-4o-mini"
)

optimizer = MetaPromptOptimizer(model="openai/gpt-4o")
result = optimizer.optimize_prompt(
    prompt=prompt,
    dataset=dataset,
    metric=levenshtein_ratio,
    max_trials=5,
    n_samples=50
)

result.display()
```

### Custom agent for framework integration

When integrating with specific agent frameworks (Google ADK, LangGraph, CrewAI, etc.), you'll create a custom `OptimizableAgent` subclass. This allows the optimizer to work with your framework's execution model.

Here's an example for Google ADK:

```python
from typing import Any, TYPE_CHECKING
from opik_optimizer import OptimizableAgent

if TYPE_CHECKING:
    from opik_optimizer.api_objects import chat_prompt

class ADKAgent(OptimizableAgent):
    project_name = "adk-agent"

    def invoke_agent(
        self,
        prompts: dict[str, chat_prompt.ChatPrompt],
        dataset_item: dict[str, Any],
        allow_tool_use: bool = False,
        seed: int | None = None,
    ) -> str:
        # Single-prompt agents extract the prompt from the dict
        if len(prompts) > 1:
            raise ValueError("ADKAgent only supports single-prompt optimization.")
        
        prompt = list(prompts.values())[0]
        messages = prompt.get_messages(dataset_item)
        
        # Your framework-specific execution logic here
        # ... create ADK agent, run it, return response ...
        
        return response
```

The key points:

* Extract the single prompt from `prompts` dict: `prompt = list(prompts.values())[0]`
* Get formatted messages: `messages = prompt.get_messages(dataset_item)`
* Execute using your framework and return the response string

<Tip>
  See `sdks/opik_optimizer/scripts/llm_frameworks/` for working examples of framework integrations (ADK, LangGraph, CrewAI, etc.). Each script doubles as both documentation and regression tests.
</Tip>

## Multi-prompt optimization

For multi-step agent workflows, you **must** use `OptimizableAgent` because `ChatPrompt` only handles a single prompt. Multi-prompt optimization allows you to optimize multiple prompts that work together in a pipeline.

### When to use multi-prompt optimization

* Sequential reasoning workflows (analyze → respond)
* Multi-hop retrieval pipelines
* Agent orchestration with multiple steps
* Any workflow where one prompt's output feeds into another

### Implementing a multi-prompt agent

Here's a simple example of a two-step workflow that analyzes input and then generates a response:

```python
from typing import Any
from opik_optimizer import ChatPrompt, OptimizableAgent
from openai import OpenAI

class AnalyzeRespondAgent(OptimizableAgent):
    """Two-step agent: analyze input, then respond based on analysis."""
    
    def __init__(self, model: str = "gpt-4o-mini"):
        super().__init__()
        self.model = model
        self.client = OpenAI()

    def invoke_agent(
        self,
        prompts: dict[str, ChatPrompt],
        dataset_item: dict[str, Any],
        allow_tool_use: bool = False,
        seed: int | None = None,
    ) -> str:
        # Step 1: Analyze the input
        analyze_prompt = prompts["analyze"]
        analyze_messages = analyze_prompt.get_messages(dataset_item)
        
        analyze_response = self.client.chat.completions.create(
            model=self.model,
            messages=analyze_messages,
            seed=seed,
        )
        analysis = analyze_response.choices[0].message.content

        # Step 2: Generate response based on analysis
        respond_prompt = prompts["respond"]
        # Pass analysis result to the respond prompt
        respond_context = {**dataset_item, "analysis": analysis}
        respond_messages = respond_prompt.get_messages(respond_context)
        
        respond_response = self.client.chat.completions.create(
            model=self.model,
            messages=respond_messages,
            seed=seed,
        )
        
        return respond_response.choices[0].message.content
```

### Using the multi-prompt agent

When optimizing, pass a dictionary of prompts instead of a single prompt:

```python
from opik_optimizer import ChatPrompt, MetaPromptOptimizer
from opik.evaluation.metrics import LevenshteinRatio