openhands 1.0.4__tar.gz → 1.0.5__tar.gz
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.4 → openhands-1.0.5}/PKG-INFO +1 -1
- {openhands-1.0.4 → openhands-1.0.5}/build.py +16 -12
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/agent_chat.py +1 -1
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/settings/settings_screen.py +18 -6
- {openhands-1.0.4 → openhands-1.0.5}/pyproject.toml +1 -1
- openhands-1.0.5/tests/commands/test_settings_command.py +57 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/settings/test_settings_workflow.py +32 -0
- {openhands-1.0.4 → openhands-1.0.5}/uv.lock +1 -1
- {openhands-1.0.4 → openhands-1.0.5}/.gitignore +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/Makefile +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/README.md +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/build.sh +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/hooks/rthook_profile_imports.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands.spec +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/__init__.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/argparsers/main_parser.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/argparsers/serve_parser.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/gui_launcher.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/listeners/__init__.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/listeners/loading_listener.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/listeners/pause_listener.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/locations.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/pt_style.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/runner.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/setup.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/simple_main.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/__init__.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/settings/mcp_screen.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/settings/store.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/status.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/tui.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/tui/utils.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/__init__.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/agent_action.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/exit_session.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/settings_action.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/types.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/user_actions/utils.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/openhands_cli/utils.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/__init__.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/commands/test_confirm_command.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/commands/test_new_command.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/commands/test_resume_command.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/commands/test_status_command.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/conftest.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/settings/test_api_key_preservation.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/settings/test_default_agent_security_analyzer.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/settings/test_first_time_user_settings.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/settings/test_settings_input.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_confirmation_mode.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_conversation_runner.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_directory_separation.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_exit_session_confirmation.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_gui_launcher.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_loading.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_main.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_mcp_config_validation.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_pause_listener.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_session_prompter.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/test_tui.py +0 -0
- {openhands-1.0.4 → openhands-1.0.5}/tests/utils.py +0 -0
|
@@ -20,15 +20,6 @@ from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
|
20
20
|
|
|
21
21
|
from openhands.sdk import LLM
|
|
22
22
|
|
|
23
|
-
dummy_agent = get_default_cli_agent(
|
|
24
|
-
llm=LLM(
|
|
25
|
-
model='dummy-model',
|
|
26
|
-
api_key='dummy-key',
|
|
27
|
-
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
|
28
|
-
),
|
|
29
|
-
cli_mode=True,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
23
|
# =================================================
|
|
33
24
|
# SECTION: Build Binary
|
|
34
25
|
# =================================================
|
|
@@ -126,7 +117,7 @@ def _is_welcome(line: str) -> bool:
|
|
|
126
117
|
return any(marker in s for marker in WELCOME_MARKERS)
|
|
127
118
|
|
|
128
119
|
|
|
129
|
-
def test_executable() -> bool:
|
|
120
|
+
def test_executable(dummy_agent) -> bool:
|
|
130
121
|
"""Test the built executable, measuring boot time and total test time."""
|
|
131
122
|
print('🧪 Testing the built executable...')
|
|
132
123
|
|
|
@@ -274,7 +265,14 @@ def main() -> int:
|
|
|
274
265
|
|
|
275
266
|
# Test the executable
|
|
276
267
|
if not args.no_test:
|
|
277
|
-
|
|
268
|
+
dummy_agent = get_default_cli_agent(
|
|
269
|
+
llm=LLM(
|
|
270
|
+
model='dummy-model',
|
|
271
|
+
api_key='dummy-key',
|
|
272
|
+
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
if not test_executable(dummy_agent):
|
|
278
276
|
print('❌ Executable test failed, build process failed')
|
|
279
277
|
return 1
|
|
280
278
|
|
|
@@ -285,4 +283,10 @@ def main() -> int:
|
|
|
285
283
|
|
|
286
284
|
|
|
287
285
|
if __name__ == '__main__':
|
|
288
|
-
|
|
286
|
+
try:
|
|
287
|
+
sys.exit(main())
|
|
288
|
+
except Exception as e:
|
|
289
|
+
print(e)
|
|
290
|
+
print('❌ Executable test failed')
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
|
|
@@ -127,7 +127,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
127
127
|
break
|
|
128
128
|
|
|
129
129
|
elif command == '/settings':
|
|
130
|
-
settings_screen = SettingsScreen(conversation)
|
|
130
|
+
settings_screen = SettingsScreen(runner.conversation if runner else None)
|
|
131
131
|
settings_screen.display_settings()
|
|
132
132
|
continue
|
|
133
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from openhands.sdk import LLM, BaseConversation, LocalFileStore
|
|
3
|
+
from openhands.sdk import LLM, BaseConversation, LLMSummarizingCondenser, LocalFileStore
|
|
4
4
|
from prompt_toolkit import HTML, print_formatted_text
|
|
5
5
|
from prompt_toolkit.shortcuts import print_container
|
|
6
6
|
from prompt_toolkit.widgets import Frame, TextArea
|
|
@@ -33,9 +33,6 @@ class SettingsScreen:
|
|
|
33
33
|
agent_spec = self.agent_store.load()
|
|
34
34
|
if not agent_spec:
|
|
35
35
|
return
|
|
36
|
-
assert self.conversation is not None, (
|
|
37
|
-
'Conversation must be set to display settings.'
|
|
38
|
-
)
|
|
39
36
|
|
|
40
37
|
llm = agent_spec.llm
|
|
41
38
|
advanced_llm_settings = True if llm.base_url else False
|
|
@@ -62,12 +59,20 @@ class SettingsScreen:
|
|
|
62
59
|
labels_and_values.extend(
|
|
63
60
|
[
|
|
64
61
|
(' API Key', '********' if llm.api_key else 'Not Set'),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if self.conversation:
|
|
66
|
+
labels_and_values.extend([
|
|
65
67
|
(
|
|
66
68
|
' Confirmation Mode',
|
|
67
69
|
'Enabled'
|
|
68
70
|
if self.conversation.is_confirmation_mode_active
|
|
69
71
|
else 'Disabled',
|
|
70
|
-
)
|
|
72
|
+
)
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
labels_and_values.extend([
|
|
71
76
|
(
|
|
72
77
|
' Memory Condensation',
|
|
73
78
|
'Enabled' if agent_spec.condenser else 'Disabled',
|
|
@@ -153,7 +158,7 @@ class SettingsScreen:
|
|
|
153
158
|
api_key = prompt_api_key(
|
|
154
159
|
step_counter,
|
|
155
160
|
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
|
|
156
|
-
self.conversation.agent.llm.api_key if self.conversation else None,
|
|
161
|
+
self.conversation.state.agent.llm.api_key if self.conversation else None,
|
|
157
162
|
escapable=escapable,
|
|
158
163
|
)
|
|
159
164
|
memory_condensation = choose_memory_condensation(step_counter)
|
|
@@ -182,7 +187,14 @@ class SettingsScreen:
|
|
|
182
187
|
if not agent:
|
|
183
188
|
agent = get_default_cli_agent(llm=llm)
|
|
184
189
|
|
|
190
|
+
# Must update all LLMs
|
|
185
191
|
agent = agent.model_copy(update={'llm': llm})
|
|
192
|
+
condenser = LLMSummarizingCondenser(
|
|
193
|
+
llm=llm.model_copy(
|
|
194
|
+
update={"usage_id": "condenser"}
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
agent = agent.model_copy(update={'condenser': condenser})
|
|
186
198
|
self.agent_store.save(agent)
|
|
187
199
|
|
|
188
200
|
def _save_advanced_settings(
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Test for the /settings command functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
from prompt_toolkit.input.defaults import create_pipe_input
|
|
5
|
+
from prompt_toolkit.output.defaults import DummyOutput
|
|
6
|
+
|
|
7
|
+
from openhands_cli.agent_chat import run_cli_entry
|
|
8
|
+
from openhands_cli.user_actions import UserConfirmation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
|
12
|
+
@patch('openhands_cli.agent_chat.get_session_prompter')
|
|
13
|
+
@patch('openhands_cli.agent_chat.setup_conversation')
|
|
14
|
+
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
|
15
|
+
@patch('openhands_cli.agent_chat.ConversationRunner')
|
|
16
|
+
@patch('openhands_cli.agent_chat.SettingsScreen')
|
|
17
|
+
def test_settings_command_works_without_conversation(
|
|
18
|
+
mock_settings_screen_class,
|
|
19
|
+
mock_runner_cls,
|
|
20
|
+
mock_verify_agent,
|
|
21
|
+
mock_setup_conversation,
|
|
22
|
+
mock_get_session_prompter,
|
|
23
|
+
mock_exit_confirm,
|
|
24
|
+
):
|
|
25
|
+
"""Test that /settings command works when no conversation is active (bug fix scenario)."""
|
|
26
|
+
# Auto-accept the exit prompt to avoid interactive UI
|
|
27
|
+
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
28
|
+
|
|
29
|
+
# Mock agent verification to succeed
|
|
30
|
+
mock_agent = MagicMock()
|
|
31
|
+
mock_verify_agent.return_value = mock_agent
|
|
32
|
+
|
|
33
|
+
# Mock the SettingsScreen instance
|
|
34
|
+
mock_settings_screen = MagicMock()
|
|
35
|
+
mock_settings_screen_class.return_value = mock_settings_screen
|
|
36
|
+
|
|
37
|
+
# No runner initially (simulates starting CLI without a conversation)
|
|
38
|
+
mock_runner_cls.return_value = None
|
|
39
|
+
|
|
40
|
+
# Real session fed by a pipe
|
|
41
|
+
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
42
|
+
with create_pipe_input() as pipe:
|
|
43
|
+
output = DummyOutput()
|
|
44
|
+
session = real_get_session_prompter(input=pipe, output=output)
|
|
45
|
+
mock_get_session_prompter.return_value = session
|
|
46
|
+
|
|
47
|
+
# Trigger /settings, then /exit (exit will be auto-accepted)
|
|
48
|
+
for ch in "/settings\r/exit\r":
|
|
49
|
+
pipe.send_text(ch)
|
|
50
|
+
|
|
51
|
+
run_cli_entry(None)
|
|
52
|
+
|
|
53
|
+
# Assert SettingsScreen was created with None conversation (the bug fix)
|
|
54
|
+
mock_settings_screen_class.assert_called_once_with(None)
|
|
55
|
+
|
|
56
|
+
# Assert display_settings was called (settings screen was shown)
|
|
57
|
+
mock_settings_screen.display_settings.assert_called_once()
|
|
@@ -121,6 +121,38 @@ def test_update_existing_settings_workflow(tmp_path: Path):
|
|
|
121
121
|
assert True # If we get here, the workflow completed successfully
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def test_all_llms_in_agent_are_updated():
|
|
125
|
+
"""Test that modifying LLM settings creates multiple LLMs with same API key but different usage_ids."""
|
|
126
|
+
# Create a screen with existing agent settings
|
|
127
|
+
screen = SettingsScreen(conversation=None)
|
|
128
|
+
initial_llm = LLM(model='openai/gpt-3.5-turbo', api_key=SecretStr('sk-initial'), usage_id='test-service')
|
|
129
|
+
initial_agent = get_default_cli_agent(llm=initial_llm)
|
|
130
|
+
|
|
131
|
+
# Mock the agent store to return the initial agent and capture the save call
|
|
132
|
+
with (
|
|
133
|
+
patch.object(screen.agent_store, 'load', return_value=initial_agent),
|
|
134
|
+
patch.object(screen.agent_store, 'save') as mock_save
|
|
135
|
+
):
|
|
136
|
+
# Modify the LLM settings with new API key
|
|
137
|
+
screen._save_llm_settings(model='openai/gpt-4o-mini', api_key='sk-updated-123')
|
|
138
|
+
mock_save.assert_called_once()
|
|
139
|
+
|
|
140
|
+
# Get the saved agent from the mock
|
|
141
|
+
saved_agent = mock_save.call_args[0][0]
|
|
142
|
+
all_llms = list(saved_agent.get_all_llms())
|
|
143
|
+
assert len(all_llms) >= 2, f"Expected at least 2 LLMs, got {len(all_llms)}"
|
|
144
|
+
|
|
145
|
+
# Verify all LLMs have the same API key
|
|
146
|
+
api_keys = [llm.api_key.get_secret_value() for llm in all_llms]
|
|
147
|
+
assert all(api_key == 'sk-updated-123' for api_key in api_keys), \
|
|
148
|
+
f"Not all LLMs have the same API key: {api_keys}"
|
|
149
|
+
|
|
150
|
+
# Verify none of the usage_id attributes match
|
|
151
|
+
usage_ids = [llm.usage_id for llm in all_llms]
|
|
152
|
+
assert len(set(usage_ids)) == len(usage_ids), \
|
|
153
|
+
f"Some usage_ids are duplicated: {usage_ids}"
|
|
154
|
+
|
|
155
|
+
|
|
124
156
|
@pytest.mark.parametrize(
|
|
125
157
|
'step_to_cancel',
|
|
126
158
|
['type', 'provider', 'model', 'apikey', 'save'],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|