Skip to content

Commit da902ae

Browse files
xingyaowwopenhands-agentjpshackelford
authored
Document custom visualizer feature (#77)
* Document custom visualizer feature - Added comprehensive guide for using custom ConversationVisualizer - Shows the new direct API: visualize=custom_visualizer - Includes examples of highlight patterns and customization options - Documents the improvement over the old callback-based approach Related to: OpenHands/software-agent-sdk#1025 Co-authored-by: openhands <[email protected]> * Update custom visualizer documentation for improved example - Fix file reference from 20_custom_visualizer.py to 26_custom_visualizer.py - Update example code to match the comprehensive MinimalProgressVisualizer implementation - Add documentation for both simple configuration and custom subclass approaches - Clarify the difference between basic highlighting and full custom visualization - Update running instructions to use correct file name This aligns with the improved example in software-agent-sdk PR #1025. Co-authored-by: openhands <[email protected]> * Comprehensive rewrite of custom visualizer documentation Transform the documentation from simple API usage examples to comprehensive educational content that teaches developers how to create effective custom visualizers: Key improvements: - Detailed explanation of the event system (ActionEvent, ObservationEvent, MessageEvent, AgentErrorEvent) - Two clear approaches: simple configuration vs. full subclassing - Comprehensive best practices section covering: * State management and progress tracking * Error handling and fallback strategies * Performance considerations for high-frequency events * Flexible output formats for different environments * Integration with external systems (webhooks, logging, monitoring) - Real-world code examples for each concept - Built-in visualizer reference with default patterns and configuration options - Updated to reference the improved 26_custom_visualizer.py example This transforms the guide from basic API documentation into a complete tutorial for building production-ready custom visualizers. Co-authored-by: openhands <[email protected]> * Address review feedback: remove 'production-ready', focus on custom visualizers, improve next steps - Remove 'production-ready' language as requested - Remove mention of API improvement to focus on custom visualizers - Update Next Steps section with more relevant related topics: * Async conversations and event callbacks * Conversation metrics and performance tracking * Interactive conversations with real-time updates * Pause/resume with custom logic Co-authored-by: openhands <[email protected]> * Add custom visualizer guide to navigation - Add convo-custom-visualizer to Agent Features section in docs.json - Position it after agent-custom as requested - This should resolve the 404 error and make the guide appear in sidebar Co-authored-by: openhands <[email protected]> * Add link to default visualizer source code - Add reference link to ConversationVisualizer implementation - Helps developers understand the code structure and implementation details - Positioned in Built-in Visualizer Reference section for easy access Co-authored-by: openhands <[email protected]> * Update custom visualizer documentation to match actual code patterns - Update example code to match the actual MinimalProgressVisualizer implementation - Remove overly speculative best practices that aren't evident in the code - Focus best practices on patterns actually demonstrated in the examples - Simplify performance considerations to match what's actually shown - Update state management examples to use the actual implementation patterns Co-authored-by: openhands <[email protected]> * Add comprehensive event types table to custom visualizer documentation Add a table overview of all event types handled by the default visualizer before diving into detailed descriptions. This provides a quick reference for developers to understand the complete event system. Co-authored-by: openhands <[email protected]> * Improve custom visualizer documentation with event properties and rendering details - Replace 'when fired' code snippets with detailed property information - Add comprehensive event property descriptions and default rendering info - Add Approach 3: Custom Object with on_event Method (no subclassing required) - Improve subclassing documentation with built-in features and patterns - Update code examples to show actual event property usage - Better explain ConversationVisualizer inheritance benefits Co-authored-by: openhands <[email protected]> * Document decorator-based event handler pattern for custom visualizers - Add section on EventHandlerMixin and @Handles decorator - Show how to avoid long if/elif chains in on_event methods - Explain benefits: self-documenting, type-safe, extensible - Provide complete working example with best practices Co-authored-by: openhands <[email protected]> * Update custom visualizer documentation for new API - Fix parameter name from 'visualize' to 'visualizer' throughout documentation - Update visualization options to reflect simplified API: * Default behavior (no parameter): Creates default visualizer automatically * None: Disable visualization entirely * ConversationVisualizer instance: Use custom visualizer - Improve error handling example with more practical code - Align all examples with the API simplification changes Related to: OpenHands/software-agent-sdk#1025 Co-authored-by: openhands <[email protected]> * Update documentation to reflect new visualizer API from PR #1025 - Changed base class from ConversationVisualizer to ConversationVisualizerBase - Updated to new DefaultConversationVisualizer for built-in visualization - Changed parameter from 'visualize' to 'visualizer' throughout - Updated all code examples to use new API - Simplified visualizer configuration options - Updated method signatures and initialization patterns - Synchronized all code blocks with latest examples from agent-sdk PR #1025 Co-authored-by: openhands <[email protected]> * Update convo-custom-visualizer.mdx --------- Co-authored-by: openhands <[email protected]> Co-authored-by: John-Mason P. Shackelford <[email protected]>
1 parent 3c93ad0 commit da902ae

File tree

6 files changed

+231
-6
lines changed

6 files changed

+231
-6
lines changed

docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@
203203
"sdk/guides/agent-interactive-terminal",
204204
"sdk/guides/agent-browser-use",
205205
"sdk/guides/agent-custom",
206+
"sdk/guides/convo-custom-visualizer",
206207
"sdk/guides/agent-stuck-detector"
207208
]
208209
},

