openhands 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhands-1.3.0.dist-info/METADATA +56 -0
- openhands-1.3.0.dist-info/RECORD +43 -0
- openhands-1.3.0.dist-info/WHEEL +4 -0
- openhands-1.3.0.dist-info/entry_points.txt +3 -0
- openhands-1.3.0.dist-info/licenses/LICENSE +21 -0
- openhands_cli/__init__.py +9 -0
- openhands_cli/acp_impl/README.md +68 -0
- openhands_cli/acp_impl/__init__.py +1 -0
- openhands_cli/acp_impl/agent.py +483 -0
- openhands_cli/acp_impl/event.py +512 -0
- openhands_cli/acp_impl/main.py +21 -0
- openhands_cli/acp_impl/test_utils.py +174 -0
- openhands_cli/acp_impl/utils/__init__.py +14 -0
- openhands_cli/acp_impl/utils/convert.py +103 -0
- openhands_cli/acp_impl/utils/mcp.py +66 -0
- openhands_cli/acp_impl/utils/resources.py +189 -0
- openhands_cli/agent_chat.py +236 -0
- openhands_cli/argparsers/main_parser.py +78 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +224 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/locations.py +14 -0
- openhands_cli/pt_style.py +33 -0
- openhands_cli/runner.py +190 -0
- openhands_cli/setup.py +136 -0
- openhands_cli/simple_main.py +71 -0
- openhands_cli/tui/__init__.py +6 -0
- openhands_cli/tui/settings/mcp_screen.py +225 -0
- openhands_cli/tui/settings/settings_screen.py +226 -0
- openhands_cli/tui/settings/store.py +132 -0
- openhands_cli/tui/status.py +110 -0
- openhands_cli/tui/tui.py +120 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/tui/visualizer.py +22 -0
- openhands_cli/user_actions/__init__.py +18 -0
- openhands_cli/user_actions/agent_action.py +82 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +176 -0
- openhands_cli/user_actions/types.py +17 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands_cli/utils.py +122 -0
- openhands_cli/version_check.py +83 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import FuzzyWordCompleter
|
|
4
|
+
from pydantic import SecretStr
|
|
5
|
+
|
|
6
|
+
from openhands.sdk.llm import UNVERIFIED_MODELS_EXCLUDING_BEDROCK, VERIFIED_MODELS
|
|
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 = ["LLM (Basic)", "LLM (Advanced)"]
|
|
26
|
+
if not first_time:
|
|
27
|
+
question = "Which settings would you like to modify?"
|
|
28
|
+
choices.append("Go back")
|
|
29
|
+
|
|
30
|
+
index = cli_confirm(question, choices, escapable=True)
|
|
31
|
+
|
|
32
|
+
if choices[index] == "Go back":
|
|
33
|
+
raise KeyboardInterrupt
|
|
34
|
+
|
|
35
|
+
options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED}
|
|
36
|
+
|
|
37
|
+
return options_map.get(index, SettingsType.BASIC)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def choose_llm_provider(step_counter: StepCounter, escapable=True) -> str:
|
|
41
|
+
question = step_counter.next_step(
|
|
42
|
+
"Select LLM Provider (TAB for options, CTRL-c to cancel): "
|
|
43
|
+
)
|
|
44
|
+
options = (
|
|
45
|
+
list(VERIFIED_MODELS.keys()).copy()
|
|
46
|
+
+ list(UNVERIFIED_MODELS_EXCLUDING_BEDROCK.keys()).copy()
|
|
47
|
+
)
|
|
48
|
+
alternate_option = "Select another provider"
|
|
49
|
+
|
|
50
|
+
display_options = options[:4] + [alternate_option]
|
|
51
|
+
|
|
52
|
+
index = cli_confirm(question, display_options, escapable=escapable)
|
|
53
|
+
chosen_option = display_options[index]
|
|
54
|
+
if display_options[index] != alternate_option:
|
|
55
|
+
return chosen_option
|
|
56
|
+
|
|
57
|
+
question = step_counter.existing_step(
|
|
58
|
+
"Type LLM Provider (TAB to complete, CTRL-c to cancel): "
|
|
59
|
+
)
|
|
60
|
+
return cli_text_input(
|
|
61
|
+
question, escapable=True, completer=FuzzyWordCompleter(options, WORD=True)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def choose_llm_model(step_counter: StepCounter, provider: str, escapable=True) -> str:
|
|
66
|
+
"""Choose LLM model using spec-driven approach. Return (model, deferred)."""
|
|
67
|
+
|
|
68
|
+
models = VERIFIED_MODELS.get(
|
|
69
|
+
provider, []
|
|
70
|
+
) + UNVERIFIED_MODELS_EXCLUDING_BEDROCK.get(provider, [])
|
|
71
|
+
|
|
72
|
+
if provider == "openhands":
|
|
73
|
+
question = (
|
|
74
|
+
step_counter.next_step("Select Available OpenHands Model:\n")
|
|
75
|
+
+ "LLM usage is billed at the providers’ rates with no markup. Details: https://docs.all-hands.dev/usage/llms/openhands-llms"
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
question = step_counter.next_step(
|
|
79
|
+
"Select LLM Model (TAB for options, CTRL-c to cancel): "
|
|
80
|
+
)
|
|
81
|
+
alternate_option = "Select another model"
|
|
82
|
+
display_options = models[:10] + [alternate_option]
|
|
83
|
+
index = cli_confirm(question, display_options, escapable=escapable)
|
|
84
|
+
chosen_option = display_options[index]
|
|
85
|
+
|
|
86
|
+
if chosen_option != alternate_option:
|
|
87
|
+
return chosen_option
|
|
88
|
+
|
|
89
|
+
question = step_counter.existing_step(
|
|
90
|
+
"Type model id (TAB to complete, CTRL-c to cancel): "
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return cli_text_input(
|
|
94
|
+
question, escapable=True, completer=FuzzyWordCompleter(models, WORD=True)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def prompt_api_key(
|
|
99
|
+
step_counter: StepCounter,
|
|
100
|
+
provider: str,
|
|
101
|
+
existing_api_key: str | SecretStr | None = None,
|
|
102
|
+
escapable=True,
|
|
103
|
+
) -> str:
|
|
104
|
+
api_key: str | None = (
|
|
105
|
+
existing_api_key.get_secret_value()
|
|
106
|
+
if isinstance(existing_api_key, SecretStr)
|
|
107
|
+
else existing_api_key
|
|
108
|
+
)
|
|
109
|
+
helper_text = (
|
|
110
|
+
"\nYou can find your OpenHands LLM API Key in the API Keys tab of "
|
|
111
|
+
"OpenHands Cloud: "
|
|
112
|
+
"https://app.all-hands.dev/settings/api-keys\n"
|
|
113
|
+
if provider == "openhands"
|
|
114
|
+
else ""
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if api_key:
|
|
118
|
+
masked_key = api_key[:3] + "***"
|
|
119
|
+
question = (
|
|
120
|
+
f"Enter API Key [{masked_key}] (CTRL-c to cancel, ENTER to keep "
|
|
121
|
+
f"current, type new to change): "
|
|
122
|
+
)
|
|
123
|
+
# For existing keys, allow empty input to keep current key
|
|
124
|
+
validator = None
|
|
125
|
+
else:
|
|
126
|
+
question = "Enter API Key (CTRL-c to cancel): "
|
|
127
|
+
# For new keys, require non-empty input
|
|
128
|
+
validator = NonEmptyValueValidator()
|
|
129
|
+
|
|
130
|
+
question = helper_text + step_counter.next_step(question)
|
|
131
|
+
user_input = cli_text_input(
|
|
132
|
+
question, escapable=escapable, validator=validator, is_password=True
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# If user pressed ENTER with existing key (empty input), return the existing key
|
|
136
|
+
if api_key and not user_input.strip():
|
|
137
|
+
return api_key
|
|
138
|
+
|
|
139
|
+
return user_input
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Advanced settings functions
|
|
143
|
+
def prompt_custom_model(step_counter: StepCounter, escapable=True) -> str:
|
|
144
|
+
"""Prompt for custom model name."""
|
|
145
|
+
question = step_counter.next_step("Custom Model (CTRL-c to cancel): ")
|
|
146
|
+
return cli_text_input(question, escapable=escapable)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def prompt_base_url(step_counter: StepCounter, escapable=True) -> str:
|
|
150
|
+
"""Prompt for base URL."""
|
|
151
|
+
question = step_counter.next_step("Base URL (CTRL-c to cancel): ")
|
|
152
|
+
return cli_text_input(
|
|
153
|
+
question, escapable=escapable, validator=NonEmptyValueValidator()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def choose_memory_condensation(step_counter: StepCounter, escapable=True) -> bool:
|
|
158
|
+
"""Choose memory condensation setting."""
|
|
159
|
+
question = step_counter.next_step("Memory Condensation (CTRL-c to cancel): ")
|
|
160
|
+
choices = ["Enable", "Disable"]
|
|
161
|
+
|
|
162
|
+
index = cli_confirm(question, choices, escapable=escapable)
|
|
163
|
+
return index == 0 # True for Enable, False for Disable
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def save_settings_confirmation() -> bool:
|
|
167
|
+
"""Prompt user to confirm saving settings."""
|
|
168
|
+
question = "Save new settings? (They will take effect after restart)"
|
|
169
|
+
discard = "No, discard"
|
|
170
|
+
options = ["Yes, save", discard]
|
|
171
|
+
|
|
172
|
+
index = cli_confirm(question, options, escapable=True)
|
|
173
|
+
if options[index] == discard:
|
|
174
|
+
raise KeyboardInterrupt
|
|
175
|
+
|
|
176
|
+
return True
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from openhands.sdk.security.confirmation_policy import ConfirmationPolicyBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserConfirmation(Enum):
|
|
9
|
+
ACCEPT = "accept"
|
|
10
|
+
REJECT = "reject"
|
|
11
|
+
DEFER = "defer"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfirmationResult(BaseModel):
|
|
15
|
+
decision: UserConfirmation
|
|
16
|
+
policy_change: ConfirmationPolicyBase | None = None
|
|
17
|
+
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: # noqa: ARG001
|
|
27
|
+
selected[0] = (selected[0] - 1) % len(choices)
|
|
28
|
+
|
|
29
|
+
@kb.add("down")
|
|
30
|
+
def _handle_down(event: KeyPressEvent) -> None: # noqa: ARG001
|
|
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=16),
|
|
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 = 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
|
+
multiline=True,
|
|
179
|
+
input=input,
|
|
180
|
+
output=output,
|
|
181
|
+
style=DEFAULT_STYLE,
|
|
182
|
+
placeholder=HTML(
|
|
183
|
+
"<placeholder>"
|
|
184
|
+
"Type your message… (tip: press <b>\\</b> + <b>Enter</b> to insert "
|
|
185
|
+
"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_cli/utils.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Utility functions for LLM configuration in OpenHands CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit import print_formatted_text
|
|
9
|
+
from prompt_toolkit.formatted_text import HTML
|
|
10
|
+
|
|
11
|
+
from openhands.sdk import LLM
|
|
12
|
+
from openhands.tools.preset import get_default_agent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def should_set_litellm_extra_body(model_name: str) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Determine if litellm_extra_body should be set based on the model name.
|
|
18
|
+
|
|
19
|
+
Only set litellm_extra_body for openhands models to avoid issues
|
|
20
|
+
with providers that don't support extra_body parameters.
|
|
21
|
+
|
|
22
|
+
The SDK internally translates "openhands/" prefix to "litellm_proxy/"
|
|
23
|
+
when making API calls.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
model_name: Name of the LLM model
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
True if litellm_extra_body should be set, False otherwise
|
|
30
|
+
"""
|
|
31
|
+
return "openhands/" in model_name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_llm_metadata(
|
|
35
|
+
model_name: str,
|
|
36
|
+
llm_type: str,
|
|
37
|
+
session_id: str | None = None,
|
|
38
|
+
user_id: str | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Generate LLM metadata for OpenHands CLI.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model_name: Name of the LLM model
|
|
45
|
+
agent_name: Name of the agent (defaults to "openhands")
|
|
46
|
+
session_id: Optional session identifier
|
|
47
|
+
user_id: Optional user identifier
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary containing metadata for LLM initialization
|
|
51
|
+
"""
|
|
52
|
+
# Import here to avoid circular imports
|
|
53
|
+
openhands_sdk_version: str = "n/a"
|
|
54
|
+
try:
|
|
55
|
+
import openhands.sdk
|
|
56
|
+
|
|
57
|
+
openhands_sdk_version = openhands.sdk.__version__
|
|
58
|
+
except (ModuleNotFoundError, AttributeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
openhands_tools_version: str = "n/a"
|
|
62
|
+
try:
|
|
63
|
+
import openhands.tools
|
|
64
|
+
|
|
65
|
+
openhands_tools_version = openhands.tools.__version__
|
|
66
|
+
except (ModuleNotFoundError, AttributeError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
metadata = {
|
|
70
|
+
"trace_version": openhands_sdk_version,
|
|
71
|
+
"tags": [
|
|
72
|
+
"app:openhands",
|
|
73
|
+
f"model:{model_name}",
|
|
74
|
+
f"type:{llm_type}",
|
|
75
|
+
f"web_host:{os.environ.get('WEB_HOST', 'unspecified')}",
|
|
76
|
+
f"openhands_sdk_version:{openhands_sdk_version}",
|
|
77
|
+
f"openhands_tools_version:{openhands_tools_version}",
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
if session_id is not None:
|
|
81
|
+
metadata["session_id"] = session_id
|
|
82
|
+
if user_id is not None:
|
|
83
|
+
metadata["trace_user_id"] = user_id
|
|
84
|
+
return metadata
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_default_cli_agent(llm: LLM):
|
|
88
|
+
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
89
|
+
|
|
90
|
+
return agent
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_seeded_instructions_from_args(args: Namespace) -> list[str] | None:
|
|
94
|
+
"""
|
|
95
|
+
Build initial CLI input(s) from parsed arguments.
|
|
96
|
+
"""
|
|
97
|
+
if getattr(args, "command", None) == "serve":
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# --file takes precedence over --task
|
|
101
|
+
if getattr(args, "file", None):
|
|
102
|
+
path = Path(args.file).expanduser()
|
|
103
|
+
try:
|
|
104
|
+
content = path.read_text(encoding="utf-8")
|
|
105
|
+
except OSError as exc:
|
|
106
|
+
print_formatted_text(HTML(f"<red>Failed to read file {path}: {exc}</red>"))
|
|
107
|
+
raise SystemExit(1)
|
|
108
|
+
|
|
109
|
+
initial_message = (
|
|
110
|
+
"Starting this session with file context.\n\n"
|
|
111
|
+
f"File path: {path}\n\n"
|
|
112
|
+
"File contents:\n"
|
|
113
|
+
"--------------------\n"
|
|
114
|
+
f"{content}\n"
|
|
115
|
+
"--------------------\n"
|
|
116
|
+
)
|
|
117
|
+
return [initial_message]
|
|
118
|
+
|
|
119
|
+
if getattr(args, "task", None):
|
|
120
|
+
return [args.task]
|
|
121
|
+
|
|
122
|
+
return None
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Version checking utilities for OpenHands CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from openhands_cli import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VersionInfo(NamedTuple):
|
|
11
|
+
"""Version information for display."""
|
|
12
|
+
|
|
13
|
+
current_version: str
|
|
14
|
+
latest_version: str | None
|
|
15
|
+
needs_update: bool
|
|
16
|
+
error: str | None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_version(version_str: str) -> tuple[int, ...]:
|
|
20
|
+
"""Parse a version string into a tuple of integers for comparison.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
version_str: Version string like "1.2.1"
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of integers like (1, 2, 1)
|
|
27
|
+
"""
|
|
28
|
+
return tuple(int(x) for x in version_str.split("."))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_for_updates(timeout: float = 2.0) -> VersionInfo:
|
|
32
|
+
"""Check if a newer version is available on PyPI.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
timeout: Timeout for PyPI request in seconds
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
VersionInfo with update information
|
|
39
|
+
"""
|
|
40
|
+
current = __version__
|
|
41
|
+
|
|
42
|
+
# Handle dev versions or special cases
|
|
43
|
+
if current == "0.0.0" or "dev" in current:
|
|
44
|
+
return VersionInfo(
|
|
45
|
+
current_version=current,
|
|
46
|
+
latest_version=None,
|
|
47
|
+
needs_update=False,
|
|
48
|
+
error=None,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Fetch latest version from PyPI
|
|
53
|
+
url = "https://pypi.org/pypi/openhands/json"
|
|
54
|
+
req = urllib.request.Request(url)
|
|
55
|
+
req.add_header("User-Agent", f"openhands-cli/{current}")
|
|
56
|
+
|
|
57
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
58
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
59
|
+
latest = data["info"]["version"]
|
|
60
|
+
|
|
61
|
+
# Compare versions
|
|
62
|
+
try:
|
|
63
|
+
current_tuple = parse_version(current)
|
|
64
|
+
latest_tuple = parse_version(latest)
|
|
65
|
+
needs_update = latest_tuple > current_tuple
|
|
66
|
+
except (ValueError, AttributeError):
|
|
67
|
+
# If we can't parse versions, assume no update needed
|
|
68
|
+
needs_update = False
|
|
69
|
+
|
|
70
|
+
return VersionInfo(
|
|
71
|
+
current_version=current,
|
|
72
|
+
latest_version=latest,
|
|
73
|
+
needs_update=needs_update,
|
|
74
|
+
error=None,
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
# Don't block on network errors - just return current version
|
|
78
|
+
return VersionInfo(
|
|
79
|
+
current_version=current,
|
|
80
|
+
latest_version=None,
|
|
81
|
+
needs_update=False,
|
|
82
|
+
error=str(e),
|
|
83
|
+
)
|