openhands 0.0.0__py3-none-any.whl → 1.0.1__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.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
openhands/sdk/context/view.py
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
from logging import getLogger
|
|
2
|
-
from typing import overload
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
-
|
|
6
|
-
from openhands.sdk.event import (
|
|
7
|
-
Condensation,
|
|
8
|
-
CondensationRequest,
|
|
9
|
-
Event,
|
|
10
|
-
LLMConvertibleEvent,
|
|
11
|
-
MessageEvent,
|
|
12
|
-
)
|
|
13
|
-
from openhands.sdk.llm import Message, TextContent
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger = getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class View(BaseModel):
|
|
20
|
-
"""Linearly ordered view of events.
|
|
21
|
-
|
|
22
|
-
Produced by a condenser to indicate the included events are ready to process as LLM
|
|
23
|
-
input.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
events: list[LLMConvertibleEvent]
|
|
27
|
-
unhandled_condensation_request: bool = False
|
|
28
|
-
|
|
29
|
-
def __len__(self) -> int:
|
|
30
|
-
return len(self.events)
|
|
31
|
-
|
|
32
|
-
# To preserve list-like indexing, we ideally support slicing and position-based
|
|
33
|
-
# indexing. The only challenge with that is switching the return type based on the
|
|
34
|
-
# input type -- we can mark the different signatures for MyPy with `@overload`
|
|
35
|
-
# decorators.
|
|
36
|
-
|
|
37
|
-
@overload
|
|
38
|
-
def __getitem__(self, key: slice) -> list[LLMConvertibleEvent]: ...
|
|
39
|
-
|
|
40
|
-
@overload
|
|
41
|
-
def __getitem__(self, key: int) -> LLMConvertibleEvent: ...
|
|
42
|
-
|
|
43
|
-
def __getitem__(
|
|
44
|
-
self, key: int | slice
|
|
45
|
-
) -> LLMConvertibleEvent | list[LLMConvertibleEvent]:
|
|
46
|
-
if isinstance(key, slice):
|
|
47
|
-
start, stop, step = key.indices(len(self))
|
|
48
|
-
return [self[i] for i in range(start, stop, step)]
|
|
49
|
-
elif isinstance(key, int):
|
|
50
|
-
return self.events[key]
|
|
51
|
-
else:
|
|
52
|
-
raise ValueError(f"Invalid key type: {type(key)}")
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def from_events(events: list[Event]) -> "View":
|
|
56
|
-
"""Create a view from a list of events, respecting the semantics of any
|
|
57
|
-
condensation events.
|
|
58
|
-
"""
|
|
59
|
-
forgotten_event_ids: set[str] = set()
|
|
60
|
-
for event in events:
|
|
61
|
-
if isinstance(event, Condensation):
|
|
62
|
-
forgotten_event_ids.update(event.forgotten)
|
|
63
|
-
# Make sure we also forget the condensation action itself
|
|
64
|
-
forgotten_event_ids.add(event.id)
|
|
65
|
-
if isinstance(event, CondensationRequest):
|
|
66
|
-
forgotten_event_ids.add(event.id)
|
|
67
|
-
|
|
68
|
-
kept_events = [
|
|
69
|
-
event
|
|
70
|
-
for event in events
|
|
71
|
-
if event.id not in forgotten_event_ids
|
|
72
|
-
and isinstance(event, LLMConvertibleEvent)
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
# If we have a summary, insert it at the specified offset.
|
|
76
|
-
summary: str | None = None
|
|
77
|
-
summary_offset: int | None = None
|
|
78
|
-
|
|
79
|
-
# The relevant summary is always in the last condensation event (i.e., the most
|
|
80
|
-
# recent one).
|
|
81
|
-
for event in reversed(events):
|
|
82
|
-
if isinstance(event, Condensation):
|
|
83
|
-
if event.summary is not None and event.summary_offset is not None:
|
|
84
|
-
summary = event.summary
|
|
85
|
-
summary_offset = event.summary_offset
|
|
86
|
-
break
|
|
87
|
-
|
|
88
|
-
if summary is not None and summary_offset is not None:
|
|
89
|
-
logger.info(f"Inserting summary at offset {summary_offset}")
|
|
90
|
-
|
|
91
|
-
kept_events.insert(
|
|
92
|
-
summary_offset,
|
|
93
|
-
MessageEvent(
|
|
94
|
-
llm_message=Message(
|
|
95
|
-
role="system",
|
|
96
|
-
content=[TextContent(text=summary)],
|
|
97
|
-
name="system",
|
|
98
|
-
),
|
|
99
|
-
source="environment",
|
|
100
|
-
),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# Check for an unhandled condensation request -- these are events closer to the
|
|
104
|
-
# end of the list than any condensation action.
|
|
105
|
-
unhandled_condensation_request = False
|
|
106
|
-
for event in reversed(events):
|
|
107
|
-
if isinstance(event, Condensation):
|
|
108
|
-
break
|
|
109
|
-
if isinstance(event, CondensationRequest):
|
|
110
|
-
unhandled_condensation_request = True
|
|
111
|
-
break
|
|
112
|
-
|
|
113
|
-
return View(
|
|
114
|
-
events=kept_events,
|
|
115
|
-
unhandled_condensation_request=unhandled_condensation_request,
|
|
116
|
-
)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from openhands.sdk.conversation.conversation import Conversation
|
|
2
|
-
from openhands.sdk.conversation.state import ConversationState
|
|
3
|
-
from openhands.sdk.conversation.types import ConversationCallbackType
|
|
4
|
-
from openhands.sdk.conversation.visualizer import ConversationVisualizer
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"Conversation",
|
|
9
|
-
"ConversationState",
|
|
10
|
-
"ConversationCallbackType",
|
|
11
|
-
"ConversationVisualizer",
|
|
12
|
-
]
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, Iterable
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
if TYPE_CHECKING:
|
|
5
|
-
from openhands.sdk.agent import AgentBase
|
|
6
|
-
|
|
7
|
-
from openhands.sdk.conversation.state import ConversationState
|
|
8
|
-
from openhands.sdk.conversation.types import ConversationCallbackType
|
|
9
|
-
from openhands.sdk.conversation.visualizer import ConversationVisualizer
|
|
10
|
-
from openhands.sdk.event import (
|
|
11
|
-
MessageEvent,
|
|
12
|
-
PauseEvent,
|
|
13
|
-
UserRejectObservation,
|
|
14
|
-
)
|
|
15
|
-
from openhands.sdk.event.utils import get_unmatched_actions
|
|
16
|
-
from openhands.sdk.llm import Message, TextContent
|
|
17
|
-
from openhands.sdk.logger import get_logger
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger = get_logger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def compose_callbacks(
|
|
24
|
-
callbacks: Iterable[ConversationCallbackType],
|
|
25
|
-
) -> ConversationCallbackType:
|
|
26
|
-
def composed(event) -> None:
|
|
27
|
-
for cb in callbacks:
|
|
28
|
-
if cb:
|
|
29
|
-
cb(event)
|
|
30
|
-
|
|
31
|
-
return composed
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class Conversation:
|
|
35
|
-
def __init__(
|
|
36
|
-
self,
|
|
37
|
-
agent: "AgentBase",
|
|
38
|
-
callbacks: list[ConversationCallbackType] | None = None,
|
|
39
|
-
max_iteration_per_run: int = 500,
|
|
40
|
-
):
|
|
41
|
-
"""Initialize the conversation."""
|
|
42
|
-
self._visualizer = ConversationVisualizer()
|
|
43
|
-
self.agent = agent
|
|
44
|
-
self.state = ConversationState()
|
|
45
|
-
|
|
46
|
-
# Default callback: persist every event to state
|
|
47
|
-
def _append_event(e):
|
|
48
|
-
self.state.events.append(e)
|
|
49
|
-
|
|
50
|
-
# Compose callbacks; default appender runs last to keep agent-emitted event order (on_event then persist) # noqa: E501
|
|
51
|
-
composed_list = (
|
|
52
|
-
[self._visualizer.on_event]
|
|
53
|
-
+ (callbacks if callbacks else [])
|
|
54
|
-
+ [_append_event]
|
|
55
|
-
)
|
|
56
|
-
self._on_event = compose_callbacks(composed_list)
|
|
57
|
-
|
|
58
|
-
self.max_iteration_per_run = max_iteration_per_run
|
|
59
|
-
|
|
60
|
-
with self.state:
|
|
61
|
-
self.agent.init_state(self.state, on_event=self._on_event)
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def id(self) -> str:
|
|
65
|
-
"""Get the unique ID of the conversation."""
|
|
66
|
-
return self.state.id
|
|
67
|
-
|
|
68
|
-
def send_message(self, message: Message) -> None:
|
|
69
|
-
"""Sending messages to the agent."""
|
|
70
|
-
assert message.role == "user", (
|
|
71
|
-
"Only user messages are allowed to be sent to the agent."
|
|
72
|
-
)
|
|
73
|
-
with self.state:
|
|
74
|
-
if self.state.agent_finished:
|
|
75
|
-
self.state.agent_finished = False # now we have a new message
|
|
76
|
-
|
|
77
|
-
# TODO: We should add test cases for all these scenarios
|
|
78
|
-
activated_microagent_names: list[str] = []
|
|
79
|
-
extended_content: list[TextContent] = []
|
|
80
|
-
|
|
81
|
-
# Handle per-turn user message (i.e., knowledge agent trigger)
|
|
82
|
-
if self.agent.agent_context:
|
|
83
|
-
ctx = self.agent.agent_context.get_user_message_suffix(
|
|
84
|
-
user_message=message,
|
|
85
|
-
# We skip microagents that were already activated
|
|
86
|
-
skip_microagent_names=self.state.activated_knowledge_microagents,
|
|
87
|
-
)
|
|
88
|
-
# TODO(calvin): we need to update
|
|
89
|
-
# self.state.activated_knowledge_microagents
|
|
90
|
-
# so condenser can work
|
|
91
|
-
if ctx:
|
|
92
|
-
content, activated_microagent_names = ctx
|
|
93
|
-
logger.debug(
|
|
94
|
-
f"Got augmented user message content: {content}, "
|
|
95
|
-
f"activated microagents: {activated_microagent_names}"
|
|
96
|
-
)
|
|
97
|
-
extended_content.append(content)
|
|
98
|
-
self.state.activated_knowledge_microagents.extend(
|
|
99
|
-
activated_microagent_names
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
user_msg_event = MessageEvent(
|
|
103
|
-
source="user",
|
|
104
|
-
llm_message=message,
|
|
105
|
-
activated_microagents=activated_microagent_names,
|
|
106
|
-
extended_content=extended_content,
|
|
107
|
-
)
|
|
108
|
-
self._on_event(user_msg_event)
|
|
109
|
-
|
|
110
|
-
def run(self) -> None:
|
|
111
|
-
"""Runs the conversation until the agent finishes.
|
|
112
|
-
|
|
113
|
-
In confirmation mode:
|
|
114
|
-
- First call: creates actions but doesn't execute them, stops and waits
|
|
115
|
-
- Second call: executes pending actions (implicit confirmation)
|
|
116
|
-
|
|
117
|
-
In normal mode:
|
|
118
|
-
- Creates and executes actions immediately
|
|
119
|
-
|
|
120
|
-
Can be paused between steps
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
with self.state:
|
|
124
|
-
self.state.agent_paused = False
|
|
125
|
-
|
|
126
|
-
iteration = 0
|
|
127
|
-
while True:
|
|
128
|
-
logger.debug(f"Conversation run iteration {iteration}")
|
|
129
|
-
# TODO(openhands): we should add a testcase that test IF:
|
|
130
|
-
# 1. a loop is running
|
|
131
|
-
# 2. in a separate thread .send_message is called
|
|
132
|
-
# and check will we be able to execute .send_message
|
|
133
|
-
# BEFORE the .run loop finishes?
|
|
134
|
-
with self.state:
|
|
135
|
-
# Pause attempts to acquire the state lock
|
|
136
|
-
# Before value can be modified step can be taken
|
|
137
|
-
# Ensure step conditions are checked when lock is already acquired
|
|
138
|
-
if self.state.agent_finished or self.state.agent_paused:
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
# clear the flag before calling agent.step() (user approved)
|
|
142
|
-
if self.state.agent_waiting_for_confirmation:
|
|
143
|
-
self.state.agent_waiting_for_confirmation = False
|
|
144
|
-
|
|
145
|
-
# step must mutate the SAME state object
|
|
146
|
-
self.agent.step(self.state, on_event=self._on_event)
|
|
147
|
-
|
|
148
|
-
# In confirmation mode, stop after one iteration if waiting for confirmation
|
|
149
|
-
if self.state.agent_waiting_for_confirmation:
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
iteration += 1
|
|
153
|
-
if iteration >= self.max_iteration_per_run:
|
|
154
|
-
break
|
|
155
|
-
|
|
156
|
-
def set_confirmation_mode(self, enabled: bool) -> None:
|
|
157
|
-
"""Enable or disable confirmation mode and store it in conversation state."""
|
|
158
|
-
with self.state:
|
|
159
|
-
self.state.confirmation_mode = enabled
|
|
160
|
-
logger.info(f"Confirmation mode {'enabled' if enabled else 'disabled'}")
|
|
161
|
-
|
|
162
|
-
def reject_pending_actions(self, reason: str = "User rejected the action") -> None:
|
|
163
|
-
"""Reject all pending actions from the agent.
|
|
164
|
-
|
|
165
|
-
This is a non-invasive method to reject actions between run() calls.
|
|
166
|
-
Also clears the agent_waiting_for_confirmation flag.
|
|
167
|
-
"""
|
|
168
|
-
pending_actions = get_unmatched_actions(self.state.events)
|
|
169
|
-
|
|
170
|
-
with self.state:
|
|
171
|
-
# Always clear the agent_waiting_for_confirmation flag
|
|
172
|
-
self.state.agent_waiting_for_confirmation = False
|
|
173
|
-
|
|
174
|
-
if not pending_actions:
|
|
175
|
-
logger.warning("No pending actions to reject")
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
for action_event in pending_actions:
|
|
179
|
-
# Create rejection observation
|
|
180
|
-
rejection_event = UserRejectObservation(
|
|
181
|
-
action_id=action_event.id,
|
|
182
|
-
tool_name=action_event.tool_name,
|
|
183
|
-
tool_call_id=action_event.tool_call_id,
|
|
184
|
-
rejection_reason=reason,
|
|
185
|
-
)
|
|
186
|
-
self._on_event(rejection_event)
|
|
187
|
-
logger.info(f"Rejected pending action: {action_event} - {reason}")
|
|
188
|
-
|
|
189
|
-
def pause(self) -> None:
|
|
190
|
-
"""Pause agent execution.
|
|
191
|
-
|
|
192
|
-
This method can be called from any thread to request that the agent
|
|
193
|
-
pause execution. The pause will take effect at the next iteration
|
|
194
|
-
of the run loop (between agent steps).
|
|
195
|
-
|
|
196
|
-
Note: If called during an LLM completion, the pause will not take
|
|
197
|
-
effect until the current LLM call completes.
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
if self.state.agent_paused:
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
with self.state:
|
|
204
|
-
self.state.agent_paused = True
|
|
205
|
-
pause_event = PauseEvent()
|
|
206
|
-
self._on_event(pause_event)
|
|
207
|
-
logger.info("Agent execution pause requested")
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from threading import RLock, get_ident
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
6
|
-
|
|
7
|
-
from openhands.sdk.event import Event
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ConversationState(BaseModel):
|
|
11
|
-
model_config = ConfigDict(
|
|
12
|
-
arbitrary_types_allowed=True, # allow RLock in PrivateAttr
|
|
13
|
-
validate_assignment=True, # validate on attribute set
|
|
14
|
-
frozen=False,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
# Public, validated fields
|
|
18
|
-
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
19
|
-
events: list[Event] = Field(default_factory=list)
|
|
20
|
-
agent_finished: bool = False
|
|
21
|
-
confirmation_mode: bool = False
|
|
22
|
-
agent_waiting_for_confirmation: bool = False
|
|
23
|
-
agent_paused: bool = False
|
|
24
|
-
activated_knowledge_microagents: list[str] = Field(
|
|
25
|
-
default_factory=list, description="List of activated knowledge microagents name"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# Private attrs (NOT Fields) — allowed to start with underscore
|
|
29
|
-
_lock: RLock = PrivateAttr(default_factory=RLock)
|
|
30
|
-
_owner_tid: Optional[int] = PrivateAttr(default=None)
|
|
31
|
-
|
|
32
|
-
# Lock/guard API
|
|
33
|
-
def acquire(self) -> None:
|
|
34
|
-
self._lock.acquire()
|
|
35
|
-
self._owner_tid = get_ident()
|
|
36
|
-
|
|
37
|
-
def release(self) -> None:
|
|
38
|
-
self._owner_tid = None
|
|
39
|
-
self._lock.release()
|
|
40
|
-
|
|
41
|
-
def __enter__(self) -> "ConversationState":
|
|
42
|
-
self.acquire()
|
|
43
|
-
return self
|
|
44
|
-
|
|
45
|
-
def __exit__(self, exc_type, exc, tb) -> None:
|
|
46
|
-
self.release()
|
|
47
|
-
|
|
48
|
-
def assert_locked(self) -> None:
|
|
49
|
-
if self._owner_tid != get_ident():
|
|
50
|
-
raise RuntimeError("State not held by current thread")
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import cast
|
|
3
|
-
|
|
4
|
-
from rich.console import Console
|
|
5
|
-
from rich.panel import Panel
|
|
6
|
-
from rich.text import Text
|
|
7
|
-
|
|
8
|
-
from openhands.sdk.event import (
|
|
9
|
-
ActionEvent,
|
|
10
|
-
AgentErrorEvent,
|
|
11
|
-
Event,
|
|
12
|
-
MessageEvent,
|
|
13
|
-
ObservationEvent,
|
|
14
|
-
PauseEvent,
|
|
15
|
-
SystemPromptEvent,
|
|
16
|
-
)
|
|
17
|
-
from openhands.sdk.llm import ImageContent, TextContent, content_to_str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ConversationVisualizer:
|
|
21
|
-
"""Handles visualization of conversation events with Rich formatting.
|
|
22
|
-
|
|
23
|
-
Provides Rich-formatted output with panels and complete content display.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(self):
|
|
27
|
-
self._console = Console()
|
|
28
|
-
|
|
29
|
-
def on_event(self, event: Event) -> None:
|
|
30
|
-
"""Main event handler that displays events with Rich formatting."""
|
|
31
|
-
panel = self._create_event_panel(event)
|
|
32
|
-
self._console.print(panel)
|
|
33
|
-
self._console.print() # Add spacing between events
|
|
34
|
-
|
|
35
|
-
def _create_event_panel(self, event: Event) -> Panel:
|
|
36
|
-
"""Create a Rich Panel for the event with appropriate styling."""
|
|
37
|
-
if isinstance(event, SystemPromptEvent):
|
|
38
|
-
return self._create_system_prompt_panel(event)
|
|
39
|
-
elif isinstance(event, ActionEvent):
|
|
40
|
-
return self._create_action_panel(event)
|
|
41
|
-
elif isinstance(event, ObservationEvent):
|
|
42
|
-
return self._create_observation_panel(event)
|
|
43
|
-
elif isinstance(event, MessageEvent):
|
|
44
|
-
return self._create_message_panel(event)
|
|
45
|
-
elif isinstance(event, AgentErrorEvent):
|
|
46
|
-
return self._create_error_panel(event)
|
|
47
|
-
elif isinstance(event, PauseEvent):
|
|
48
|
-
return self._create_pause_panel(event)
|
|
49
|
-
else:
|
|
50
|
-
# Fallback panel for unknown event types
|
|
51
|
-
content = Text(f"Unknown event type: {event.__class__.__name__}")
|
|
52
|
-
return Panel(
|
|
53
|
-
content,
|
|
54
|
-
title=f"[bold blue]{event.__class__.__name__}[/bold blue]",
|
|
55
|
-
subtitle=f"[dim]({event.source})[/dim]",
|
|
56
|
-
border_style="blue",
|
|
57
|
-
expand=True,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
def _create_system_prompt_panel(self, event: SystemPromptEvent) -> Panel:
|
|
61
|
-
"""Create a Rich Panel for SystemPromptEvent with complete content."""
|
|
62
|
-
content = Text()
|
|
63
|
-
content.append("System Prompt:\n", style="bold cyan")
|
|
64
|
-
content.append(event.system_prompt.text, style="white")
|
|
65
|
-
content.append(f"\n\nTools Available: {len(event.tools)}", style="cyan")
|
|
66
|
-
for tool in event.tools:
|
|
67
|
-
tool_fn = tool.get("function", None)
|
|
68
|
-
# make each field short
|
|
69
|
-
for k, v in tool.items():
|
|
70
|
-
if isinstance(v, str) and len(v) > 30:
|
|
71
|
-
tool[k] = v[:27] + "..."
|
|
72
|
-
if tool_fn:
|
|
73
|
-
assert "name" in tool_fn
|
|
74
|
-
assert "description" in tool_fn
|
|
75
|
-
assert "parameters" in tool_fn
|
|
76
|
-
params_str = json.dumps(tool_fn["parameters"])
|
|
77
|
-
content.append(
|
|
78
|
-
f"\n - {tool_fn['name']}: "
|
|
79
|
-
f"{tool_fn['description'].split('\n')[0][:100]}...\n",
|
|
80
|
-
style="dim cyan",
|
|
81
|
-
)
|
|
82
|
-
content.append(f" Parameters: {params_str}", style="dim white")
|
|
83
|
-
else:
|
|
84
|
-
content.append(
|
|
85
|
-
f"\n - Cannot access .function for {tool}", style="dim cyan"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
return Panel(
|
|
89
|
-
content,
|
|
90
|
-
title="[bold magenta]System Prompt[/bold magenta]",
|
|
91
|
-
border_style="magenta",
|
|
92
|
-
expand=True,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
def _format_metrics_subtitle(
|
|
96
|
-
self, event: ActionEvent | MessageEvent | AgentErrorEvent
|
|
97
|
-
) -> str | None:
|
|
98
|
-
"""Format LLM metrics as a visually appealing subtitle string with icons,
|
|
99
|
-
colors, and k/m abbreviations (cache hit rate only)."""
|
|
100
|
-
if not event.metrics or not event.metrics.accumulated_token_usage:
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
usage = event.metrics.accumulated_token_usage
|
|
104
|
-
cost = event.metrics.accumulated_cost or 0.0
|
|
105
|
-
|
|
106
|
-
# helper: 1234 -> "1.2K", 1200000 -> "1.2M"
|
|
107
|
-
def abbr(n: int | float) -> str:
|
|
108
|
-
n = int(n or 0)
|
|
109
|
-
if n >= 1_000_000_000:
|
|
110
|
-
s = f"{n / 1_000_000_000:.2f}B"
|
|
111
|
-
elif n >= 1_000_000:
|
|
112
|
-
s = f"{n / 1_000_000:.2f}M"
|
|
113
|
-
elif n >= 1_000:
|
|
114
|
-
s = f"{n / 1_000:.2f}K"
|
|
115
|
-
else:
|
|
116
|
-
return str(n)
|
|
117
|
-
return s.replace(".0", "")
|
|
118
|
-
|
|
119
|
-
input_tokens = abbr(usage.prompt_tokens or 0)
|
|
120
|
-
output_tokens = abbr(usage.completion_tokens or 0)
|
|
121
|
-
|
|
122
|
-
# Cache hit rate (prompt + cache)
|
|
123
|
-
prompt = usage.prompt_tokens or 0
|
|
124
|
-
cache_read = usage.cache_read_tokens or 0
|
|
125
|
-
cache_rate = f"{(cache_read / prompt * 100):.2f}%" if prompt > 0 else "N/A"
|
|
126
|
-
reasoning_tokens = usage.reasoning_tokens or 0
|
|
127
|
-
|
|
128
|
-
# Cost
|
|
129
|
-
cost_str = f"{cost:.4f}" if cost > 0 else "$0.00"
|
|
130
|
-
|
|
131
|
-
# Build with fixed color scheme
|
|
132
|
-
parts: list[str] = []
|
|
133
|
-
parts.append(f"[cyan]↑ input {input_tokens}[/cyan]")
|
|
134
|
-
parts.append(f"[magenta]cache hit {cache_rate}[/magenta]")
|
|
135
|
-
if reasoning_tokens > 0:
|
|
136
|
-
parts.append(f"[yellow] reasoning {abbr(reasoning_tokens)}[/yellow]")
|
|
137
|
-
parts.append(f"[blue]↓ output {output_tokens}[/blue]")
|
|
138
|
-
parts.append(f"[green]$ {cost_str}[/green]")
|
|
139
|
-
|
|
140
|
-
return "Tokens: " + " [dim]•[/dim] ".join(parts)
|
|
141
|
-
|
|
142
|
-
def _create_action_panel(self, event: ActionEvent) -> Panel:
|
|
143
|
-
"""Create a Rich Panel for ActionEvent with complete content."""
|
|
144
|
-
content = Text()
|
|
145
|
-
|
|
146
|
-
# Display reasoning content first if available (common to all three types)
|
|
147
|
-
if event.reasoning_content:
|
|
148
|
-
content.append("Reasoning:\n", style="bold magenta")
|
|
149
|
-
content.append(event.reasoning_content, style="white")
|
|
150
|
-
content.append("\n\n")
|
|
151
|
-
|
|
152
|
-
# Display complete thought content
|
|
153
|
-
thought_text = " ".join([t.text for t in event.thought])
|
|
154
|
-
if thought_text:
|
|
155
|
-
content.append("Thought:\n", style="bold green")
|
|
156
|
-
content.append(thought_text, style="white")
|
|
157
|
-
content.append("\n\n")
|
|
158
|
-
|
|
159
|
-
# Display action information
|
|
160
|
-
action_name = event.action.__class__.__name__
|
|
161
|
-
content.append("Action: ", style="bold green")
|
|
162
|
-
content.append(action_name, style="yellow")
|
|
163
|
-
content.append("\n\n")
|
|
164
|
-
|
|
165
|
-
# Display all action fields systematically
|
|
166
|
-
content.append("Action Fields:\n", style="bold green")
|
|
167
|
-
action_fields = event.action.model_dump()
|
|
168
|
-
for field_name, field_value in action_fields.items():
|
|
169
|
-
content.append(f" {field_name}: ", style="cyan")
|
|
170
|
-
if field_value is None:
|
|
171
|
-
content.append("None", style="dim white")
|
|
172
|
-
elif isinstance(field_value, str):
|
|
173
|
-
# Handle multiline strings with proper indentation
|
|
174
|
-
if "\n" in field_value:
|
|
175
|
-
content.append("\n", style="white")
|
|
176
|
-
for line in field_value.split("\n"):
|
|
177
|
-
content.append(f" {line}\n", style="white")
|
|
178
|
-
else:
|
|
179
|
-
content.append(f'"{field_value}"', style="white")
|
|
180
|
-
elif isinstance(field_value, (list, dict)):
|
|
181
|
-
content.append(str(field_value), style="white")
|
|
182
|
-
else:
|
|
183
|
-
content.append(str(field_value), style="white")
|
|
184
|
-
content.append("\n")
|
|
185
|
-
|
|
186
|
-
return Panel(
|
|
187
|
-
content,
|
|
188
|
-
title="[bold green]Agent Action[/bold green]",
|
|
189
|
-
subtitle=self._format_metrics_subtitle(event),
|
|
190
|
-
border_style="green",
|
|
191
|
-
expand=True,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
def _create_observation_panel(self, event: ObservationEvent) -> Panel:
|
|
195
|
-
"""Create a Rich Panel for ObservationEvent with complete content."""
|
|
196
|
-
content = Text()
|
|
197
|
-
content.append("Tool: ", style="bold blue")
|
|
198
|
-
content.append(event.tool_name, style="cyan")
|
|
199
|
-
content.append("\n\nResult:\n", style="bold blue")
|
|
200
|
-
|
|
201
|
-
text_parts = content_to_str(event.observation.agent_observation)
|
|
202
|
-
if text_parts:
|
|
203
|
-
full_content = " ".join(text_parts)
|
|
204
|
-
content.append(full_content, style="white")
|
|
205
|
-
else:
|
|
206
|
-
content.append("[no text content]", style="dim white")
|
|
207
|
-
|
|
208
|
-
return Panel(
|
|
209
|
-
content,
|
|
210
|
-
title="[bold blue]Tool Observation[/bold blue]",
|
|
211
|
-
border_style="blue",
|
|
212
|
-
expand=True,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
def _create_message_panel(self, event: MessageEvent) -> Panel:
|
|
216
|
-
"""Create a Rich Panel for MessageEvent with complete content."""
|
|
217
|
-
content = Text()
|
|
218
|
-
|
|
219
|
-
# Role-based styling
|
|
220
|
-
role_colors = {
|
|
221
|
-
"user": "bright_cyan",
|
|
222
|
-
"assistant": "bright_green",
|
|
223
|
-
"system": "bright_magenta",
|
|
224
|
-
}
|
|
225
|
-
role_color = role_colors.get(event.llm_message.role, "white")
|
|
226
|
-
|
|
227
|
-
content.append(
|
|
228
|
-
f"{event.llm_message.role.title()}:\n", style=f"bold {role_color}"
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
text_parts = content_to_str(event.llm_message.content)
|
|
232
|
-
if text_parts:
|
|
233
|
-
full_content = " ".join(text_parts)
|
|
234
|
-
content.append(full_content, style="white")
|
|
235
|
-
else:
|
|
236
|
-
content.append("[no text content]", style="dim white")
|
|
237
|
-
|
|
238
|
-
# Add microagent information if present
|
|
239
|
-
if event.activated_microagents:
|
|
240
|
-
content.append(
|
|
241
|
-
f"\n\nActivated Microagents: {', '.join(event.activated_microagents)}",
|
|
242
|
-
style="dim yellow",
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
# Add extended content if available
|
|
246
|
-
if event.extended_content:
|
|
247
|
-
assert not any(
|
|
248
|
-
isinstance(c, ImageContent) for c in event.extended_content
|
|
249
|
-
), "Extended content should not contain images"
|
|
250
|
-
text_parts = content_to_str(
|
|
251
|
-
cast(list[TextContent | ImageContent], event.extended_content)
|
|
252
|
-
)
|
|
253
|
-
content.append("\n\nExtended Content:\n", style="dim yellow")
|
|
254
|
-
content.append(" ".join(text_parts), style="white")
|
|
255
|
-
|
|
256
|
-
# Panel styling based on role
|
|
257
|
-
panel_colors = {
|
|
258
|
-
"user": "cyan",
|
|
259
|
-
"assistant": "green",
|
|
260
|
-
"system": "magenta",
|
|
261
|
-
}
|
|
262
|
-
border_color = panel_colors.get(event.llm_message.role, "white")
|
|
263
|
-
|
|
264
|
-
title_text = (
|
|
265
|
-
f"[bold {role_color}]Message (source={event.source})[/bold {role_color}]"
|
|
266
|
-
)
|
|
267
|
-
return Panel(
|
|
268
|
-
content,
|
|
269
|
-
title=title_text,
|
|
270
|
-
subtitle=self._format_metrics_subtitle(event),
|
|
271
|
-
border_style=border_color,
|
|
272
|
-
expand=True,
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
def _create_error_panel(self, event: AgentErrorEvent) -> Panel:
|
|
276
|
-
"""Create a Rich Panel for AgentErrorEvent with complete content."""
|
|
277
|
-
content = Text()
|
|
278
|
-
|
|
279
|
-
content.append("Error Details:\n", style="bold red")
|
|
280
|
-
content.append(event.error, style="bright_red")
|
|
281
|
-
|
|
282
|
-
return Panel(
|
|
283
|
-
content,
|
|
284
|
-
title="[bold red]Agent Error[/bold red]",
|
|
285
|
-
subtitle=self._format_metrics_subtitle(event),
|
|
286
|
-
border_style="red",
|
|
287
|
-
expand=True,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
def _create_pause_panel(self, event: PauseEvent) -> Panel:
|
|
291
|
-
"""Create a Rich Panel for PauseEvent with complete content."""
|
|
292
|
-
content = Text()
|
|
293
|
-
content.append("Conversation Paused", style="bold yellow")
|
|
294
|
-
|
|
295
|
-
return Panel(
|
|
296
|
-
content,
|
|
297
|
-
title="[bold yellow]User Paused[/bold yellow]",
|
|
298
|
-
border_style="yellow",
|
|
299
|
-
expand=True,
|
|
300
|
-
)
|