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_cli/tui/tui.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit import print_formatted_text
|
|
5
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
6
|
+
from prompt_toolkit.document import Document
|
|
7
|
+
from prompt_toolkit.formatted_text import HTML
|
|
8
|
+
from prompt_toolkit.shortcuts import clear
|
|
9
|
+
|
|
10
|
+
from openhands_cli import __version__
|
|
11
|
+
from openhands_cli.pt_style import get_cli_style
|
|
12
|
+
|
|
13
|
+
DEFAULT_STYLE = get_cli_style()
|
|
14
|
+
|
|
15
|
+
# Available commands with descriptions
|
|
16
|
+
COMMANDS = {
|
|
17
|
+
'/exit': 'Exit the application',
|
|
18
|
+
'/help': 'Display available commands',
|
|
19
|
+
'/clear': 'Clear the screen',
|
|
20
|
+
'/new': 'Start a fresh conversation',
|
|
21
|
+
'/status': 'Display conversation details',
|
|
22
|
+
'/confirm': 'Toggle confirmation mode on/off',
|
|
23
|
+
'/resume': 'Resume a paused conversation',
|
|
24
|
+
'/settings': 'Display and modify current settings',
|
|
25
|
+
'/mcp': 'View MCP (Model Context Protocol) server configuration',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommandCompleter(Completer):
|
|
30
|
+
"""Custom completer for commands with interactive dropdown."""
|
|
31
|
+
|
|
32
|
+
def get_completions(
|
|
33
|
+
self, document: Document, complete_event: CompleteEvent
|
|
34
|
+
) -> Generator[Completion, None, None]:
|
|
35
|
+
text = document.text_before_cursor.lstrip()
|
|
36
|
+
if text.startswith('/'):
|
|
37
|
+
for command, description in COMMANDS.items():
|
|
38
|
+
if command.startswith(text):
|
|
39
|
+
yield Completion(
|
|
40
|
+
command,
|
|
41
|
+
start_position=-len(text),
|
|
42
|
+
display_meta=description,
|
|
43
|
+
style='bg:ansidarkgray fg:gold',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def display_banner(conversation_id: str, resume: bool = False) -> None:
|
|
48
|
+
print_formatted_text(
|
|
49
|
+
HTML(r"""<gold>
|
|
50
|
+
___ _ _ _
|
|
51
|
+
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
|
|
52
|
+
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
|
|
53
|
+
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
|
|
54
|
+
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|
|
55
|
+
|_|
|
|
56
|
+
</gold>"""),
|
|
57
|
+
style=DEFAULT_STYLE,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
print_formatted_text('')
|
|
61
|
+
if not resume:
|
|
62
|
+
print_formatted_text(
|
|
63
|
+
HTML(f'<grey>Initialized conversation {conversation_id}</grey>')
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
print_formatted_text(
|
|
67
|
+
HTML(f'<grey>Resumed conversation {conversation_id}</grey>')
|
|
68
|
+
)
|
|
69
|
+
print_formatted_text('')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def display_help() -> None:
|
|
73
|
+
"""Display help information about available commands."""
|
|
74
|
+
print_formatted_text('')
|
|
75
|
+
print_formatted_text(HTML('<gold>🤖 OpenHands CLI Help</gold>'))
|
|
76
|
+
print_formatted_text(HTML('<grey>Available commands:</grey>'))
|
|
77
|
+
print_formatted_text('')
|
|
78
|
+
|
|
79
|
+
for command, description in COMMANDS.items():
|
|
80
|
+
print_formatted_text(HTML(f' <white>{command}</white> - {description}'))
|
|
81
|
+
|
|
82
|
+
print_formatted_text('')
|
|
83
|
+
print_formatted_text(HTML('<grey>Tips:</grey>'))
|
|
84
|
+
print_formatted_text(' • Type / and press Tab to see command suggestions')
|
|
85
|
+
print_formatted_text(' • Use arrow keys to navigate through suggestions')
|
|
86
|
+
print_formatted_text(' • Press Enter to select a command')
|
|
87
|
+
print_formatted_text('')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def display_welcome(conversation_id: UUID, resume: bool = False) -> None:
|
|
91
|
+
"""Display welcome message."""
|
|
92
|
+
clear()
|
|
93
|
+
display_banner(str(conversation_id), resume)
|
|
94
|
+
print_formatted_text(HTML("<gold>Let's start building!</gold>"))
|
|
95
|
+
print_formatted_text(
|
|
96
|
+
HTML(
|
|
97
|
+
'<green>What do you want to build? <grey>Type /help for help</grey></green>'
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
print()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class StepCounter:
|
|
2
|
+
"""Automatically manages step numbering for settings flows."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, total_steps: int):
|
|
5
|
+
self.current_step = 0
|
|
6
|
+
self.total_steps = total_steps
|
|
7
|
+
|
|
8
|
+
def next_step(self, prompt: str) -> str:
|
|
9
|
+
"""Get the next step prompt with automatic numbering."""
|
|
10
|
+
self.current_step += 1
|
|
11
|
+
return f'(Step {self.current_step}/{self.total_steps}) {prompt}'
|
|
12
|
+
|
|
13
|
+
def existing_step(self, prompt: str) -> str:
|
|
14
|
+
return f'(Step {self.current_step}/{self.total_steps}) {prompt}'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from openhands_cli.user_actions.agent_action import ask_user_confirmation
|
|
2
|
+
from openhands_cli.user_actions.exit_session import (
|
|
3
|
+
exit_session_confirmation,
|
|
4
|
+
)
|
|
5
|
+
from openhands_cli.user_actions.settings_action import (
|
|
6
|
+
choose_llm_provider,
|
|
7
|
+
settings_type_confirmation,
|
|
8
|
+
)
|
|
9
|
+
from openhands_cli.user_actions.types import UserConfirmation
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'ask_user_confirmation',
|
|
13
|
+
'exit_session_confirmation',
|
|
14
|
+
'UserConfirmation',
|
|
15
|
+
'settings_type_confirmation',
|
|
16
|
+
'choose_llm_provider',
|
|
17
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import html
|
|
2
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
3
|
+
|
|
4
|
+
from openhands.sdk.security.confirmation_policy import (
|
|
5
|
+
ConfirmRisky,
|
|
6
|
+
NeverConfirm,
|
|
7
|
+
SecurityRisk,
|
|
8
|
+
)
|
|
9
|
+
from openhands_cli.user_actions.types import ConfirmationResult, UserConfirmation
|
|
10
|
+
from openhands_cli.user_actions.utils import cli_confirm, cli_text_input
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ask_user_confirmation(
|
|
14
|
+
pending_actions: list, using_risk_based_policy: bool = False
|
|
15
|
+
) -> ConfirmationResult:
|
|
16
|
+
"""Ask user to confirm pending actions.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
pending_actions: List of pending actions from the agent
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
ConfirmationResult with decision, optional policy_change, and reason
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if not pending_actions:
|
|
26
|
+
return ConfirmationResult(decision=UserConfirmation.ACCEPT)
|
|
27
|
+
|
|
28
|
+
print_formatted_text(
|
|
29
|
+
HTML(
|
|
30
|
+
f'<yellow>🔍 Agent created {len(pending_actions)} action(s) and is waiting for confirmation:</yellow>'
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
for i, action in enumerate(pending_actions, 1):
|
|
35
|
+
tool_name = getattr(action, 'tool_name', '[unknown tool]')
|
|
36
|
+
action_content = (
|
|
37
|
+
str(getattr(action, 'action', ''))[:100].replace('\n', ' ')
|
|
38
|
+
or '[unknown action]'
|
|
39
|
+
)
|
|
40
|
+
print_formatted_text(
|
|
41
|
+
HTML(f'<grey> {i}. {tool_name}: {html.escape(action_content)}...</grey>')
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
question = 'Choose an option:'
|
|
45
|
+
options = [
|
|
46
|
+
'Yes, proceed',
|
|
47
|
+
'No, reject (w/o reason)',
|
|
48
|
+
'No, reject with reason',
|
|
49
|
+
"Always proceed (don't ask again)",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if not using_risk_based_policy:
|
|
53
|
+
options.append('Auto-confirm LOW/MEDIUM risk, ask for HIGH risk')
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
index = cli_confirm(question, options, escapable=True)
|
|
57
|
+
except (EOFError, KeyboardInterrupt):
|
|
58
|
+
print_formatted_text(HTML('\n<red>No input received; pausing agent.</red>'))
|
|
59
|
+
return ConfirmationResult(decision=UserConfirmation.DEFER)
|
|
60
|
+
|
|
61
|
+
if index == 0:
|
|
62
|
+
return ConfirmationResult(decision=UserConfirmation.ACCEPT)
|
|
63
|
+
elif index == 1:
|
|
64
|
+
return ConfirmationResult(decision=UserConfirmation.REJECT)
|
|
65
|
+
elif index == 2:
|
|
66
|
+
try:
|
|
67
|
+
reason_result = cli_text_input(
|
|
68
|
+
'Please enter your reason for rejecting these actions: '
|
|
69
|
+
)
|
|
70
|
+
except Exception:
|
|
71
|
+
return ConfirmationResult(decision=UserConfirmation.DEFER)
|
|
72
|
+
|
|
73
|
+
# Support both string return and (reason, cancelled) tuple for tests
|
|
74
|
+
cancelled = False
|
|
75
|
+
if isinstance(reason_result, tuple) and len(reason_result) >= 1:
|
|
76
|
+
reason = reason_result[0] or ''
|
|
77
|
+
cancelled = bool(reason_result[1]) if len(reason_result) > 1 else False
|
|
78
|
+
else:
|
|
79
|
+
reason = str(reason_result or '').strip()
|
|
80
|
+
|
|
81
|
+
if cancelled:
|
|
82
|
+
return ConfirmationResult(decision=UserConfirmation.DEFER)
|
|
83
|
+
|
|
84
|
+
return ConfirmationResult(decision=UserConfirmation.REJECT, reason=reason)
|
|
85
|
+
elif index == 3:
|
|
86
|
+
return ConfirmationResult(
|
|
87
|
+
decision=UserConfirmation.ACCEPT, policy_change=NeverConfirm()
|
|
88
|
+
)
|
|
89
|
+
elif index == 4:
|
|
90
|
+
return ConfirmationResult(
|
|
91
|
+
decision=UserConfirmation.ACCEPT,
|
|
92
|
+
policy_change=ConfirmRisky(threshold=SecurityRisk.HIGH),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return ConfirmationResult(decision=UserConfirmation.REJECT)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from openhands_cli.user_actions.types import UserConfirmation
|
|
2
|
+
from openhands_cli.user_actions.utils import cli_confirm
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def exit_session_confirmation() -> UserConfirmation:
|
|
6
|
+
"""
|
|
7
|
+
Ask user to confirm exiting session.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
question = 'Terminate session?'
|
|
11
|
+
options = ['Yes, proceed', 'No, dismiss']
|
|
12
|
+
index = cli_confirm(question, options) # Blocking UI, not escapable
|
|
13
|
+
|
|
14
|
+
options_mapping = {
|
|
15
|
+
0: UserConfirmation.ACCEPT, # User accepts termination session
|
|
16
|
+
1: UserConfirmation.REJECT, # User does not terminate session
|
|
17
|
+
}
|
|
18
|
+
return options_mapping.get(index, UserConfirmation.REJECT)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from openhands.sdk.llm import UNVERIFIED_MODELS_EXCLUDING_BEDROCK, VERIFIED_MODELS
|
|
4
|
+
from prompt_toolkit.completion import FuzzyWordCompleter
|
|
5
|
+
from pydantic import SecretStr
|
|
6
|
+
|
|
7
|
+
from openhands_cli.tui.utils import StepCounter
|
|
8
|
+
from openhands_cli.user_actions.utils import (
|
|
9
|
+
NonEmptyValueValidator,
|
|
10
|
+
cli_confirm,
|
|
11
|
+
cli_text_input,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SettingsType(Enum):
|
|
16
|
+
BASIC = 'basic'
|
|
17
|
+
ADVANCED = 'advanced'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def settings_type_confirmation(first_time: bool = False) -> SettingsType:
|
|
21
|
+
question = (
|
|
22
|
+
'\nWelcome to OpenHands! Let\'s configure your LLM settings.\n'
|
|
23
|
+
'Choose your preferred setup method:'
|
|
24
|
+
)
|
|
25
|
+
choices = [
|
|
26
|
+
'LLM (Basic)',
|
|
27
|
+
'LLM (Advanced)'
|
|
28
|
+
]
|
|
29
|
+
if not first_time:
|
|
30
|
+
question = 'Which settings would you like to modify?'
|
|
31
|
+
choices.append('Go back')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
index = cli_confirm(question, choices, escapable=True)
|
|
35
|
+
|
|
36
|
+
if choices[index] == 'Go back':
|
|
37
|
+
raise KeyboardInterrupt
|
|
38
|
+
|
|
39
|
+
options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED}
|
|
40
|
+
|
|
41
|
+
return options_map.get(index)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def choose_llm_provider(step_counter: StepCounter, escapable=True) -> str:
|
|
45
|
+
question = step_counter.next_step(
|
|
46
|
+
'Select LLM Provider (TAB for options, CTRL-c to cancel): '
|
|
47
|
+
)
|
|
48
|
+
options = (
|
|
49
|
+
list(VERIFIED_MODELS.keys()).copy()
|
|
50
|
+
+ list(UNVERIFIED_MODELS_EXCLUDING_BEDROCK.keys()).copy()
|
|
51
|
+
)
|
|
52
|
+
alternate_option = 'Select another provider'
|
|
53
|
+
|
|
54
|
+
display_options = options[:4] + [alternate_option]
|
|
55
|
+
|
|
56
|
+
index = cli_confirm(question, display_options, escapable=escapable)
|
|
57
|
+
chosen_option = display_options[index]
|
|
58
|
+
if display_options[index] != alternate_option:
|
|
59
|
+
return chosen_option
|
|
60
|
+
|
|
61
|
+
question = step_counter.existing_step(
|
|
62
|
+
'Type LLM Provider (TAB to complete, CTRL-c to cancel): '
|
|
63
|
+
)
|
|
64
|
+
return cli_text_input(
|
|
65
|
+
question, escapable=True, completer=FuzzyWordCompleter(options, WORD=True)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def choose_llm_model(step_counter: StepCounter, provider: str, escapable=True) -> str:
|
|
70
|
+
"""Choose LLM model using spec-driven approach. Return (model, deferred)."""
|
|
71
|
+
|
|
72
|
+
models = VERIFIED_MODELS.get(
|
|
73
|
+
provider, []
|
|
74
|
+
) + UNVERIFIED_MODELS_EXCLUDING_BEDROCK.get(provider, [])
|
|
75
|
+
|
|
76
|
+
if provider == 'openhands':
|
|
77
|
+
question = (
|
|
78
|
+
step_counter.next_step('Select Available OpenHands Model:\n')
|
|
79
|
+
+ 'LLM usage is billed at the providers’ rates with no markup. Details: https://docs.all-hands.dev/usage/llms/openhands-llms'
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
question = step_counter.next_step(
|
|
83
|
+
'Select LLM Model (TAB for options, CTRL-c to cancel): '
|
|
84
|
+
)
|
|
85
|
+
alternate_option = 'Select another model'
|
|
86
|
+
display_options = models[:4] + [alternate_option]
|
|
87
|
+
index = cli_confirm(question, display_options, escapable=escapable)
|
|
88
|
+
chosen_option = display_options[index]
|
|
89
|
+
|
|
90
|
+
if chosen_option != alternate_option:
|
|
91
|
+
return chosen_option
|
|
92
|
+
|
|
93
|
+
question = step_counter.existing_step(
|
|
94
|
+
'Type model id (TAB to complete, CTRL-c to cancel): '
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return cli_text_input(
|
|
98
|
+
question, escapable=True, completer=FuzzyWordCompleter(models, WORD=True)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def prompt_api_key(
|
|
103
|
+
step_counter: StepCounter,
|
|
104
|
+
provider: str,
|
|
105
|
+
existing_api_key: SecretStr | None = None,
|
|
106
|
+
escapable=True,
|
|
107
|
+
) -> str:
|
|
108
|
+
helper_text = (
|
|
109
|
+
'\nYou can find your OpenHands LLM API Key in the API Keys tab of OpenHands Cloud: '
|
|
110
|
+
'https://app.all-hands.dev/settings/api-keys\n'
|
|
111
|
+
if provider == 'openhands'
|
|
112
|
+
else ''
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if existing_api_key:
|
|
116
|
+
masked_key = existing_api_key.get_secret_value()[:3] + '***'
|
|
117
|
+
question = f'Enter API Key [{masked_key}] (CTRL-c to cancel, ENTER to keep current, type new to change): '
|
|
118
|
+
# For existing keys, allow empty input to keep current key
|
|
119
|
+
validator = None
|
|
120
|
+
else:
|
|
121
|
+
question = 'Enter API Key (CTRL-c to cancel): '
|
|
122
|
+
# For new keys, require non-empty input
|
|
123
|
+
validator = NonEmptyValueValidator()
|
|
124
|
+
|
|
125
|
+
question = helper_text + step_counter.next_step(question)
|
|
126
|
+
user_input = cli_text_input(
|
|
127
|
+
question, escapable=escapable, validator=validator, is_password=True
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# If user pressed ENTER with existing key (empty input), return the existing key
|
|
131
|
+
if existing_api_key and not user_input.strip():
|
|
132
|
+
return existing_api_key.get_secret_value()
|
|
133
|
+
|
|
134
|
+
return user_input
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Advanced settings functions
|
|
138
|
+
def prompt_custom_model(step_counter: StepCounter, escapable=True) -> str:
|
|
139
|
+
"""Prompt for custom model name."""
|
|
140
|
+
question = step_counter.next_step('Custom Model (CTRL-c to cancel): ')
|
|
141
|
+
return cli_text_input(question, escapable=escapable)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def prompt_base_url(step_counter: StepCounter, escapable=True) -> str:
|
|
145
|
+
"""Prompt for base URL."""
|
|
146
|
+
question = step_counter.next_step('Base URL (CTRL-c to cancel): ')
|
|
147
|
+
return cli_text_input(
|
|
148
|
+
question, escapable=escapable, validator=NonEmptyValueValidator()
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def choose_memory_condensation(step_counter: StepCounter, escapable=True) -> bool:
|
|
153
|
+
"""Choose memory condensation setting."""
|
|
154
|
+
question = step_counter.next_step('Memory Condensation (CTRL-c to cancel): ')
|
|
155
|
+
choices = ['Enable', 'Disable']
|
|
156
|
+
|
|
157
|
+
index = cli_confirm(question, choices, escapable=escapable)
|
|
158
|
+
return index == 0 # True for Enable, False for Disable
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def save_settings_confirmation() -> bool:
|
|
162
|
+
"""Prompt user to confirm saving settings."""
|
|
163
|
+
question = 'Save new settings? (They will take effect after restart)'
|
|
164
|
+
discard = 'No, discard'
|
|
165
|
+
options = ['Yes, save', discard]
|
|
166
|
+
|
|
167
|
+
index = cli_confirm(question, options, escapable=True)
|
|
168
|
+
if options[index] == discard:
|
|
169
|
+
raise KeyboardInterrupt
|
|
170
|
+
|
|
171
|
+
return options[index]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from openhands.sdk.security.confirmation_policy import ConfirmationPolicyBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserConfirmation(Enum):
|
|
10
|
+
ACCEPT = 'accept'
|
|
11
|
+
REJECT = 'reject'
|
|
12
|
+
DEFER = 'defer'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfirmationResult(BaseModel):
|
|
16
|
+
decision: UserConfirmation
|
|
17
|
+
policy_change: Optional[ConfirmationPolicyBase] = None
|
|
18
|
+
reason: str = ''
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from prompt_toolkit import HTML, PromptSession
|
|
2
|
+
from prompt_toolkit.application import Application
|
|
3
|
+
from prompt_toolkit.completion import Completer
|
|
4
|
+
from prompt_toolkit.input.base import Input
|
|
5
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
6
|
+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
7
|
+
from prompt_toolkit.layout.containers import HSplit, Window
|
|
8
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
9
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
10
|
+
from prompt_toolkit.layout.layout import Layout
|
|
11
|
+
from prompt_toolkit.output.base import Output
|
|
12
|
+
from prompt_toolkit.shortcuts import prompt
|
|
13
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
|
14
|
+
|
|
15
|
+
from openhands_cli.tui import DEFAULT_STYLE
|
|
16
|
+
from openhands_cli.tui.tui import CommandCompleter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_keybindings(
|
|
20
|
+
choices: list[str], selected: list[int], escapable: bool
|
|
21
|
+
) -> KeyBindings:
|
|
22
|
+
"""Create keybindings for the confirm UI. Split for testability."""
|
|
23
|
+
kb = KeyBindings()
|
|
24
|
+
|
|
25
|
+
@kb.add('up')
|
|
26
|
+
def _handle_up(event: KeyPressEvent) -> None:
|
|
27
|
+
selected[0] = (selected[0] - 1) % len(choices)
|
|
28
|
+
|
|
29
|
+
@kb.add('down')
|
|
30
|
+
def _handle_down(event: KeyPressEvent) -> None:
|
|
31
|
+
selected[0] = (selected[0] + 1) % len(choices)
|
|
32
|
+
|
|
33
|
+
@kb.add('enter')
|
|
34
|
+
def _handle_enter(event: KeyPressEvent) -> None:
|
|
35
|
+
event.app.exit(result=selected[0])
|
|
36
|
+
|
|
37
|
+
if escapable:
|
|
38
|
+
|
|
39
|
+
@kb.add('c-c') # Ctrl+C
|
|
40
|
+
def _handle_hard_interrupt(event: KeyPressEvent) -> None:
|
|
41
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
42
|
+
|
|
43
|
+
@kb.add('c-p') # Ctrl+P
|
|
44
|
+
def _handle_pause_interrupt(event: KeyPressEvent) -> None:
|
|
45
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
46
|
+
|
|
47
|
+
@kb.add('escape') # Escape key
|
|
48
|
+
def _handle_escape(event: KeyPressEvent) -> None:
|
|
49
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
50
|
+
|
|
51
|
+
return kb
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_layout(question: str, choices: list[str], selected_ref: list[int]) -> Layout:
|
|
55
|
+
"""Create the layout for the confirm UI. Split for testability."""
|
|
56
|
+
|
|
57
|
+
def get_choice_text() -> list[tuple[str, str]]:
|
|
58
|
+
lines: list[tuple[str, str]] = []
|
|
59
|
+
lines.append(('class:question', f'{question}\n\n'))
|
|
60
|
+
for i, choice in enumerate(choices):
|
|
61
|
+
is_selected = i == selected_ref[0]
|
|
62
|
+
prefix = '> ' if is_selected else ' '
|
|
63
|
+
style = 'class:selected' if is_selected else 'class:unselected'
|
|
64
|
+
lines.append((style, f'{prefix}{choice}\n'))
|
|
65
|
+
return lines
|
|
66
|
+
|
|
67
|
+
content_window = Window(
|
|
68
|
+
FormattedTextControl(get_choice_text),
|
|
69
|
+
always_hide_cursor=True,
|
|
70
|
+
height=Dimension(max=8),
|
|
71
|
+
)
|
|
72
|
+
return Layout(HSplit([content_window]))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cli_confirm(
|
|
76
|
+
question: str = 'Are you sure?',
|
|
77
|
+
choices: list[str] | None = None,
|
|
78
|
+
initial_selection: int = 0,
|
|
79
|
+
escapable: bool = False,
|
|
80
|
+
input: Input | None = None, # strictly for unit testing
|
|
81
|
+
output: Output | None = None, # strictly for unit testing
|
|
82
|
+
) -> int:
|
|
83
|
+
"""Display a confirmation prompt with the given question and choices.
|
|
84
|
+
|
|
85
|
+
Returns the index of the selected choice.
|
|
86
|
+
"""
|
|
87
|
+
if choices is None:
|
|
88
|
+
choices = ['Yes', 'No']
|
|
89
|
+
selected = [initial_selection] # Using list to allow modification in closure
|
|
90
|
+
|
|
91
|
+
kb = build_keybindings(choices, selected, escapable)
|
|
92
|
+
layout = build_layout(question, choices, selected)
|
|
93
|
+
|
|
94
|
+
app = Application(
|
|
95
|
+
layout=layout,
|
|
96
|
+
key_bindings=kb,
|
|
97
|
+
style=DEFAULT_STYLE,
|
|
98
|
+
full_screen=False,
|
|
99
|
+
input=input,
|
|
100
|
+
output=output,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return int(app.run(in_thread=True))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cli_text_input(
|
|
107
|
+
question: str,
|
|
108
|
+
escapable: bool = True,
|
|
109
|
+
completer: Completer | None = None,
|
|
110
|
+
validator: Validator = None,
|
|
111
|
+
is_password: bool = False,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Prompt user to enter text input with optional validation.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
question: The prompt question to display
|
|
117
|
+
escapable: Whether the user can escape with Ctrl+C or Ctrl+P
|
|
118
|
+
completer: Optional completer for tab completion
|
|
119
|
+
validator: Optional callable that takes a string and returns True if valid.
|
|
120
|
+
If validation fails, the callable should display error messages
|
|
121
|
+
and the user will be reprompted.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The validated user input string (stripped of whitespace)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
kb = KeyBindings()
|
|
128
|
+
|
|
129
|
+
if escapable:
|
|
130
|
+
|
|
131
|
+
@kb.add('c-c')
|
|
132
|
+
def _(event: KeyPressEvent) -> None:
|
|
133
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
134
|
+
|
|
135
|
+
@kb.add('c-p')
|
|
136
|
+
def _(event: KeyPressEvent) -> None:
|
|
137
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
138
|
+
|
|
139
|
+
@kb.add('enter')
|
|
140
|
+
def _handle_enter(event: KeyPressEvent):
|
|
141
|
+
event.app.exit(result=event.current_buffer.text)
|
|
142
|
+
|
|
143
|
+
reason = str(
|
|
144
|
+
prompt(
|
|
145
|
+
question,
|
|
146
|
+
style=DEFAULT_STYLE,
|
|
147
|
+
key_bindings=kb,
|
|
148
|
+
completer=completer,
|
|
149
|
+
is_password=is_password,
|
|
150
|
+
validator=validator,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return reason.strip()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_session_prompter(
|
|
157
|
+
input: Input | None = None, # strictly for unit testing
|
|
158
|
+
output: Output | None = None, # strictly for unit testing
|
|
159
|
+
) -> PromptSession:
|
|
160
|
+
bindings = KeyBindings()
|
|
161
|
+
|
|
162
|
+
@bindings.add('\\', 'enter')
|
|
163
|
+
def _(event: KeyPressEvent) -> None:
|
|
164
|
+
# Typing '\' + Enter forces a newline regardless
|
|
165
|
+
event.current_buffer.insert_text('\n')
|
|
166
|
+
|
|
167
|
+
@bindings.add('enter')
|
|
168
|
+
def _handle_enter(event: KeyPressEvent):
|
|
169
|
+
event.app.exit(result=event.current_buffer.text)
|
|
170
|
+
|
|
171
|
+
@bindings.add('c-c')
|
|
172
|
+
def _keyboard_interrupt(event: KeyPressEvent):
|
|
173
|
+
event.app.exit(exception=KeyboardInterrupt())
|
|
174
|
+
|
|
175
|
+
session = PromptSession(
|
|
176
|
+
completer=CommandCompleter(),
|
|
177
|
+
key_bindings=bindings,
|
|
178
|
+
prompt_continuation=lambda width, line_number, is_soft_wrap: '...',
|
|
179
|
+
multiline=True,
|
|
180
|
+
input=input,
|
|
181
|
+
output=output,
|
|
182
|
+
style=DEFAULT_STYLE,
|
|
183
|
+
placeholder=HTML(
|
|
184
|
+
'<placeholder>'
|
|
185
|
+
'Type your message… (tip: press <b>\\</b> + <b>Enter</b> to insert a newline)'
|
|
186
|
+
'</placeholder>'
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return session
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class NonEmptyValueValidator(Validator):
|
|
194
|
+
def validate(self, document):
|
|
195
|
+
text = document.text
|
|
196
|
+
if not text:
|
|
197
|
+
raise ValidationError(
|
|
198
|
+
message='API key cannot be empty. Please enter a valid API key.'
|
|
199
|
+
)
|
openhands/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
openhands/sdk/__init__.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
-
|
|
3
|
-
from openhands.sdk.agent import Agent, AgentBase
|
|
4
|
-
from openhands.sdk.context import AgentContext
|
|
5
|
-
from openhands.sdk.conversation import Conversation, ConversationCallbackType
|
|
6
|
-
from openhands.sdk.event import Event, EventBase, LLMConvertibleEvent
|
|
7
|
-
from openhands.sdk.llm import (
|
|
8
|
-
LLM,
|
|
9
|
-
ImageContent,
|
|
10
|
-
LLMRegistry,
|
|
11
|
-
Message,
|
|
12
|
-
RegistryEvent,
|
|
13
|
-
TextContent,
|
|
14
|
-
)
|
|
15
|
-
from openhands.sdk.logger import get_logger
|
|
16
|
-
from openhands.sdk.mcp import MCPClient, MCPTool, create_mcp_tools
|
|
17
|
-
from openhands.sdk.tool import ActionBase, ObservationBase, Tool
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
__version__ = "1.0.0a0"
|
|
21
|
-
|
|
22
|
-
__all__ = [
|
|
23
|
-
"LLM",
|
|
24
|
-
"LLMRegistry",
|
|
25
|
-
"RegistryEvent",
|
|
26
|
-
"Message",
|
|
27
|
-
"TextContent",
|
|
28
|
-
"ImageContent",
|
|
29
|
-
"Tool",
|
|
30
|
-
"AgentBase",
|
|
31
|
-
"Agent",
|
|
32
|
-
"ActionBase",
|
|
33
|
-
"ObservationBase",
|
|
34
|
-
"MCPClient",
|
|
35
|
-
"MCPTool",
|
|
36
|
-
"create_mcp_tools",
|
|
37
|
-
"get_logger",
|
|
38
|
-
"Conversation",
|
|
39
|
-
"ConversationCallbackType",
|
|
40
|
-
"Event",
|
|
41
|
-
"EventBase",
|
|
42
|
-
"LLMConvertibleEvent",
|
|
43
|
-
"AgentContext",
|
|
44
|
-
"__version__",
|
|
45
|
-
]
|