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.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. openhands-0.0.0.dist-info/top_level.txt +0 -1
@@ -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,6 +0,0 @@
1
- from typing import Callable
2
-
3
- from openhands.sdk.event import Event
4
-
5
-
6
- ConversationCallbackType = Callable[[Event], None]
@@ -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
- )