sdk/guides/agent-delegation.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ from openhands.sdk import (
4040
Tool,
4141
get_logger,
4242
)
43+
from openhands.sdk.conversation import DefaultConversationVisualizer
4344
from openhands.sdk.tool import register_tool
4445
from openhands.tools.delegate import DelegateTool
4546
from openhands.tools.preset.default import get_default_tools
@@ -72,7 +73,7 @@ main_agent = Agent(
7273
conversation = Conversation(
7374
agent=main_agent,
7475
workspace=cwd,
75-
name_for_visualization="Delegator",
76+
visualizer=DefaultConversationVisualizer(name="Delegator"),
7677
)
7778

7879
task_message = (

sdk/guides/agent-server/api-sandbox.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ with APIRemoteWorkspace(
7979
logger.info(f"Command completed: {result.exit_code}, {result.stdout}")
8080

8181
conversation = Conversation(
82-
agent=agent, workspace=workspace, callbacks=[event_callback], visualize=True
82+
agent=agent, workspace=workspace, callbacks=[event_callback]
8383
)
8484
assert isinstance(conversation, RemoteConversation)
8585

sdk/guides/agent-server/docker-sandbox.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ with DockerWorkspace(
9494
agent=agent,
9595
workspace=workspace,
9696
callbacks=[event_callback],
97-
visualize=True,
9897
)
9998
assert isinstance(conversation, RemoteConversation)
10099

@@ -309,7 +308,6 @@ with DockerWorkspace(
309308
agent=agent,
310309
workspace=workspace,
311310
callbacks=[event_callback],
312-
visualize=True,
313311
)
314312
assert isinstance(conversation, RemoteConversation)
315313

@@ -497,7 +495,6 @@ with DockerWorkspace(
497495
agent=agent,
498496
workspace=workspace,
499497
callbacks=[event_callback],
500-
visualize=True,
501498
)
502499
assert isinstance(conversation, RemoteConversation)
503500

sdk/guides/agent-server/local-server.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ with ManagedAPIServer(port=8001) as server:
185185
agent=agent,
186186
workspace=workspace,
187187
callbacks=[event_callback],
188-
visualize=True,
189188
)
190189
assert isinstance(conversation, RemoteConversation)
191190

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
title: Custom Visualizer
3+
description: Customize conversation visualization by creating custom visualizers or configuring the default visualizer.
4+
---
5+
6+
<Note>
7+
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)
8+
</Note>
9+
10+
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`.
11+
12+
## Basic Example
13+
14+
```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py
15+
"""Custom Visualizer Example
16+
17+
This example demonstrates how to create and use a custom visualizer by subclassing
18+
ConversationVisualizer. This approach provides:
19+
- Clean, testable code with class-based state management
20+
- Direct configuration (just pass the visualizer instance to visualizer parameter)
21+
- Reusable visualizer that can be shared across conversations
22+
- Better separation of concerns compared to callback functions
23+
- Event handler registration to avoid long if/elif chains
24+
25+
This demonstrates how you can pass a ConversationVisualizer instance directly
26+
to the visualizer parameter for clean, reusable visualization logic.
27+
"""
28+
29+
import logging
30+
import os
31+
32+
from pydantic import SecretStr
33+
34+
from openhands.sdk import LLM, Conversation
35+
from openhands.sdk.conversation.visualizer import ConversationVisualizerBase
36+
from openhands.sdk.event import (
37+
Event,
38+
)
39+
from openhands.tools.preset.default import get_default_agent
40+
41+
42+
class MinimalVisualizer(ConversationVisualizerBase):
43+
"""A minimal visualizer that print the raw events as they occur."""
44+
45+
def __init__(self, name: str | None = None):
46+
"""Initialize the minimal progress visualizer.
47+
48+
Args:
49+
name: Optional name to identify the agent/conversation.
50+
Note: This simple visualizer doesn't use it in output,
51+
but accepts it for compatibility with the base class.
52+
"""
53+
# Initialize parent - state will be set later via initialize()
54+
super().__init__(name=name)
55+
56+
def on_event(self, event: Event) -> None:
57+
"""Handle events for minimal progress visualization."""
58+
print(f"\n\n[EVENT] {type(event).__name__}: {event.model_dump_json()[:200]}...")
59+
60+
61+
api_key = os.getenv("LLM_API_KEY")
62+
assert api_key is not None, "LLM_API_KEY environment variable is not set."
63+
model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929")
64+
base_url = os.getenv("LLM_BASE_URL")
65+
llm = LLM(
66+
model=model,
67+
api_key=SecretStr(api_key),
68+
base_url=base_url,
69+
usage_id="agent",
70+
)
71+
agent = get_default_agent(llm=llm, cli_mode=True)
72+
73+
# ============================================================================
74+
# Configure Visualization
75+
# ============================================================================
76+
# Set logging level to reduce verbosity
77+
logging.getLogger().setLevel(logging.WARNING)
78+
79+
# Start a conversation with custom visualizer
80+
cwd = os.getcwd()
81+
conversation = Conversation(
82+
agent=agent,
83+
workspace=cwd,
84+
visualizer=MinimalVisualizer(),
85+
)
86+
87+
# Send a message and let the agent run
88+
print("Sending task to agent...")
89+
conversation.send_message("Write 3 facts about the current project into FACTS.txt.")
90+
conversation.run()
91+
print("Task completed!")
92+
93+
# Report cost
94+
cost = llm.metrics.accumulated_cost
95+
print(f"EXAMPLE_COST: ${cost:.4f}")
96+
```
97+
98+
```bash Running the Example
99+
export LLM_API_KEY="your-api-key"
100+
cd agent-sdk
101+
uv run python examples/01_standalone_sdk/26_custom_visualizer.py
102+
```
103+
104+
## Visualizer Configuration Options
105+
106+
The `visualizer` parameter in `Conversation` controls how events are displayed:
107+
108+
```python
109+
from openhands.sdk import Conversation
110+
from openhands.sdk.conversation import DefaultConversationVisualizer, ConversationVisualizerBase
111+
112+
# Option 1: Use default visualizer (enabled by default)
113+
conversation = Conversation(agent=agent, workspace=workspace)
114+
115+
# Option 2: Disable visualization
116+
conversation = Conversation(agent=agent, workspace=workspace, visualizer=None)
117+
118+
# Option 3: Pass a visualizer class (will be instantiated automatically)
119+
conversation = Conversation(agent=agent, workspace=workspace, visualizer=DefaultConversationVisualizer)
120+
121+
# Option 4: Pass a configured visualizer instance
122+
custom_viz = DefaultConversationVisualizer(
123+
name="MyAgent",
124+
highlight_regex={r"^Reasoning:": "bold cyan"}
125+
)
126+
conversation = Conversation(agent=agent, workspace=workspace, visualizer=custom_viz)
127+
128+
# Option 5: Use custom visualizer class
129+
class MyVisualizer(ConversationVisualizerBase):
130+
def on_event(self, event):
131+
print(f"Event: {event}")
132+
133+
conversation = Conversation(agent=agent, workspace=workspace, visualizer=MyVisualizer())
134+
```
135+
136+
## Customizing the Default Visualizer
137+
138+
`DefaultConversationVisualizer` uses Rich panels and supports customization through configuration:
139+
140+
```python
141+
from openhands.sdk.conversation import DefaultConversationVisualizer
142+
143+
# Configure highlighting patterns using regex
144+
custom_visualizer = DefaultConversationVisualizer(
145+
name="MyAgent", # Prefix panel titles with agent name
146+
highlight_regex={
147+
r"^Reasoning:": "bold cyan", # Lines starting with "Reasoning:"
148+
r"^Thought:": "bold green", # Lines starting with "Thought:"
149+
r"^Action:": "bold yellow", # Lines starting with "Action:"
150+
r"\[ERROR\]": "bold red", # Error markers anywhere
151+
r"\*\*(.*?)\*\*": "bold", # Markdown bold **text**
152+
},
153+
skip_user_messages=False, # Show user messages
154+
)
155+
156+
conversation = Conversation(
157+
agent=agent,
158+
workspace=workspace,
159+
visualizer=custom_visualizer
160+
)
161+
```
162+
163+
**When to use**: Perfect for customizing colors and highlighting without changing the panel-based layout.
164+
165+
## Creating Custom Visualizers
166+
167+
For complete control over visualization, subclass `ConversationVisualizerBase`:
168+
169+
```python
170+
from openhands.sdk.conversation import ConversationVisualizerBase
171+
from openhands.sdk.event import ActionEvent, ObservationEvent, AgentErrorEvent, Event
172+
173+
class MinimalVisualizer(ConversationVisualizerBase):
174+
"""A minimal visualizer that prints raw event information."""
175+
176+
def __init__(self, name: str | None = None):
177+
super().__init__(name=name)
178+
self.step_count = 0
179+
180+
def on_event(self, event: Event) -> None:
181+
"""Handle each event."""
182+
if isinstance(event, ActionEvent):
183+
self.step_count += 1
184+
tool_name = event.tool_name or "unknown"
185+
print(f"Step {self.step_count}: {tool_name}")
186+
187+
elif isinstance(event, ObservationEvent):
188+
print(f" → Result received")
189+
190+
elif isinstance(event, AgentErrorEvent):
191+
print(f"❌ Error: {event.error}")
192+
193+
# Use your custom visualizer
194+
conversation = Conversation(
195+
agent=agent,
196+
workspace=workspace,
197+
visualizer=MinimalVisualizer(name="Agent")
198+
)
199+
```
200+
201+
### Key Methods
202+
203+
**`__init__(self, name: str | None = None)`**
204+
- Initialize your visualizer with optional configuration
205+
- `name` parameter is available from the base class for agent identification
206+
- Call `super().__init__(name=name)` to initialize the base class
207+
208+
**`initialize(self, state: ConversationStateProtocol)`**
209+
- Called automatically by `Conversation` after state is created
210+
- Provides access to conversation state and statistics via `self._state`
211+
- Override if you need custom initialization, but call `super().initialize(state)`
212+
213+
**`on_event(self, event: Event)`** *(required)*
214+
- Called for each conversation event
215+
- Implement your visualization logic here
216+
- Access conversation stats via `self.conversation_stats` property
217+
218+
**When to use**: When you need a completely different output format, custom state tracking, or integration with external systems.
219+
220+
## Next Steps
221+
222+
Now that you understand custom visualizers, explore these related topics:
223+
224+
- **[Events](/sdk/arch/events)** - Learn more about different event types
225+
- **[Conversation Metrics](/sdk/guides/metrics)** - Track LLM usage, costs, and performance data
226+
- **[Send Messages While Running](/sdk/guides/convo-send-message-while-running)** - Interactive conversations with real-time updates
227+
- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control agent execution flow with custom logic

0 commit comments

Comments
 (0)