> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openhands.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Visualizer

> Customize conversation visualization by creating custom visualizers or configuring the default visualizer.

export const path_to_script_0 = "examples/01_standalone_sdk/26_custom_visualizer.py"

> A ready-to-run example is available [here](#ready-to-run-example)!

The SDK provides flexible visualization options. You can use the default rich-formatted visualizer, customize it with highlighting patterns, or build completely custom visualizers by subclassing `ConversationVisualizerBase`.

## Visualizer Configuration Options

The `visualizer` parameter in `Conversation` controls how events are displayed:

```python icon="python" focus={4-5, 7-8, 10-11, 13, 18, 20, 25} theme={null}
from openhands.sdk import Conversation
from openhands.sdk.conversation import DefaultConversationVisualizer, ConversationVisualizerBase

# Option 1: Use default visualizer (enabled by default)
conversation = Conversation(agent=agent, workspace=workspace)

# Option 2: Disable visualization
conversation = Conversation(agent=agent, workspace=workspace, visualizer=None)

# Option 3: Pass a visualizer class (will be instantiated automatically)
conversation = Conversation(agent=agent, workspace=workspace, visualizer=DefaultConversationVisualizer)

# Option 4: Pass a configured visualizer instance
custom_viz = DefaultConversationVisualizer(
    name="MyAgent",
    highlight_regex={r"^Reasoning:": "bold cyan"}
)
conversation = Conversation(agent=agent, workspace=workspace, visualizer=custom_viz)

# Option 5: Use custom visualizer class
class MyVisualizer(ConversationVisualizerBase):
    def on_event(self, event):
        print(f"Event: {event}")

conversation = Conversation(agent=agent, workspace=workspace, visualizer=MyVisualizer())
```

## Customizing the Default Visualizer

`DefaultConversationVisualizer` uses Rich panels and supports customization through configuration:

```python icon="python" focus={3-14, 19} theme={null}
from openhands.sdk.conversation import DefaultConversationVisualizer

# Configure highlighting patterns using regex
custom_visualizer = DefaultConversationVisualizer(
    name="MyAgent",                       # Prefix panel titles with agent name
    highlight_regex={
        r"^Reasoning:": "bold cyan",      # Lines starting with "Reasoning:"
        r"^Thought:": "bold green",       # Lines starting with "Thought:"
        r"^Action:": "bold yellow",       # Lines starting with "Action:"
        r"\[ERROR\]": "bold red",         # Error markers anywhere
        r"\*\*(.*?)\*\*": "bold",         # Markdown bold **text**
    },
    skip_user_messages=False,             # Show user messages
)

conversation = Conversation(
    agent=agent,
    workspace=workspace,
    visualizer=custom_visualizer
)
```

**When to use**: Perfect for customizing colors and highlighting without changing the panel-based layout.

## Creating Custom Visualizers

For complete control over visualization, subclass `ConversationVisualizerBase`:

```python icon="python" focus={4, 11, 28} theme={null}
from openhands.sdk.conversation import ConversationVisualizerBase
from openhands.sdk.event import ActionEvent, ObservationEvent, AgentErrorEvent, Event

class MinimalVisualizer(ConversationVisualizerBase):
    """A minimal visualizer that prints raw event information."""
    
    def __init__(self, name: str | None = None):
        super().__init__(name=name)
        self.step_count = 0
    
    def on_event(self, event: Event) -> None:
        """Handle each event."""
        if isinstance(event, ActionEvent):
            self.step_count += 1
            tool_name = event.tool_name or "unknown"
            print(f"Step {self.step_count}: {tool_name}")
            
        elif isinstance(event, ObservationEvent):
            print(f"  → Result received")
                
        elif isinstance(event, AgentErrorEvent):
            print(f"❌ Error: {event.error}")

# Use your custom visualizer
conversation = Conversation(
    agent=agent,
    workspace=workspace,
    visualizer=MinimalVisualizer(name="Agent")
)
```

### Key Methods

**`__init__(self, name: str | None = None)`**

* Initialize your visualizer with optional configuration
* `name` parameter is available from the base class for agent identification
* Call `super().__init__(name=name)` to initialize the base class

**`initialize(self, state: ConversationStateProtocol)`**

* Called automatically by `Conversation` after state is created
* Provides access to conversation state and statistics via `self._state`
* Override if you need custom initialization, but call `super().initialize(state)`

**`on_event(self, event: Event)`** *(required)*

* Called for each conversation event
* Implement your visualization logic here
* Access conversation stats via `self.conversation_stats` property

**When to use**: When you need a completely different output format, custom state tracking, or integration with external systems.

## Ready-to-run Example

<Note>
  This example is available on GitHub: [examples/01\_standalone\_sdk/26\_custom\_visualizer.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/26_custom_visualizer.py)
</Note>

```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py theme={null}
"""Custom Visualizer Example

This example demonstrates how to create and use a custom visualizer by subclassing
ConversationVisualizer. This approach provides:
- Clean, testable code with class-based state management
- Direct configuration (just pass the visualizer instance to visualizer parameter)
- Reusable visualizer that can be shared across conversations

This demonstrates how you can pass a ConversationVisualizer instance directly
to the visualizer parameter for clean, reusable visualization logic.
"""

import logging
import os

from pydantic import SecretStr

from openhands.sdk import LLM, Conversation
from openhands.sdk.conversation.visualizer import ConversationVisualizerBase
from openhands.sdk.event import (
    Event,
)
from openhands.tools.preset.default import get_default_agent


class MinimalVisualizer(ConversationVisualizerBase):
    """A minimal visualizer that print the raw events as they occur."""

    def on_event(self, event: Event) -> None:
        """Handle events for minimal progress visualization."""
        print(f"\n\n[EVENT] {type(event).__name__}: {event.model_dump_json()[:200]}...")


api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
    model=model,
    api_key=SecretStr(api_key),
    base_url=base_url,
    usage_id="agent",
)
agent = get_default_agent(llm=llm, cli_mode=True)

# ============================================================================
# Configure Visualization
# ============================================================================
# Set logging level to reduce verbosity
logging.getLogger().setLevel(logging.WARNING)

# Start a conversation with custom visualizer
cwd = os.getcwd()
conversation = Conversation(
    agent=agent,
    workspace=cwd,
    visualizer=MinimalVisualizer(),
)

# Send a message and let the agent run
print("Sending task to agent...")
conversation.send_message("Write 3 facts about the current project into FACTS.txt.")
conversation.run()
print("Task completed!")

# Report cost
cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost:.4f}")
```

You can run the example code as-is.

<Note>
  The model name should follow the [LiteLLM convention](https://models.litellm.ai/): `provider/model_name` (e.g., `anthropic/claude-sonnet-4-5-20250929`, `openai/gpt-4o`).
  The `LLM_API_KEY` should be the API key for your chosen provider.
</Note>

<CodeGroup>
  <CodeBlock language="bash" filename="Bring-your-own provider key" icon="terminal" wrap>
    {`export LLM_API_KEY="your-api-key"\nexport LLM_MODEL="anthropic/claude-sonnet-4-5-20250929"  # or openai/gpt-4o, etc.\ncd software-agent-sdk\nuv run python ${path_to_script_0}`}
  </CodeBlock>

  <CodeBlock language="bash" filename="OpenHands Cloud" icon="terminal" wrap>
    {`# https://app.all-hands.dev/settings/api-keys\nexport LLM_API_KEY="your-openhands-api-key"\nexport LLM_MODEL="openhands/claude-sonnet-4-5-20250929"\ncd software-agent-sdk\nuv run python ${path_to_script_0}`}
  </CodeBlock>
</CodeGroup>

<Tip>
  **ChatGPT Plus/Pro subscribers**: You can use `LLM.subscription_login()` to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the [LLM Subscriptions guide](/sdk/guides/llm-subscriptions) for details.
</Tip>

## Next Steps

Now that you understand custom visualizers, explore these related topics:

* **[Events](/sdk/arch/events)** - Learn more about different event types
* **[Conversation Metrics](/sdk/guides/metrics)** - Track LLM usage, costs, and performance data
* **[Send Messages While Running](/sdk/guides/convo-send-message-while-running)** - Interactive conversations with real-time updates
* **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control agent execution flow with custom logic
