Skip to content

Commit 4c276bd

Browse files
xingyaowwopenhands-agentjpshackelfordsimonrosenbergenyst
authored
Improve visualization API: allow passing ConversationVisualizer directly (#1025)
Co-authored-by: openhands <[email protected]> Co-authored-by: John-Mason P. Shackelford <[email protected]> Co-authored-by: simonrosenberg <[email protected]> Co-authored-by: Engel Nyst <[email protected]> Co-authored-by: Rohit Malhotra <[email protected]>
1 parent 5e93f74 commit 4c276bd

29 files changed

+526
-215
lines changed

.github/scripts/check_documented_examples.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,30 @@ def main() -> None:
165165
print(f" - {example}")
166166
print("\n⚠️ Please add documentation for these examples in the docs repo.")
167167
print("=" * 60)
168+
print("\n📚 How to Document Examples:")
169+
print("=" * 60)
170+
print("1. Clone the docs repository:")
171+
print(" git clone https://github.com/OpenHands/docs.git")
172+
print()
173+
print("2. Create a new .mdx file in sdk/guides/ directory")
174+
print(" (e.g., sdk/guides/my-feature.mdx)")
175+
print()
176+
print("3. Add the example code block with this format:")
177+
print(' ```python icon="python" expandable examples/path/to/file.py')
178+
print(" <code will be auto-synced>")
179+
print(" ```")
180+
print()
181+
print("4. See the format documentation at:")
182+
print(
183+
" https://github.com/OpenHands/docs/blob/main/.github/scripts/README.md"
184+
)
185+
print()
186+
print("5. Example documentation files can be found in:")
187+
print(" https://github.com/OpenHands/docs/tree/main/sdk/guides")
188+
print()
189+
print("6. After creating the PR in docs repo, reference it in your")
190+
print(" agent-sdk PR description.")
191+
print("=" * 60)
168192
sys.exit(1)
169193
else:
170194
print("✅ All examples are documented!")

.github/workflows/run-examples.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ jobs:
8080
"examples/01_standalone_sdk/22_anthropic_thinking.py"
8181
"examples/01_standalone_sdk/23_responses_reasoning.py"
8282
"examples/01_standalone_sdk/24_planning_agent_workflow.py"
83+
"examples/01_standalone_sdk/25_agent_delegation.py"
84+
"examples/01_standalone_sdk/26_custom_visualizer.py"
8385
"examples/02_remote_agent_server/01_convo_with_local_agent_server.py"
8486
"examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py"
8587
"examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py"

examples/01_standalone_sdk/25_agent_delegation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Tool,
1919
get_logger,
2020
)
21+
from openhands.sdk.conversation import DefaultConversationVisualizer
2122
from openhands.sdk.tool import register_tool
2223
from openhands.tools.delegate import DelegateTool
2324
from openhands.tools.preset.default import get_default_tools
@@ -50,7 +51,7 @@
5051
conversation = Conversation(
5152
agent=main_agent,
5253
workspace=cwd,
53-
name_for_visualization="Delegator",
54+
visualizer=DefaultConversationVisualizer(name="Delegator"),
5455
)
5556

