diff --git a/docs.json b/docs.json index 0f3bd601..b5543491 100644 --- a/docs.json +++ b/docs.json @@ -203,6 +203,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" ] }, 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-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/convo-custom-visualizer.mdx b/sdk/guides/convo-custom-visualizer.mdx new file mode 100644 index 00000000..655654e9 --- /dev/null +++ b/sdk/guides/convo-custom-visualizer.mdx @@ -0,0 +1,227 @@ +--- +title: Custom Visualizer +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) + + +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`. + +## Basic Example + +```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 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 + +This demonstrates how you can pass a ConversationVisualizer instance directly +to the visualizer parameter for clean, reusable visualization logic. +""" + +import logging +import os + +from pydantic import SecretStr + +from openhands.sdk import LLM, Conversation +from openhands.sdk.conversation.visualizer import ConversationVisualizerBase +from openhands.sdk.event import ( + Event, +) +from openhands.tools.preset.default import get_default_agent + + +class MinimalVisualizer(ConversationVisualizerBase): + """A minimal visualizer that print the raw events as they occur.""" + + def __init__(self, name: str | None = None): + """Initialize the minimal progress visualizer. + + Args: + 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 - state will be set later via initialize() + super().__init__(name=name) + + 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) + +# ============================================================================ +# 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 +export LLM_API_KEY="your-api-key" +cd agent-sdk +uv run python examples/01_standalone_sdk/26_custom_visualizer.py +``` + +## 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}") + +conversation = Conversation(agent=agent, workspace=workspace, visualizer=MyVisualizer()) +``` + +## Customizing the Default Visualizer + +`DefaultConversationVisualizer` uses Rich panels and supports customization through configuration: + +```python +from openhands.sdk.conversation import DefaultConversationVisualizer + +# Configure highlighting patterns using regex +custom_visualizer = DefaultConversationVisualizer( + name="MyAgent", # Prefix panel titles with agent name + highlight_regex={ + r"^Reasoning:": "bold cyan", # Lines starting with "Reasoning:" + r"^Thought:": "bold green", # Lines starting with "Thought:" + r"^Action:": "bold yellow", # Lines starting with "Action:" + r"\[ERROR\]": "bold red", # Error markers anywhere + r"\*\*(.*?)\*\*": "bold", # Markdown bold **text** + }, + skip_user_messages=False, # Show user messages +) + +conversation = Conversation( + agent=agent, + workspace=workspace, + visualizer=custom_visualizer +) +``` + +**When to use**: Perfect for customizing colors and highlighting without changing the panel-based layout. + +## Creating Custom Visualizers + +For complete control over visualization, subclass `ConversationVisualizerBase`: + +```python +from openhands.sdk.conversation import ConversationVisualizerBase +from openhands.sdk.event import ActionEvent, ObservationEvent, AgentErrorEvent, Event + +class MinimalVisualizer(ConversationVisualizerBase): + """A minimal visualizer that prints raw event information.""" + + def __init__(self, name: str | None = None): + super().__init__(name=name) + self.step_count = 0 + + def on_event(self, event: Event) -> None: + """Handle each event.""" + if isinstance(event, ActionEvent): + self.step_count += 1 + tool_name = event.tool_name or "unknown" + print(f"Step {self.step_count}: {tool_name}") + + elif isinstance(event, ObservationEvent): + print(f" → Result received") + + elif isinstance(event, AgentErrorEvent): + print(f"❌ Error: {event.error}") + +# Use your custom visualizer +conversation = Conversation( + agent=agent, + workspace=workspace, + visualizer=MinimalVisualizer(name="Agent") +) +``` + +### Key Methods + +**`__init__(self, name: str | None = None)`** +- Initialize your visualizer with optional configuration +- `name` parameter is available from the base class for agent identification +- Call `super().__init__(name=name)` to initialize the base class + +**`initialize(self, state: ConversationStateProtocol)`** +- Called automatically by `Conversation` after state is created +- Provides access to conversation state and statistics via `self._state` +- Override if you need custom initialization, but call `super().initialize(state)` + +**`on_event(self, event: Event)`** *(required)* +- Called for each conversation event +- Implement your visualization logic here +- Access conversation stats via `self.conversation_stats` property + +**When to use**: When you need a completely different output format, custom state tracking, or integration with external systems. + +## Next Steps + +Now that you understand custom visualizers, explore these related topics: + +- **[Events](/sdk/arch/events)** - Learn more about different event types +- **[Conversation Metrics](/sdk/guides/metrics)** - Track LLM usage, costs, and performance data +- **[Send Messages While Running](/sdk/guides/convo-send-message-while-running)** - Interactive conversations with real-time updates +- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control agent execution flow with custom logic