From 83b676c561c0886a8d6e9cb30470fa928f2fcc9e Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Nov 2025 20:15:29 +0000 Subject: [PATCH 01/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 192 +++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 sdk/guides/convo-custom-visualizer.mdx diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx new file mode 100644 index 00000000..a3b1813b --- /dev/null +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -0,0 +1,192 @@ +--- +title: Custom Visualizer +description: Customize conversation visualization with custom highlighting patterns and display options. +--- + + +This example is available on GitHub: [examples/01_standalone_sdk/20_custom_visualizer.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_custom_visualizer.py) + + +Customize how your agent's conversation is displayed by passing a custom `ConversationVisualizer` instance directly to the `visualize` parameter: + +```python icon="python" expandable examples/01_standalone_sdk/20_custom_visualizer.py +"""Example demonstrating custom visualizer usage. + +This example shows how to pass a custom ConversationVisualizer directly +to the Conversation, making it easy to customize the visualization without +the need for callbacks. +""" + +import os + +from pydantic import SecretStr + +from openhands.sdk import LLM, Agent, Conversation +from openhands.sdk.conversation.visualizer import ConversationVisualizer + + +def main(): + # Get API key from environment + api_key = os.environ.get("LLM_API_KEY") + if not api_key: + raise ValueError("LLM_API_KEY environment variable is not set") + + # Create LLM and Agent + llm = LLM(model="gpt-4o-mini", api_key=SecretStr(api_key)) + agent = Agent(llm=llm, tools=[]) + + # Create a custom visualizer with specific highlighting + custom_visualizer = ConversationVisualizer( + highlight_regex={ + r"^Reasoning:": "bold cyan", + r"^Thought:": "bold green", + r"^Action:": "bold yellow", + }, + skip_user_messages=False, # Show user messages + ) + + # Pass the custom visualizer directly to the conversation + # This is more intuitive than visualize=False + callbacks=[...] + conversation = Conversation( + agent=agent, + workspace="./workspace", + visualize=custom_visualizer, # Direct and clear! + ) + + # Send a message and run + conversation.send_message("What is 2 + 2?") + conversation.run() + + print("\nāœ… Example completed!") + print("The conversation used a custom visualizer with custom highlighting.") + + +if __name__ == "__main__": + main() +``` + +```bash Running the Example +export LLM_API_KEY="your-api-key" +cd agent-sdk +uv run python examples/01_standalone_sdk/20_custom_visualizer.py +``` + +## Creating a Custom Visualizer + +Configure a `ConversationVisualizer` with custom highlighting patterns: + +```python +from openhands.sdk.conversation.visualizer import ConversationVisualizer + +custom_visualizer = ConversationVisualizer( + highlight_regex={ + r"^Reasoning:": "bold cyan", + r"^Thought:": "bold green", + r"^Action:": "bold yellow", + }, + skip_user_messages=False, # Show user messages +) +``` + +### Visualization Options + +The `visualize` parameter accepts three types: + +- **`True`** (default): Use the default visualizer with standard formatting +- **`False` or `None`**: Disable visualization entirely +- **`ConversationVisualizer` instance**: Use your custom visualizer + +### Before and After + +**Previous approach** (confusing): +```python +# Had to set visualize=False and pass callback manually +conversation = Conversation( + agent=agent, + workspace=cwd, + visualize=False, # Confusing: we DO want visualization! + callbacks=[custom_visualizer.on_event], +) +``` + +**New approach** (clear and direct): +```python +# Pass the visualizer directly +conversation = Conversation( + agent=agent, + workspace=cwd, + visualize=custom_visualizer, # Direct and clear! +) +``` + +## Customization Options + +### Highlight Patterns + +Use regex patterns to highlight specific content: + +```python +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 + r"\[SUCCESS\]": "bold green", # Success markers +} +``` + +### Available Colors and Styles + +Colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` + +Styles: `bold`, `dim`, `italic`, `underline` + +Combine them: `"bold cyan"`, `"underline red"`, `"bold italic green"` + +### Message Filtering + +Control which messages are displayed: + +```python +ConversationVisualizer( + skip_user_messages=False, # Show user messages (default: True) + skip_system_messages=False, # Show system messages (default: False) +) +``` + +## Advanced Usage + +### Combining with Callbacks + +You can still use custom callbacks alongside a custom visualizer: + +```python +def my_callback(event): + # Custom logging or processing + print(f"Event received: {event.event_type}") + +conversation = Conversation( + agent=agent, + workspace=cwd, + visualize=custom_visualizer, + callbacks=[my_callback], # Additional callbacks +) +``` + +### Disabling Visualization + +To disable visualization entirely: + +```python +conversation = Conversation( + agent=agent, + workspace=cwd, + visualize=None, # or False +) +``` + +## Next Steps + +- **[Callbacks](/sdk/guides/convo-async)** - Learn about custom event callbacks +- **[Persistence](/sdk/guides/convo-persistence)** - Save and restore conversation state +- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control agent execution flow From 6b735f20264cb62cc3d1de4aab93ddb5d7c90a0a Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Nov 2025 20:35:19 +0000 Subject: [PATCH 02/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 199 +++++++++++++++++++++---- 1 file changed, 170 insertions(+), 29 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index a3b1813b..53eb5b60 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -4,61 +4,172 @@ description: Customize conversation visualization with custom highlighting patte --- -This example is available on GitHub: [examples/01_standalone_sdk/20_custom_visualizer.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_custom_visualizer.py) +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) -Customize how your agent's conversation is displayed by passing a custom `ConversationVisualizer` instance directly to the `visualize` parameter: +Customize how your agent's conversation is displayed by passing a custom `ConversationVisualizer` instance directly to the `visualize` parameter. -```python icon="python" expandable examples/01_standalone_sdk/20_custom_visualizer.py -"""Example demonstrating custom visualizer usage. +The example below shows a comprehensive custom visualizer that subclasses `ConversationVisualizer` to create a minimal progress tracker. For simpler use cases, you can also configure the built-in `ConversationVisualizer` with custom highlighting patterns (see [Creating a Custom Visualizer](#creating-a-custom-visualizer) below). -This example shows how to pass a custom ConversationVisualizer directly -to the Conversation, making it easy to customize the visualization without -the need for callbacks. +```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py +"""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 visualize parameter) +- Reusable visualizer that can be shared across conversations +- Better separation of concerns compared to callback functions + +The MinimalProgressVisualizer produces concise output showing: +- LLM call completions +- Tool execution steps with command/path details +- Agent thinking indicators +- Error messages + +This demonstrates the new API improvement where you can pass a ConversationVisualizer +instance directly to the visualize parameter instead of using callbacks. """ +import logging import os from pydantic import SecretStr -from openhands.sdk import LLM, Agent, Conversation +from openhands.sdk import LLM, Conversation from openhands.sdk.conversation.visualizer import ConversationVisualizer +from openhands.sdk.event import ( + ActionEvent, + AgentErrorEvent, + MessageEvent, + ObservationEvent, +) +from openhands.tools.preset.default import get_default_agent + + +class MinimalProgressVisualizer(ConversationVisualizer): + """A minimal progress visualizer that shows step counts and tool names. + + This visualizer produces concise output showing: + - LLM call completions + - Tool execution steps with command/path details + - Agent thinking indicators + - Error messages + + Example output: + šŸ¤– LLM call completed + Step 1: Executing str_replace_editor (view: .../FACTS.txt)... āœ“ + šŸ’­ Agent thinking... + šŸ¤– LLM call completed + Step 2: Executing str_replace_editor (str_replace: .../FACTS.txt)... āœ“ + āŒ Error: File not found + """ + + def __init__(self, **kwargs): + """Initialize the minimal progress visualizer. + + Args: + **kwargs: Additional arguments passed to ConversationVisualizer. + Note: We override visualization, so most ConversationVisualizer + parameters are ignored, but we keep the signature for + compatibility. + """ + # Initialize parent but we'll override on_event + # We don't need the console/panels from the parent + super().__init__(**kwargs) + + # Track state for our custom visualization + self.step_count = 0 + self.current_action = None + self.logger = logging.getLogger(__name__) + + def on_event(self, event): + """Handle conversation events with minimal progress display.""" + try: + if isinstance(event, MessageEvent): + if event.source == "agent": + print("šŸ¤– LLM call completed") + elif event.source == "user": + print(f"šŸ‘¤ User: {event.content}") + + elif isinstance(event, ActionEvent): + self.step_count += 1 + action_name = event.action.__class__.__name__ + + # Extract useful details from common actions + details = "" + if hasattr(event.action, "command"): + details = f"command: {event.action.command[:50]}..." + elif hasattr(event.action, "path"): + path = event.action.path + if hasattr(event.action, "command"): + details = f"{event.action.command}: {path}" + else: + details = f"path: {path}" + + if details: + print(f"Step {self.step_count}: Executing {action_name} ({details})...", end=" ", flush=True) + else: + print(f"Step {self.step_count}: Executing {action_name}...", end=" ", flush=True) + + self.current_action = action_name + + elif isinstance(event, ObservationEvent): + if self.current_action: + # Simple success/error indication + if hasattr(event.observation, "error") and event.observation.error: + print("āŒ") + print(f"āŒ Error: {event.observation.error}") + else: + print("āœ“") + self.current_action = None + else: + print("šŸ’­ Agent thinking...") + + elif isinstance(event, AgentErrorEvent): + print(f"āŒ Agent Error: {event.message}") + + except Exception as e: + # Fallback to prevent visualization errors from breaking the conversation + self.logger.warning(f"Visualization error: {e}") def main(): + """Demonstrate the custom visualizer with a simple task.""" # Get API key from environment api_key = os.environ.get("LLM_API_KEY") if not api_key: raise ValueError("LLM_API_KEY environment variable is not set") - # Create LLM and Agent + # Create LLM and Agent with default tools llm = LLM(model="gpt-4o-mini", api_key=SecretStr(api_key)) - agent = Agent(llm=llm, tools=[]) - - # Create a custom visualizer with specific highlighting - custom_visualizer = ConversationVisualizer( - highlight_regex={ - r"^Reasoning:": "bold cyan", - r"^Thought:": "bold green", - r"^Action:": "bold yellow", - }, - skip_user_messages=False, # Show user messages - ) + agent = get_default_agent(llm=llm) + + # Create our custom visualizer + custom_visualizer = MinimalProgressVisualizer() - # Pass the custom visualizer directly to the conversation - # This is more intuitive than visualize=False + callbacks=[...] + print("šŸš€ Starting conversation with custom visualizer...") + print("=" * 60) + + # NEW API: Pass the visualizer instance directly! + # This is much cleaner than: visualize=False, callbacks=[custom_visualizer.on_event] conversation = Conversation( agent=agent, workspace="./workspace", - visualize=custom_visualizer, # Direct and clear! + visualize=custom_visualizer, # Direct and intuitive! ) # Send a message and run - conversation.send_message("What is 2 + 2?") + conversation.send_message( + "Create a file called FACTS.txt with 3 interesting facts about Python programming." + ) conversation.run() - print("\nāœ… Example completed!") - print("The conversation used a custom visualizer with custom highlighting.") + print("=" * 60) + print("āœ… Example completed!") + print(f"šŸ“Š Total steps executed: {custom_visualizer.step_count}") + print("\nThe conversation used our custom MinimalProgressVisualizer!") + print("Compare this to the default visualizer to see the difference.") if __name__ == "__main__": @@ -68,26 +179,56 @@ if __name__ == "__main__": ```bash Running the Example export LLM_API_KEY="your-api-key" cd agent-sdk -uv run python examples/01_standalone_sdk/20_custom_visualizer.py +uv run python examples/01_standalone_sdk/26_custom_visualizer.py ``` ## Creating a Custom Visualizer -Configure a `ConversationVisualizer` with custom highlighting patterns: +There are two approaches to creating custom visualizers: + +### 1. Simple Configuration (Built-in Visualizer) + +For basic customization, configure the built-in `ConversationVisualizer` with custom highlighting patterns: ```python from openhands.sdk.conversation.visualizer import ConversationVisualizer +# Simple approach: configure the built-in visualizer custom_visualizer = ConversationVisualizer( highlight_regex={ r"^Reasoning:": "bold cyan", - r"^Thought:": "bold green", + r"^Thought:": "bold green", r"^Action:": "bold yellow", }, skip_user_messages=False, # Show user messages ) ``` +### 2. Custom Subclass (Full Control) + +For complete control over visualization logic, subclass `ConversationVisualizer`: + +```python +from openhands.sdk.conversation.visualizer import ConversationVisualizer +from openhands.sdk.event import ActionEvent, MessageEvent + +class MinimalProgressVisualizer(ConversationVisualizer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.step_count = 0 + + def on_event(self, event): + if isinstance(event, MessageEvent) and event.source == "agent": + print("šŸ¤– LLM call completed") + elif isinstance(event, ActionEvent): + self.step_count += 1 + action_name = event.action.__class__.__name__ + print(f"Step {self.step_count}: Executing {action_name}...") + +# Use your custom visualizer +custom_visualizer = MinimalProgressVisualizer() +``` + ### Visualization Options The `visualize` parameter accepts three types: From ace4583886f834cbb779195e3ab4e06e4fdd23ff Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Nov 2025 20:39:26 +0000 Subject: [PATCH 03/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 316 +++++++++++++++++++------ 1 file changed, 240 insertions(+), 76 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 53eb5b60..5e4b75b5 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -7,9 +7,9 @@ description: Customize conversation visualization with custom highlighting patte 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) -Customize how your agent's conversation is displayed by passing a custom `ConversationVisualizer` instance directly to the `visualize` parameter. +Learn how to create effective custom visualizers by understanding the event system and implementing your own visualization logic. This guide teaches you to build visualizers that range from simple highlighting to complete custom interfaces. -The example below shows a comprehensive custom visualizer that subclasses `ConversationVisualizer` to create a minimal progress tracker. For simpler use cases, you can also configure the built-in `ConversationVisualizer` with custom highlighting patterns (see [Creating a Custom Visualizer](#creating-a-custom-visualizer) below). +The comprehensive example below demonstrates a production-ready custom visualizer that tracks conversation progress with minimal output. We'll break down the key concepts and best practices used in this implementation. ```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py """Custom Visualizer Example @@ -182,147 +182,311 @@ cd agent-sdk uv run python examples/01_standalone_sdk/26_custom_visualizer.py ``` -## Creating a Custom Visualizer +## Understanding Custom Visualizers -There are two approaches to creating custom visualizers: +Custom visualizers give you complete control over how conversation events are displayed. There are two main approaches, each suited for different needs. -### 1. Simple Configuration (Built-in Visualizer) +### Approach 1: Configure the Built-in Visualizer -For basic customization, configure the built-in `ConversationVisualizer` with custom highlighting patterns: +The built-in `ConversationVisualizer` uses Rich panels and provides extensive customization through configuration: ```python from openhands.sdk.conversation.visualizer import ConversationVisualizer -# Simple approach: configure the built-in visualizer +# Configure highlighting patterns using regex custom_visualizer = ConversationVisualizer( highlight_regex={ - r"^Reasoning:": "bold cyan", - r"^Thought:": "bold green", - r"^Action:": "bold yellow", + 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 + skip_user_messages=False, # Show user messages + name_for_visualization="MyAgent", # Prefix panel titles ) ``` -### 2. Custom Subclass (Full Control) +**When to use**: Perfect for customizing colors and highlighting without changing the overall panel-based layout. -For complete control over visualization logic, subclass `ConversationVisualizer`: +### Approach 2: Subclass for Complete Control + +For entirely different visualization approaches, subclass `ConversationVisualizer` and override the `on_event` method: ```python from openhands.sdk.conversation.visualizer import ConversationVisualizer -from openhands.sdk.event import ActionEvent, MessageEvent +from openhands.sdk.event import ActionEvent, MessageEvent, ObservationEvent, AgentErrorEvent class MinimalProgressVisualizer(ConversationVisualizer): def __init__(self, **kwargs): super().__init__(**kwargs) self.step_count = 0 + self.pending_action = False def on_event(self, event): - if isinstance(event, MessageEvent) and event.source == "agent": - print("šŸ¤– LLM call completed") - elif isinstance(event, ActionEvent): + if isinstance(event, ActionEvent): self.step_count += 1 - action_name = event.action.__class__.__name__ - print(f"Step {self.step_count}: Executing {action_name}...") - -# Use your custom visualizer -custom_visualizer = MinimalProgressVisualizer() + tool_name = event.tool_name or "unknown" + print(f"Step {self.step_count}: Executing {tool_name}...", end="", flush=True) + self.pending_action = True + + elif isinstance(event, ObservationEvent): + if self.pending_action: + print(" āœ“") + self.pending_action = False + + elif isinstance(event, AgentErrorEvent): + print(f"āŒ Error: {event.error}") ``` -### Visualization Options +**When to use**: When you need completely different output format, custom state tracking, or integration with external systems. -The `visualize` parameter accepts three types: +## Key Event Types -- **`True`** (default): Use the default visualizer with standard formatting -- **`False` or `None`**: Disable visualization entirely -- **`ConversationVisualizer` instance**: Use your custom visualizer +Understanding the event system is crucial for effective custom visualizers: -### Before and After +### ActionEvent +Fired when the agent decides to use a tool or take an action. -**Previous approach** (confusing): ```python -# Had to set visualize=False and pass callback manually -conversation = Conversation( - agent=agent, - workspace=cwd, - visualize=False, # Confusing: we DO want visualization! - callbacks=[custom_visualizer.on_event], -) +def handle_action(self, event: ActionEvent): + # Access tool information + tool_name = event.tool_name # e.g., "str_replace_editor" + action = event.action # The actual action object + + # Track LLM calls + if event.llm_response_id: + print(f"šŸ¤– LLM call {event.llm_response_id}") + + # Extract action details + if hasattr(action, 'command'): + print(f"Command: {action.command}") + if hasattr(action, 'path'): + print(f"File: {action.path}") ``` -**New approach** (clear and direct): +### ObservationEvent +Fired when a tool execution completes and returns results. + ```python -# Pass the visualizer directly -conversation = Conversation( - agent=agent, - workspace=cwd, - visualize=custom_visualizer, # Direct and clear! -) +def handle_observation(self, event: ObservationEvent): + # Check for errors + if hasattr(event.observation, 'error') and event.observation.error: + print(f"āŒ Tool failed: {event.observation.error}") + else: + print("āœ… Tool completed successfully") + + # Access results + if hasattr(event.observation, 'content'): + content = event.observation.content + print(f"Result: {content[:100]}...") # Show first 100 chars ``` -## Customization Options +### MessageEvent +Fired for LLM messages (both user input and agent responses). -### Highlight Patterns +```python +def handle_message(self, event: MessageEvent): + if event.source == "user": + print(f"šŸ‘¤ User: {event.content}") + elif event.source == "agent": + print(f"šŸ¤– Agent: {event.content}") + + # Track LLM response IDs to avoid duplicates + if event.llm_response_id: + self.seen_responses.add(event.llm_response_id) +``` -Use regex patterns to highlight specific content: +### AgentErrorEvent +Fired when the agent encounters an error. ```python -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 - r"\[SUCCESS\]": "bold green", # Success markers -} +def handle_error(self, event: AgentErrorEvent): + print(f"🚨 Agent Error: {event.error}") + # Optionally log to external systems + self.logger.error(f"Agent failed: {event.error}") ``` -### Available Colors and Styles +## Best Practices -Colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` +### 1. State Management +Track conversation state to provide meaningful progress indicators: -Styles: `bold`, `dim`, `italic`, `underline` +```python +class StatefulVisualizer(ConversationVisualizer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.step_count = 0 + self.pending_action = False + self.seen_llm_responses = set() + self.start_time = time.time() + + def on_event(self, event): + # Use state to provide context + elapsed = time.time() - self.start_time + print(f"[{elapsed:.1f}s] ", end="") + # ... handle event +``` -Combine them: `"bold cyan"`, `"underline red"`, `"bold italic green"` +### 2. Error Handling +Always include error handling to prevent visualization issues from breaking conversations: -### Message Filtering +```python +def on_event(self, event): + try: + # Your visualization logic + self._handle_event(event) + except Exception as e: + # Fallback to prevent breaking the conversation + print(f"Visualization error: {e}") + # Optionally log for debugging + logging.warning(f"Visualizer failed: {e}") +``` -Control which messages are displayed: +### 3. Performance Considerations +For high-frequency events, optimize your visualization: ```python -ConversationVisualizer( - skip_user_messages=False, # Show user messages (default: True) - skip_system_messages=False, # Show system messages (default: False) -) +def on_event(self, event): + # Skip verbose events if needed + if isinstance(event, SomeVerboseEvent) and not self.verbose_mode: + return + + # Batch updates for better performance + if self.should_batch_updates(): + self.pending_updates.append(event) + return + + self._render_event(event) ``` -## Advanced Usage +### 4. Flexible Output +Design visualizers that work in different environments: -### Combining with Callbacks +```python +class AdaptiveVisualizer(ConversationVisualizer): + def __init__(self, output_format="console", **kwargs): + super().__init__(**kwargs) + self.output_format = output_format + + if output_format == "json": + self.output_handler = self._json_output + elif output_format == "markdown": + self.output_handler = self._markdown_output + else: + self.output_handler = self._console_output + + def on_event(self, event): + self.output_handler(event) +``` -You can still use custom callbacks alongside a custom visualizer: +### 5. Integration with External Systems +Custom visualizers can integrate with logging, monitoring, or notification systems: ```python -def my_callback(event): - # Custom logging or processing - print(f"Event received: {event.event_type}") +class IntegratedVisualizer(ConversationVisualizer): + def __init__(self, webhook_url=None, **kwargs): + super().__init__(**kwargs) + self.webhook_url = webhook_url + self.metrics = {"actions": 0, "errors": 0} + + def on_event(self, event): + # Update metrics + if isinstance(event, ActionEvent): + self.metrics["actions"] += 1 + elif isinstance(event, AgentErrorEvent): + self.metrics["errors"] += 1 + + # Send to external system + if self.webhook_url and isinstance(event, AgentErrorEvent): + self._send_alert(event) + + def _send_alert(self, event): + # Send webhook notification for errors + payload = {"error": event.error, "timestamp": time.time()} + requests.post(self.webhook_url, json=payload) +``` + +## Using Your Custom Visualizer + +### Direct Assignment (Recommended) +Pass your visualizer instance directly to the `visualize` parameter: + +```python +# Create your custom visualizer +custom_visualizer = MinimalProgressVisualizer() + +# Use it directly - clean and intuitive conversation = Conversation( agent=agent, - workspace=cwd, - visualize=custom_visualizer, - callbacks=[my_callback], # Additional callbacks + workspace="./workspace", + visualize=custom_visualizer, # Direct assignment ) ``` -### Disabling Visualization +### Visualization Options -To disable visualization entirely: +The `visualize` parameter accepts three types: + +- **`True`** (default): Use the default Rich panel visualizer +- **`False` or `None`**: Disable visualization entirely +- **`ConversationVisualizer` instance**: Use your custom visualizer + +### Combining with Additional Callbacks + +Custom visualizers work alongside other event callbacks: ```python +def metrics_callback(event): + # Track metrics separately from visualization + if isinstance(event, ActionEvent): + metrics.increment("actions_taken") + conversation = Conversation( agent=agent, - workspace=cwd, - visualize=None, # or False + workspace="./workspace", + visualize=custom_visualizer, # Handle visualization + callbacks=[metrics_callback], # Handle other concerns +) +``` + +## Built-in Visualizer Reference + +### Default Highlighting Patterns + +The built-in visualizer includes these default patterns: + +```python +DEFAULT_HIGHLIGHT_REGEX = { + r"^Reasoning:": "bold bright_black", + r"^Thought:": "bold bright_black", + r"^Action:": "bold blue", + r"^Arguments:": "bold blue", + r"^Tool:": "bold yellow", + r"^Result:": "bold yellow", + r"^Rejection Reason:": "bold red", + r"\*\*(.*?)\*\*": "bold", # Markdown bold + r"\*(.*?)\*": "italic", # Markdown italic +} +``` + +### Available Colors and Styles + +**Colors**: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `bright_black`, `bright_red`, etc. + +**Styles**: `bold`, `dim`, `italic`, `underline` + +**Combinations**: `"bold cyan"`, `"underline red"`, `"bold italic green"` + +### Configuration Options + +```python +ConversationVisualizer( + highlight_regex=custom_patterns, # Your highlighting rules + skip_user_messages=False, # Show user input (default: False) + name_for_visualization="MyAgent", # Prefix for panel titles + conversation_stats=stats_tracker, # Show token/cost metrics ) ``` From 4d4e4da6c4eafbef0343d8217f54291a0570a64b Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Nov 2025 20:57:46 +0000 Subject: [PATCH 04/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 5e4b75b5..9f388643 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -9,7 +9,7 @@ This example is available on GitHub: [examples/01_standalone_sdk/26_custom_visua Learn how to create effective custom visualizers by understanding the event system and implementing your own visualization logic. This guide teaches you to build visualizers that range from simple highlighting to complete custom interfaces. -The comprehensive example below demonstrates a production-ready custom visualizer that tracks conversation progress with minimal output. We'll break down the key concepts and best practices used in this implementation. +The comprehensive example below demonstrates a custom visualizer that tracks conversation progress with minimal output. We'll break down the key concepts and best practices used in this implementation. ```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py """Custom Visualizer Example @@ -27,8 +27,7 @@ The MinimalProgressVisualizer produces concise output showing: - Agent thinking indicators - Error messages -This demonstrates the new API improvement where you can pass a ConversationVisualizer -instance directly to the visualize parameter instead of using callbacks. +This shows how you can pass a ConversationVisualizer instance directly to the visualize parameter for clean, reusable visualization logic. """ import logging @@ -492,6 +491,9 @@ ConversationVisualizer( ## Next Steps -- **[Callbacks](/sdk/guides/convo-async)** - Learn about custom event callbacks -- **[Persistence](/sdk/guides/convo-persistence)** - Save and restore conversation state -- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control agent execution flow +Now that you understand custom visualizers, explore these related topics: + +- **[Async Conversations](/sdk/guides/convo-async)** - Learn about custom event callbacks and async processing +- **[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 From f1c7377230b3f6e2723764f47d2367eb576599b5 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 4 Nov 2025 23:03:25 +0000 Subject: [PATCH 05/13] 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 --- docs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs.json b/docs.json index 4aea36b7..868b56be 100644 --- a/docs.json +++ b/docs.json @@ -204,6 +204,7 @@ "sdk/guides/agent-interactive-terminal", "sdk/guides/agent-browser-use", "sdk/guides/agent-custom", + "sdk/guides/convo-custom-visualizer", "sdk/guides/agent-stuck-detector" ] }, From 039594439f1f721e5b0a8513bb44409e9a1d1169 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 01:07:12 +0000 Subject: [PATCH 06/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 9f388643..27d6808d 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -452,6 +452,8 @@ conversation = Conversation( ## Built-in Visualizer Reference +For reference, you can view the complete implementation of the default visualizer: [ConversationVisualizer source code](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/visualizer.py) + ### Default Highlighting Patterns The built-in visualizer includes these default patterns: From 88c60645d61d5631ac6263e15920a8d3284e194b Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 01:26:22 +0000 Subject: [PATCH 07/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 306 ++++++++++++------------- 1 file changed, 152 insertions(+), 154 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 27d6808d..21f7122e 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -27,7 +27,8 @@ The MinimalProgressVisualizer produces concise output showing: - Agent thinking indicators - Error messages -This shows how you can pass a ConversationVisualizer instance directly to the visualize parameter for clean, reusable visualization logic. +This demonstrates how you can pass a ConversationVisualizer instance directly +to the visualize parameter for clean, reusable visualization logic. """ import logging @@ -77,98 +78,152 @@ class MinimalProgressVisualizer(ConversationVisualizer): # We don't need the console/panels from the parent super().__init__(**kwargs) - # Track state for our custom visualization - self.step_count = 0 - self.current_action = None - self.logger = logging.getLogger(__name__) + # Track state for minimal progress output + self._step_count = 0 + self._pending_action = False + self._seen_llm_response_ids: set[str] = set() - def on_event(self, event): - """Handle conversation events with minimal progress display.""" - try: - if isinstance(event, MessageEvent): - if event.source == "agent": - print("šŸ¤– LLM call completed") - elif event.source == "user": - print(f"šŸ‘¤ User: {event.content}") - - elif isinstance(event, ActionEvent): - self.step_count += 1 - action_name = event.action.__class__.__name__ - - # Extract useful details from common actions - details = "" - if hasattr(event.action, "command"): - details = f"command: {event.action.command[:50]}..." - elif hasattr(event.action, "path"): - path = event.action.path - if hasattr(event.action, "command"): - details = f"{event.action.command}: {path}" - else: - details = f"path: {path}" - - if details: - print(f"Step {self.step_count}: Executing {action_name} ({details})...", end=" ", flush=True) - else: - print(f"Step {self.step_count}: Executing {action_name}...", end=" ", flush=True) - - self.current_action = action_name - - elif isinstance(event, ObservationEvent): - if self.current_action: - # Simple success/error indication - if hasattr(event.observation, "error") and event.observation.error: - print("āŒ") - print(f"āŒ Error: {event.observation.error}") - else: - print("āœ“") - self.current_action = None + def on_event(self, event) -> None: + """Handle events and produce minimal progress output.""" + if isinstance(event, ActionEvent): + self._handle_action_event(event) + elif isinstance(event, ObservationEvent): + self._handle_observation_event() + elif isinstance(event, AgentErrorEvent): + self._handle_error_event(event) + elif isinstance(event, MessageEvent): + self._handle_message_event(event) + + def _handle_action_event(self, event: ActionEvent) -> None: + """Handle ActionEvent - track LLM calls and show tool execution.""" + # Track LLM calls by monitoring new llm_response_id values + if ( + event.llm_response_id + and event.llm_response_id not in self._seen_llm_response_ids + ): + self._seen_llm_response_ids.add(event.llm_response_id) + # This is a new LLM call - show it completed + if not self._pending_action: + print("šŸ¤– LLM call completed", flush=True) + + # If previous action hasn't completed, complete it first + if self._pending_action: + print(" āœ“", flush=True) + + self._step_count += 1 + tool_name = event.tool_name if event.tool_name else "unknown" + + # Extract command/action details if available + action_details = "" + if event.action: + action_dict = ( + event.action.model_dump() if hasattr(event.action, "model_dump") else {} + ) + if "command" in action_dict: + command = action_dict["command"] + # Show file path if available (for file operations) + path = action_dict.get("path", "") + if path: + # Truncate long paths + if len(path) > 40: + path = "..." + path[-37:] + action_details = f" ({command}: {path})" else: - print("šŸ’­ Agent thinking...") - - elif isinstance(event, AgentErrorEvent): - print(f"āŒ Agent Error: {event.message}") - - except Exception as e: - # Fallback to prevent visualization errors from breaking the conversation - self.logger.warning(f"Visualization error: {e}") + action_details = f" ({command})" + + # Show step number and tool being executed on its own line + print( + f"Step {self._step_count}: Executing {tool_name}{action_details}...", + end="", + flush=True, + ) + self._pending_action = True + + def _handle_observation_event(self) -> None: + """Handle ObservationEvent - show completion indicator.""" + if self._pending_action: + print(" āœ“", flush=True) + self._pending_action = False + + def _handle_error_event(self, event: AgentErrorEvent) -> None: + """Handle AgentErrorEvent - show errors.""" + if self._pending_action: + print(" āœ—", flush=True) # Mark previous action as failed + self._pending_action = False + + error_msg = event.error + # Truncate long error messages + error_preview = error_msg[:100] + "..." if len(error_msg) > 100 else error_msg + print(f"āš ļø Error: {error_preview}", flush=True) + + def _handle_message_event(self, event: MessageEvent) -> None: + """Handle MessageEvent - track LLM calls and show thinking indicators.""" + # Track LLM calls from MessageEvent (agent messages without tool calls) + if ( + event.source == "agent" + and event.llm_response_id + and event.llm_response_id not in self._seen_llm_response_ids + ): + self._seen_llm_response_ids.add(event.llm_response_id) + # This is a new LLM call - show it completed + if not self._pending_action: + print("šŸ¤– LLM call completed", flush=True) + + # Show when agent is "thinking" (making LLM calls between actions) + if event.source == "agent" and event.llm_message.role == "assistant": + # Agent is thinking/planning - show a thinking indicator + if not self._pending_action: + # Only show if we haven't already shown the LLM call completion + if ( + not event.llm_response_id + or event.llm_response_id in self._seen_llm_response_ids + ): + print("šŸ’­ Agent thinking...", flush=True) def main(): - """Demonstrate the custom visualizer with a simple task.""" - # Get API key from environment - api_key = os.environ.get("LLM_API_KEY") - if not api_key: - raise ValueError("LLM_API_KEY environment variable is not set") - - # Create LLM and Agent with default tools - llm = LLM(model="gpt-4o-mini", api_key=SecretStr(api_key)) - agent = get_default_agent(llm=llm) + # ============================================================================ + # Configure LLM and Agent + # ============================================================================ + # You can get an API key from https://app.all-hands.dev/settings/api-keys + 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", "openhands/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) - # Create our custom visualizer - custom_visualizer = MinimalProgressVisualizer() + # ============================================================================ + # Configure Visualization + # ============================================================================ + # Set logging level to reduce verbosity + logging.getLogger().setLevel(logging.WARNING) - print("šŸš€ Starting conversation with custom visualizer...") - print("=" * 60) + # Create custom visualizer instance + minimal_visualizer = MinimalProgressVisualizer() - # NEW API: Pass the visualizer instance directly! - # This is much cleaner than: visualize=False, callbacks=[custom_visualizer.on_event] + # Start a conversation with custom visualizer + cwd = os.getcwd() conversation = Conversation( agent=agent, - workspace="./workspace", - visualize=custom_visualizer, # Direct and intuitive! + workspace=cwd, + visualize=minimal_visualizer, ) - # Send a message and run - conversation.send_message( - "Create a file called FACTS.txt with 3 interesting facts about Python programming." - ) + # 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!") - print("=" * 60) - print("āœ… Example completed!") - print(f"šŸ“Š Total steps executed: {custom_visualizer.step_count}") - print("\nThe conversation used our custom MinimalProgressVisualizer!") - print("Compare this to the default visualizer to see the difference.") + # Report cost + cost = llm.metrics.accumulated_cost + print(f"EXAMPLE_COST: ${cost:.4f}") if __name__ == "__main__": @@ -309,22 +364,26 @@ def handle_error(self, event: AgentErrorEvent): ## Best Practices ### 1. State Management -Track conversation state to provide meaningful progress indicators: +Track conversation state to provide meaningful progress indicators. The example shows tracking step counts, pending actions, and LLM response IDs: ```python -class StatefulVisualizer(ConversationVisualizer): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.step_count = 0 - self.pending_action = False - self.seen_llm_responses = set() - self.start_time = time.time() +def __init__(self, **kwargs): + super().__init__(**kwargs) + self._step_count = 0 + self._pending_action = False + self._seen_llm_response_ids: set[str] = set() + +def _handle_action_event(self, event: ActionEvent) -> None: + # Track new LLM calls by monitoring llm_response_id + if ( + event.llm_response_id + and event.llm_response_id not in self._seen_llm_response_ids + ): + self._seen_llm_response_ids.add(event.llm_response_id) + print("šŸ¤– LLM call completed", flush=True) - def on_event(self, event): - # Use state to provide context - elapsed = time.time() - self.start_time - print(f"[{elapsed:.1f}s] ", end="") - # ... handle event + self._step_count += 1 + # ... handle event ``` ### 2. Error Handling @@ -343,68 +402,7 @@ def on_event(self, event): ``` ### 3. Performance Considerations -For high-frequency events, optimize your visualization: - -```python -def on_event(self, event): - # Skip verbose events if needed - if isinstance(event, SomeVerboseEvent) and not self.verbose_mode: - return - - # Batch updates for better performance - if self.should_batch_updates(): - self.pending_updates.append(event) - return - - self._render_event(event) -``` - -### 4. Flexible Output -Design visualizers that work in different environments: - -```python -class AdaptiveVisualizer(ConversationVisualizer): - def __init__(self, output_format="console", **kwargs): - super().__init__(**kwargs) - self.output_format = output_format - - if output_format == "json": - self.output_handler = self._json_output - elif output_format == "markdown": - self.output_handler = self._markdown_output - else: - self.output_handler = self._console_output - - def on_event(self, event): - self.output_handler(event) -``` - -### 5. Integration with External Systems -Custom visualizers can integrate with logging, monitoring, or notification systems: - -```python -class IntegratedVisualizer(ConversationVisualizer): - def __init__(self, webhook_url=None, **kwargs): - super().__init__(**kwargs) - self.webhook_url = webhook_url - self.metrics = {"actions": 0, "errors": 0} - - def on_event(self, event): - # Update metrics - if isinstance(event, ActionEvent): - self.metrics["actions"] += 1 - elif isinstance(event, AgentErrorEvent): - self.metrics["errors"] += 1 - - # Send to external system - if self.webhook_url and isinstance(event, AgentErrorEvent): - self._send_alert(event) - - def _send_alert(self, event): - # Send webhook notification for errors - payload = {"error": event.error, "timestamp": time.time()} - requests.post(self.webhook_url, json=payload) -``` +For high-frequency events, consider optimizing your visualization by filtering events or using efficient output methods like `flush=True` for immediate display. ## Using Your Custom Visualizer From ebfb5e4ad31e95d86e3e0aa5a11e81f302d68ba0 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 01:31:31 +0000 Subject: [PATCH 08/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 21f7122e..fa45ad69 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -297,7 +297,19 @@ class MinimalProgressVisualizer(ConversationVisualizer): ## Key Event Types -Understanding the event system is crucial for effective custom visualizers: +Understanding the event system is crucial for effective custom visualizers. Here's a comprehensive overview of all event types handled by the default visualizer: + +| Event Type | Description | Key Properties | When It Occurs | +|------------|-------------|----------------|----------------| +| `SystemPromptEvent` | System-level prompts and instructions | `content`, `source` | Agent initialization, system messages | +| `ActionEvent` | Agent actions and tool calls | `action`, `tool_name`, `llm_response_id` | When agent decides to take an action | +| `ObservationEvent` | Results from executed actions | `observation`, `source` | After action execution completes | +| `MessageEvent` | LLM messages (user/assistant) | `llm_message` (role, content) | User input, agent responses | +| `AgentErrorEvent` | Error conditions and failures | `error`, `source` | When agent encounters errors | +| `PauseEvent` | User-initiated pauses | `source` | When user pauses conversation | +| `Condensation` | Memory condensation events | `content`, `source` | During conversation memory management | + +### Detailed Event Descriptions ### ActionEvent Fired when the agent decides to use a tool or take an action. From 24302780bc90f4fbeed4d82bc6ffed9c949cd3b7 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 01:43:34 +0000 Subject: [PATCH 09/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 194 ++++++++++++++++++++----- 1 file changed, 158 insertions(+), 36 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index fa45ad69..08632a9c 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -295,6 +295,53 @@ class MinimalProgressVisualizer(ConversationVisualizer): **When to use**: When you need completely different output format, custom state tracking, or integration with external systems. +### Approach 3: Custom Object with on_event Method + +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: + +```python +from rich.console import Console +from rich.panel import Panel +from openhands.sdk.event import Event + +class CustomVisualizer: + """Custom visualizer without subclassing ConversationVisualizer.""" + + def __init__(self): + self.event_count = 0 + self.console = Console() + + def on_event(self, event: Event) -> None: + """Handle any event - this is the only required method.""" + self.event_count += 1 + + # Use the event's built-in visualize property + content = event.visualize + if content.plain.strip(): + # Create custom panel styling + panel = Panel( + content, + title=f"[bold cyan]Event #{self.event_count}: {event.__class__.__name__}[/]", + border_style="cyan", + padding=(0, 1) + ) + self.console.print(panel) + +# Use it directly +conversation = LocalConversation( + agent=agent, + workspace=workspace, + visualize=CustomVisualizer() # Pass your custom object +) +``` + +**Key Requirements:** +- Must have an `on_event(self, event: Event) -> None` method +- Can be any Python object (class instance, function with state, etc.) +- No inheritance required + +**When to use**: When you want maximum flexibility without inheriting from ConversationVisualizer, or when integrating with existing class hierarchies. + ## Key Event Types 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 ### ActionEvent Fired when the agent decides to use a tool or take an action. +**Key Properties:** +- `thought`: Agent's reasoning before taking action (list of TextContent) +- `action`: The actual tool action (None if non-executable) +- `tool_name`: Name of the tool being called +- `tool_call_id`: Unique identifier for the tool call +- `security_risk`: LLM's assessment of action safety +- `reasoning_content`: Intermediate reasoning from reasoning models + +**Default Rendering:** Blue panel titled "Agent Action" showing reasoning, thought process, and action details. + ```python def handle_action(self, event: ActionEvent): - # Access tool information - tool_name = event.tool_name # e.g., "str_replace_editor" - action = event.action # The actual action object - - # Track LLM calls - if event.llm_response_id: - print(f"šŸ¤– LLM call {event.llm_response_id}") + # Access thought process + thought_text = " ".join([t.text for t in event.thought]) + print(f"šŸ’­ Thought: {thought_text}") - # Extract action details - if hasattr(action, 'command'): - print(f"Command: {action.command}") - if hasattr(action, 'path'): - print(f"File: {action.path}") + # Check if action is executable + if event.action: + print(f"šŸ”§ Tool: {event.tool_name}") + print(f"⚔ Action: {event.action}") + else: + print(f"āš ļø Non-executable call: {event.tool_call.name}") ``` ### ObservationEvent -Fired when a tool execution completes and returns results. +Contains the result of an executed action. + +**Key Properties:** +- `observation`: The tool execution result (varies by tool) +- `tool_name`: Name of the tool that was executed +- `tool_call_id`: ID linking back to the original action +- `action_id`: ID of the action this observation responds to + +**Default Rendering:** Yellow panel titled "Observation" showing tool name and execution results. ```python def handle_observation(self, event: ObservationEvent): - # Check for errors - if hasattr(event.observation, 'error') and event.observation.error: - print(f"āŒ Tool failed: {event.observation.error}") - else: - print("āœ… Tool completed successfully") + print(f"šŸ”§ Tool: {event.tool_name}") + print(f"šŸ”— Action ID: {event.action_id}") - # Access results - if hasattr(event.observation, 'content'): - content = event.observation.content - print(f"Result: {content[:100]}...") # Show first 100 chars + # Access the observation result + obs = event.observation + if hasattr(obs, 'error') and obs.error: + print(f"āŒ Error: {obs.error}") + elif hasattr(obs, 'content'): + print(f"šŸ“„ Content: {obs.content[:100]}...") ``` ### MessageEvent -Fired for LLM messages (both user input and agent responses). +Represents messages between user and agent. + +**Key Properties:** +- `llm_message`: The complete LLM message (role, content, tool_calls) +- `source`: Whether from "user" or "agent" +- `activated_skills`: List of skills activated for this message +- `extended_content`: Additional content added by agent context + +**Default Rendering:** Gold panel for user messages, blue panel for agent messages, with role-specific titles. ```python def handle_message(self, event: MessageEvent): - if event.source == "user": - print(f"šŸ‘¤ User: {event.content}") - elif event.source == "agent": - print(f"šŸ¤– Agent: {event.content}") - - # Track LLM response IDs to avoid duplicates - if event.llm_response_id: - self.seen_responses.add(event.llm_response_id) + if event.llm_message: + role = event.llm_message.role + content = event.llm_message.content + + if role == "user": + print(f"šŸ‘¤ User: {content[0].text if content else ''}") + elif role == "assistant": + print(f"šŸ¤– Agent: {content[0].text if content else ''}") + + # Check for tool calls + if event.llm_message.tool_calls: + print(f"šŸ”§ Tool calls: {len(event.llm_message.tool_calls)}") ``` ### AgentErrorEvent -Fired when the agent encounters an error. +Error conditions encountered by the agent. + +**Key Properties:** +- `error`: The error message from the agent/scaffold +- `tool_name`: Tool that caused the error (if applicable) +- `tool_call_id`: ID of the failed tool call (if applicable) + +**Default Rendering:** Red panel titled "Agent Error" displaying error details. ```python def handle_error(self, event: AgentErrorEvent): - print(f"🚨 Agent Error: {event.error}") - # Optionally log to external systems - self.logger.error(f"Agent failed: {event.error}") + print(f"🚨 Error: {event.error}") + if event.tool_name: + print(f"šŸ”§ Failed tool: {event.tool_name}") + if event.tool_call_id: + print(f"šŸ”— Call ID: {event.tool_call_id}") ``` ## Best Practices -### 1. State Management +### 1. Understanding ConversationVisualizer Subclassing + +When subclassing `ConversationVisualizer`, you inherit several useful features: + +**Built-in Features:** +- Rich Console instance (`self._console`) for formatted output +- Highlighting patterns (`self._highlight_patterns`) for text styling +- Conversation stats integration (`self._conversation_stats`) for metrics +- Name prefixing (`self._name_for_visualization`) for multi-agent scenarios + +**Key Methods to Override:** +- `on_event(self, event: Event)`: Main event handler (most common override) +- `_create_event_panel(self, event: Event)`: Custom panel creation +- `_apply_highlighting(self, text: Text)`: Custom text highlighting + +**Initialization Pattern:** +```python +class MyVisualizer(ConversationVisualizer): + def __init__(self, custom_param: str = "default", **kwargs): + # Always call super().__init__ to get base functionality + super().__init__(**kwargs) + + # Add your custom state + self.custom_param = custom_param + self.event_count = 0 + + def on_event(self, event: Event) -> None: + # Your custom logic here + self.event_count += 1 + + # Option 1: Completely custom handling + if isinstance(event, ActionEvent): + print(f"Custom action handling: {event.tool_name}") + + # Option 2: Use parent's panel creation with modifications + panel = self._create_event_panel(event) + if panel: + self._console.print(panel) +``` + +### 2. State Management Track conversation state to provide meaningful progress indicators. The example shows tracking step counts, pending actions, and LLM response IDs: ```python From 20288861d73f5309a10d31dc9fd3519eb13af3d7 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 11:08:40 +0000 Subject: [PATCH 10/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 56 +++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 08632a9c..31bb33ef 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -520,7 +520,59 @@ def _handle_action_event(self, event: ActionEvent) -> None: # ... handle event ``` -### 2. Error Handling +### 3. Event Handler Registration Pattern +For cleaner, more maintainable code, avoid long if/elif chains in your `on_event` method. Instead, use a decorator-based handler registration pattern: + +```python +from typing import Dict, Callable, Type + +def handles(event_type: Type[Event]): + """Decorator to register a method as an event handler.""" + def decorator(func): + func._handles_event_type = event_type + return func + return decorator + +class EventHandlerMixin: + """Mixin that provides event handler registration via decorators.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._event_handlers: Dict[Type[Event], Callable[[Event], None]] = {} + self._register_handlers() + + def _register_handlers(self): + """Automatically discover and register event handlers.""" + for attr_name in dir(self): + attr = getattr(self, attr_name) + if hasattr(attr, '_handles_event_type'): + event_type = attr._handles_event_type + self._event_handlers[event_type] = attr + + def on_event(self, event: Event) -> None: + """Dispatch events to registered handlers.""" + event_type = type(event) + handler = self._event_handlers.get(event_type) + if handler: + handler(event) + +class MyVisualizer(EventHandlerMixin, ConversationVisualizer): + @handles(ActionEvent) + def _handle_action_event(self, event: ActionEvent) -> None: + print(f"Action: {event.tool_name}") + + @handles(MessageEvent) + def _handle_message_event(self, event: MessageEvent) -> None: + print(f"Message: {event.llm_message.role}") +``` + +This pattern provides: +- **Self-documenting code**: `@handles(ActionEvent)` clearly shows what each method does +- **Type safety**: The decorator enforces the event type relationship +- **Easy extensibility**: Add new event types without touching dispatch logic +- **No boilerplate**: No need to maintain separate dictionaries or long if/elif chains + +### 4. Error Handling Always include error handling to prevent visualization issues from breaking conversations: ```python @@ -535,7 +587,7 @@ def on_event(self, event): logging.warning(f"Visualizer failed: {e}") ``` -### 3. Performance Considerations +### 5. Performance Considerations For high-frequency events, consider optimizing your visualization by filtering events or using efficient output methods like `flush=True` for immediate display. ## Using Your Custom Visualizer From 91f0bb9e39fa274b91a7953ffb76e9aaeddc997f Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 5 Nov 2025 14:56:40 +0000 Subject: [PATCH 11/13] 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 --- sdk/guides/convo-custom-visualizer.mdx | 36 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 31bb33ef..e336fb1a 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -17,7 +17,7 @@ The comprehensive example below demonstrates a custom visualizer that tracks con 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 visualize parameter) +- Direct configuration (just pass the visualizer instance to visualizer parameter) - Reusable visualizer that can be shared across conversations - Better separation of concerns compared to callback functions @@ -28,7 +28,7 @@ The MinimalProgressVisualizer produces concise output showing: - Error messages This demonstrates how you can pass a ConversationVisualizer instance directly -to the visualize parameter for clean, reusable visualization logic. +to the visualizer parameter for clean, reusable visualization logic. """ import logging @@ -212,7 +212,7 @@ def main(): conversation = Conversation( agent=agent, workspace=cwd, - visualize=minimal_visualizer, + visualizer=minimal_visualizer, ) # Send a message and let the agent run @@ -328,10 +328,10 @@ class CustomVisualizer: self.console.print(panel) # Use it directly -conversation = LocalConversation( +conversation = Conversation( agent=agent, workspace=workspace, - visualize=CustomVisualizer() # Pass your custom object + visualizer=CustomVisualizer() # Pass your custom object ) ``` @@ -576,17 +576,25 @@ This pattern provides: Always include error handling to prevent visualization issues from breaking conversations: ```python +import logging + def on_event(self, event): try: # Your visualization logic - self._handle_event(event) + if isinstance(event, ActionEvent): + self._handle_action_event(event) + elif isinstance(event, ObservationEvent): + self._handle_observation_event(event) + # ... other event types except Exception as e: # Fallback to prevent breaking the conversation - print(f"Visualization error: {e}") + print(f"āš ļø Visualization error: {e}", flush=True) # Optionally log for debugging - logging.warning(f"Visualizer failed: {e}") + logging.warning(f"Visualizer failed for {event.__class__.__name__}: {e}") ``` +This pattern ensures that visualization problems never interrupt the agent's work. + ### 5. Performance Considerations For high-frequency events, consider optimizing your visualization by filtering events or using efficient output methods like `flush=True` for immediate display. @@ -594,7 +602,7 @@ For high-frequency events, consider optimizing your visualization by filtering e ### Direct Assignment (Recommended) -Pass your visualizer instance directly to the `visualize` parameter: +Pass your visualizer instance directly to the `visualizer` parameter: ```python # Create your custom visualizer @@ -604,16 +612,16 @@ custom_visualizer = MinimalProgressVisualizer() conversation = Conversation( agent=agent, workspace="./workspace", - visualize=custom_visualizer, # Direct assignment + visualizer=custom_visualizer, # Direct assignment ) ``` ### Visualization Options -The `visualize` parameter accepts three types: +The `visualizer` parameter accepts three types: -- **`True`** (default): Use the default Rich panel visualizer -- **`False` or `None`**: Disable visualization entirely +- **Default behavior** (no parameter): Creates a default Rich panel visualizer automatically +- **`None`**: Disable visualization entirely - **`ConversationVisualizer` instance**: Use your custom visualizer ### Combining with Additional Callbacks @@ -629,7 +637,7 @@ def metrics_callback(event): conversation = Conversation( agent=agent, workspace="./workspace", - visualize=custom_visualizer, # Handle visualization + visualizer=custom_visualizer, # Handle visualization callbacks=[metrics_callback], # Handle other concerns ) ``` From 22a6021df20982f564a286588ab82a22012a79dd Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 6 Nov 2025 15:35:11 +0000 Subject: [PATCH 12/13] 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 --- sdk/getting-started.mdx | 4 +- sdk/guides/agent-browser-use.mdx | 4 +- sdk/guides/agent-delegation.mdx | 3 +- sdk/guides/agent-interactive-terminal.mdx | 4 +- sdk/guides/agent-server/api-sandbox.mdx | 2 +- sdk/guides/agent-server/docker-sandbox.mdx | 3 - sdk/guides/agent-server/local-server.mdx | 1 - sdk/guides/context-condenser.mdx | 4 +- sdk/guides/convo-async.mdx | 4 +- sdk/guides/convo-custom-visualizer.mdx | 546 +++++++----------- sdk/guides/convo-pause-and-resume.mdx | 4 +- sdk/guides/convo-persistence.mdx | 4 +- .../convo-send-message-while-running.mdx | 4 +- sdk/guides/custom-tools.mdx | 12 +- sdk/guides/hello-world.mdx | 4 +- sdk/guides/llm-image-input.mdx | 4 +- sdk/guides/llm-reasoning.mdx | 4 +- sdk/guides/llm-registry.mdx | 4 +- sdk/guides/mcp.mdx | 8 +- sdk/guides/metrics.mdx | 12 +- sdk/guides/secrets.mdx | 4 +- sdk/guides/security.mdx | 4 +- sdk/guides/skill.mdx | 4 +- 23 files changed, 263 insertions(+), 384 deletions(-) diff --git a/sdk/getting-started.mdx b/sdk/getting-started.mdx index a706b53b..e0d608cc 100644 --- a/sdk/getting-started.mdx +++ b/sdk/getting-started.mdx @@ -75,9 +75,9 @@ Here's a complete example that creates an agent and asks it to perform a simple import os from openhands.sdk import LLM, Agent, Conversation, Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool llm = LLM( @@ -88,7 +88,7 @@ llm = LLM( agent = Agent( llm=llm, tools=[ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), Tool(name=TaskTrackerTool.name), ], diff --git a/sdk/guides/agent-browser-use.mdx b/sdk/guides/agent-browser-use.mdx index 3a16a5e3..d9d73042 100644 --- a/sdk/guides/agent-browser-use.mdx +++ b/sdk/guides/agent-browser-use.mdx @@ -24,8 +24,8 @@ from openhands.sdk import ( ) from openhands.sdk.tool import Tool from openhands.tools.browser_use import BrowserToolSet -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -46,7 +46,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), Tool(name=BrowserToolSet.name), diff --git a/sdk/guides/agent-delegation.mdx b/sdk/guides/agent-delegation.mdx index 2b0202a7..542b09ad 100644 --- a/sdk/guides/agent-delegation.mdx +++ b/sdk/guides/agent-delegation.mdx @@ -40,6 +40,7 @@ from openhands.sdk import ( Tool, get_logger, ) +from openhands.sdk.conversation import DefaultConversationVisualizer from openhands.sdk.tool import register_tool from openhands.tools.delegate import DelegateTool from openhands.tools.preset.default import get_default_tools @@ -72,7 +73,7 @@ main_agent = Agent( conversation = Conversation( agent=main_agent, workspace=cwd, - name_for_visualization="Delegator", + visualizer=DefaultConversationVisualizer(name="Delegator"), ) task_message = ( diff --git a/sdk/guides/agent-interactive-terminal.mdx b/sdk/guides/agent-interactive-terminal.mdx index 26c8e9b1..a855c7a3 100644 --- a/sdk/guides/agent-interactive-terminal.mdx +++ b/sdk/guides/agent-interactive-terminal.mdx @@ -24,7 +24,7 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -45,7 +45,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, params={"no_change_timeout_seconds": 3}, ) ] diff --git a/sdk/guides/agent-server/api-sandbox.mdx b/sdk/guides/agent-server/api-sandbox.mdx index d6434de5..012e0d5a 100644 --- a/sdk/guides/agent-server/api-sandbox.mdx +++ b/sdk/guides/agent-server/api-sandbox.mdx @@ -79,7 +79,7 @@ with APIRemoteWorkspace( logger.info(f"Command completed: {result.exit_code}, {result.stdout}") conversation = Conversation( - agent=agent, workspace=workspace, callbacks=[event_callback], visualize=True + agent=agent, workspace=workspace, callbacks=[event_callback] ) assert isinstance(conversation, RemoteConversation) diff --git a/sdk/guides/agent-server/docker-sandbox.mdx b/sdk/guides/agent-server/docker-sandbox.mdx index 7c3de889..fc972941 100644 --- a/sdk/guides/agent-server/docker-sandbox.mdx +++ b/sdk/guides/agent-server/docker-sandbox.mdx @@ -94,7 +94,6 @@ with DockerWorkspace( agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) @@ -309,7 +308,6 @@ with DockerWorkspace( agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) @@ -497,7 +495,6 @@ with DockerWorkspace( agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) diff --git a/sdk/guides/agent-server/local-server.mdx b/sdk/guides/agent-server/local-server.mdx index b582acca..11b369a1 100644 --- a/sdk/guides/agent-server/local-server.mdx +++ b/sdk/guides/agent-server/local-server.mdx @@ -185,7 +185,6 @@ with ManagedAPIServer(port=8001) as server: agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) diff --git a/sdk/guides/context-condenser.mdx b/sdk/guides/context-condenser.mdx index 991921cf..ee35cd4b 100644 --- a/sdk/guides/context-condenser.mdx +++ b/sdk/guides/context-condenser.mdx @@ -77,9 +77,9 @@ from openhands.sdk import ( ) from openhands.sdk.context.condenser import LLMSummarizingCondenser from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -100,7 +100,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), Tool(name=TaskTrackerTool.name), diff --git a/sdk/guides/convo-async.mdx b/sdk/guides/convo-async.mdx index 531804d1..79a96785 100644 --- a/sdk/guides/convo-async.mdx +++ b/sdk/guides/convo-async.mdx @@ -32,9 +32,9 @@ from openhands.sdk import ( from openhands.sdk.conversation.types import ConversationCallbackType from openhands.sdk.tool import Tool from openhands.sdk.utils.async_utils import AsyncCallbackWrapper -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -55,7 +55,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), Tool(name=TaskTrackerTool.name), diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index e336fb1a..3fe0a305 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -1,15 +1,15 @@ --- title: Custom Visualizer -description: Customize conversation visualization with custom highlighting patterns and display options. +description: Customize conversation visualization by creating custom visualizers or configuring the default visualizer. --- 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) -Learn how to create effective custom visualizers by understanding the event system and implementing your own visualization logic. This guide teaches you to build visualizers that range from simple highlighting to complete custom interfaces. +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`. -The comprehensive example below demonstrates a custom visualizer that tracks conversation progress with minimal output. We'll break down the key concepts and best practices used in this implementation. +## Basic Example ```python icon="python" expandable examples/01_standalone_sdk/26_custom_visualizer.py """Custom Visualizer Example @@ -20,14 +20,9 @@ ConversationVisualizer. This approach provides: - Direct configuration (just pass the visualizer instance to visualizer parameter) - Reusable visualizer that can be shared across conversations - Better separation of concerns compared to callback functions +- Event handler registration to avoid long if/elif chains -The MinimalProgressVisualizer produces concise output showing: -- LLM call completions -- Tool execution steps with command/path details -- Agent thinking indicators -- Error messages - -This demonstrates how you can pass a ConversationVisualizer instance directly +This demonstrates how you can pass a ConversationVisualizer instance directly to the visualizer parameter for clean, reusable visualization logic. """ @@ -37,197 +32,67 @@ import os from pydantic import SecretStr from openhands.sdk import LLM, Conversation -from openhands.sdk.conversation.visualizer import ConversationVisualizer +from openhands.sdk.conversation.visualizer import ConversationVisualizerBase from openhands.sdk.event import ( - ActionEvent, - AgentErrorEvent, - MessageEvent, - ObservationEvent, + Event, ) from openhands.tools.preset.default import get_default_agent -class MinimalProgressVisualizer(ConversationVisualizer): - """A minimal progress visualizer that shows step counts and tool names. - - This visualizer produces concise output showing: - - LLM call completions - - Tool execution steps with command/path details - - Agent thinking indicators - - Error messages - - Example output: - šŸ¤– LLM call completed - Step 1: Executing str_replace_editor (view: .../FACTS.txt)... āœ“ - šŸ’­ Agent thinking... - šŸ¤– LLM call completed - Step 2: Executing str_replace_editor (str_replace: .../FACTS.txt)... āœ“ - āŒ Error: File not found - """ +class MinimalVisualizer(ConversationVisualizerBase): + """A minimal visualizer that print the raw events as they occur.""" - def __init__(self, **kwargs): + def __init__(self, name: str | None = None): """Initialize the minimal progress visualizer. Args: - **kwargs: Additional arguments passed to ConversationVisualizer. - Note: We override visualization, so most ConversationVisualizer - parameters are ignored, but we keep the signature for - compatibility. + name: Optional name to identify the agent/conversation. + Note: This simple visualizer doesn't use it in output, + but accepts it for compatibility with the base class. """ - # Initialize parent but we'll override on_event - # We don't need the console/panels from the parent - super().__init__(**kwargs) + # Initialize parent - state will be set later via initialize() + super().__init__(name=name) - # Track state for minimal progress output - self._step_count = 0 - self._pending_action = False - self._seen_llm_response_ids: set[str] = set() + 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", "openhands/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) - def on_event(self, event) -> None: - """Handle events and produce minimal progress output.""" - if isinstance(event, ActionEvent): - self._handle_action_event(event) - elif isinstance(event, ObservationEvent): - self._handle_observation_event() - elif isinstance(event, AgentErrorEvent): - self._handle_error_event(event) - elif isinstance(event, MessageEvent): - self._handle_message_event(event) - - def _handle_action_event(self, event: ActionEvent) -> None: - """Handle ActionEvent - track LLM calls and show tool execution.""" - # Track LLM calls by monitoring new llm_response_id values - if ( - event.llm_response_id - and event.llm_response_id not in self._seen_llm_response_ids - ): - self._seen_llm_response_ids.add(event.llm_response_id) - # This is a new LLM call - show it completed - if not self._pending_action: - print("šŸ¤– LLM call completed", flush=True) - - # If previous action hasn't completed, complete it first - if self._pending_action: - print(" āœ“", flush=True) - - self._step_count += 1 - tool_name = event.tool_name if event.tool_name else "unknown" - - # Extract command/action details if available - action_details = "" - if event.action: - action_dict = ( - event.action.model_dump() if hasattr(event.action, "model_dump") else {} - ) - if "command" in action_dict: - command = action_dict["command"] - # Show file path if available (for file operations) - path = action_dict.get("path", "") - if path: - # Truncate long paths - if len(path) > 40: - path = "..." + path[-37:] - action_details = f" ({command}: {path})" - else: - action_details = f" ({command})" - - # Show step number and tool being executed on its own line - print( - f"Step {self._step_count}: Executing {tool_name}{action_details}...", - end="", - flush=True, - ) - self._pending_action = True - - def _handle_observation_event(self) -> None: - """Handle ObservationEvent - show completion indicator.""" - if self._pending_action: - print(" āœ“", flush=True) - self._pending_action = False - - def _handle_error_event(self, event: AgentErrorEvent) -> None: - """Handle AgentErrorEvent - show errors.""" - if self._pending_action: - print(" āœ—", flush=True) # Mark previous action as failed - self._pending_action = False - - error_msg = event.error - # Truncate long error messages - error_preview = error_msg[:100] + "..." if len(error_msg) > 100 else error_msg - print(f"āš ļø Error: {error_preview}", flush=True) - - def _handle_message_event(self, event: MessageEvent) -> None: - """Handle MessageEvent - track LLM calls and show thinking indicators.""" - # Track LLM calls from MessageEvent (agent messages without tool calls) - if ( - event.source == "agent" - and event.llm_response_id - and event.llm_response_id not in self._seen_llm_response_ids - ): - self._seen_llm_response_ids.add(event.llm_response_id) - # This is a new LLM call - show it completed - if not self._pending_action: - print("šŸ¤– LLM call completed", flush=True) - - # Show when agent is "thinking" (making LLM calls between actions) - if event.source == "agent" and event.llm_message.role == "assistant": - # Agent is thinking/planning - show a thinking indicator - if not self._pending_action: - # Only show if we haven't already shown the LLM call completion - if ( - not event.llm_response_id - or event.llm_response_id in self._seen_llm_response_ids - ): - print("šŸ’­ Agent thinking...", flush=True) - - -def main(): - # ============================================================================ - # Configure LLM and Agent - # ============================================================================ - # You can get an API key from https://app.all-hands.dev/settings/api-keys - 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", "openhands/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) - - # Create custom visualizer instance - minimal_visualizer = MinimalProgressVisualizer() - - # Start a conversation with custom visualizer - cwd = os.getcwd() - conversation = Conversation( - agent=agent, - workspace=cwd, - visualizer=minimal_visualizer, - ) - - # 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}") - - -if __name__ == "__main__": - main() +# ============================================================================ +# 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}") ``` ```bash Running the Example @@ -236,19 +101,48 @@ cd agent-sdk uv run python examples/01_standalone_sdk/26_custom_visualizer.py ``` -## Understanding Custom Visualizers +## Visualizer Configuration Options + +The `visualizer` parameter in `Conversation` controls how events are displayed: + +```python +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}") -Custom visualizers give you complete control over how conversation events are displayed. There are two main approaches, each suited for different needs. +conversation = Conversation(agent=agent, workspace=workspace, visualizer=MyVisualizer()) +``` -### Approach 1: Configure the Built-in Visualizer +## Customizing the Default Visualizer -The built-in `ConversationVisualizer` uses Rich panels and provides extensive customization through configuration: +`DefaultConversationVisualizer` uses Rich panels and supports customization through configuration: ```python -from openhands.sdk.conversation.visualizer import ConversationVisualizer +from openhands.sdk.conversation import DefaultConversationVisualizer # Configure highlighting patterns using regex -custom_visualizer = ConversationVisualizer( +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:" @@ -257,90 +151,71 @@ custom_visualizer = ConversationVisualizer( r"\*\*(.*?)\*\*": "bold", # Markdown bold **text** }, skip_user_messages=False, # Show user messages - name_for_visualization="MyAgent", # Prefix panel titles +) + +conversation = Conversation( + agent=agent, + workspace=workspace, + visualizer=custom_visualizer ) ``` -**When to use**: Perfect for customizing colors and highlighting without changing the overall panel-based layout. +**When to use**: Perfect for customizing colors and highlighting without changing the panel-based layout. -### Approach 2: Subclass for Complete Control +## Creating Custom Visualizers -For entirely different visualization approaches, subclass `ConversationVisualizer` and override the `on_event` method: +For complete control over visualization, subclass `ConversationVisualizerBase`: ```python -from openhands.sdk.conversation.visualizer import ConversationVisualizer -from openhands.sdk.event import ActionEvent, MessageEvent, ObservationEvent, AgentErrorEvent +from openhands.sdk.conversation import ConversationVisualizerBase +from openhands.sdk.event import ActionEvent, ObservationEvent, AgentErrorEvent, Event -class MinimalProgressVisualizer(ConversationVisualizer): - def __init__(self, **kwargs): - super().__init__(**kwargs) +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 - self.pending_action = False - def on_event(self, event): + 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}: Executing {tool_name}...", end="", flush=True) - self.pending_action = True + print(f"Step {self.step_count}: {tool_name}") elif isinstance(event, ObservationEvent): - if self.pending_action: - print(" āœ“") - self.pending_action = False + print(f" → Result received") elif isinstance(event, AgentErrorEvent): print(f"āŒ Error: {event.error}") -``` - -**When to use**: When you need completely different output format, custom state tracking, or integration with external systems. -### Approach 3: Custom Object with on_event Method - -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: - -```python -from rich.console import Console -from rich.panel import Panel -from openhands.sdk.event import Event - -class CustomVisualizer: - """Custom visualizer without subclassing ConversationVisualizer.""" - - def __init__(self): - self.event_count = 0 - self.console = Console() - - def on_event(self, event: Event) -> None: - """Handle any event - this is the only required method.""" - self.event_count += 1 - - # Use the event's built-in visualize property - content = event.visualize - if content.plain.strip(): - # Create custom panel styling - panel = Panel( - content, - title=f"[bold cyan]Event #{self.event_count}: {event.__class__.__name__}[/]", - border_style="cyan", - padding=(0, 1) - ) - self.console.print(panel) - -# Use it directly +# Use your custom visualizer conversation = Conversation( agent=agent, workspace=workspace, - visualizer=CustomVisualizer() # Pass your custom object + visualizer=MinimalVisualizer(name="Agent") ) ``` -**Key Requirements:** -- Must have an `on_event(self, event: Event) -> None` method -- Can be any Python object (class instance, function with state, etc.) -- No inheritance required +### 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)` -**When to use**: When you want maximum flexibility without inheriting from ConversationVisualizer, or when integrating with existing class hierarchies. +**`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 completely different output format, custom state tracking, or integration with external systems. ## Key Event Types @@ -457,44 +332,49 @@ def handle_error(self, event: AgentErrorEvent): ## Best Practices -### 1. Understanding ConversationVisualizer Subclassing +### 1. Understanding ConversationVisualizerBase Subclassing -When subclassing `ConversationVisualizer`, you inherit several useful features: +When subclassing `ConversationVisualizerBase`, you have access to: **Built-in Features:** -- Rich Console instance (`self._console`) for formatted output -- Highlighting patterns (`self._highlight_patterns`) for text styling -- Conversation stats integration (`self._conversation_stats`) for metrics -- Name prefixing (`self._name_for_visualization`) for multi-agent scenarios +- `self._name`: Optional agent name for identification (capitalized automatically) +- `self._state`: Conversation state (set after `initialize()` is called) +- `self.conversation_stats`: Property to access conversation statistics -**Key Methods to Override:** -- `on_event(self, event: Event)`: Main event handler (most common override) -- `_create_event_panel(self, event: Event)`: Custom panel creation -- `_apply_highlighting(self, text: Text)`: Custom text highlighting +**Key Methods:** +- `__init__(self, name: str | None = None)`: Initialize with optional name +- `initialize(self, state: ConversationStateProtocol)`: Called by Conversation to provide state +- `on_event(self, event: Event)`: Main event handler (must be implemented) **Initialization Pattern:** ```python -class MyVisualizer(ConversationVisualizer): - def __init__(self, custom_param: str = "default", **kwargs): - # Always call super().__init__ to get base functionality - super().__init__(**kwargs) +from openhands.sdk.conversation import ConversationVisualizerBase +from openhands.sdk.event import Event + +class MyVisualizer(ConversationVisualizerBase): + def __init__(self, name: str | None = None, custom_param: str = "default"): + # Always call super().__init__ with name parameter + super().__init__(name=name) # Add your custom state self.custom_param = custom_param self.event_count = 0 + + def initialize(self, state): + # Optional: Override to add custom initialization + super().initialize(state) # Sets self._state + print(f"Visualizer initialized for {self._name or 'unnamed agent'}") def on_event(self, event: Event) -> None: - # Your custom logic here + # Required: Implement your visualization logic self.event_count += 1 - # Option 1: Completely custom handling if isinstance(event, ActionEvent): print(f"Custom action handling: {event.tool_name}") - # Option 2: Use parent's panel creation with modifications - panel = self._create_event_panel(event) - if panel: - self._console.print(panel) + # Access conversation stats if needed + if self.conversation_stats: + print(f"Total cost so far: ${self.conversation_stats.accumulated_cost:.4f}") ``` ### 2. State Management @@ -520,57 +400,42 @@ def _handle_action_event(self, event: ActionEvent) -> None: # ... handle event ``` -### 3. Event Handler Registration Pattern -For cleaner, more maintainable code, avoid long if/elif chains in your `on_event` method. Instead, use a decorator-based handler registration pattern: +### 3. Efficient Event Handling +For cleaner code, dispatch events to specific handler methods: ```python -from typing import Dict, Callable, Type - -def handles(event_type: Type[Event]): - """Decorator to register a method as an event handler.""" - def decorator(func): - func._handles_event_type = event_type - return func - return decorator +from openhands.sdk.conversation import ConversationVisualizerBase +from openhands.sdk.event import Event, ActionEvent, ObservationEvent, MessageEvent -class EventHandlerMixin: - """Mixin that provides event handler registration via decorators.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._event_handlers: Dict[Type[Event], Callable[[Event], None]] = {} - self._register_handlers() - - def _register_handlers(self): - """Automatically discover and register event handlers.""" - for attr_name in dir(self): - attr = getattr(self, attr_name) - if hasattr(attr, '_handles_event_type'): - event_type = attr._handles_event_type - self._event_handlers[event_type] = attr - +class OrganizedVisualizer(ConversationVisualizerBase): def on_event(self, event: Event) -> None: - """Dispatch events to registered handlers.""" - event_type = type(event) - handler = self._event_handlers.get(event_type) - if handler: - handler(event) - -class MyVisualizer(EventHandlerMixin, ConversationVisualizer): - @handles(ActionEvent) - def _handle_action_event(self, event: ActionEvent) -> None: + """Dispatch to specific handlers.""" + if isinstance(event, ActionEvent): + self._handle_action(event) + elif isinstance(event, ObservationEvent): + self._handle_observation(event) + elif isinstance(event, MessageEvent): + self._handle_message(event) + + def _handle_action(self, event: ActionEvent) -> None: + """Handle action events.""" print(f"Action: {event.tool_name}") - @handles(MessageEvent) - def _handle_message_event(self, event: MessageEvent) -> None: - print(f"Message: {event.llm_message.role}") + def _handle_observation(self, event: ObservationEvent) -> None: + """Handle observation events.""" + print(f"Observation received") + + def _handle_message(self, event: MessageEvent) -> None: + """Handle message events.""" + if event.llm_message: + print(f"Message: {event.llm_message.role}") ``` This pattern provides: -- **Self-documenting code**: `@handles(ActionEvent)` clearly shows what each method does -- **Type safety**: The decorator enforces the event type relationship -- **Easy extensibility**: Add new event types without touching dispatch logic -- **No boilerplate**: No need to maintain separate dictionaries or long if/elif chains +- **Organized code**: Each event type has its own method +- **Type hints**: Better IDE support and type checking +- **Easy maintenance**: Add new event types without touching dispatch logic +- **Clear structure**: Easy to understand and test ### 4. Error Handling Always include error handling to prevent visualization issues from breaking conversations: @@ -600,35 +465,51 @@ For high-frequency events, consider optimizing your visualization by filtering e ## Using Your Custom Visualizer -### Direct Assignment (Recommended) - -Pass your visualizer instance directly to the `visualizer` parameter: +Pass your visualizer to the `visualizer` parameter: ```python +from openhands.sdk import Conversation + # Create your custom visualizer -custom_visualizer = MinimalProgressVisualizer() +custom_visualizer = MinimalVisualizer(name="Agent") -# Use it directly - clean and intuitive +# Use it in conversation conversation = Conversation( agent=agent, workspace="./workspace", - visualizer=custom_visualizer, # Direct assignment + visualizer=custom_visualizer, ) ``` ### Visualization Options -The `visualizer` parameter accepts three types: +The `visualizer` parameter accepts: + +- **Visualizer class** (uninstantiated): `DefaultConversationVisualizer` (default) +- **Visualizer instance**: `MinimalVisualizer(name="Agent")` +- **`None`**: Disable visualization entirely + +```python +# Default visualization (enabled by default) +conversation = Conversation(agent=agent, workspace=workspace) + +# Disable visualization +conversation = Conversation(agent=agent, workspace=workspace, visualizer=None) -- **Default behavior** (no parameter): Creates a default Rich panel visualizer automatically -- **`None`**: Disable visualization entirely -- **`ConversationVisualizer` instance**: Use your custom visualizer +# Use custom visualizer instance +conversation = Conversation(agent=agent, workspace=workspace, visualizer=MinimalVisualizer()) + +# Pass visualizer class (will be instantiated automatically) +conversation = Conversation(agent=agent, workspace=workspace, visualizer=MinimalVisualizer) +``` ### Combining with Additional Callbacks Custom visualizers work alongside other event callbacks: ```python +from openhands.sdk.event import ActionEvent + def metrics_callback(event): # Track metrics separately from visualization if isinstance(event, ActionEvent): @@ -638,13 +519,13 @@ conversation = Conversation( agent=agent, workspace="./workspace", visualizer=custom_visualizer, # Handle visualization - callbacks=[metrics_callback], # Handle other concerns + callbacks=[metrics_callback], # Handle other concerns ) ``` ## Built-in Visualizer Reference -For reference, you can view the complete implementation of the default visualizer: [ConversationVisualizer source code](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/visualizer.py) +The default visualizer (`DefaultConversationVisualizer`) provides rich-formatted output with panels. View the complete implementation: [DefaultConversationVisualizer source code](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/visualizer/default.py) ### Default Highlighting Patterns @@ -675,11 +556,12 @@ DEFAULT_HIGHLIGHT_REGEX = { ### Configuration Options ```python -ConversationVisualizer( - highlight_regex=custom_patterns, # Your highlighting rules - skip_user_messages=False, # Show user input (default: False) - name_for_visualization="MyAgent", # Prefix for panel titles - conversation_stats=stats_tracker, # Show token/cost metrics +from openhands.sdk.conversation import DefaultConversationVisualizer + +DefaultConversationVisualizer( + name="MyAgent", # Prefix for panel titles + highlight_regex=custom_patterns, # Your highlighting rules + skip_user_messages=False, # Show user input (default: False) ) ``` diff --git a/sdk/guides/convo-pause-and-resume.mdx b/sdk/guides/convo-pause-and-resume.mdx index 007144d9..a45975b6 100644 --- a/sdk/guides/convo-pause-and-resume.mdx +++ b/sdk/guides/convo-pause-and-resume.mdx @@ -23,8 +23,8 @@ from openhands.sdk import ( Conversation, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM @@ -42,7 +42,7 @@ llm = LLM( # Tools tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/convo-persistence.mdx b/sdk/guides/convo-persistence.mdx index 99d95811..da6ddd27 100644 --- a/sdk/guides/convo-persistence.mdx +++ b/sdk/guides/convo-persistence.mdx @@ -24,8 +24,8 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -45,7 +45,7 @@ llm = LLM( # Tools cwd = os.getcwd() tools = [ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/convo-send-message-while-running.mdx b/sdk/guides/convo-send-message-while-running.mdx index 1cc7cc6c..78953438 100644 --- a/sdk/guides/convo-send-message-while-running.mdx +++ b/sdk/guides/convo-send-message-while-running.mdx @@ -63,8 +63,8 @@ from openhands.sdk import ( Conversation, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM @@ -83,7 +83,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/custom-tools.mdx b/sdk/guides/custom-tools.mdx index 58efa1b6..c9ae37c5 100644 --- a/sdk/guides/custom-tools.mdx +++ b/sdk/guides/custom-tools.mdx @@ -66,12 +66,12 @@ from openhands.sdk.tool import ( ToolExecutor, register_tool, ) -from openhands.tools.execute_bash import ( +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import ( BashExecutor, - BashTool, ExecuteBashAction, + TerminalTool, ) -from openhands.tools.file_editor import FileEditorTool logger = get_logger(__name__) @@ -210,11 +210,11 @@ cwd = os.getcwd() def _make_bash_and_grep_tools(conv_state) -> list[ToolDefinition]: - """Create execute_bash and custom grep tools sharing one executor.""" + """Create terminal and custom grep tools sharing one executor.""" bash_executor = BashExecutor(working_dir=conv_state.workspace.working_dir) - # bash_tool = execute_bash_tool.set_executor(executor=bash_executor) - bash_tool = BashTool.create(conv_state, executor=bash_executor)[0] + # bash_tool = terminal_tool.set_executor(executor=bash_executor) + bash_tool = TerminalTool.create(conv_state, executor=bash_executor)[0] # Use the GrepTool.create() method with shared bash_executor grep_tool = GrepTool.create(conv_state, bash_executor=bash_executor)[0] diff --git a/sdk/guides/hello-world.mdx b/sdk/guides/hello-world.mdx index ca1ef62c..5aa8485a 100644 --- a/sdk/guides/hello-world.mdx +++ b/sdk/guides/hello-world.mdx @@ -13,9 +13,9 @@ This is the most basic example showing how to set up and run an OpenHands agent: import os from openhands.sdk import LLM, Agent, Conversation, Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool llm = LLM( @@ -26,7 +26,7 @@ llm = LLM( agent = Agent( llm=llm, tools=[ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), Tool(name=TaskTrackerTool.name), ], diff --git a/sdk/guides/llm-image-input.mdx b/sdk/guides/llm-image-input.mdx index ddd761d1..3c896f21 100644 --- a/sdk/guides/llm-image-input.mdx +++ b/sdk/guides/llm-image-input.mdx @@ -32,9 +32,9 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool.spec import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -58,7 +58,7 @@ agent = Agent( llm=llm, tools=[ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), Tool(name=TaskTrackerTool.name), diff --git a/sdk/guides/llm-reasoning.mdx b/sdk/guides/llm-reasoning.mdx index 2273ed82..88482524 100644 --- a/sdk/guides/llm-reasoning.mdx +++ b/sdk/guides/llm-reasoning.mdx @@ -33,7 +33,7 @@ from openhands.sdk import ( ThinkingBlock, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool +from openhands.tools.terminal import TerminalTool # Configure LLM for Anthropic Claude with extended thinking @@ -50,7 +50,7 @@ llm = LLM( ) # Setup agent with bash tool -agent = Agent(llm=llm, tools=[Tool(name=BashTool.name)]) +agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name)]) # Callback to display thinking blocks diff --git a/sdk/guides/llm-registry.mdx b/sdk/guides/llm-registry.mdx index 2538bf0b..70b002b7 100644 --- a/sdk/guides/llm-registry.mdx +++ b/sdk/guides/llm-registry.mdx @@ -26,7 +26,7 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -54,7 +54,7 @@ llm = llm_registry.get("agent") # Tools cwd = os.getcwd() -tools = [Tool(name=BashTool.name)] +tools = [Tool(name=TerminalTool.name)] # Agent agent = Agent(llm=llm, tools=tools) diff --git a/sdk/guides/mcp.mdx b/sdk/guides/mcp.mdx index f5c2a35f..32b5c1af 100644 --- a/sdk/guides/mcp.mdx +++ b/sdk/guides/mcp.mdx @@ -29,8 +29,8 @@ from openhands.sdk import ( ) from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -49,7 +49,7 @@ llm = LLM( cwd = os.getcwd() tools = [ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ] @@ -161,8 +161,8 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -182,7 +182,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/metrics.mdx b/sdk/guides/metrics.mdx index 789270b0..6d9393a6 100644 --- a/sdk/guides/metrics.mdx +++ b/sdk/guides/metrics.mdx @@ -31,8 +31,8 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -51,7 +51,7 @@ llm = LLM( cwd = os.getcwd() tools = [ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ] @@ -159,7 +159,7 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -187,7 +187,7 @@ llm = llm_registry.get("agent") # Tools cwd = os.getcwd() -tools = [Tool(name=BashTool.name)] +tools = [Tool(name=TerminalTool.name)] # Agent agent = Agent(llm=llm, tools=tools) @@ -276,7 +276,7 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.tool.spec import Tool -from openhands.tools.execute_bash import BashTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -310,7 +310,7 @@ agent = Agent( llm=llm, tools=[ Tool( - name=BashTool.name, + name=TerminalTool.name, ), ], condenser=condenser, diff --git a/sdk/guides/secrets.mdx b/sdk/guides/secrets.mdx index 6d33392e..0a1b413e 100644 --- a/sdk/guides/secrets.mdx +++ b/sdk/guides/secrets.mdx @@ -21,8 +21,8 @@ from openhands.sdk import ( ) from openhands.sdk.conversation.secret_source import SecretSource from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM @@ -39,7 +39,7 @@ llm = LLM( # Tools tools = [ - Tool(name=BashTool.name), + Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/security.mdx b/sdk/guides/security.mdx index c87aaf77..54b68764 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -249,8 +249,8 @@ from openhands.sdk.conversation.state import ( from openhands.sdk.security.confirmation_policy import ConfirmRisky from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Clean ^C exit: no stack trace noise @@ -337,7 +337,7 @@ llm = LLM( # Tools tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), ] diff --git a/sdk/guides/skill.mdx b/sdk/guides/skill.mdx index 1d301c29..dcfae816 100644 --- a/sdk/guides/skill.mdx +++ b/sdk/guides/skill.mdx @@ -28,8 +28,8 @@ from openhands.sdk.context import ( Skill, ) from openhands.sdk.tool import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -50,7 +50,7 @@ llm = LLM( cwd = os.getcwd() tools = [ Tool( - name=BashTool.name, + name=TerminalTool.name, ), Tool(name=FileEditorTool.name), ] From 7ed212a7287bf42d26fa4888d417fff8d4455c75 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Thu, 6 Nov 2025 23:52:31 +0800 Subject: [PATCH 13/13] Update convo-custom-visualizer.mdx --- sdk/guides/convo-custom-visualizer.mdx | 352 +------------------------ 1 file changed, 2 insertions(+), 350 deletions(-) diff --git a/sdk/guides/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx index 3fe0a305..655654e9 100644 --- a/sdk/guides/convo-custom-visualizer.mdx +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -215,361 +215,13 @@ conversation = Conversation( - Implement your visualization logic here - Access conversation stats via `self.conversation_stats` property -**When to use**: When you need completely different output format, custom state tracking, or integration with external systems. - -## Key Event Types - -Understanding the event system is crucial for effective custom visualizers. Here's a comprehensive overview of all event types handled by the default visualizer: - -| Event Type | Description | Key Properties | When It Occurs | -|------------|-------------|----------------|----------------| -| `SystemPromptEvent` | System-level prompts and instructions | `content`, `source` | Agent initialization, system messages | -| `ActionEvent` | Agent actions and tool calls | `action`, `tool_name`, `llm_response_id` | When agent decides to take an action | -| `ObservationEvent` | Results from executed actions | `observation`, `source` | After action execution completes | -| `MessageEvent` | LLM messages (user/assistant) | `llm_message` (role, content) | User input, agent responses | -| `AgentErrorEvent` | Error conditions and failures | `error`, `source` | When agent encounters errors | -| `PauseEvent` | User-initiated pauses | `source` | When user pauses conversation | -| `Condensation` | Memory condensation events | `content`, `source` | During conversation memory management | - -### Detailed Event Descriptions - -### ActionEvent -Fired when the agent decides to use a tool or take an action. - -**Key Properties:** -- `thought`: Agent's reasoning before taking action (list of TextContent) -- `action`: The actual tool action (None if non-executable) -- `tool_name`: Name of the tool being called -- `tool_call_id`: Unique identifier for the tool call -- `security_risk`: LLM's assessment of action safety -- `reasoning_content`: Intermediate reasoning from reasoning models - -**Default Rendering:** Blue panel titled "Agent Action" showing reasoning, thought process, and action details. - -```python -def handle_action(self, event: ActionEvent): - # Access thought process - thought_text = " ".join([t.text for t in event.thought]) - print(f"šŸ’­ Thought: {thought_text}") - - # Check if action is executable - if event.action: - print(f"šŸ”§ Tool: {event.tool_name}") - print(f"⚔ Action: {event.action}") - else: - print(f"āš ļø Non-executable call: {event.tool_call.name}") -``` - -### ObservationEvent -Contains the result of an executed action. - -**Key Properties:** -- `observation`: The tool execution result (varies by tool) -- `tool_name`: Name of the tool that was executed -- `tool_call_id`: ID linking back to the original action -- `action_id`: ID of the action this observation responds to - -**Default Rendering:** Yellow panel titled "Observation" showing tool name and execution results. - -```python -def handle_observation(self, event: ObservationEvent): - print(f"šŸ”§ Tool: {event.tool_name}") - print(f"šŸ”— Action ID: {event.action_id}") - - # Access the observation result - obs = event.observation - if hasattr(obs, 'error') and obs.error: - print(f"āŒ Error: {obs.error}") - elif hasattr(obs, 'content'): - print(f"šŸ“„ Content: {obs.content[:100]}...") -``` - -### MessageEvent -Represents messages between user and agent. - -**Key Properties:** -- `llm_message`: The complete LLM message (role, content, tool_calls) -- `source`: Whether from "user" or "agent" -- `activated_skills`: List of skills activated for this message -- `extended_content`: Additional content added by agent context - -**Default Rendering:** Gold panel for user messages, blue panel for agent messages, with role-specific titles. - -```python -def handle_message(self, event: MessageEvent): - if event.llm_message: - role = event.llm_message.role - content = event.llm_message.content - - if role == "user": - print(f"šŸ‘¤ User: {content[0].text if content else ''}") - elif role == "assistant": - print(f"šŸ¤– Agent: {content[0].text if content else ''}") - - # Check for tool calls - if event.llm_message.tool_calls: - print(f"šŸ”§ Tool calls: {len(event.llm_message.tool_calls)}") -``` - -### AgentErrorEvent -Error conditions encountered by the agent. - -**Key Properties:** -- `error`: The error message from the agent/scaffold -- `tool_name`: Tool that caused the error (if applicable) -- `tool_call_id`: ID of the failed tool call (if applicable) - -**Default Rendering:** Red panel titled "Agent Error" displaying error details. - -```python -def handle_error(self, event: AgentErrorEvent): - print(f"🚨 Error: {event.error}") - if event.tool_name: - print(f"šŸ”§ Failed tool: {event.tool_name}") - if event.tool_call_id: - print(f"šŸ”— Call ID: {event.tool_call_id}") -``` - -## Best Practices - -### 1. Understanding ConversationVisualizerBase Subclassing - -When subclassing `ConversationVisualizerBase`, you have access to: - -**Built-in Features:** -- `self._name`: Optional agent name for identification (capitalized automatically) -- `self._state`: Conversation state (set after `initialize()` is called) -- `self.conversation_stats`: Property to access conversation statistics - -**Key Methods:** -- `__init__(self, name: str | None = None)`: Initialize with optional name -- `initialize(self, state: ConversationStateProtocol)`: Called by Conversation to provide state -- `on_event(self, event: Event)`: Main event handler (must be implemented) - -**Initialization Pattern:** -```python -from openhands.sdk.conversation import ConversationVisualizerBase -from openhands.sdk.event import Event - -class MyVisualizer(ConversationVisualizerBase): - def __init__(self, name: str | None = None, custom_param: str = "default"): - # Always call super().__init__ with name parameter - super().__init__(name=name) - - # Add your custom state - self.custom_param = custom_param - self.event_count = 0 - - def initialize(self, state): - # Optional: Override to add custom initialization - super().initialize(state) # Sets self._state - print(f"Visualizer initialized for {self._name or 'unnamed agent'}") - - def on_event(self, event: Event) -> None: - # Required: Implement your visualization logic - self.event_count += 1 - - if isinstance(event, ActionEvent): - print(f"Custom action handling: {event.tool_name}") - - # Access conversation stats if needed - if self.conversation_stats: - print(f"Total cost so far: ${self.conversation_stats.accumulated_cost:.4f}") -``` - -### 2. State Management -Track conversation state to provide meaningful progress indicators. The example shows tracking step counts, pending actions, and LLM response IDs: - -```python -def __init__(self, **kwargs): - super().__init__(**kwargs) - self._step_count = 0 - self._pending_action = False - self._seen_llm_response_ids: set[str] = set() - -def _handle_action_event(self, event: ActionEvent) -> None: - # Track new LLM calls by monitoring llm_response_id - if ( - event.llm_response_id - and event.llm_response_id not in self._seen_llm_response_ids - ): - self._seen_llm_response_ids.add(event.llm_response_id) - print("šŸ¤– LLM call completed", flush=True) - - self._step_count += 1 - # ... handle event -``` - -### 3. Efficient Event Handling -For cleaner code, dispatch events to specific handler methods: - -```python -from openhands.sdk.conversation import ConversationVisualizerBase -from openhands.sdk.event import Event, ActionEvent, ObservationEvent, MessageEvent - -class OrganizedVisualizer(ConversationVisualizerBase): - def on_event(self, event: Event) -> None: - """Dispatch to specific handlers.""" - if isinstance(event, ActionEvent): - self._handle_action(event) - elif isinstance(event, ObservationEvent): - self._handle_observation(event) - elif isinstance(event, MessageEvent): - self._handle_message(event) - - def _handle_action(self, event: ActionEvent) -> None: - """Handle action events.""" - print(f"Action: {event.tool_name}") - - def _handle_observation(self, event: ObservationEvent) -> None: - """Handle observation events.""" - print(f"Observation received") - - def _handle_message(self, event: MessageEvent) -> None: - """Handle message events.""" - if event.llm_message: - print(f"Message: {event.llm_message.role}") -``` - -This pattern provides: -- **Organized code**: Each event type has its own method -- **Type hints**: Better IDE support and type checking -- **Easy maintenance**: Add new event types without touching dispatch logic -- **Clear structure**: Easy to understand and test - -### 4. Error Handling -Always include error handling to prevent visualization issues from breaking conversations: - -```python -import logging - -def on_event(self, event): - try: - # Your visualization logic - if isinstance(event, ActionEvent): - self._handle_action_event(event) - elif isinstance(event, ObservationEvent): - self._handle_observation_event(event) - # ... other event types - except Exception as e: - # Fallback to prevent breaking the conversation - print(f"āš ļø Visualization error: {e}", flush=True) - # Optionally log for debugging - logging.warning(f"Visualizer failed for {event.__class__.__name__}: {e}") -``` - -This pattern ensures that visualization problems never interrupt the agent's work. - -### 5. Performance Considerations -For high-frequency events, consider optimizing your visualization by filtering events or using efficient output methods like `flush=True` for immediate display. - -## Using Your Custom Visualizer - -Pass your visualizer to the `visualizer` parameter: - -```python -from openhands.sdk import Conversation - -# Create your custom visualizer -custom_visualizer = MinimalVisualizer(name="Agent") - -# Use it in conversation -conversation = Conversation( - agent=agent, - workspace="./workspace", - visualizer=custom_visualizer, -) -``` - -### Visualization Options - -The `visualizer` parameter accepts: - -- **Visualizer class** (uninstantiated): `DefaultConversationVisualizer` (default) -- **Visualizer instance**: `MinimalVisualizer(name="Agent")` -- **`None`**: Disable visualization entirely - -```python -# Default visualization (enabled by default) -conversation = Conversation(agent=agent, workspace=workspace) - -# Disable visualization -conversation = Conversation(agent=agent, workspace=workspace, visualizer=None) - -# Use custom visualizer instance -conversation = Conversation(agent=agent, workspace=workspace, visualizer=MinimalVisualizer()) - -# Pass visualizer class (will be instantiated automatically) -conversation = Conversation(agent=agent, workspace=workspace, visualizer=MinimalVisualizer) -``` - -### Combining with Additional Callbacks - -Custom visualizers work alongside other event callbacks: - -```python -from openhands.sdk.event import ActionEvent - -def metrics_callback(event): - # Track metrics separately from visualization - if isinstance(event, ActionEvent): - metrics.increment("actions_taken") - -conversation = Conversation( - agent=agent, - workspace="./workspace", - visualizer=custom_visualizer, # Handle visualization - callbacks=[metrics_callback], # Handle other concerns -) -``` - -## Built-in Visualizer Reference - -The default visualizer (`DefaultConversationVisualizer`) provides rich-formatted output with panels. View the complete implementation: [DefaultConversationVisualizer source code](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/visualizer/default.py) - -### Default Highlighting Patterns - -The built-in visualizer includes these default patterns: - -```python -DEFAULT_HIGHLIGHT_REGEX = { - r"^Reasoning:": "bold bright_black", - r"^Thought:": "bold bright_black", - r"^Action:": "bold blue", - r"^Arguments:": "bold blue", - r"^Tool:": "bold yellow", - r"^Result:": "bold yellow", - r"^Rejection Reason:": "bold red", - r"\*\*(.*?)\*\*": "bold", # Markdown bold - r"\*(.*?)\*": "italic", # Markdown italic -} -``` - -### Available Colors and Styles - -**Colors**: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `bright_black`, `bright_red`, etc. - -**Styles**: `bold`, `dim`, `italic`, `underline` - -**Combinations**: `"bold cyan"`, `"underline red"`, `"bold italic green"` - -### Configuration Options - -```python -from openhands.sdk.conversation import DefaultConversationVisualizer - -DefaultConversationVisualizer( - name="MyAgent", # Prefix for panel titles - highlight_regex=custom_patterns, # Your highlighting rules - skip_user_messages=False, # Show user input (default: False) -) -``` +**When to use**: When you need a completely different output format, custom state tracking, or integration with external systems. ## Next Steps Now that you understand custom visualizers, explore these related topics: -- **[Async Conversations](/sdk/guides/convo-async)** - Learn about custom event callbacks and async processing +- **[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