5657
task_message = (
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Custom Visualizer Example
2+
3+
This example demonstrates how to create and use a custom visualizer by subclassing
4+
ConversationVisualizer. This approach provides:
5+
- Clean, testable code with class-based state management
6+
- Direct configuration (just pass the visualizer instance to visualizer parameter)
7+
- Reusable visualizer that can be shared across conversations
8+
9+
This demonstrates how you can pass a ConversationVisualizer instance directly
10+
to the visualizer parameter for clean, reusable visualization logic.
11+
"""
12+
13+
import logging
14+
import os
15+
16+
from pydantic import SecretStr
17+
18+
from openhands.sdk import LLM, Conversation
19+
from openhands.sdk.conversation.visualizer import ConversationVisualizerBase
20+
from openhands.sdk.event import (
21+
Event,
22+
)
23+
from openhands.tools.preset.default import get_default_agent
24+
25+
26+
class MinimalVisualizer(ConversationVisualizerBase):
27+
"""A minimal visualizer that print the raw events as they occur."""
28+
29+
def __init__(self, name: str | None = None):
30+
"""Initialize the minimal progress visualizer.
31+
32+
Args:
33+
name: Optional name to identify the agent/conversation.
34+
"""
35+
# Initialize parent - state will be set later via initialize()
36+
super().__init__(name=name)
37+
38+
def on_event(self, event: Event) -> None:
39+
"""Handle events for minimal progress visualization."""
40+
print(f"\n\n[EVENT] {type(event).__name__}: {event.model_dump_json()[:200]}...")
41+
42+
43+
api_key = os.getenv("LLM_API_KEY")
44+
assert api_key is not None, "LLM_API_KEY environment variable is not set."
45+
model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929")
46+
base_url = os.getenv("LLM_BASE_URL")
47+
llm = LLM(
48+
model=model,
49+
api_key=SecretStr(api_key),
50+
base_url=base_url,
51+
usage_id="agent",
52+
)
53+
agent = get_default_agent(llm=llm, cli_mode=True)
54+
55+
# ============================================================================
56+
# Configure Visualization
57+
# ============================================================================
58+
# Set logging level to reduce verbosity
59+
logging.getLogger().setLevel(logging.WARNING)
60+
61+
# Start a conversation with custom visualizer
62+
cwd = os.getcwd()
63+
conversation = Conversation(
64+
agent=agent,
65+
workspace=cwd,
66+
visualizer=MinimalVisualizer(),
67+
)
68+
69+
# Send a message and let the agent run
70+
print("Sending task to agent...")
71+
conversation.send_message("Write 3 facts about the current project into FACTS.txt.")
72+
conversation.run()
73+
print("Task completed!")
74+
75+
# Report cost
76+
cost = llm.metrics.accumulated_cost
77+
print(f"EXAMPLE_COST: ${cost:.4f}")

examples/02_remote_agent_server/01_convo_with_local_agent_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ def event_callback(event):
169169
agent=agent,
170170
workspace=workspace,
171171
callbacks=[event_callback],
172-
visualize=True,
173172
)
174173
assert isinstance(conversation, RemoteConversation)
175174

examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ def event_callback(event) -> None:
7474
agent=agent,
7575
workspace=workspace,
7676
callbacks=[event_callback],
77-
visualize=True,
7877
)
7978
assert isinstance(conversation, RemoteConversation)
8079

examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ def event_callback(event) -> None:
6161
agent=agent,
6262
workspace=workspace,
6363
callbacks=[event_callback],
64-
visualize=True,
6564
)
6665
assert isinstance(conversation, RemoteConversation)
6766

examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def event_callback(event) -> None:
6363
logger.info(f"Command completed: {result.exit_code}, {result.stdout}")
6464

6565
conversation = Conversation(
66-
agent=agent, workspace=workspace, callbacks=[event_callback], visualize=True
66+
agent=agent, workspace=workspace, callbacks=[event_callback]
6767
)
6868
assert isinstance(conversation, RemoteConversation)
6969

examples/02_remote_agent_server/04_vscode_with_docker_sandboxed_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def event_callback(event) -> None:
6464
agent=agent,
6565
workspace=workspace,
6666
callbacks=[event_callback],
67-
visualize=True,
6867
)
6968
assert isinstance(conversation, RemoteConversation)
7069

openhands-agent-server/openhands/agent_server/event_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ async def start(self):
251251
],
252252
max_iteration_per_run=self.stored.max_iterations,
253253
stuck_detection=self.stored.stuck_detection,
254-
visualize=False,
254+
visualizer=None,
255255
secrets=self.stored.secrets,
256256
)
257257

0 commit comments

Comments
 (0)