openhands-sdk 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhands/sdk/__init__.py +111 -0
- openhands/sdk/agent/__init__.py +8 -0
- openhands/sdk/agent/agent.py +607 -0
- openhands/sdk/agent/base.py +454 -0
- openhands/sdk/agent/prompts/in_context_learning_example.j2 +169 -0
- openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
- openhands/sdk/agent/prompts/security_policy.j2 +22 -0
- openhands/sdk/agent/prompts/security_risk_assessment.j2 +21 -0
- openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
- openhands/sdk/agent/prompts/system_prompt.j2 +132 -0
- openhands/sdk/agent/prompts/system_prompt_interactive.j2 +14 -0
- openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +40 -0
- openhands/sdk/agent/prompts/system_prompt_planning.j2 +40 -0
- openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +122 -0
- openhands/sdk/agent/utils.py +223 -0
- openhands/sdk/context/__init__.py +28 -0
- openhands/sdk/context/agent_context.py +240 -0
- openhands/sdk/context/condenser/__init__.py +18 -0
- openhands/sdk/context/condenser/base.py +95 -0
- openhands/sdk/context/condenser/llm_summarizing_condenser.py +89 -0
- openhands/sdk/context/condenser/no_op_condenser.py +13 -0
- openhands/sdk/context/condenser/pipeline_condenser.py +55 -0
- openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +59 -0
- openhands/sdk/context/prompts/__init__.py +6 -0
- openhands/sdk/context/prompts/prompt.py +114 -0
- openhands/sdk/context/prompts/templates/ask_agent_template.j2 +11 -0
- openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +8 -0
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +32 -0
- openhands/sdk/context/skills/__init__.py +28 -0
- openhands/sdk/context/skills/exceptions.py +11 -0
- openhands/sdk/context/skills/skill.py +630 -0
- openhands/sdk/context/skills/trigger.py +36 -0
- openhands/sdk/context/skills/types.py +48 -0
- openhands/sdk/context/view.py +306 -0
- openhands/sdk/conversation/__init__.py +40 -0
- openhands/sdk/conversation/base.py +281 -0
- openhands/sdk/conversation/conversation.py +146 -0
- openhands/sdk/conversation/conversation_stats.py +85 -0
- openhands/sdk/conversation/event_store.py +157 -0
- openhands/sdk/conversation/events_list_base.py +17 -0
- openhands/sdk/conversation/exceptions.py +50 -0
- openhands/sdk/conversation/fifo_lock.py +133 -0
- openhands/sdk/conversation/impl/__init__.py +5 -0
- openhands/sdk/conversation/impl/local_conversation.py +620 -0
- openhands/sdk/conversation/impl/remote_conversation.py +883 -0
- openhands/sdk/conversation/persistence_const.py +9 -0
- openhands/sdk/conversation/response_utils.py +41 -0
- openhands/sdk/conversation/secret_registry.py +126 -0
- openhands/sdk/conversation/serialization_diff.py +0 -0
- openhands/sdk/conversation/state.py +352 -0
- openhands/sdk/conversation/stuck_detector.py +311 -0
- openhands/sdk/conversation/title_utils.py +191 -0
- openhands/sdk/conversation/types.py +45 -0
- openhands/sdk/conversation/visualizer/__init__.py +12 -0
- openhands/sdk/conversation/visualizer/base.py +67 -0
- openhands/sdk/conversation/visualizer/default.py +373 -0
- openhands/sdk/critic/__init__.py +15 -0
- openhands/sdk/critic/base.py +38 -0
- openhands/sdk/critic/impl/__init__.py +12 -0
- openhands/sdk/critic/impl/agent_finished.py +83 -0
- openhands/sdk/critic/impl/empty_patch.py +49 -0
- openhands/sdk/critic/impl/pass_critic.py +42 -0
- openhands/sdk/event/__init__.py +42 -0
- openhands/sdk/event/base.py +149 -0
- openhands/sdk/event/condenser.py +82 -0
- openhands/sdk/event/conversation_error.py +25 -0
- openhands/sdk/event/conversation_state.py +104 -0
- openhands/sdk/event/llm_completion_log.py +39 -0
- openhands/sdk/event/llm_convertible/__init__.py +20 -0
- openhands/sdk/event/llm_convertible/action.py +139 -0
- openhands/sdk/event/llm_convertible/message.py +142 -0
- openhands/sdk/event/llm_convertible/observation.py +141 -0
- openhands/sdk/event/llm_convertible/system.py +61 -0
- openhands/sdk/event/token.py +16 -0
- openhands/sdk/event/types.py +11 -0
- openhands/sdk/event/user_action.py +21 -0
- openhands/sdk/git/exceptions.py +43 -0
- openhands/sdk/git/git_changes.py +249 -0
- openhands/sdk/git/git_diff.py +129 -0
- openhands/sdk/git/models.py +21 -0
- openhands/sdk/git/utils.py +189 -0
- openhands/sdk/io/__init__.py +6 -0
- openhands/sdk/io/base.py +48 -0
- openhands/sdk/io/local.py +82 -0
- openhands/sdk/io/memory.py +54 -0
- openhands/sdk/llm/__init__.py +45 -0
- openhands/sdk/llm/exceptions/__init__.py +45 -0
- openhands/sdk/llm/exceptions/classifier.py +50 -0
- openhands/sdk/llm/exceptions/mapping.py +54 -0
- openhands/sdk/llm/exceptions/types.py +101 -0
- openhands/sdk/llm/llm.py +1140 -0
- openhands/sdk/llm/llm_registry.py +122 -0
- openhands/sdk/llm/llm_response.py +59 -0
- openhands/sdk/llm/message.py +656 -0
- openhands/sdk/llm/mixins/fn_call_converter.py +1243 -0
- openhands/sdk/llm/mixins/non_native_fc.py +93 -0
- openhands/sdk/llm/options/__init__.py +1 -0
- openhands/sdk/llm/options/chat_options.py +93 -0
- openhands/sdk/llm/options/common.py +19 -0
- openhands/sdk/llm/options/responses_options.py +67 -0
- openhands/sdk/llm/router/__init__.py +10 -0
- openhands/sdk/llm/router/base.py +117 -0
- openhands/sdk/llm/router/impl/multimodal.py +76 -0
- openhands/sdk/llm/router/impl/random.py +22 -0
- openhands/sdk/llm/streaming.py +9 -0
- openhands/sdk/llm/utils/metrics.py +312 -0
- openhands/sdk/llm/utils/model_features.py +191 -0
- openhands/sdk/llm/utils/model_info.py +90 -0
- openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
- openhands/sdk/llm/utils/retry_mixin.py +128 -0
- openhands/sdk/llm/utils/telemetry.py +362 -0
- openhands/sdk/llm/utils/unverified_models.py +156 -0
- openhands/sdk/llm/utils/verified_models.py +66 -0
- openhands/sdk/logger/__init__.py +22 -0
- openhands/sdk/logger/logger.py +195 -0
- openhands/sdk/logger/rolling.py +113 -0
- openhands/sdk/mcp/__init__.py +24 -0
- openhands/sdk/mcp/client.py +76 -0
- openhands/sdk/mcp/definition.py +106 -0
- openhands/sdk/mcp/exceptions.py +19 -0
- openhands/sdk/mcp/tool.py +270 -0
- openhands/sdk/mcp/utils.py +83 -0
- openhands/sdk/observability/__init__.py +4 -0
- openhands/sdk/observability/laminar.py +166 -0
- openhands/sdk/observability/utils.py +20 -0
- openhands/sdk/py.typed +0 -0
- openhands/sdk/secret/__init__.py +19 -0
- openhands/sdk/secret/secrets.py +92 -0
- openhands/sdk/security/__init__.py +6 -0
- openhands/sdk/security/analyzer.py +111 -0
- openhands/sdk/security/confirmation_policy.py +61 -0
- openhands/sdk/security/llm_analyzer.py +29 -0
- openhands/sdk/security/risk.py +100 -0
- openhands/sdk/tool/__init__.py +34 -0
- openhands/sdk/tool/builtins/__init__.py +34 -0
- openhands/sdk/tool/builtins/finish.py +106 -0
- openhands/sdk/tool/builtins/think.py +117 -0
- openhands/sdk/tool/registry.py +161 -0
- openhands/sdk/tool/schema.py +276 -0
- openhands/sdk/tool/spec.py +39 -0
- openhands/sdk/tool/tool.py +481 -0
- openhands/sdk/utils/__init__.py +22 -0
- openhands/sdk/utils/async_executor.py +115 -0
- openhands/sdk/utils/async_utils.py +39 -0
- openhands/sdk/utils/cipher.py +68 -0
- openhands/sdk/utils/command.py +90 -0
- openhands/sdk/utils/deprecation.py +166 -0
- openhands/sdk/utils/github.py +44 -0
- openhands/sdk/utils/json.py +48 -0
- openhands/sdk/utils/models.py +570 -0
- openhands/sdk/utils/paging.py +63 -0
- openhands/sdk/utils/pydantic_diff.py +85 -0
- openhands/sdk/utils/pydantic_secrets.py +64 -0
- openhands/sdk/utils/truncate.py +117 -0
- openhands/sdk/utils/visualize.py +58 -0
- openhands/sdk/workspace/__init__.py +17 -0
- openhands/sdk/workspace/base.py +158 -0
- openhands/sdk/workspace/local.py +189 -0
- openhands/sdk/workspace/models.py +35 -0
- openhands/sdk/workspace/remote/__init__.py +8 -0
- openhands/sdk/workspace/remote/async_remote_workspace.py +149 -0
- openhands/sdk/workspace/remote/base.py +164 -0
- openhands/sdk/workspace/remote/remote_workspace_mixin.py +323 -0
- openhands/sdk/workspace/workspace.py +49 -0
- openhands_sdk-1.7.0.dist-info/METADATA +17 -0
- openhands_sdk-1.7.0.dist-info/RECORD +172 -0
- openhands_sdk-1.7.0.dist-info/WHEEL +5 -0
- openhands_sdk-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from rich.console import Console, Group
|
|
7
|
+
from rich.rule import Rule
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from openhands.sdk.conversation.visualizer.base import (
|
|
11
|
+
ConversationVisualizerBase,
|
|
12
|
+
)
|
|
13
|
+
from openhands.sdk.event import (
|
|
14
|
+
ActionEvent,
|
|
15
|
+
AgentErrorEvent,
|
|
16
|
+
ConversationStateUpdateEvent,
|
|
17
|
+
MessageEvent,
|
|
18
|
+
ObservationEvent,
|
|
19
|
+
PauseEvent,
|
|
20
|
+
SystemPromptEvent,
|
|
21
|
+
UserRejectObservation,
|
|
22
|
+
)
|
|
23
|
+
from openhands.sdk.event.base import Event
|
|
24
|
+
from openhands.sdk.event.condenser import Condensation, CondensationRequest
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# These are external inputs
|
|
31
|
+
_OBSERVATION_COLOR = "yellow"
|
|
32
|
+
_MESSAGE_USER_COLOR = "gold3"
|
|
33
|
+
_PAUSE_COLOR = "bright_yellow"
|
|
34
|
+
# These are internal system stuff
|
|
35
|
+
_SYSTEM_COLOR = "magenta"
|
|
36
|
+
_THOUGHT_COLOR = "bright_black"
|
|
37
|
+
_ERROR_COLOR = "red"
|
|
38
|
+
# These are agent actions
|
|
39
|
+
_ACTION_COLOR = "blue"
|
|
40
|
+
_MESSAGE_ASSISTANT_COLOR = _ACTION_COLOR
|
|
41
|
+
|
|
42
|
+
DEFAULT_HIGHLIGHT_REGEX = {
|
|
43
|
+
r"^Reasoning:": f"bold {_THOUGHT_COLOR}",
|
|
44
|
+
r"^Thought:": f"bold {_THOUGHT_COLOR}",
|
|
45
|
+
r"^Action:": f"bold {_ACTION_COLOR}",
|
|
46
|
+
r"^Arguments:": f"bold {_ACTION_COLOR}",
|
|
47
|
+
r"^Tool:": f"bold {_OBSERVATION_COLOR}",
|
|
48
|
+
r"^Result:": f"bold {_OBSERVATION_COLOR}",
|
|
49
|
+
r"^Rejection Reason:": f"bold {_ERROR_COLOR}",
|
|
50
|
+
# Markdown-style
|
|
51
|
+
r"\*\*(.*?)\*\*": "bold",
|
|
52
|
+
r"\*(.*?)\*": "italic",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EventVisualizationConfig(BaseModel):
|
|
57
|
+
"""Configuration for how to visualize an event type."""
|
|
58
|
+
|
|
59
|
+
title: str | Callable[[Event], str]
|
|
60
|
+
"""The title to display for this event. Can be a string or callable."""
|
|
61
|
+
|
|
62
|
+
color: str | Callable[[Event], str]
|
|
63
|
+
"""The Rich color to use for the title and rule. Can be a string or callable."""
|
|
64
|
+
|
|
65
|
+
show_metrics: bool = False
|
|
66
|
+
"""Whether to show the metrics subtitle."""
|
|
67
|
+
|
|
68
|
+
indent_content: bool = False
|
|
69
|
+
"""Whether to indent the content."""
|
|
70
|
+
|
|
71
|
+
skip: bool = False
|
|
72
|
+
"""If True, skip visualization of this event type entirely."""
|
|
73
|
+
|
|
74
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def indent_content(content: Text, spaces: int = 4) -> Text:
|
|
78
|
+
"""Indent content for visual hierarchy while preserving all formatting."""
|
|
79
|
+
prefix = " " * spaces
|
|
80
|
+
lines = content.split("\n")
|
|
81
|
+
|
|
82
|
+
indented = Text()
|
|
83
|
+
for i, line in enumerate(lines):
|
|
84
|
+
if i > 0:
|
|
85
|
+
indented.append("\n")
|
|
86
|
+
indented.append(prefix)
|
|
87
|
+
indented.append(line)
|
|
88
|
+
|
|
89
|
+
return indented
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def section_header(title: str, color: str) -> Rule:
|
|
93
|
+
"""Create a semantic divider with title."""
|
|
94
|
+
return Rule(
|
|
95
|
+
f"[{color} bold]{title}[/{color} bold]",
|
|
96
|
+
style=color,
|
|
97
|
+
characters="─",
|
|
98
|
+
align="left",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_event_block(
|
|
103
|
+
content: Text,
|
|
104
|
+
title: str,
|
|
105
|
+
title_color: str,
|
|
106
|
+
subtitle: str | None = None,
|
|
107
|
+
indent: bool = False,
|
|
108
|
+
) -> Group:
|
|
109
|
+
"""Build a complete event block with header, content, and optional subtitle."""
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
# Header with rule
|
|
113
|
+
parts.append(section_header(title, title_color))
|
|
114
|
+
parts.append(Text()) # Blank line after header
|
|
115
|
+
|
|
116
|
+
# Content (optionally indented)
|
|
117
|
+
if indent:
|
|
118
|
+
parts.append(indent_content(content))
|
|
119
|
+
else:
|
|
120
|
+
parts.append(content)
|
|
121
|
+
|
|
122
|
+
# Subtitle (metrics) if provided
|
|
123
|
+
if subtitle:
|
|
124
|
+
parts.append(Text()) # Blank line before subtitle
|
|
125
|
+
subtitle_text = Text.from_markup(subtitle)
|
|
126
|
+
subtitle_text.stylize("dim")
|
|
127
|
+
parts.append(subtitle_text)
|
|
128
|
+
|
|
129
|
+
parts.append(Text()) # Blank line after block
|
|
130
|
+
|
|
131
|
+
return Group(*parts)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _get_action_title(event: Event) -> str:
|
|
135
|
+
"""Get title for ActionEvent based on whether action is None."""
|
|
136
|
+
if isinstance(event, ActionEvent):
|
|
137
|
+
return "Agent Action (Not Executed)" if event.action is None else "Agent Action"
|
|
138
|
+
return "Action"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _get_message_title(event: Event) -> str:
|
|
142
|
+
"""Get title for MessageEvent based on role."""
|
|
143
|
+
if isinstance(event, MessageEvent) and event.llm_message:
|
|
144
|
+
return (
|
|
145
|
+
"Message from User"
|
|
146
|
+
if event.llm_message.role == "user"
|
|
147
|
+
else "Message from Agent"
|
|
148
|
+
)
|
|
149
|
+
return "Message"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _get_message_color(event: Event) -> str:
|
|
153
|
+
"""Get color for MessageEvent based on role."""
|
|
154
|
+
if isinstance(event, MessageEvent) and event.llm_message:
|
|
155
|
+
return (
|
|
156
|
+
_MESSAGE_USER_COLOR
|
|
157
|
+
if event.llm_message.role == "user"
|
|
158
|
+
else _MESSAGE_ASSISTANT_COLOR
|
|
159
|
+
)
|
|
160
|
+
return "white"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Event type to visualization configuration mapping
|
|
164
|
+
# This replaces the large isinstance chain with a cleaner lookup approach
|
|
165
|
+
EVENT_VISUALIZATION_CONFIG: dict[type[Event], EventVisualizationConfig] = {
|
|
166
|
+
SystemPromptEvent: EventVisualizationConfig(
|
|
167
|
+
title="System Prompt",
|
|
168
|
+
color=_SYSTEM_COLOR,
|
|
169
|
+
),
|
|
170
|
+
ActionEvent: EventVisualizationConfig(
|
|
171
|
+
title=_get_action_title,
|
|
172
|
+
color=_ACTION_COLOR,
|
|
173
|
+
show_metrics=True,
|
|
174
|
+
),
|
|
175
|
+
ObservationEvent: EventVisualizationConfig(
|
|
176
|
+
title="Observation",
|
|
177
|
+
color=_OBSERVATION_COLOR,
|
|
178
|
+
),
|
|
179
|
+
UserRejectObservation: EventVisualizationConfig(
|
|
180
|
+
title="User Rejected Action",
|
|
181
|
+
color=_ERROR_COLOR,
|
|
182
|
+
),
|
|
183
|
+
MessageEvent: EventVisualizationConfig(
|
|
184
|
+
title=_get_message_title,
|
|
185
|
+
color=_get_message_color,
|
|
186
|
+
show_metrics=True,
|
|
187
|
+
),
|
|
188
|
+
AgentErrorEvent: EventVisualizationConfig(
|
|
189
|
+
title="Agent Error",
|
|
190
|
+
color=_ERROR_COLOR,
|
|
191
|
+
show_metrics=True,
|
|
192
|
+
),
|
|
193
|
+
PauseEvent: EventVisualizationConfig(
|
|
194
|
+
title="User Paused",
|
|
195
|
+
color=_PAUSE_COLOR,
|
|
196
|
+
),
|
|
197
|
+
Condensation: EventVisualizationConfig(
|
|
198
|
+
title="Condensation",
|
|
199
|
+
color="white",
|
|
200
|
+
show_metrics=True,
|
|
201
|
+
),
|
|
202
|
+
CondensationRequest: EventVisualizationConfig(
|
|
203
|
+
title="Condensation Request",
|
|
204
|
+
color=_SYSTEM_COLOR,
|
|
205
|
+
),
|
|
206
|
+
ConversationStateUpdateEvent: EventVisualizationConfig(
|
|
207
|
+
title="Conversation State Update",
|
|
208
|
+
color=_SYSTEM_COLOR,
|
|
209
|
+
skip=True,
|
|
210
|
+
),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class DefaultConversationVisualizer(ConversationVisualizerBase):
|
|
215
|
+
"""Handles visualization of conversation events with Rich formatting.
|
|
216
|
+
|
|
217
|
+
Provides Rich-formatted output with semantic dividers and complete content display.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
_console: Console
|
|
221
|
+
_skip_user_messages: bool
|
|
222
|
+
_highlight_patterns: dict[str, str]
|
|
223
|
+
|
|
224
|
+
def __init__(
|
|
225
|
+
self,
|
|
226
|
+
highlight_regex: dict[str, str] | None = DEFAULT_HIGHLIGHT_REGEX,
|
|
227
|
+
skip_user_messages: bool = False,
|
|
228
|
+
):
|
|
229
|
+
"""Initialize the visualizer.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
highlight_regex: Dictionary mapping regex patterns to Rich color styles
|
|
233
|
+
for highlighting keywords in the visualizer.
|
|
234
|
+
For example: {"Reasoning:": "bold blue",
|
|
235
|
+
"Thought:": "bold green"}
|
|
236
|
+
skip_user_messages: If True, skip displaying user messages. Useful for
|
|
237
|
+
scenarios where user input is not relevant to show.
|
|
238
|
+
"""
|
|
239
|
+
super().__init__()
|
|
240
|
+
self._console = Console()
|
|
241
|
+
self._skip_user_messages = skip_user_messages
|
|
242
|
+
self._highlight_patterns = highlight_regex or {}
|
|
243
|
+
|
|
244
|
+
def on_event(self, event: Event) -> None:
|
|
245
|
+
"""Main event handler that displays events with Rich formatting."""
|
|
246
|
+
output = self._create_event_block(event)
|
|
247
|
+
if output:
|
|
248
|
+
self._console.print(output)
|
|
249
|
+
|
|
250
|
+
def _apply_highlighting(self, text: Text) -> Text:
|
|
251
|
+
"""Apply regex-based highlighting to text content.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
text: The Rich Text object to highlight
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
A new Text object with highlighting applied
|
|
258
|
+
"""
|
|
259
|
+
if not self._highlight_patterns:
|
|
260
|
+
return text
|
|
261
|
+
|
|
262
|
+
# Create a copy to avoid modifying the original
|
|
263
|
+
highlighted = text.copy()
|
|
264
|
+
|
|
265
|
+
# Apply each pattern using Rich's built-in highlight_regex method
|
|
266
|
+
for pattern, style in self._highlight_patterns.items():
|
|
267
|
+
pattern_compiled = re.compile(pattern, re.MULTILINE)
|
|
268
|
+
highlighted.highlight_regex(pattern_compiled, style)
|
|
269
|
+
|
|
270
|
+
return highlighted
|
|
271
|
+
|
|
272
|
+
def _create_event_block(self, event: Event) -> Group | None:
|
|
273
|
+
"""Create a Rich event block for the event with full detail."""
|
|
274
|
+
# Look up visualization config for this event type
|
|
275
|
+
config = EVENT_VISUALIZATION_CONFIG.get(type(event))
|
|
276
|
+
|
|
277
|
+
if not config:
|
|
278
|
+
# Warn about unknown event types and skip
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Event type %s is not registered in EVENT_VISUALIZATION_CONFIG. "
|
|
281
|
+
"Skipping visualization.",
|
|
282
|
+
event.__class__.__name__,
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Check if this event type should be skipped
|
|
287
|
+
if config.skip:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
# Check if we should skip user messages based on runtime configuration
|
|
291
|
+
if (
|
|
292
|
+
self._skip_user_messages
|
|
293
|
+
and isinstance(event, MessageEvent)
|
|
294
|
+
and event.llm_message
|
|
295
|
+
and event.llm_message.role == "user"
|
|
296
|
+
):
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
# Use the event's visualize property for content
|
|
300
|
+
content = event.visualize
|
|
301
|
+
|
|
302
|
+
if not content.plain.strip():
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
# Apply highlighting if configured
|
|
306
|
+
if self._highlight_patterns:
|
|
307
|
+
content = self._apply_highlighting(content)
|
|
308
|
+
|
|
309
|
+
# Resolve title (may be a string or callable)
|
|
310
|
+
title = config.title(event) if callable(config.title) else config.title
|
|
311
|
+
|
|
312
|
+
# Resolve color (may be a string or callable)
|
|
313
|
+
title_color = config.color(event) if callable(config.color) else config.color
|
|
314
|
+
|
|
315
|
+
# Build subtitle if needed
|
|
316
|
+
subtitle = self._format_metrics_subtitle() if config.show_metrics else None
|
|
317
|
+
|
|
318
|
+
return build_event_block(
|
|
319
|
+
content=content,
|
|
320
|
+
title=title,
|
|
321
|
+
title_color=title_color,
|
|
322
|
+
subtitle=subtitle,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def _format_metrics_subtitle(self) -> str | None:
|
|
326
|
+
"""Format LLM metrics as a visually appealing subtitle string with icons,
|
|
327
|
+
colors, and k/m abbreviations using conversation stats."""
|
|
328
|
+
stats = self.conversation_stats
|
|
329
|
+
if not stats:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
combined_metrics = stats.get_combined_metrics()
|
|
333
|
+
if not combined_metrics or not combined_metrics.accumulated_token_usage:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
usage = combined_metrics.accumulated_token_usage
|
|
337
|
+
cost = combined_metrics.accumulated_cost or 0.0
|
|
338
|
+
|
|
339
|
+
# helper: 1234 -> "1.2K", 1200000 -> "1.2M"
|
|
340
|
+
def abbr(n: int | float) -> str:
|
|
341
|
+
n = int(n or 0)
|
|
342
|
+
if n >= 1_000_000_000:
|
|
343
|
+
val, suffix = n / 1_000_000_000, "B"
|
|
344
|
+
elif n >= 1_000_000:
|
|
345
|
+
val, suffix = n / 1_000_000, "M"
|
|
346
|
+
elif n >= 1_000:
|
|
347
|
+
val, suffix = n / 1_000, "K"
|
|
348
|
+
else:
|
|
349
|
+
return str(n)
|
|
350
|
+
return f"{val:.2f}".rstrip("0").rstrip(".") + suffix
|
|
351
|
+
|
|
352
|
+
input_tokens = abbr(usage.prompt_tokens or 0)
|
|
353
|
+
output_tokens = abbr(usage.completion_tokens or 0)
|
|
354
|
+
|
|
355
|
+
# Cache hit rate (prompt + cache)
|
|
356
|
+
prompt = usage.prompt_tokens or 0
|
|
357
|
+
cache_read = usage.cache_read_tokens or 0
|
|
358
|
+
cache_rate = f"{(cache_read / prompt * 100):.2f}%" if prompt > 0 else "N/A"
|
|
359
|
+
reasoning_tokens = usage.reasoning_tokens or 0
|
|
360
|
+
|
|
361
|
+
# Cost
|
|
362
|
+
cost_str = f"{cost:.4f}" if cost > 0 else "0.00"
|
|
363
|
+
|
|
364
|
+
# Build with fixed color scheme
|
|
365
|
+
parts: list[str] = []
|
|
366
|
+
parts.append(f"[cyan]↑ input {input_tokens}[/cyan]")
|
|
367
|
+
parts.append(f"[magenta]cache hit {cache_rate}[/magenta]")
|
|
368
|
+
if reasoning_tokens > 0:
|
|
369
|
+
parts.append(f"[yellow] reasoning {abbr(reasoning_tokens)}[/yellow]")
|
|
370
|
+
parts.append(f"[blue]↓ output {output_tokens}[/blue]")
|
|
371
|
+
parts.append(f"[green]$ {cost_str}[/green]")
|
|
372
|
+
|
|
373
|
+
return "Tokens: " + " • ".join(parts)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from openhands.sdk.critic.base import CriticBase, CriticResult
|
|
2
|
+
from openhands.sdk.critic.impl import (
|
|
3
|
+
AgentFinishedCritic,
|
|
4
|
+
EmptyPatchCritic,
|
|
5
|
+
PassCritic,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CriticBase",
|
|
11
|
+
"CriticResult",
|
|
12
|
+
"AgentFinishedCritic",
|
|
13
|
+
"EmptyPatchCritic",
|
|
14
|
+
"PassCritic",
|
|
15
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from openhands.sdk.event import LLMConvertibleEvent
|
|
8
|
+
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CriticResult(BaseModel):
|
|
12
|
+
"""A critic result is a score and a message."""
|
|
13
|
+
|
|
14
|
+
THRESHOLD: ClassVar[float] = 0.5
|
|
15
|
+
|
|
16
|
+
score: float = Field(
|
|
17
|
+
description="A predicted probability of success between 0 and 1.",
|
|
18
|
+
ge=0.0,
|
|
19
|
+
le=1.0,
|
|
20
|
+
)
|
|
21
|
+
message: str | None = Field(description="An optional message explaining the score.")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def success(self) -> bool:
|
|
25
|
+
"""Whether the agent is successful."""
|
|
26
|
+
return self.score >= CriticResult.THRESHOLD
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CriticBase(DiscriminatedUnionMixin, abc.ABC):
|
|
30
|
+
"""A critic is a function that takes in a list of events,
|
|
31
|
+
optional git patch, and returns a score about the quality of agent's action.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def evaluate(
|
|
36
|
+
self, events: Sequence[LLMConvertibleEvent], git_patch: str | None = None
|
|
37
|
+
) -> CriticResult:
|
|
38
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Critic implementations module."""
|
|
2
|
+
|
|
3
|
+
from openhands.sdk.critic.impl.agent_finished import AgentFinishedCritic
|
|
4
|
+
from openhands.sdk.critic.impl.empty_patch import EmptyPatchCritic
|
|
5
|
+
from openhands.sdk.critic.impl.pass_critic import PassCritic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AgentFinishedCritic",
|
|
10
|
+
"EmptyPatchCritic",
|
|
11
|
+
"PassCritic",
|
|
12
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentFinishedCritic implementation.
|
|
3
|
+
|
|
4
|
+
This critic evaluates whether an agent properly finished a task by checking:
|
|
5
|
+
1. The agent's last action was a FinishAction (proper completion)
|
|
6
|
+
2. The generated git patch is non-empty (actual changes were made)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from openhands.sdk.critic.base import CriticBase, CriticResult
|
|
12
|
+
from openhands.sdk.event import ActionEvent, LLMConvertibleEvent
|
|
13
|
+
from openhands.sdk.logger import get_logger
|
|
14
|
+
from openhands.sdk.tool.builtins.finish import FinishAction
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentFinishedCritic(CriticBase):
|
|
21
|
+
"""
|
|
22
|
+
Critic that evaluates whether an agent properly finished a task.
|
|
23
|
+
|
|
24
|
+
This critic checks two main criteria:
|
|
25
|
+
1. The agent's last action was a FinishAction (proper completion)
|
|
26
|
+
2. The generated git patch is non-empty (actual changes were made)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def evaluate(
|
|
30
|
+
self, events: Sequence[LLMConvertibleEvent], git_patch: str | None = None
|
|
31
|
+
) -> CriticResult:
|
|
32
|
+
"""
|
|
33
|
+
Evaluate if an agent properly finished with a non-empty git patch.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
events: List of events from the agent's execution
|
|
37
|
+
git_patch: Optional git patch generated by the agent
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
CriticResult with score 1.0 if successful, 0.0 otherwise
|
|
41
|
+
"""
|
|
42
|
+
reasons = []
|
|
43
|
+
|
|
44
|
+
# Check if git patch is non-empty
|
|
45
|
+
if not git_patch or not git_patch.strip():
|
|
46
|
+
reasons.append("Empty git patch")
|
|
47
|
+
logger.debug("AgentFinishedCritic: Empty git patch")
|
|
48
|
+
return CriticResult(
|
|
49
|
+
score=0.0,
|
|
50
|
+
message="Agent did not produce a non-empty git patch. "
|
|
51
|
+
+ "; ".join(reasons),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check if agent properly finished with FinishAction
|
|
55
|
+
if not self._has_finish_action(events):
|
|
56
|
+
reasons.append("No FinishAction found")
|
|
57
|
+
logger.debug("AgentFinishedCritic: No FinishAction")
|
|
58
|
+
return CriticResult(
|
|
59
|
+
score=0.0,
|
|
60
|
+
message="Agent did not finish properly. " + "; ".join(reasons),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger.debug("AgentFinishedCritic: Successfully completed")
|
|
64
|
+
return CriticResult(
|
|
65
|
+
score=1.0,
|
|
66
|
+
message="Agent completed with FinishAction and non-empty patch",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _has_finish_action(self, events: Sequence[LLMConvertibleEvent]) -> bool:
|
|
70
|
+
"""Check if the last action was a FinishAction."""
|
|
71
|
+
if not events:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Look for the last ActionEvent in the history
|
|
75
|
+
for event in reversed(events):
|
|
76
|
+
if isinstance(event, ActionEvent):
|
|
77
|
+
# Check if this is a FinishAction
|
|
78
|
+
if event.action and isinstance(event.action, FinishAction):
|
|
79
|
+
return True
|
|
80
|
+
# If we find any other action type, the agent didn't finish
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
return False
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EmptyPatchCritic implementation.
|
|
3
|
+
|
|
4
|
+
This critic only evaluates whether a git patch is non-empty.
|
|
5
|
+
Unlike AgentFinishedCritic, it does not check for proper agent completion.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from openhands.sdk.critic.base import CriticBase, CriticResult
|
|
11
|
+
from openhands.sdk.event import LLMConvertibleEvent
|
|
12
|
+
from openhands.sdk.logger import get_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EmptyPatchCritic(CriticBase):
|
|
19
|
+
"""
|
|
20
|
+
Critic that only evaluates whether a git patch is non-empty.
|
|
21
|
+
|
|
22
|
+
This critic checks only one criterion:
|
|
23
|
+
- The generated git patch is non-empty (actual changes were made)
|
|
24
|
+
|
|
25
|
+
Unlike AgentFinishedCritic, this critic does not check for proper
|
|
26
|
+
agent completion with FinishAction.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def evaluate(
|
|
30
|
+
self,
|
|
31
|
+
events: Sequence[LLMConvertibleEvent], # noqa: ARG002
|
|
32
|
+
git_patch: str | None = None,
|
|
33
|
+
) -> CriticResult:
|
|
34
|
+
"""
|
|
35
|
+
Evaluate if a git patch is non-empty.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
events: List of events from the agent's execution (not used)
|
|
39
|
+
git_patch: Optional git patch generated by the agent
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
CriticResult with score 1.0 if patch is non-empty, 0.0 otherwise
|
|
43
|
+
"""
|
|
44
|
+
if not git_patch or not git_patch.strip():
|
|
45
|
+
logger.debug("EmptyPatchCritic: Empty git patch")
|
|
46
|
+
return CriticResult(score=0.0, message="Git patch is empty or missing")
|
|
47
|
+
|
|
48
|
+
logger.debug("EmptyPatchCritic: Non-empty git patch found")
|
|
49
|
+
return CriticResult(score=1.0, message="Git patch is non-empty")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PassCritic implementation.
|
|
3
|
+
|
|
4
|
+
This critic always returns success, useful when no evaluation is needed
|
|
5
|
+
or when all instances should be considered successful.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from openhands.sdk.critic.base import CriticBase, CriticResult
|
|
11
|
+
from openhands.sdk.event import LLMConvertibleEvent
|
|
12
|
+
from openhands.sdk.logger import get_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PassCritic(CriticBase):
|
|
19
|
+
"""
|
|
20
|
+
Critic that always returns success.
|
|
21
|
+
|
|
22
|
+
This critic can be used when no evaluation is needed or when
|
|
23
|
+
all instances should be considered successful regardless of their output.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def evaluate(
|
|
27
|
+
self,
|
|
28
|
+
events: Sequence[LLMConvertibleEvent], # noqa: ARG002
|
|
29
|
+
git_patch: str | None = None, # noqa: ARG002
|
|
30
|
+
) -> CriticResult:
|
|
31
|
+
"""
|
|
32
|
+
Always evaluate as successful.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
events: List of events from the agent's execution (not used)
|
|
36
|
+
git_patch: Optional git patch generated by the agent (not used)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
CriticResult with score 1.0 (always successful)
|
|
40
|
+
"""
|
|
41
|
+
logger.debug("PassCritic: Always returns success")
|
|
42
|
+
return CriticResult(score=1.0, message="PassCritic always succeeds")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from openhands.sdk.event.base import Event, LLMConvertibleEvent
|
|
2
|
+
from openhands.sdk.event.condenser import (
|
|
3
|
+
Condensation,
|
|
4
|
+
CondensationRequest,
|
|
5
|
+
CondensationSummaryEvent,
|
|
6
|
+
)
|
|
7
|
+
from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
|
|
8
|
+
from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
|
|
9
|
+
from openhands.sdk.event.llm_convertible import (
|
|
10
|
+
ActionEvent,
|
|
11
|
+
AgentErrorEvent,
|
|
12
|
+
MessageEvent,
|
|
13
|
+
ObservationBaseEvent,
|
|
14
|
+
ObservationEvent,
|
|
15
|
+
SystemPromptEvent,
|
|
16
|
+
UserRejectObservation,
|
|
17
|
+
)
|
|
18
|
+
from openhands.sdk.event.token import TokenEvent
|
|
19
|
+
from openhands.sdk.event.types import EventID, ToolCallID
|
|
20
|
+
from openhands.sdk.event.user_action import PauseEvent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Event",
|
|
25
|
+
"LLMConvertibleEvent",
|
|
26
|
+
"SystemPromptEvent",
|
|
27
|
+
"ActionEvent",
|
|
28
|
+
"TokenEvent",
|
|
29
|
+
"ObservationEvent",
|
|
30
|
+
"ObservationBaseEvent",
|
|
31
|
+
"MessageEvent",
|
|
32
|
+
"AgentErrorEvent",
|
|
33
|
+
"UserRejectObservation",
|
|
34
|
+
"PauseEvent",
|
|
35
|
+
"Condensation",
|
|
36
|
+
"CondensationRequest",
|
|
37
|
+
"CondensationSummaryEvent",
|
|
38
|
+
"ConversationStateUpdateEvent",
|
|
39
|
+
"LLMCompletionLogEvent",
|
|
40
|
+
"EventID",
|
|
41
|
+
"ToolCallID",
|
|
42
|
+
]
|