Skip to content

Commit 2430278

Browse files
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]>
1 parent ebfb5e4 commit 2430278

File tree

1 file changed

+158
-36
lines changed

1 file changed

+158
-36
lines changed

sdk/guides/convo-custom-visualizer.mdx

Lines changed: 158 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,53 @@ class MinimalProgressVisualizer(ConversationVisualizer):
295295

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

298+
### Approach 3: Custom Object with on_event Method
299+
300+
You can implement custom visualizers without subclassing by creating any object with an `on_event` method. The conversation system only requires that your visualizer has this method:
301+
302+
```python
303+
from rich.console import Console
304+
from rich.panel import Panel
305+
from openhands.sdk.event import Event
306+
307+
class CustomVisualizer:
308+
"""Custom visualizer without subclassing ConversationVisualizer."""
309+
310+
def __init__(self):
311+
self.event_count = 0
312+
self.console = Console()
313+
314+
def on_event(self, event: Event) -> None:
315+
"""Handle any event - this is the only required method."""
316+
self.event_count += 1
317+
318+
# Use the event's built-in visualize property
319+
content = event.visualize
320+
if content.plain.strip():
321+
# Create custom panel styling
322+
panel = Panel(
323+
content,
324+
title=f"[bold cyan]Event #{self.event_count}: {event.__class__.__name__}[/]",
325+
border_style="cyan",
326+
padding=(0, 1)
327+
)
328+
self.console.print(panel)
329+
330+
# Use it directly
331+
conversation = LocalConversation(
332+
agent=agent,
333+
workspace=workspace,
334+
visualize=CustomVisualizer() # Pass your custom object
335+
)
336+
```
337+
338+
**Key Requirements:**
339+
- Must have an `on_event(self, event: Event) -> None` method
340+
- Can be any Python object (class instance, function with state, etc.)
341+
- No inheritance required
342+
343+
**When to use**: When you want maximum flexibility without inheriting from ConversationVisualizer, or when integrating with existing class hierarchies.
344+
298345
## Key Event Types
299346

300347
Understanding the event system is crucial for effective custom visualizers. Here's a comprehensive overview of all event types handled by the default visualizer:
@@ -314,68 +361,143 @@ Understanding the event system is crucial for effective custom visualizers. Here
314361
### ActionEvent
315362
Fired when the agent decides to use a tool or take an action.
316363

364+
**Key Properties:**
365+
- `thought`: Agent's reasoning before taking action (list of TextContent)
366+
- `action`: The actual tool action (None if non-executable)
367+
- `tool_name`: Name of the tool being called
368+
- `tool_call_id`: Unique identifier for the tool call
369+
- `security_risk`: LLM's assessment of action safety
370+
- `reasoning_content`: Intermediate reasoning from reasoning models
371+
372+
**Default Rendering:** Blue panel titled "Agent Action" showing reasoning, thought process, and action details.
373+
317374
```python
318375
def handle_action(self, event: ActionEvent):
319-
# Access tool information
320-
tool_name = event.tool_name # e.g., "str_replace_editor"
321-
action = event.action # The actual action object
322-
323-
# Track LLM calls
324-
if event.llm_response_id:
325-
print(f"🤖 LLM call {event.llm_response_id}")
376+
# Access thought process
377+
thought_text = " ".join([t.text for t in event.thought])
378+
print(f"💭 Thought: {thought_text}")
326379

327-
# Extract action details
328-
if hasattr(action, 'command'):
329-
print(f"Command: {action.command}")
330-
if hasattr(action, 'path'):
331-
print(f"File: {action.path}")
380+
# Check if action is executable
381+
if event.action:
382+
print(f"🔧 Tool: {event.tool_name}")
383+
print(f"⚡ Action: {event.action}")
384+
else:
385+
print(f"⚠️ Non-executable call: {event.tool_call.name}")
332386
```
333387

334388
### ObservationEvent
335-
Fired when a tool execution completes and returns results.
389+
Contains the result of an executed action.
390+
391+
**Key Properties:**
392+
- `observation`: The tool execution result (varies by tool)
393+
- `tool_name`: Name of the tool that was executed
394+
- `tool_call_id`: ID linking back to the original action
395+
- `action_id`: ID of the action this observation responds to
396+
397+
**Default Rendering:** Yellow panel titled "Observation" showing tool name and execution results.
336398

