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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
6
|
+
from prompt_toolkit.input import Input, create_input
|
|
7
|
+
from prompt_toolkit.keys import Keys
|
|
8
|
+
|
|
9
|
+
from openhands.sdk import BaseConversation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PauseListener(threading.Thread):
|
|
13
|
+
"""Background key listener that triggers pause on Ctrl-P.
|
|
14
|
+
|
|
15
|
+
Starts and stops around agent run() loops to avoid interfering with user prompts.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
on_pause: Callable,
|
|
21
|
+
input_source: Input | None = None, # used to pipe inputs for unit tests
|
|
22
|
+
):
|
|
23
|
+
super().__init__(daemon=True)
|
|
24
|
+
self.on_pause = on_pause
|
|
25
|
+
self._stop_event = threading.Event()
|
|
26
|
+
self._pause_event = threading.Event()
|
|
27
|
+
self._input = input_source or create_input()
|
|
28
|
+
|
|
29
|
+
def _detect_pause_key_presses(self) -> bool:
|
|
30
|
+
pause_detected = False
|
|
31
|
+
|
|
32
|
+
for key_press in self._input.read_keys():
|
|
33
|
+
pause_detected = pause_detected or key_press.key == Keys.ControlP
|
|
34
|
+
pause_detected = pause_detected or key_press.key == Keys.ControlC
|
|
35
|
+
pause_detected = pause_detected or key_press.key == Keys.ControlD
|
|
36
|
+
|
|
37
|
+
return pause_detected
|
|
38
|
+
|
|
39
|
+
def _execute_pause(self) -> None:
|
|
40
|
+
self._pause_event.set() # Mark pause event occurred
|
|
41
|
+
print_formatted_text(HTML(''))
|
|
42
|
+
print_formatted_text(
|
|
43
|
+
HTML('<gold>Pausing agent once step is completed...</gold>')
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
self.on_pause()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def run(self) -> None:
|
|
51
|
+
try:
|
|
52
|
+
with self._input.raw_mode():
|
|
53
|
+
# User hasn't paused and pause listener hasn't been shut down
|
|
54
|
+
while not (self.is_paused() or self.is_stopped()):
|
|
55
|
+
if self._detect_pause_key_presses():
|
|
56
|
+
self._execute_pause()
|
|
57
|
+
finally:
|
|
58
|
+
try:
|
|
59
|
+
self._input.close()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def stop(self) -> None:
|
|
64
|
+
self._stop_event.set()
|
|
65
|
+
|
|
66
|
+
def is_stopped(self) -> bool:
|
|
67
|
+
return self._stop_event.is_set()
|
|
68
|
+
|
|
69
|
+
def is_paused(self) -> bool:
|
|
70
|
+
return self._pause_event.is_set()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@contextmanager
|
|
74
|
+
def pause_listener(
|
|
75
|
+
conversation: BaseConversation, input_source: Input | None = None
|
|
76
|
+
) -> Iterator[PauseListener]:
|
|
77
|
+
"""Ensure PauseListener always starts/stops cleanly."""
|
|
78
|
+
listener = PauseListener(on_pause=conversation.pause, input_source=input_source)
|
|
79
|
+
listener.start()
|
|
80
|
+
try:
|
|
81
|
+
yield listener
|
|
82
|
+
finally:
|
|
83
|
+
listener.stop()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Utility functions for LLM configuration in OpenHands CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_llm_metadata(
|
|
8
|
+
model_name: str,
|
|
9
|
+
llm_type: str,
|
|
10
|
+
session_id: str | None = None,
|
|
11
|
+
user_id: str | None = None,
|
|
12
|
+
) -> dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Generate LLM metadata for OpenHands CLI.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
model_name: Name of the LLM model
|
|
18
|
+
agent_name: Name of the agent (defaults to "openhands")
|
|
19
|
+
session_id: Optional session identifier
|
|
20
|
+
user_id: Optional user identifier
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dictionary containing metadata for LLM initialization
|
|
24
|
+
"""
|
|
25
|
+
# Import here to avoid circular imports
|
|
26
|
+
openhands_sdk_version: str = 'n/a'
|
|
27
|
+
try:
|
|
28
|
+
import openhands.sdk
|
|
29
|
+
|
|
30
|
+
openhands_sdk_version = openhands.sdk.__version__
|
|
31
|
+
except (ModuleNotFoundError, AttributeError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
openhands_tools_version: str = 'n/a'
|
|
35
|
+
try:
|
|
36
|
+
import openhands.tools
|
|
37
|
+
|
|
38
|
+
openhands_tools_version = openhands.tools.__version__
|
|
39
|
+
except (ModuleNotFoundError, AttributeError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
metadata = {
|
|
43
|
+
'trace_version': openhands_sdk_version,
|
|
44
|
+
'tags': [
|
|
45
|
+
'app:openhands',
|
|
46
|
+
f'model:{model_name}',
|
|
47
|
+
f'type:{llm_type}',
|
|
48
|
+
f'web_host:{os.environ.get("WEB_HOST", "unspecified")}',
|
|
49
|
+
f'openhands_sdk_version:{openhands_sdk_version}',
|
|
50
|
+
f'openhands_tools_version:{openhands_tools_version}',
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
if session_id is not None:
|
|
54
|
+
metadata['session_id'] = session_id
|
|
55
|
+
if user_id is not None:
|
|
56
|
+
metadata['trace_user_id'] = user_id
|
|
57
|
+
return metadata
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
# Configuration directory for storing agent settings and CLI configuration
|
|
4
|
+
PERSISTENCE_DIR = os.path.expanduser('~/.openhands')
|
|
5
|
+
CONVERSATIONS_DIR = os.path.join(PERSISTENCE_DIR, 'conversations')
|
|
6
|
+
|
|
7
|
+
# Working directory for agent operations (current directory where CLI is run)
|
|
8
|
+
WORK_DIR = os.getcwd()
|
|
9
|
+
|
|
10
|
+
AGENT_SETTINGS_PATH = 'agent_settings.json'
|
|
11
|
+
|
|
12
|
+
# MCP configuration file (relative to PERSISTENCE_DIR)
|
|
13
|
+
MCP_CONFIG_FILE = 'mcp.json'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from prompt_toolkit.styles import Style, merge_styles
|
|
2
|
+
from prompt_toolkit.styles.base import BaseStyle
|
|
3
|
+
from prompt_toolkit.styles.defaults import default_ui_style
|
|
4
|
+
|
|
5
|
+
# Centralized helper for CLI styles so we can safely merge our custom colors
|
|
6
|
+
# with prompt_toolkit's default UI style. This preserves completion menu and
|
|
7
|
+
# fuzzy-match visibility across different terminal themes (e.g., Ubuntu).
|
|
8
|
+
|
|
9
|
+
COLOR_GOLD = '#FFD700'
|
|
10
|
+
COLOR_GREY = '#808080'
|
|
11
|
+
COLOR_AGENT_BLUE = '#4682B4' # Steel blue - readable on light/dark backgrounds
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_cli_style() -> BaseStyle:
|
|
15
|
+
base = default_ui_style()
|
|
16
|
+
custom = Style.from_dict(
|
|
17
|
+
{
|
|
18
|
+
'gold': COLOR_GOLD,
|
|
19
|
+
'grey': COLOR_GREY,
|
|
20
|
+
'prompt': f'{COLOR_GOLD} bold',
|
|
21
|
+
# Ensure good contrast for fuzzy matches on the selected completion row
|
|
22
|
+
# across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
|
|
23
|
+
# See https://github.com/All-Hands-AI/OpenHands/issues/10330
|
|
24
|
+
'completion-menu.completion.current fuzzymatch.outside': 'fg:#ffffff bg:#888888',
|
|
25
|
+
'selected': COLOR_GOLD,
|
|
26
|
+
'risk-high': '#FF0000 bold', # Red bold for HIGH risk
|
|
27
|
+
'placeholder': '#888888 italic',
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
return merge_styles([base, custom])
|
openhands_cli/runner.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
2
|
+
|
|
3
|
+
from openhands.sdk import BaseConversation, Message
|
|
4
|
+
from openhands.sdk.conversation.state import AgentExecutionStatus, ConversationState
|
|
5
|
+
from openhands.sdk.security.confirmation_policy import (
|
|
6
|
+
AlwaysConfirm,
|
|
7
|
+
ConfirmationPolicyBase,
|
|
8
|
+
ConfirmRisky,
|
|
9
|
+
NeverConfirm,
|
|
10
|
+
)
|
|
11
|
+
from openhands_cli.listeners.pause_listener import PauseListener, pause_listener
|
|
12
|
+
from openhands_cli.user_actions import ask_user_confirmation
|
|
13
|
+
from openhands_cli.user_actions.types import UserConfirmation
|
|
14
|
+
from openhands_cli.setup import setup_conversation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConversationRunner:
|
|
18
|
+
"""Handles the conversation state machine logic cleanly."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, conversation: BaseConversation):
|
|
21
|
+
self.conversation = conversation
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_confirmation_mode_active(self):
|
|
25
|
+
return self.conversation.is_confirmation_mode_active
|
|
26
|
+
|
|
27
|
+
def toggle_confirmation_mode(self):
|
|
28
|
+
new_confirmation_mode_state = not self.is_confirmation_mode_active
|
|
29
|
+
|
|
30
|
+
self.conversation = setup_conversation(
|
|
31
|
+
self.conversation.id,
|
|
32
|
+
include_security_analyzer=new_confirmation_mode_state
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if new_confirmation_mode_state:
|
|
36
|
+
# Enable confirmation mode: set AlwaysConfirm policy
|
|
37
|
+
self.set_confirmation_policy(AlwaysConfirm())
|
|
38
|
+
else:
|
|
39
|
+
# Disable confirmation mode: set NeverConfirm policy and remove security analyzer
|
|
40
|
+
self.set_confirmation_policy(NeverConfirm())
|
|
41
|
+
|
|
42
|
+
def set_confirmation_policy(
|
|
43
|
+
self, confirmation_policy: ConfirmationPolicyBase
|
|
44
|
+
) -> None:
|
|
45
|
+
self.conversation.set_confirmation_policy(confirmation_policy)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _start_listener(self) -> None:
|
|
49
|
+
self.listener = PauseListener(on_pause=self.conversation.pause)
|
|
50
|
+
self.listener.start()
|
|
51
|
+
|
|
52
|
+
def _print_run_status(self) -> None:
|
|
53
|
+
print_formatted_text('')
|
|
54
|
+
if self.conversation.state.agent_status == AgentExecutionStatus.PAUSED:
|
|
55
|
+
print_formatted_text(
|
|
56
|
+
HTML(
|
|
57
|
+
'<yellow>Resuming paused conversation...</yellow><grey> (Press Ctrl-P to pause)</grey>'
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
print_formatted_text(
|
|
63
|
+
HTML(
|
|
64
|
+
'<yellow>Agent running...</yellow><grey> (Press Ctrl-P to pause)</grey>'
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
print_formatted_text('')
|
|
68
|
+
|
|
69
|
+
def process_message(self, message: Message | None) -> None:
|
|
70
|
+
"""Process a user message through the conversation.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: The user message to process
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
self._print_run_status()
|
|
77
|
+
|
|
78
|
+
# Send message to conversation
|
|
79
|
+
if message:
|
|
80
|
+
self.conversation.send_message(message)
|
|
81
|
+
|
|
82
|
+
if self.is_confirmation_mode_active:
|
|
83
|
+
self._run_with_confirmation()
|
|
84
|
+
else:
|
|
85
|
+
self._run_without_confirmation()
|
|
86
|
+
|
|
87
|
+
def _run_without_confirmation(self) -> None:
|
|
88
|
+
with pause_listener(self.conversation):
|
|
89
|
+
self.conversation.run()
|
|
90
|
+
|
|
91
|
+
def _run_with_confirmation(self) -> None:
|
|
92
|
+
# If agent was paused, resume with confirmation request
|
|
93
|
+
if (
|
|
94
|
+
self.conversation.state.agent_status
|
|
95
|
+
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
|
96
|
+
):
|
|
97
|
+
user_confirmation = self._handle_confirmation_request()
|
|
98
|
+
if user_confirmation == UserConfirmation.DEFER:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
with pause_listener(self.conversation) as listener:
|
|
103
|
+
self.conversation.run()
|
|
104
|
+
|
|
105
|
+
if listener.is_paused():
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
# In confirmation mode, agent either finishes or waits for user confirmation
|
|
109
|
+
if self.conversation.state.agent_status == AgentExecutionStatus.FINISHED:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
elif (
|
|
113
|
+
self.conversation.state.agent_status
|
|
114
|
+
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
|
115
|
+
):
|
|
116
|
+
user_confirmation = self._handle_confirmation_request()
|
|
117
|
+
if user_confirmation == UserConfirmation.DEFER:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
raise Exception('Infinite loop')
|
|
122
|
+
|
|
123
|
+
def _handle_confirmation_request(self) -> UserConfirmation:
|
|
124
|
+
"""Handle confirmation request from user.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
UserConfirmation indicating the user's choice
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
pending_actions = ConversationState.get_unmatched_actions(
|
|
131
|
+
self.conversation.state.events
|
|
132
|
+
)
|
|
133
|
+
if not pending_actions:
|
|
134
|
+
return UserConfirmation.ACCEPT
|
|
135
|
+
|
|
136
|
+
result = ask_user_confirmation(
|
|
137
|
+
pending_actions,
|
|
138
|
+
isinstance(self.conversation.state.confirmation_policy, ConfirmRisky),
|
|
139
|
+
)
|
|
140
|
+
decision = result.decision
|
|
141
|
+
policy_change = result.policy_change
|
|
142
|
+
|
|
143
|
+
if decision == UserConfirmation.REJECT:
|
|
144
|
+
self.conversation.reject_pending_actions(
|
|
145
|
+
result.reason or 'User rejected the actions'
|
|
146
|
+
)
|
|
147
|
+
return decision
|
|
148
|
+
|
|
149
|
+
if decision == UserConfirmation.DEFER:
|
|
150
|
+
self.conversation.pause()
|
|
151
|
+
return decision
|
|
152
|
+
|
|
153
|
+
if isinstance(policy_change, NeverConfirm):
|
|
154
|
+
print_formatted_text(
|
|
155
|
+
HTML(
|
|
156
|
+
'<yellow>Confirmation mode disabled. Agent will proceed without asking.</yellow>'
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Remove security analyzer when policy is never confirm
|
|
161
|
+
self.toggle_confirmation_mode()
|
|
162
|
+
return decision
|
|
163
|
+
|
|
164
|
+
if isinstance(policy_change, ConfirmRisky):
|
|
165
|
+
print_formatted_text(
|
|
166
|
+
HTML(
|
|
167
|
+
'<yellow>Security-based confirmation enabled. '
|
|
168
|
+
'LOW/MEDIUM risk actions will auto-confirm, HIGH risk actions will ask for confirmation.</yellow>'
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Keep security analyzer, change existing policy
|
|
173
|
+
self.set_confirmation_policy(policy_change)
|
|
174
|
+
return decision
|
|
175
|
+
|
|
176
|
+
# Accept action without changing existing policies
|
|
177
|
+
assert decision == UserConfirmation.ACCEPT
|
|
178
|
+
return decision
|
openhands_cli/setup.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
4
|
+
|
|
5
|
+
from openhands.sdk import BaseConversation, Conversation, Workspace, register_tool
|
|
6
|
+
from openhands.tools.execute_bash import BashTool
|
|
7
|
+
from openhands.tools.file_editor import FileEditorTool
|
|
8
|
+
from openhands.tools.task_tracker import TaskTrackerTool
|
|
9
|
+
from openhands_cli.listeners import LoadingContext
|
|
10
|
+
from openhands_cli.locations import CONVERSATIONS_DIR, WORK_DIR
|
|
11
|
+
from openhands_cli.tui.settings.store import AgentStore
|
|
12
|
+
from openhands.sdk.security.confirmation_policy import (
|
|
13
|
+
AlwaysConfirm,
|
|
14
|
+
)
|
|
15
|
+
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
register_tool('BashTool', BashTool)
|
|
19
|
+
register_tool('FileEditorTool', FileEditorTool)
|
|
20
|
+
register_tool('TaskTrackerTool', TaskTrackerTool)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MissingAgentSpec(Exception):
|
|
24
|
+
"""Raised when agent specification is not found or invalid."""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def setup_conversation(
|
|
30
|
+
conversation_id: str | None = None,
|
|
31
|
+
include_security_analyzer: bool = True
|
|
32
|
+
) -> BaseConversation:
|
|
33
|
+
"""
|
|
34
|
+
Setup the conversation with agent.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
conversation_id: conversation ID to use. If not provided, a random UUID will be generated.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
MissingAgentSpec: If agent specification is not found or invalid.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Use provided conversation_id or generate a random one
|
|
44
|
+
if conversation_id is None:
|
|
45
|
+
conversation_id = uuid.uuid4()
|
|
46
|
+
elif isinstance(conversation_id, str):
|
|
47
|
+
try:
|
|
48
|
+
conversation_id = uuid.UUID(conversation_id)
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
print_formatted_text(
|
|
51
|
+
HTML(
|
|
52
|
+
f"<yellow>Warning: '{conversation_id}' is not a valid UUID.</yellow>"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
raise e
|
|
56
|
+
|
|
57
|
+
with LoadingContext('Initializing OpenHands agent...'):
|
|
58
|
+
agent_store = AgentStore()
|
|
59
|
+
agent = agent_store.load(session_id=str(conversation_id))
|
|
60
|
+
if not agent:
|
|
61
|
+
raise MissingAgentSpec(
|
|
62
|
+
'Agent specification not found. Please configure your agent settings.'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if not include_security_analyzer:
|
|
67
|
+
# Remove security analyzer from agent spec
|
|
68
|
+
agent = agent.model_copy(
|
|
69
|
+
update={"security_analyzer": None}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create conversation - agent context is now set in AgentStore.load()
|
|
73
|
+
conversation: BaseConversation = Conversation(
|
|
74
|
+
agent=agent,
|
|
75
|
+
workspace=Workspace(working_dir=WORK_DIR),
|
|
76
|
+
# Conversation will add /<conversation_id> to this path
|
|
77
|
+
persistence_dir=CONVERSATIONS_DIR,
|
|
78
|
+
conversation_id=conversation_id,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if include_security_analyzer:
|
|
82
|
+
conversation.set_confirmation_policy(AlwaysConfirm())
|
|
83
|
+
|
|
84
|
+
print_formatted_text(
|
|
85
|
+
HTML(f'<green>✓ Agent initialized with model: {agent.llm.model}</green>')
|
|
86
|
+
)
|
|
87
|
+
return conversation
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def start_fresh_conversation(
|
|
92
|
+
resume_conversation_id: str | None = None
|
|
93
|
+
) -> BaseConversation:
|
|
94
|
+
"""Start a fresh conversation by creating a new conversation instance.
|
|
95
|
+
|
|
96
|
+
Handles the complete conversation setup process including settings screen
|
|
97
|
+
if agent configuration is missing.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
resume_conversation_id: Optional conversation ID to resume
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
BaseConversation: A new conversation instance
|
|
104
|
+
"""
|
|
105
|
+
conversation = None
|
|
106
|
+
settings_screen = SettingsScreen()
|
|
107
|
+
try:
|
|
108
|
+
conversation = setup_conversation(resume_conversation_id)
|
|
109
|
+
return conversation
|
|
110
|
+
except MissingAgentSpec:
|
|
111
|
+
# For first-time users, show the full settings flow with choice between basic/advanced
|
|
112
|
+
settings_screen.configure_settings(first_time=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Try once again after settings setup attempt
|
|
116
|
+
return setup_conversation(resume_conversation_id)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple main entry point for OpenHands CLI.
|
|
4
|
+
This is a simplified version that demonstrates the TUI functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
debug_env = os.getenv('DEBUG', 'false').lower()
|
|
13
|
+
if debug_env != '1' and debug_env != 'true':
|
|
14
|
+
logging.disable(logging.WARNING)
|
|
15
|
+
warnings.filterwarnings('ignore')
|
|
16
|
+
|
|
17
|
+
from prompt_toolkit import print_formatted_text
|
|
18
|
+
from prompt_toolkit.formatted_text import HTML
|
|
19
|
+
|
|
20
|
+
from openhands_cli.argparsers.main_parser import create_main_parser
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""Main entry point for the OpenHands CLI.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ImportError: If agent chat dependencies are missing
|
|
28
|
+
Exception: On other error conditions
|
|
29
|
+
"""
|
|
30
|
+
parser = create_main_parser()
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
if args.command == 'serve':
|
|
35
|
+
# Import gui_launcher only when needed
|
|
36
|
+
from openhands_cli.gui_launcher import launch_gui_server
|
|
37
|
+
|
|
38
|
+
launch_gui_server(mount_cwd=args.mount_cwd, gpu=args.gpu)
|
|
39
|
+
else:
|
|
40
|
+
# Default CLI behavior - no subcommand needed
|
|
41
|
+
# Import agent_chat only when needed
|
|
42
|
+
from openhands_cli.agent_chat import run_cli_entry
|
|
43
|
+
|
|
44
|
+
# Start agent chat
|
|
45
|
+
run_cli_entry(resume_conversation_id=args.resume)
|
|
46
|
+
except KeyboardInterrupt:
|
|
47
|
+
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
48
|
+
except EOFError:
|
|
49
|
+
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print_formatted_text(HTML(f'<red>Error: {e}</red>'))
|
|
52
|
+
import traceback
|
|
53
|
+
|
|
54
|
+
traceback.print_exc()
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == '__main__':
|
|
59
|
+
main()
|