mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
CLI: custom visualizer (#11677)
This commit is contained in:
parent
27c8c330f4
commit
e0d26c1f4e
@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from openhands.sdk.conversation import visualizer
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace
|
||||
@ -9,7 +10,7 @@ from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
)
|
||||
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
||||
|
||||
from openhands_cli.tui.visualizer import CLIVisualizer
|
||||
|
||||
# register tools
|
||||
from openhands.tools.terminal import TerminalTool
|
||||
@ -86,6 +87,7 @@ def setup_conversation(
|
||||
# Conversation will add /<conversation_id> to this path
|
||||
persistence_dir=CONVERSATIONS_DIR,
|
||||
conversation_id=conversation_id,
|
||||
visualizer=CLIVisualizer
|
||||
)
|
||||
|
||||
if include_security_analyzer:
|
||||
|
||||
312
openhands-cli/openhands_cli/tui/visualizer.py
Normal file
312
openhands-cli/openhands_cli/tui/visualizer.py
Normal file
@ -0,0 +1,312 @@
|
||||
import re
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from openhands.sdk.conversation.visualizer.base import (
|
||||
ConversationVisualizerBase,
|
||||
)
|
||||
from openhands.sdk.event import (
|
||||
ActionEvent,
|
||||
AgentErrorEvent,
|
||||
MessageEvent,
|
||||
ObservationEvent,
|
||||
PauseEvent,
|
||||
SystemPromptEvent,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.sdk.event.base import Event
|
||||
from openhands.sdk.event.condenser import Condensation
|
||||
|
||||
|
||||
# These are external inputs
|
||||
_OBSERVATION_COLOR = "yellow"
|
||||
_MESSAGE_USER_COLOR = "gold3"
|
||||
_PAUSE_COLOR = "bright_yellow"
|
||||
# These are internal system stuff
|
||||
_SYSTEM_COLOR = "magenta"
|
||||
_THOUGHT_COLOR = "bright_black"
|
||||
_ERROR_COLOR = "red"
|
||||
# These are agent actions
|
||||
_ACTION_COLOR = "blue"
|
||||
_MESSAGE_ASSISTANT_COLOR = _ACTION_COLOR
|
||||
|
||||
DEFAULT_HIGHLIGHT_REGEX = {
|
||||
r"^Reasoning:": f"bold {_THOUGHT_COLOR}",
|
||||
r"^Thought:": f"bold {_THOUGHT_COLOR}",
|
||||
r"^Action:": f"bold {_ACTION_COLOR}",
|
||||
r"^Arguments:": f"bold {_ACTION_COLOR}",
|
||||
r"^Tool:": f"bold {_OBSERVATION_COLOR}",
|
||||
r"^Result:": f"bold {_OBSERVATION_COLOR}",
|
||||
r"^Rejection Reason:": f"bold {_ERROR_COLOR}",
|
||||
# Markdown-style
|
||||
r"\*\*(.*?)\*\*": "bold",
|
||||
r"\*(.*?)\*": "italic",
|
||||
}
|
||||
|
||||
_PANEL_PADDING = (1, 1)
|
||||
|
||||
|
||||
class CLIVisualizer(ConversationVisualizerBase):
|
||||
"""Handles visualization of conversation events with Rich formatting.
|
||||
|
||||
Provides Rich-formatted output with panels and complete content display.
|
||||
"""
|
||||
|
||||
_console: Console
|
||||
_skip_user_messages: bool
|
||||
_highlight_patterns: dict[str, str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
highlight_regex: dict[str, str] | None = DEFAULT_HIGHLIGHT_REGEX,
|
||||
skip_user_messages: bool = False,
|
||||
):
|
||||
"""Initialize the visualizer.
|
||||
|
||||
Args:
|
||||
name: Optional name to prefix in panel titles to identify
|
||||
which agent/conversation is speaking.
|
||||
highlight_regex: Dictionary mapping regex patterns to Rich color styles
|
||||
for highlighting keywords in the visualizer.
|
||||
For example: {"Reasoning:": "bold blue",
|
||||
"Thought:": "bold green"}
|
||||
skip_user_messages: If True, skip displaying user messages. Useful for
|
||||
scenarios where user input is not relevant to show.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
)
|
||||
self._console = Console()
|
||||
self._skip_user_messages = skip_user_messages
|
||||
self._highlight_patterns = highlight_regex or {}
|
||||
|
||||
def on_event(self, event: Event) -> None:
|
||||
"""Main event handler that displays events with Rich formatting."""
|
||||
panel = self._create_event_panel(event)
|
||||
if panel:
|
||||
self._console.print(panel)
|
||||
self._console.print() # Add spacing between events
|
||||
|
||||
def _apply_highlighting(self, text: Text) -> Text:
|
||||
"""Apply regex-based highlighting to text content.
|
||||
|
||||
Args:
|
||||
text: The Rich Text object to highlight
|
||||
|
||||
Returns:
|
||||
A new Text object with highlighting applied
|
||||
"""
|
||||
if not self._highlight_patterns:
|
||||
return text
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
highlighted = text.copy()
|
||||
|
||||
# Apply each pattern using Rich's built-in highlight_regex method
|
||||
for pattern, style in self._highlight_patterns.items():
|
||||
pattern_compiled = re.compile(pattern, re.MULTILINE)
|
||||
highlighted.highlight_regex(pattern_compiled, style)
|
||||
|
||||
return highlighted
|
||||
|
||||
def _create_event_panel(self, event: Event) -> Panel | None:
|
||||
"""Create a Rich Panel for the event with appropriate styling."""
|
||||
# Use the event's visualize property for content
|
||||
content = event.visualize
|
||||
|
||||
if not content.plain.strip():
|
||||
return None
|
||||
|
||||
# Apply highlighting if configured
|
||||
if self._highlight_patterns:
|
||||
content = self._apply_highlighting(content)
|
||||
|
||||
# Don't emit system prompt in CLI
|
||||
if isinstance(event, SystemPromptEvent):
|
||||
title = f"[bold {_SYSTEM_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"System Prompt[/bold {_SYSTEM_COLOR}]"
|
||||
return None
|
||||
elif isinstance(event, ActionEvent):
|
||||
# Check if action is None (non-executable)
|
||||
title = f"[bold {_ACTION_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
if event.action is None:
|
||||
title += f"Agent Action (Not Executed)[/bold {_ACTION_COLOR}]"
|
||||
else:
|
||||
title += f"Agent Action[/bold {_ACTION_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_ACTION_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, ObservationEvent):
|
||||
title = f"[bold {_OBSERVATION_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Observation[/bold {_OBSERVATION_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_OBSERVATION_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, UserRejectObservation):
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"User Rejected Action[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, MessageEvent):
|
||||
if (
|
||||
self._skip_user_messages
|
||||
and event.llm_message
|
||||
and event.llm_message.role == "user"
|
||||
):
|
||||
return
|
||||
assert event.llm_message is not None
|
||||
# Role-based styling
|
||||
role_colors = {
|
||||
"user": _MESSAGE_USER_COLOR,
|
||||
"assistant": _MESSAGE_ASSISTANT_COLOR,
|
||||
}
|
||||
role_color = role_colors.get(event.llm_message.role, "white")
|
||||
|
||||
# "User Message To [Name] Agent" for user
|
||||
# "Message from [Name] Agent" for agent
|
||||
agent_name = f"{self._name} " if self._name else ""
|
||||
|
||||
if event.llm_message.role == "user":
|
||||
title_text = (
|
||||
f"[bold {role_color}]User Message to "
|
||||
f"{agent_name}Agent[/bold {role_color}]"
|
||||
)
|
||||
else:
|
||||
title_text = (
|
||||
f"[bold {role_color}]Message from "
|
||||
f"{agent_name}Agent[/bold {role_color}]"
|
||||
)
|
||||
return Panel(
|
||||
content,
|
||||
title=title_text,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=role_color,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, AgentErrorEvent):
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Agent Error[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, PauseEvent):
|
||||
title = f"[bold {_PAUSE_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"User Paused[/bold {_PAUSE_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_PAUSE_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, Condensation):
|
||||
title = f"[bold {_SYSTEM_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Condensation[/bold {_SYSTEM_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_SYSTEM_COLOR,
|
||||
expand=True,
|
||||
)
|
||||
else:
|
||||
# Fallback panel for unknown event types
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"UNKNOWN Event: {event.__class__.__name__}[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=f"({event.source})",
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
def _format_metrics_subtitle(self) -> str | None:
|
||||
"""Format LLM metrics as a visually appealing subtitle string with icons,
|
||||
colors, and k/m abbreviations using conversation stats."""
|
||||
stats = self.conversation_stats
|
||||
if not stats:
|
||||
return None
|
||||
|
||||
combined_metrics = stats.get_combined_metrics()
|
||||
if not combined_metrics or not combined_metrics.accumulated_token_usage:
|
||||
return None
|
||||
|
||||
usage = combined_metrics.accumulated_token_usage
|
||||
cost = combined_metrics.accumulated_cost or 0.0
|
||||
|
||||
# helper: 1234 -> "1.2K", 1200000 -> "1.2M"
|
||||
def abbr(n: int | float) -> str:
|
||||
n = int(n or 0)
|
||||
if n >= 1_000_000_000:
|
||||
val, suffix = n / 1_000_000_000, "B"
|
||||
elif n >= 1_000_000:
|
||||
val, suffix = n / 1_000_000, "M"
|
||||
elif n >= 1_000:
|
||||
val, suffix = n / 1_000, "K"
|
||||
else:
|
||||
return str(n)
|
||||
return f"{val:.2f}".rstrip("0").rstrip(".") + suffix
|
||||
|
||||
input_tokens = abbr(usage.prompt_tokens or 0)
|
||||
output_tokens = abbr(usage.completion_tokens or 0)
|
||||
|
||||
# Cache hit rate (prompt + cache)
|
||||
prompt = usage.prompt_tokens or 0
|
||||
cache_read = usage.cache_read_tokens or 0
|
||||
cache_rate = f"{(cache_read / prompt * 100):.2f}%" if prompt > 0 else "N/A"
|
||||
reasoning_tokens = usage.reasoning_tokens or 0
|
||||
|
||||
# Cost
|
||||
cost_str = f"{cost:.4f}" if cost > 0 else "0.00"
|
||||
|
||||
# Build with fixed color scheme
|
||||
parts: list[str] = []
|
||||
parts.append(f"[cyan]↑ input {input_tokens}[/cyan]")
|
||||
parts.append(f"[magenta]cache hit {cache_rate}[/magenta]")
|
||||
if reasoning_tokens > 0:
|
||||
parts.append(f"[yellow] reasoning {abbr(reasoning_tokens)}[/yellow]")
|
||||
parts.append(f"[blue]↓ output {output_tokens}[/blue]")
|
||||
parts.append(f"[green]$ {cost_str}[/green]")
|
||||
|
||||
return "Tokens: " + " • ".join(parts)
|
||||
@ -45,6 +45,7 @@ class TestConfirmationMode:
|
||||
patch('openhands_cli.setup.print_formatted_text') as mock_print,
|
||||
patch('openhands_cli.setup.HTML'),
|
||||
patch('openhands_cli.setup.uuid') as mock_uuid,
|
||||
patch('openhands_cli.setup.CLIVisualizer') as mock_visualizer,
|
||||
):
|
||||
# Mock dependencies
|
||||
mock_conversation_id = MagicMock()
|
||||
@ -72,6 +73,7 @@ class TestConfirmationMode:
|
||||
workspace=ANY,
|
||||
persistence_dir=ANY,
|
||||
conversation_id=mock_conversation_id,
|
||||
visualizer=mock_visualizer
|
||||
)
|
||||
|
||||
def test_setup_conversation_raises_missing_agent_spec(self) -> None:
|
||||
|
||||
238
openhands-cli/tests/visualizer/test_visualizer.py
Normal file
238
openhands-cli/tests/visualizer/test_visualizer.py
Normal file
@ -0,0 +1,238 @@
|
||||
"""Tests for the conversation visualizer and event visualization."""
|
||||
|
||||
import json
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from openhands_cli.tui.visualizer import (
|
||||
CLIVisualizer,
|
||||
)
|
||||
from openhands.sdk.event import (
|
||||
ActionEvent,
|
||||
SystemPromptEvent,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.sdk.llm import (
|
||||
MessageToolCall,
|
||||
TextContent,
|
||||
)
|
||||
from openhands.sdk.tool import Action
|
||||
|
||||
|
||||
class VisualizerMockAction(Action):
|
||||
"""Mock action for testing."""
|
||||
|
||||
command: str = "test command"
|
||||
working_dir: str = "/tmp"
|
||||
|
||||
|
||||
class VisualizerCustomAction(Action):
|
||||
"""Custom action with overridden visualize method."""
|
||||
|
||||
task_list: list[dict] = []
|
||||
|
||||
@property
|
||||
def visualize(self) -> Text:
|
||||
"""Custom visualization for task tracker."""
|
||||
content = Text()
|
||||
content.append("Task Tracker Action\n", style="bold")
|
||||
content.append(f"Tasks: {len(self.task_list)}")
|
||||
for i, task in enumerate(self.task_list):
|
||||
content.append(f"\n {i + 1}. {task.get('title', 'Untitled')}")
|
||||
return content
|
||||
|
||||
|
||||
def create_tool_call(
|
||||
call_id: str, function_name: str, arguments: dict
|
||||
) -> MessageToolCall:
|
||||
"""Helper to create a MessageToolCall."""
|
||||
return MessageToolCall(
|
||||
id=call_id,
|
||||
name=function_name,
|
||||
arguments=json.dumps(arguments),
|
||||
origin="completion",
|
||||
)
|
||||
|
||||
|
||||
def test_conversation_visualizer_initialization():
|
||||
"""Test DefaultConversationVisualizer can be initialized."""
|
||||
visualizer = CLIVisualizer()
|
||||
assert visualizer is not None
|
||||
assert hasattr(visualizer, "on_event")
|
||||
assert hasattr(visualizer, "_create_event_panel")
|
||||
|
||||
|
||||
def test_visualizer_event_panel_creation():
|
||||
"""Test that visualizer creates panels for different event types."""
|
||||
conv_viz = CLIVisualizer()
|
||||
|
||||
# Test with a simple action event
|
||||
action = VisualizerMockAction(command="test")
|
||||
tool_call = create_tool_call("call_1", "test", {})
|
||||
action_event = ActionEvent(
|
||||
thought=[TextContent(text="Testing")],
|
||||
action=action,
|
||||
tool_name="test",
|
||||
tool_call_id="call_1",
|
||||
tool_call=tool_call,
|
||||
llm_response_id="response_1",
|
||||
)
|
||||
panel = conv_viz._create_event_panel(action_event)
|
||||
assert panel is not None
|
||||
assert hasattr(panel, "renderable")
|
||||
|
||||
|
||||
def test_visualizer_action_event_with_none_action_panel():
|
||||
"""ActionEvent with action=None should render as 'Agent Action (Not Executed)'."""
|
||||
visualizer = CLIVisualizer()
|
||||
tc = create_tool_call("call_ne_1", "missing_fn", {})
|
||||
action_event = ActionEvent(
|
||||
thought=[TextContent(text="...")],
|
||||
tool_call=tc,
|
||||
tool_name=tc.name,
|
||||
tool_call_id=tc.id,
|
||||
llm_response_id="resp_viz_1",
|
||||
action=None,
|
||||
)
|
||||
panel = visualizer._create_event_panel(action_event)
|
||||
assert panel is not None
|
||||
# Ensure it doesn't fall back to UNKNOWN
|
||||
assert "UNKNOWN Event" not in str(panel.title)
|
||||
# And uses the 'Agent Action (Not Executed)' title
|
||||
assert "Agent Action (Not Executed)" in str(panel.title)
|
||||
|
||||
|
||||
def test_visualizer_user_reject_observation_panel():
|
||||
"""UserRejectObservation should render a dedicated panel."""
|
||||
visualizer = CLIVisualizer()
|
||||
event = UserRejectObservation(
|
||||
tool_name="demo_tool",
|
||||
tool_call_id="fc_call_1",
|
||||
action_id="action_1",
|
||||
rejection_reason="User rejected the proposed action.",
|
||||
)
|
||||
|
||||
panel = visualizer._create_event_panel(event)
|
||||
assert panel is not None
|
||||
title = str(panel.title)
|
||||
assert "UNKNOWN Event" not in title
|
||||
assert "User Rejected Action" in title
|
||||
# ensure the reason is part of the renderable text
|
||||
renderable = panel.renderable
|
||||
assert isinstance(renderable, Text)
|
||||
assert "User rejected the proposed action." in renderable.plain
|
||||
|
||||
|
||||
def test_metrics_formatting():
|
||||
"""Test metrics subtitle formatting."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openhands.sdk.conversation.conversation_stats import ConversationStats
|
||||
from openhands.sdk.llm.utils.metrics import Metrics
|
||||
|
||||
# Create conversation stats with metrics
|
||||
conversation_stats = ConversationStats()
|
||||
|
||||
# Create metrics and add to conversation stats
|
||||
metrics = Metrics(model_name="test-model")
|
||||
metrics.add_cost(0.0234)
|
||||
metrics.add_token_usage(
|
||||
prompt_tokens=1500,
|
||||
completion_tokens=500,
|
||||
cache_read_tokens=300,
|
||||
cache_write_tokens=0,
|
||||
reasoning_tokens=200,
|
||||
context_window=8000,
|
||||
response_id="test_response",
|
||||
)
|
||||
|
||||
# Add metrics to conversation stats
|
||||
conversation_stats.usage_to_metrics["test_usage"] = metrics
|
||||
|
||||
# Create visualizer and initialize with mock state
|
||||
visualizer = CLIVisualizer()
|
||||
mock_state = MagicMock()
|
||||
mock_state.stats = conversation_stats
|
||||
visualizer.initialize(mock_state)
|
||||
|
||||
# Test the metrics subtitle formatting
|
||||
subtitle = visualizer._format_metrics_subtitle()
|
||||
assert subtitle is not None
|
||||
assert "1.5K" in subtitle # Input tokens abbreviated (trailing zeros removed)
|
||||
assert "500" in subtitle # Output tokens
|
||||
assert "20.00%" in subtitle # Cache hit rate
|
||||
assert "200" in subtitle # Reasoning tokens
|
||||
assert "0.0234" in subtitle # Cost
|
||||
|
||||
|
||||
def test_metrics_abbreviation_formatting():
|
||||
"""Test number abbreviation with various edge cases."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openhands.sdk.conversation.conversation_stats import ConversationStats
|
||||
from openhands.sdk.llm.utils.metrics import Metrics
|
||||
|
||||
test_cases = [
|
||||
# (input_tokens, expected_abbr)
|
||||
(999, "999"), # Below threshold
|
||||
(1000, "1K"), # Exact K boundary, trailing zeros removed
|
||||
(1500, "1.5K"), # K with one decimal, trailing zero removed
|
||||
(89080, "89.08K"), # K with two decimals (regression test for bug)
|
||||
(89000, "89K"), # K with trailing zeros removed
|
||||
(1000000, "1M"), # Exact M boundary
|
||||
(1234567, "1.23M"), # M with decimals
|
||||
(1000000000, "1B"), # Exact B boundary
|
||||
]
|
||||
|
||||
for tokens, expected in test_cases:
|
||||
stats = ConversationStats()
|
||||
metrics = Metrics(model_name="test-model")
|
||||
metrics.add_token_usage(
|
||||
prompt_tokens=tokens,
|
||||
completion_tokens=100,
|
||||
cache_read_tokens=0,
|
||||
cache_write_tokens=0,
|
||||
reasoning_tokens=0,
|
||||
context_window=8000,
|
||||
response_id="test",
|
||||
)
|
||||
stats.usage_to_metrics["test"] = metrics
|
||||
|
||||
visualizer = CLIVisualizer()
|
||||
mock_state = MagicMock()
|
||||
mock_state.stats = stats
|
||||
visualizer.initialize(mock_state)
|
||||
subtitle = visualizer._format_metrics_subtitle()
|
||||
|
||||
assert subtitle is not None, f"Failed for {tokens}"
|
||||
assert expected in subtitle, (
|
||||
f"Expected '{expected}' in subtitle for {tokens}, got: {subtitle}"
|
||||
)
|
||||
|
||||
|
||||
def test_event_base_fallback_visualize():
|
||||
"""Test that Event provides fallback visualization."""
|
||||
from openhands.sdk.event.base import Event
|
||||
from openhands.sdk.event.types import SourceType
|
||||
|
||||
class UnknownEvent(Event):
|
||||
source: SourceType = "agent"
|
||||
|
||||
event = UnknownEvent()
|
||||
|
||||
conv_viz = CLIVisualizer()
|
||||
panel = conv_viz._create_event_panel(event)
|
||||
|
||||
assert "UNKNOWN Event" in str(panel.title)
|
||||
|
||||
|
||||
def test_visualizer_does_not_render_system_prompt():
|
||||
"""Test that Event provides fallback visualization."""
|
||||
system_prompt_event = SystemPromptEvent(
|
||||
source="agent",
|
||||
system_prompt=TextContent(text="dummy"),
|
||||
tools=[]
|
||||
)
|
||||
conv_viz = CLIVisualizer()
|
||||
panel = conv_viz._create_event_panel(system_prompt_event)
|
||||
assert panel is None
|
||||
6
openhands-cli/uv.lock
generated
6
openhands-cli/uv.lock
generated
@ -1902,7 +1902,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openhands"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openhands-sdk" },
|
||||
@ -1929,8 +1929,8 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openhands-sdk", specifier = "==1.0.0" },
|
||||
{ name = "openhands-tools", specifier = "==1.0.0" },
|
||||
{ name = "openhands-sdk", specifier = "==1" },
|
||||
{ name = "openhands-tools", specifier = "==1" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3" },
|
||||
{ name = "typer", specifier = ">=0.17.4" },
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user