337399
```python
338400
def handle_observation(self, event: ObservationEvent):
339-
# Check for errors
340-
if hasattr(event.observation, 'error') and event.observation.error:
341-
print(f"❌ Tool failed: {event.observation.error}")
342-
else:
343-
print("✅ Tool completed successfully")
401+
print(f"🔧 Tool: {event.tool_name}")
402+
print(f"🔗 Action ID: {event.action_id}")
344403

345-
# Access results
346-
if hasattr(event.observation, 'content'):
347-
content = event.observation.content
348-
print(f"Result: {content[:100]}...") # Show first 100 chars
404+
# Access the observation result
405+
obs = event.observation
406+
if hasattr(obs, 'error') and obs.error:
407+
print(f"❌ Error: {obs.error}")
408+
elif hasattr(obs, 'content'):
409+
print(f"📄 Content: {obs.content[:100]}...")
349410
```
350411

351412
### MessageEvent
352-
Fired for LLM messages (both user input and agent responses).
413+
Represents messages between user and agent.
414+
415+
**Key Properties:**
416+
- `llm_message`: The complete LLM message (role, content, tool_calls)
417+
- `source`: Whether from "user" or "agent"
418+
- `activated_skills`: List of skills activated for this message
419+
- `extended_content`: Additional content added by agent context
420+
421+
**Default Rendering:** Gold panel for user messages, blue panel for agent messages, with role-specific titles.
353422

354423
```python
355424
def handle_message(self, event: MessageEvent):
356-
if event.source == "user":
357-
print(f"👤 User: {event.content}")
358-
elif event.source == "agent":
359-
print(f"🤖 Agent: {event.content}")
360-
361-
# Track LLM response IDs to avoid duplicates
362-
if event.llm_response_id:
363-
self.seen_responses.add(event.llm_response_id)
425+
if event.llm_message:
426+
role = event.llm_message.role
427+
content = event.llm_message.content
428+
429+
if role == "user":
430+
print(f"👤 User: {content[0].text if content else ''}")
431+
elif role == "assistant":
432+
print(f"🤖 Agent: {content[0].text if content else ''}")
433+
434+
# Check for tool calls
435+
if event.llm_message.tool_calls:
436+
print(f"🔧 Tool calls: {len(event.llm_message.tool_calls)}")
364437
```
365438

366439
### AgentErrorEvent
367-
Fired when the agent encounters an error.
440+
Error conditions encountered by the agent.
441+
442+
**Key Properties:**
443+
- `error`: The error message from the agent/scaffold
444+
- `tool_name`: Tool that caused the error (if applicable)
445+
- `tool_call_id`: ID of the failed tool call (if applicable)
446+
447+
**Default Rendering:** Red panel titled "Agent Error" displaying error details.
368448

369449
```python
370450
def handle_error(self, event: AgentErrorEvent):
371-
print(f"🚨 Agent Error: {event.error}")
372-
# Optionally log to external systems
373-
self.logger.error(f"Agent failed: {event.error}")
451+
print(f"🚨 Error: {event.error}")
452+
if event.tool_name:
453+
print(f"🔧 Failed tool: {event.tool_name}")
454+
if event.tool_call_id:
455+
print(f"🔗 Call ID: {event.tool_call_id}")
374456
```
375457

376458
## Best Practices
377459

378-
### 1. State Management
460+
### 1. Understanding ConversationVisualizer Subclassing
461+
462+
When subclassing `ConversationVisualizer`, you inherit several useful features:
463+
464+
**Built-in Features:**
465+
- Rich Console instance (`self._console`) for formatted output
466+
- Highlighting patterns (`self._highlight_patterns`) for text styling
467+
- Conversation stats integration (`self._conversation_stats`) for metrics
468+
- Name prefixing (`self._name_for_visualization`) for multi-agent scenarios
469+
470+
**Key Methods to Override:**
471+
- `on_event(self, event: Event)`: Main event handler (most common override)
472+
- `_create_event_panel(self, event: Event)`: Custom panel creation
473+
- `_apply_highlighting(self, text: Text)`: Custom text highlighting
474+
475+
**Initialization Pattern:**
476+
```python
477+
class MyVisualizer(ConversationVisualizer):
478+
def __init__(self, custom_param: str = "default", **kwargs):
479+
# Always call super().__init__ to get base functionality
480+
super().__init__(**kwargs)
481+
482+
# Add your custom state
483+
self.custom_param = custom_param
484+
self.event_count = 0
485+
486+
def on_event(self, event: Event) -> None:
487+
# Your custom logic here
488+
self.event_count += 1
489+
490+
# Option 1: Completely custom handling
491+
if isinstance(event, ActionEvent):
492+
print(f"Custom action handling: {event.tool_name}")
493+
494+
# Option 2: Use parent's panel creation with modifications
495+
panel = self._create_event_panel(event)
496+
if panel:
497+
self._console.print(panel)
498+
```
499+
500+
### 2. State Management
379501
Track conversation state to provide meaningful progress indicators. The example shows tracking step counts, pending actions, and LLM response IDs:
380502

381503
```python

0 commit comments

Comments
 (0)