openhands 1.0.2__tar.gz → 1.0.4__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.2 → openhands-1.0.4}/PKG-INFO +5 -5
- {openhands-1.0.2 → openhands-1.0.4}/README.md +2 -2
- {openhands-1.0.2 → openhands-1.0.4}/build.py +3 -4
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/agent_chat.py +37 -10
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/gui_launcher.py +2 -2
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/pt_style.py +1 -1
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/runner.py +1 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/setup.py +33 -51
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/settings/settings_screen.py +3 -5
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/settings/store.py +1 -1
- openhands-1.0.2/openhands_cli/llm_utils.py → openhands-1.0.4/openhands_cli/utils.py +20 -1
- {openhands-1.0.2 → openhands-1.0.4}/pyproject.toml +3 -3
- {openhands-1.0.2 → openhands-1.0.4}/tests/commands/test_new_command.py +34 -35
- openhands-1.0.4/tests/commands/test_resume_command.py +147 -0
- openhands-1.0.4/tests/settings/test_default_agent_security_analyzer.py +104 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/settings/test_settings_workflow.py +4 -4
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_confirmation_mode.py +3 -2
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_conversation_runner.py +8 -8
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_directory_separation.py +1 -1
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_gui_launcher.py +1 -1
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_mcp_config_validation.py +1 -1
- {openhands-1.0.2 → openhands-1.0.4}/uv.lock +9 -9
- {openhands-1.0.2 → openhands-1.0.4}/.gitignore +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/Makefile +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/build.sh +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/hooks/rthook_profile_imports.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands.spec +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/__init__.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/argparsers/main_parser.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/argparsers/serve_parser.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/listeners/__init__.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/listeners/loading_listener.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/listeners/pause_listener.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/locations.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/simple_main.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/__init__.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/settings/mcp_screen.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/status.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/tui.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/tui/utils.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/__init__.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/agent_action.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/exit_session.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/settings_action.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/types.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/openhands_cli/user_actions/utils.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/__init__.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/commands/test_confirm_command.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/commands/test_status_command.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/conftest.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/settings/test_api_key_preservation.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/settings/test_first_time_user_settings.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/settings/test_settings_input.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_exit_session_confirmation.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_loading.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_main.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_pause_listener.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_session_prompter.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/test_tui.py +0 -0
- {openhands-1.0.2 → openhands-1.0.4}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: OpenHands CLI - Terminal User Interface for OpenHands AI Agent
|
|
5
5
|
Author-email: OpenHands Team <contact@all-hands.dev>
|
|
6
6
|
License: MIT
|
|
@@ -8,17 +8,17 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.12
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.13
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
|
-
Requires-Dist: openhands-sdk==1.0.
|
|
12
|
-
Requires-Dist: openhands-tools==1.0.
|
|
11
|
+
Requires-Dist: openhands-sdk==1.0.0a5
|
|
12
|
+
Requires-Dist: openhands-tools==1.0.0a5
|
|
13
13
|
Requires-Dist: prompt-toolkit>=3
|
|
14
14
|
Requires-Dist: typer>=0.17.4
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# OpenHands V1 CLI
|
|
18
18
|
|
|
19
|
-
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/
|
|
19
|
+
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/OpenHands/agent-sdk)).
|
|
20
20
|
|
|
21
|
-
The [OpenHands V0 CLI (legacy)](https://github.com/
|
|
21
|
+
The [OpenHands V0 CLI (legacy)](https://github.com/OpenHands/OpenHands/tree/main/openhands/cli) is being deprecated.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# OpenHands V1 CLI
|
|
2
2
|
|
|
3
|
-
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/
|
|
3
|
+
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/OpenHands/agent-sdk)).
|
|
4
4
|
|
|
5
|
-
The [OpenHands V0 CLI (legacy)](https://github.com/
|
|
5
|
+
The [OpenHands V0 CLI (legacy)](https://github.com/OpenHands/OpenHands/tree/main/openhands/cli) is being deprecated.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -15,13 +15,12 @@ import sys
|
|
|
15
15
|
import time
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
-
from openhands_cli.
|
|
19
|
-
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
18
|
+
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
|
19
|
+
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
20
20
|
|
|
21
21
|
from openhands.sdk import LLM
|
|
22
|
-
from openhands.tools.preset.default import get_default_agent
|
|
23
22
|
|
|
24
|
-
dummy_agent =
|
|
23
|
+
dummy_agent = get_default_cli_agent(
|
|
25
24
|
llm=LLM(
|
|
26
25
|
model='dummy-model',
|
|
27
26
|
api_key='dummy-key',
|
|
@@ -6,6 +6,7 @@ Provides a conversation interface with an AI agent using OpenHands patterns.
|
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
8
|
from datetime import datetime
|
|
9
|
+
import uuid
|
|
9
10
|
|
|
10
11
|
from openhands.sdk import (
|
|
11
12
|
Message,
|
|
@@ -16,7 +17,11 @@ from prompt_toolkit import print_formatted_text
|
|
|
16
17
|
from prompt_toolkit.formatted_text import HTML
|
|
17
18
|
|
|
18
19
|
from openhands_cli.runner import ConversationRunner
|
|
19
|
-
from openhands_cli.setup import
|
|
20
|
+
from openhands_cli.setup import (
|
|
21
|
+
MissingAgentSpec,
|
|
22
|
+
setup_conversation,
|
|
23
|
+
verify_agent_exists_or_setup_agent
|
|
24
|
+
)
|
|
20
25
|
from openhands_cli.tui.settings.mcp_screen import MCPScreen
|
|
21
26
|
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
22
27
|
from openhands_cli.tui.status import display_status
|
|
@@ -65,21 +70,33 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
65
70
|
EOFError: If EOF is encountered
|
|
66
71
|
"""
|
|
67
72
|
|
|
73
|
+
conversation_id = uuid.uuid4()
|
|
74
|
+
if resume_conversation_id:
|
|
75
|
+
try:
|
|
76
|
+
conversation_id = uuid.UUID(resume_conversation_id)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
print_formatted_text(
|
|
79
|
+
HTML(
|
|
80
|
+
f"<yellow>Warning: '{resume_conversation_id}' is not a valid UUID.</yellow>"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
|
|
68
85
|
try:
|
|
69
|
-
|
|
86
|
+
initialized_agent = verify_agent_exists_or_setup_agent()
|
|
70
87
|
except MissingAgentSpec:
|
|
71
88
|
print_formatted_text(HTML('\n<yellow>Setup is required to use OpenHands CLI.</yellow>'))
|
|
72
89
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
73
90
|
return
|
|
74
91
|
|
|
75
92
|
|
|
76
|
-
display_welcome(
|
|
93
|
+
display_welcome(conversation_id, bool(resume_conversation_id))
|
|
77
94
|
|
|
78
95
|
# Track session start time for uptime calculation
|
|
79
96
|
session_start_time = datetime.now()
|
|
80
97
|
|
|
81
98
|
# Create conversation runner to handle state machine logic
|
|
82
|
-
runner =
|
|
99
|
+
runner = None
|
|
83
100
|
session = get_session_prompter()
|
|
84
101
|
|
|
85
102
|
# Main chat loop
|
|
@@ -106,7 +123,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
106
123
|
exit_confirmation = exit_session_confirmation()
|
|
107
124
|
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
108
125
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
109
|
-
_print_exit_hint(
|
|
126
|
+
_print_exit_hint(conversation_id)
|
|
110
127
|
break
|
|
111
128
|
|
|
112
129
|
elif command == '/settings':
|
|
@@ -116,19 +133,19 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
116
133
|
|
|
117
134
|
elif command == '/mcp':
|
|
118
135
|
mcp_screen = MCPScreen()
|
|
119
|
-
mcp_screen.display_mcp_info(
|
|
136
|
+
mcp_screen.display_mcp_info(initialized_agent)
|
|
120
137
|
continue
|
|
121
138
|
|
|
122
139
|
elif command == '/clear':
|
|
123
|
-
display_welcome(
|
|
140
|
+
display_welcome(conversation_id)
|
|
124
141
|
continue
|
|
125
142
|
|
|
126
143
|
elif command == '/new':
|
|
127
144
|
try:
|
|
128
145
|
# Start a fresh conversation (no resume ID = new conversation)
|
|
129
|
-
conversation = setup_conversation()
|
|
146
|
+
conversation = setup_conversation(conversation_id)
|
|
130
147
|
runner = ConversationRunner(conversation)
|
|
131
|
-
display_welcome(
|
|
148
|
+
display_welcome(conversation_id, resume=False)
|
|
132
149
|
print_formatted_text(
|
|
133
150
|
HTML('<green>✓ Started fresh conversation</green>')
|
|
134
151
|
)
|
|
@@ -158,6 +175,13 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
158
175
|
continue
|
|
159
176
|
|
|
160
177
|
elif command == '/resume':
|
|
178
|
+
if not runner:
|
|
179
|
+
print_formatted_text(
|
|
180
|
+
HTML('<yellow>No active conversation running...</yellow>')
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
conversation = runner.conversation
|
|
161
185
|
if not (
|
|
162
186
|
conversation.state.agent_status == AgentExecutionStatus.PAUSED
|
|
163
187
|
or conversation.state.agent_status
|
|
@@ -171,6 +195,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
171
195
|
# Resume without new message
|
|
172
196
|
message = None
|
|
173
197
|
|
|
198
|
+
if not runner:
|
|
199
|
+
conversation = setup_conversation(conversation_id)
|
|
200
|
+
runner = ConversationRunner(conversation)
|
|
174
201
|
runner.process_message(message)
|
|
175
202
|
|
|
176
203
|
print() # Add spacing
|
|
@@ -179,7 +206,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
179
206
|
exit_confirmation = exit_session_confirmation()
|
|
180
207
|
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
181
208
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
182
|
-
_print_exit_hint(
|
|
209
|
+
_print_exit_hint(conversation_id)
|
|
183
210
|
break
|
|
184
211
|
|
|
185
212
|
# Clean up terminal state
|
|
@@ -104,8 +104,8 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
|
|
|
104
104
|
|
|
105
105
|
# Get the current version for the Docker image
|
|
106
106
|
version = get_openhands_version()
|
|
107
|
-
runtime_image = f'docker.
|
|
108
|
-
app_image = f'docker.
|
|
107
|
+
runtime_image = f'docker.openhands.dev/openhands/runtime:{version}-nikolaik'
|
|
108
|
+
app_image = f'docker.openhands.dev/openhands/openhands:{version}'
|
|
109
109
|
|
|
110
110
|
print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
|
|
111
111
|
|
|
@@ -20,7 +20,7 @@ def get_cli_style() -> BaseStyle:
|
|
|
20
20
|
'prompt': f'{COLOR_GOLD} bold',
|
|
21
21
|
# Ensure good contrast for fuzzy matches on the selected completion row
|
|
22
22
|
# across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
|
|
23
|
-
# See https://github.com/
|
|
23
|
+
# See https://github.com/OpenHands/OpenHands/issues/10330
|
|
24
24
|
'completion-menu.completion.current fuzzymatch.outside': 'fg:#ffffff bg:#888888',
|
|
25
25
|
'selected': COLOR_GOLD,
|
|
26
26
|
'risk-high': '#FF0000 bold', # Red bold for HIGH risk
|
|
@@ -2,7 +2,7 @@ import uuid
|
|
|
2
2
|
|
|
3
3
|
from prompt_toolkit import HTML, print_formatted_text
|
|
4
4
|
|
|
5
|
-
from openhands.sdk import BaseConversation, Conversation, Workspace, register_tool
|
|
5
|
+
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace, register_tool
|
|
6
6
|
from openhands.tools.execute_bash import BashTool
|
|
7
7
|
from openhands.tools.file_editor import FileEditorTool
|
|
8
8
|
from openhands.tools.task_tracker import TaskTrackerTool
|
|
@@ -26,8 +26,38 @@ class MissingAgentSpec(Exception):
|
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
def load_agent_specs(
|
|
30
31
|
conversation_id: str | None = None,
|
|
32
|
+
) -> Agent:
|
|
33
|
+
agent_store = AgentStore()
|
|
34
|
+
agent = agent_store.load(session_id=conversation_id)
|
|
35
|
+
if not agent:
|
|
36
|
+
raise MissingAgentSpec(
|
|
37
|
+
'Agent specification not found. Please configure your agent settings.'
|
|
38
|
+
)
|
|
39
|
+
return agent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify_agent_exists_or_setup_agent() -> Agent:
|
|
43
|
+
"""Verify agent specs exists by attempting to load it.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
settings_screen = SettingsScreen()
|
|
47
|
+
try:
|
|
48
|
+
agent = load_agent_specs()
|
|
49
|
+
return agent
|
|
50
|
+
except MissingAgentSpec:
|
|
51
|
+
# For first-time users, show the full settings flow with choice between basic/advanced
|
|
52
|
+
settings_screen.configure_settings(first_time=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Try once again after settings setup attempt
|
|
56
|
+
return load_agent_specs()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_conversation(
|
|
60
|
+
conversation_id: uuid,
|
|
31
61
|
include_security_analyzer: bool = True
|
|
32
62
|
) -> BaseConversation:
|
|
33
63
|
"""
|
|
@@ -40,28 +70,8 @@ def setup_conversation(
|
|
|
40
70
|
MissingAgentSpec: If agent specification is not found or invalid.
|
|
41
71
|
"""
|
|
42
72
|
|
|
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
73
|
with LoadingContext('Initializing OpenHands agent...'):
|
|
58
|
-
|
|
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
|
-
|
|
74
|
+
agent = load_agent_specs(str(conversation_id))
|
|
65
75
|
|
|
66
76
|
if not include_security_analyzer:
|
|
67
77
|
# Remove security analyzer from agent spec
|
|
@@ -86,31 +96,3 @@ def setup_conversation(
|
|
|
86
96
|
)
|
|
87
97
|
return conversation
|
|
88
98
|
|
|
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)
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from openhands.sdk import LLM, BaseConversation, LocalFileStore
|
|
4
|
-
from openhands.sdk.security.confirmation_policy import NeverConfirm
|
|
5
|
-
from openhands.tools.preset.default import get_default_agent
|
|
6
4
|
from prompt_toolkit import HTML, print_formatted_text
|
|
7
5
|
from prompt_toolkit.shortcuts import print_container
|
|
8
6
|
from prompt_toolkit.widgets import Frame, TextArea
|
|
9
7
|
|
|
10
|
-
from openhands_cli.
|
|
8
|
+
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
|
11
9
|
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
12
10
|
from openhands_cli.pt_style import COLOR_GREY
|
|
13
11
|
from openhands_cli.tui.settings.store import AgentStore
|
|
@@ -176,13 +174,13 @@ class SettingsScreen:
|
|
|
176
174
|
model=model,
|
|
177
175
|
api_key=api_key,
|
|
178
176
|
base_url=base_url,
|
|
179
|
-
|
|
177
|
+
usage_id='agent',
|
|
180
178
|
metadata=get_llm_metadata(model_name=model, llm_type='agent'),
|
|
181
179
|
)
|
|
182
180
|
|
|
183
181
|
agent = self.agent_store.load()
|
|
184
182
|
if not agent:
|
|
185
|
-
agent =
|
|
183
|
+
agent = get_default_cli_agent(llm=llm)
|
|
186
184
|
|
|
187
185
|
agent = agent.model_copy(update={'llm': llm})
|
|
188
186
|
self.agent_store.save(agent)
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from fastmcp.mcp_config import MCPConfig
|
|
8
|
-
from openhands_cli.
|
|
8
|
+
from openhands_cli.utils import get_llm_metadata
|
|
9
9
|
from openhands_cli.locations import (
|
|
10
10
|
AGENT_SETTINGS_PATH,
|
|
11
11
|
MCP_CONFIG_FILE,
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from typing import Any
|
|
5
|
-
|
|
5
|
+
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
|
6
|
+
from openhands.tools.preset import get_default_agent
|
|
7
|
+
from openhands.sdk import LLM
|
|
6
8
|
|
|
7
9
|
def get_llm_metadata(
|
|
8
10
|
model_name: str,
|
|
@@ -55,3 +57,20 @@ def get_llm_metadata(
|
|
|
55
57
|
if user_id is not None:
|
|
56
58
|
metadata['trace_user_id'] = user_id
|
|
57
59
|
return metadata
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_default_cli_agent(
|
|
63
|
+
llm: LLM
|
|
64
|
+
):
|
|
65
|
+
agent = get_default_agent(
|
|
66
|
+
llm=llm,
|
|
67
|
+
cli_mode=True
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
agent = agent.model_copy(
|
|
71
|
+
update={
|
|
72
|
+
'security_analyzer': LLMSecurityAnalyzer()
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return agent
|
|
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openhands"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -18,8 +18,8 @@ classifiers = [
|
|
|
18
18
|
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
|
|
19
19
|
# TODO: pin package versions once agent-sdk has published PyPI packages
|
|
20
20
|
dependencies = [
|
|
21
|
-
"openhands-sdk==1.0.
|
|
22
|
-
"openhands-tools==1.0.
|
|
21
|
+
"openhands-sdk==1.0.0a5",
|
|
22
|
+
"openhands-tools==1.0.0a5",
|
|
23
23
|
"prompt-toolkit>=3",
|
|
24
24
|
"typer>=0.17.4",
|
|
25
25
|
]
|
|
@@ -4,51 +4,49 @@ from unittest.mock import MagicMock, patch
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
from prompt_toolkit.input.defaults import create_pipe_input
|
|
6
6
|
from prompt_toolkit.output.defaults import DummyOutput
|
|
7
|
-
from openhands_cli.setup import MissingAgentSpec,
|
|
7
|
+
from openhands_cli.setup import MissingAgentSpec, verify_agent_exists_or_setup_agent, setup_conversation
|
|
8
8
|
from openhands_cli.user_actions import UserConfirmation
|
|
9
9
|
|
|
10
|
-
@patch('openhands_cli.setup.
|
|
11
|
-
def
|
|
12
|
-
"""Test that
|
|
13
|
-
# Mock the
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
mock_setup_conversation.return_value = mock_conversation
|
|
10
|
+
@patch('openhands_cli.setup.load_agent_specs')
|
|
11
|
+
def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs):
|
|
12
|
+
"""Test that verify_agent_exists_or_setup_agent returns agent successfully."""
|
|
13
|
+
# Mock the agent object
|
|
14
|
+
mock_agent = MagicMock()
|
|
15
|
+
mock_load_agent_specs.return_value = mock_agent
|
|
17
16
|
|
|
18
17
|
# Call the function
|
|
19
|
-
result =
|
|
18
|
+
result = verify_agent_exists_or_setup_agent()
|
|
20
19
|
|
|
21
20
|
# Verify the result
|
|
22
|
-
assert result ==
|
|
23
|
-
|
|
21
|
+
assert result == mock_agent
|
|
22
|
+
mock_load_agent_specs.assert_called_once_with()
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@patch('openhands_cli.setup.SettingsScreen')
|
|
27
|
-
@patch('openhands_cli.setup.
|
|
28
|
-
def
|
|
29
|
-
|
|
26
|
+
@patch('openhands_cli.setup.load_agent_specs')
|
|
27
|
+
def test_verify_agent_exists_or_setup_agent_missing_agent_spec(
|
|
28
|
+
mock_load_agent_specs,
|
|
30
29
|
mock_settings_screen_class
|
|
31
30
|
):
|
|
32
|
-
"""Test that
|
|
31
|
+
"""Test that verify_agent_exists_or_setup_agent handles MissingAgentSpec exception."""
|
|
33
32
|
# Mock the SettingsScreen instance
|
|
34
33
|
mock_settings_screen = MagicMock()
|
|
35
34
|
mock_settings_screen_class.return_value = mock_settings_screen
|
|
36
35
|
|
|
37
|
-
# Mock
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
mock_setup_conversation.side_effect = [
|
|
36
|
+
# Mock load_agent_specs to raise MissingAgentSpec on first call, then succeed
|
|
37
|
+
mock_agent = MagicMock()
|
|
38
|
+
mock_load_agent_specs.side_effect = [
|
|
41
39
|
MissingAgentSpec("Agent spec missing"),
|
|
42
|
-
|
|
40
|
+
mock_agent
|
|
43
41
|
]
|
|
44
42
|
|
|
45
43
|
# Call the function
|
|
46
|
-
result =
|
|
44
|
+
result = verify_agent_exists_or_setup_agent()
|
|
47
45
|
|
|
48
46
|
# Verify the result
|
|
49
|
-
assert result ==
|
|
47
|
+
assert result == mock_agent
|
|
50
48
|
# Should be called twice: first fails, second succeeds
|
|
51
|
-
assert
|
|
49
|
+
assert mock_load_agent_specs.call_count == 2
|
|
52
50
|
# Settings screen should be called once with first_time=True (new behavior)
|
|
53
51
|
mock_settings_screen.configure_settings.assert_called_once_with(first_time=True)
|
|
54
52
|
|
|
@@ -59,11 +57,11 @@ def test_start_fresh_conversation_missing_agent_spec(
|
|
|
59
57
|
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
|
60
58
|
@patch('openhands_cli.agent_chat.get_session_prompter')
|
|
61
59
|
@patch('openhands_cli.agent_chat.setup_conversation')
|
|
62
|
-
@patch('openhands_cli.agent_chat.
|
|
60
|
+
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
|
63
61
|
@patch('openhands_cli.agent_chat.ConversationRunner')
|
|
64
62
|
def test_new_command_resets_confirmation_mode(
|
|
65
63
|
mock_runner_cls,
|
|
66
|
-
|
|
64
|
+
mock_verify_agent,
|
|
67
65
|
mock_setup_conversation,
|
|
68
66
|
mock_get_session_prompter,
|
|
69
67
|
mock_exit_confirm,
|
|
@@ -71,15 +69,17 @@ def test_new_command_resets_confirmation_mode(
|
|
|
71
69
|
# Auto-accept the exit prompt to avoid interactive UI and EOFError
|
|
72
70
|
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
73
71
|
|
|
72
|
+
# Mock agent verification to succeed
|
|
73
|
+
mock_agent = MagicMock()
|
|
74
|
+
mock_verify_agent.return_value = mock_agent
|
|
75
|
+
|
|
76
|
+
# Mock conversation - only one is created when /new is called
|
|
74
77
|
conv1 = MagicMock(); conv1.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
75
|
-
|
|
76
|
-
mock_start_fresh_conversation.return_value = conv1
|
|
77
|
-
mock_setup_conversation.side_effect = [conv2]
|
|
78
|
+
mock_setup_conversation.return_value = conv1
|
|
78
79
|
|
|
79
|
-
#
|
|
80
|
+
# One runner instance for the conversation
|
|
80
81
|
runner1 = MagicMock(); runner1.is_confirmation_mode_active = True
|
|
81
|
-
|
|
82
|
-
mock_runner_cls.side_effect = [runner1, runner2]
|
|
82
|
+
mock_runner_cls.return_value = runner1
|
|
83
83
|
|
|
84
84
|
# Real session fed by a pipe (no interactive confirmation now)
|
|
85
85
|
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
@@ -89,13 +89,12 @@ def test_new_command_resets_confirmation_mode(
|
|
|
89
89
|
mock_get_session_prompter.return_value = session
|
|
90
90
|
|
|
91
91
|
from openhands_cli.agent_chat import run_cli_entry
|
|
92
|
-
# Trigger /new, then /
|
|
92
|
+
# Trigger /new, then /exit (exit will be auto-accepted)
|
|
93
93
|
for ch in "/new\r/exit\r":
|
|
94
94
|
pipe.send_text(ch)
|
|
95
95
|
|
|
96
96
|
run_cli_entry(None)
|
|
97
97
|
|
|
98
|
-
# Assert we
|
|
99
|
-
assert mock_runner_cls.call_count ==
|
|
98
|
+
# Assert we created one runner for the conversation when /new was called
|
|
99
|
+
assert mock_runner_cls.call_count == 1
|
|
100
100
|
assert mock_runner_cls.call_args_list[0].args[0] is conv1
|
|
101
|
-
assert mock_runner_cls.call_args_list[1].args[0] is conv2
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Tests for the /resume command functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
import pytest
|
|
6
|
+
from prompt_toolkit.input.defaults import create_pipe_input
|
|
7
|
+
from prompt_toolkit.output.defaults import DummyOutput
|
|
8
|
+
|
|
9
|
+
from openhands.sdk.conversation.state import AgentExecutionStatus
|
|
10
|
+
from openhands_cli.user_actions import UserConfirmation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------- Fixtures & helpers ----------
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_agent():
|
|
17
|
+
"""Mock agent for verification."""
|
|
18
|
+
return MagicMock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_conversation():
|
|
23
|
+
"""Mock conversation with default settings."""
|
|
24
|
+
conv = MagicMock()
|
|
25
|
+
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
26
|
+
return conv
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_runner():
|
|
31
|
+
"""Mock conversation runner."""
|
|
32
|
+
return MagicMock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_resume_command_test(commands, agent_status=None, expect_runner_created=True):
|
|
36
|
+
"""Helper function to run resume command tests with common setup."""
|
|
37
|
+
with patch('openhands_cli.agent_chat.exit_session_confirmation') as mock_exit_confirm, \
|
|
38
|
+
patch('openhands_cli.agent_chat.get_session_prompter') as mock_get_session_prompter, \
|
|
39
|
+
patch('openhands_cli.agent_chat.setup_conversation') as mock_setup_conversation, \
|
|
40
|
+
patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent') as mock_verify_agent, \
|
|
41
|
+
patch('openhands_cli.agent_chat.ConversationRunner') as mock_runner_cls:
|
|
42
|
+
|
|
43
|
+
# Auto-accept the exit prompt to avoid interactive UI
|
|
44
|
+
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
45
|
+
|
|
46
|
+
# Mock agent verification to succeed
|
|
47
|
+
mock_agent = MagicMock()
|
|
48
|
+
mock_verify_agent.return_value = mock_agent
|
|
49
|
+
|
|
50
|
+
# Mock conversation setup
|
|
51
|
+
conv = MagicMock()
|
|
52
|
+
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
53
|
+
if agent_status:
|
|
54
|
+
conv.state.agent_status = agent_status
|
|
55
|
+
mock_setup_conversation.return_value = conv
|
|
56
|
+
|
|
57
|
+
# Mock runner
|
|
58
|
+
runner = MagicMock()
|
|
59
|
+
runner.conversation = conv
|
|
60
|
+
mock_runner_cls.return_value = runner
|
|
61
|
+
|
|
62
|
+
# Real session fed by a pipe
|
|
63
|
+
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
64
|
+
with create_pipe_input() as pipe:
|
|
65
|
+
output = DummyOutput()
|
|
66
|
+
session = real_get_session_prompter(input=pipe, output=output)
|
|
67
|
+
mock_get_session_prompter.return_value = session
|
|
68
|
+
|
|
69
|
+
from openhands_cli.agent_chat import run_cli_entry
|
|
70
|
+
|
|
71
|
+
# Send commands
|
|
72
|
+
for ch in commands:
|
|
73
|
+
pipe.send_text(ch)
|
|
74
|
+
|
|
75
|
+
# Capture printed output
|
|
76
|
+
with patch('openhands_cli.agent_chat.print_formatted_text') as mock_print:
|
|
77
|
+
run_cli_entry(None)
|
|
78
|
+
|
|
79
|
+
return mock_runner_cls, runner, mock_print
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------- Warning tests (parametrized) ----------
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
"commands,expected_warning,expect_runner_created",
|
|
86
|
+
[
|
|
87
|
+
# No active conversation - /resume immediately
|
|
88
|
+
("/resume\r/exit\r", "No active conversation running", False),
|
|
89
|
+
# Conversation exists but not in paused state - send message first, then /resume
|
|
90
|
+
("hello\r/resume\r/exit\r", "No paused conversation to resume", True),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def test_resume_command_warnings(commands, expected_warning, expect_runner_created):
|
|
94
|
+
"""Test /resume command shows appropriate warnings."""
|
|
95
|
+
# Set agent status to FINISHED for the "conversation exists but not paused" test
|
|
96
|
+
agent_status = AgentExecutionStatus.FINISHED if expect_runner_created else None
|
|
97
|
+
|
|
98
|
+
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
|
99
|
+
commands, agent_status=agent_status, expect_runner_created=expect_runner_created
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Verify warning message was printed
|
|
103
|
+
warning_calls = [call for call in mock_print.call_args_list
|
|
104
|
+
if expected_warning in str(call)]
|
|
105
|
+
assert len(warning_calls) > 0, f"Expected warning about {expected_warning}"
|
|
106
|
+
|
|
107
|
+
# Verify runner creation expectation
|
|
108
|
+
if expect_runner_created:
|
|
109
|
+
assert mock_runner_cls.call_count == 1
|
|
110
|
+
runner.process_message.assert_called()
|
|
111
|
+
else:
|
|
112
|
+
assert mock_runner_cls.call_count == 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------- Successful resume tests (parametrized) ----------
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(
|
|
118
|
+
"agent_status",
|
|
119
|
+
[
|
|
120
|
+
AgentExecutionStatus.PAUSED,
|
|
121
|
+
AgentExecutionStatus.WAITING_FOR_CONFIRMATION,
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
def test_resume_command_successful_resume(agent_status):
|
|
125
|
+
"""Test /resume command successfully resumes paused/waiting conversations."""
|
|
126
|
+
commands = "hello\r/resume\r/exit\r"
|
|
127
|
+
|
|
128
|
+
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
|
129
|
+
commands, agent_status=agent_status, expect_runner_created=True
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify runner was created and process_message was called
|
|
133
|
+
assert mock_runner_cls.call_count == 1
|
|
134
|
+
|
|
135
|
+
# Verify process_message was called twice: once with the initial message, once with None for resume
|
|
136
|
+
assert runner.process_message.call_count == 2
|
|
137
|
+
|
|
138
|
+
# Check the calls to process_message
|
|
139
|
+
calls = runner.process_message.call_args_list
|
|
140
|
+
|
|
141
|
+
# First call should have a message (the "hello" message)
|
|
142
|
+
first_call_args = calls[0][0]
|
|
143
|
+
assert first_call_args[0] is not None, "First call should have a message"
|
|
144
|
+
|
|
145
|
+
# Second call should have None (the /resume command)
|
|
146
|
+
second_call_args = calls[1][0]
|
|
147
|
+
assert second_call_args[0] is None, "Second call should have None message for resume"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Test that first-time settings screen usage creates a default agent with security analyzer."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
import pytest
|
|
5
|
+
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
6
|
+
from openhands_cli.user_actions.settings_action import SettingsType
|
|
7
|
+
from openhands.sdk import LLM
|
|
8
|
+
from pydantic import SecretStr
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_first_time_settings_creates_default_agent_with_security_analyzer():
|
|
12
|
+
"""Test that using the settings screen for the first time creates a default agent with a non-None security analyzer."""
|
|
13
|
+
|
|
14
|
+
# Create a settings screen instance (no conversation initially)
|
|
15
|
+
screen = SettingsScreen(conversation=None)
|
|
16
|
+
|
|
17
|
+
# Mock all the user interaction steps to simulate first-time setup
|
|
18
|
+
with (
|
|
19
|
+
patch(
|
|
20
|
+
'openhands_cli.tui.settings.settings_screen.settings_type_confirmation',
|
|
21
|
+
return_value=SettingsType.BASIC,
|
|
22
|
+
),
|
|
23
|
+
patch(
|
|
24
|
+
'openhands_cli.tui.settings.settings_screen.choose_llm_provider',
|
|
25
|
+
return_value='openai',
|
|
26
|
+
),
|
|
27
|
+
patch(
|
|
28
|
+
'openhands_cli.tui.settings.settings_screen.choose_llm_model',
|
|
29
|
+
return_value='gpt-4o-mini',
|
|
30
|
+
),
|
|
31
|
+
patch(
|
|
32
|
+
'openhands_cli.tui.settings.settings_screen.prompt_api_key',
|
|
33
|
+
return_value='sk-test-key-123',
|
|
34
|
+
),
|
|
35
|
+
patch(
|
|
36
|
+
'openhands_cli.tui.settings.settings_screen.save_settings_confirmation',
|
|
37
|
+
return_value=True,
|
|
38
|
+
),
|
|
39
|
+
):
|
|
40
|
+
# Run the settings configuration workflow
|
|
41
|
+
screen.configure_settings(first_time=True)
|
|
42
|
+
|
|
43
|
+
# Load the saved agent from the store
|
|
44
|
+
saved_agent = screen.agent_store.load()
|
|
45
|
+
|
|
46
|
+
# Verify that an agent was created and saved
|
|
47
|
+
assert saved_agent is not None, "Agent should be created and saved after first-time settings configuration"
|
|
48
|
+
|
|
49
|
+
# Verify that the agent has the expected LLM configuration
|
|
50
|
+
assert saved_agent.llm.model == 'openai/gpt-4o-mini', f"Expected model 'openai/gpt-4o-mini', got '{saved_agent.llm.model}'"
|
|
51
|
+
assert saved_agent.llm.api_key.get_secret_value() == 'sk-test-key-123', "API key should match the provided value"
|
|
52
|
+
|
|
53
|
+
# Verify that the agent has a security analyzer and it's not None
|
|
54
|
+
assert hasattr(saved_agent, 'security_analyzer'), "Agent should have a security_analyzer attribute"
|
|
55
|
+
assert saved_agent.security_analyzer is not None, "Security analyzer should not be None"
|
|
56
|
+
|
|
57
|
+
# Verify the security analyzer has the expected type/kind
|
|
58
|
+
assert hasattr(saved_agent.security_analyzer, 'kind'), "Security analyzer should have a 'kind' attribute"
|
|
59
|
+
assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', f"Expected security analyzer kind 'LLMSecurityAnalyzer', got '{saved_agent.security_analyzer.kind}'"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_first_time_settings_with_advanced_configuration():
|
|
63
|
+
"""Test that advanced settings also create a default agent with security analyzer."""
|
|
64
|
+
|
|
65
|
+
screen = SettingsScreen(conversation=None)
|
|
66
|
+
|
|
67
|
+
with (
|
|
68
|
+
patch(
|
|
69
|
+
'openhands_cli.tui.settings.settings_screen.settings_type_confirmation',
|
|
70
|
+
return_value=SettingsType.ADVANCED,
|
|
71
|
+
),
|
|
72
|
+
patch(
|
|
73
|
+
'openhands_cli.tui.settings.settings_screen.prompt_custom_model',
|
|
74
|
+
return_value='anthropic/claude-3-5-sonnet',
|
|
75
|
+
),
|
|
76
|
+
patch(
|
|
77
|
+
'openhands_cli.tui.settings.settings_screen.prompt_base_url',
|
|
78
|
+
return_value='https://api.anthropic.com',
|
|
79
|
+
),
|
|
80
|
+
patch(
|
|
81
|
+
'openhands_cli.tui.settings.settings_screen.prompt_api_key',
|
|
82
|
+
return_value='sk-ant-test-key',
|
|
83
|
+
),
|
|
84
|
+
patch(
|
|
85
|
+
'openhands_cli.tui.settings.settings_screen.choose_memory_condensation',
|
|
86
|
+
return_value=True,
|
|
87
|
+
),
|
|
88
|
+
patch(
|
|
89
|
+
'openhands_cli.tui.settings.settings_screen.save_settings_confirmation',
|
|
90
|
+
return_value=True,
|
|
91
|
+
),
|
|
92
|
+
):
|
|
93
|
+
screen.configure_settings(first_time=True)
|
|
94
|
+
|
|
95
|
+
saved_agent = screen.agent_store.load()
|
|
96
|
+
|
|
97
|
+
# Verify agent creation and security analyzer
|
|
98
|
+
assert saved_agent is not None, "Agent should be created with advanced settings"
|
|
99
|
+
assert saved_agent.security_analyzer is not None, "Security analyzer should not be None in advanced settings"
|
|
100
|
+
assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', "Security analyzer should be LLMSecurityAnalyzer"
|
|
101
|
+
|
|
102
|
+
# Verify advanced settings were applied
|
|
103
|
+
assert saved_agent.llm.model == 'anthropic/claude-3-5-sonnet', "Custom model should be set"
|
|
104
|
+
assert saved_agent.llm.base_url == 'https://api.anthropic.com', "Base URL should be set"
|
|
@@ -6,10 +6,10 @@ import pytest
|
|
|
6
6
|
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
7
7
|
from openhands_cli.tui.settings.store import AgentStore
|
|
8
8
|
from openhands_cli.user_actions.settings_action import SettingsType
|
|
9
|
+
from openhands_cli.utils import get_default_cli_agent
|
|
9
10
|
from pydantic import SecretStr
|
|
10
11
|
|
|
11
12
|
from openhands.sdk import LLM, Conversation, LocalFileStore
|
|
12
|
-
from openhands.tools.preset.default import get_default_agent
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def read_json(path: Path) -> dict:
|
|
@@ -18,7 +18,7 @@ def read_json(path: Path) -> dict:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def make_screen_with_conversation(model='openai/gpt-4o-mini', api_key='sk-xyz'):
|
|
21
|
-
llm = LLM(model=model, api_key=SecretStr(api_key),
|
|
21
|
+
llm = LLM(model=model, api_key=SecretStr(api_key), usage_id='test-service')
|
|
22
22
|
# Conversation(agent) signature may vary across versions; adapt if needed:
|
|
23
23
|
from openhands.sdk.agent import Agent
|
|
24
24
|
|
|
@@ -30,8 +30,8 @@ def make_screen_with_conversation(model='openai/gpt-4o-mini', api_key='sk-xyz'):
|
|
|
30
30
|
def seed_file(path: Path, model: str = 'openai/gpt-4o-mini', api_key: str = 'sk-old'):
|
|
31
31
|
store = AgentStore()
|
|
32
32
|
store.file_store = LocalFileStore(root=str(path))
|
|
33
|
-
agent =
|
|
34
|
-
llm=LLM(model=model, api_key=SecretStr(api_key),
|
|
33
|
+
agent = get_default_cli_agent(
|
|
34
|
+
llm=LLM(model=model, api_key=SecretStr(api_key), usage_id='test-service')
|
|
35
35
|
)
|
|
36
36
|
store.save(agent)
|
|
37
37
|
|
|
@@ -4,6 +4,7 @@ Tests for confirmation mode functionality in OpenHands CLI.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import uuid
|
|
7
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
9
|
from typing import Any
|
|
9
10
|
from unittest.mock import ANY, MagicMock, patch
|
|
@@ -60,7 +61,7 @@ class TestConfirmationMode:
|
|
|
60
61
|
mock_conversation_instance = MagicMock()
|
|
61
62
|
mock_conversation_class.return_value = mock_conversation_instance
|
|
62
63
|
|
|
63
|
-
result = setup_conversation()
|
|
64
|
+
result = setup_conversation(mock_conversation_id)
|
|
64
65
|
|
|
65
66
|
# Verify conversation was created and returned
|
|
66
67
|
assert result == mock_conversation_instance
|
|
@@ -87,7 +88,7 @@ class TestConfirmationMode:
|
|
|
87
88
|
|
|
88
89
|
# Should raise MissingAgentSpec
|
|
89
90
|
with pytest.raises(MissingAgentSpec) as exc_info:
|
|
90
|
-
setup_conversation()
|
|
91
|
+
setup_conversation(uuid.uuid4())
|
|
91
92
|
|
|
92
93
|
assert 'Agent specification not found' in str(exc_info.value)
|
|
93
94
|
mock_agent_store_class.assert_called_once()
|
|
@@ -6,13 +6,13 @@ from openhands_cli.runner import ConversationRunner
|
|
|
6
6
|
from openhands_cli.user_actions.types import UserConfirmation
|
|
7
7
|
from pydantic import ConfigDict, SecretStr, model_validator
|
|
8
8
|
|
|
9
|
-
from openhands.sdk import Conversation, ConversationCallbackType
|
|
9
|
+
from openhands.sdk import Conversation, ConversationCallbackType, LocalConversation
|
|
10
10
|
from openhands.sdk.agent.base import AgentBase
|
|
11
11
|
from openhands.sdk.conversation import ConversationState
|
|
12
12
|
from openhands.sdk.conversation.state import AgentExecutionStatus
|
|
13
13
|
from openhands.sdk.llm import LLM
|
|
14
14
|
from openhands.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm
|
|
15
|
-
|
|
15
|
+
from unittest.mock import MagicMock
|
|
16
16
|
|
|
17
17
|
class FakeLLM(LLM):
|
|
18
18
|
@model_validator(mode='after')
|
|
@@ -41,16 +41,16 @@ class FakeAgent(AgentBase):
|
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
def step(
|
|
44
|
-
self,
|
|
44
|
+
self, conversation: LocalConversation, on_event: ConversationCallbackType
|
|
45
45
|
) -> None:
|
|
46
46
|
self.step_count += 1
|
|
47
47
|
if self.step_count == self.finish_on_step:
|
|
48
|
-
state.agent_status = AgentExecutionStatus.FINISHED
|
|
48
|
+
conversation.state.agent_status = AgentExecutionStatus.FINISHED
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@pytest.fixture()
|
|
52
52
|
def agent() -> FakeAgent:
|
|
53
|
-
llm = LLM(**default_config(),
|
|
53
|
+
llm = LLM(**default_config(), usage_id='test-service')
|
|
54
54
|
return FakeAgent(llm=llm, tools=[])
|
|
55
55
|
|
|
56
56
|
|
|
@@ -102,15 +102,15 @@ class TestConversationRunner:
|
|
|
102
102
|
"""
|
|
103
103
|
if final_status == AgentExecutionStatus.FINISHED:
|
|
104
104
|
agent.finish_on_step = 1
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
# Add a mock security analyzer to enable confirmation mode
|
|
107
|
-
from unittest.mock import MagicMock
|
|
108
107
|
agent.security_analyzer = MagicMock()
|
|
109
|
-
|
|
108
|
+
|
|
110
109
|
convo = Conversation(agent)
|
|
111
110
|
convo.state.agent_status = AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
|
112
111
|
cr = ConversationRunner(convo)
|
|
113
112
|
cr.set_confirmation_policy(AlwaysConfirm())
|
|
113
|
+
|
|
114
114
|
with patch.object(
|
|
115
115
|
cr, '_handle_confirmation_request', return_value=confirmation
|
|
116
116
|
) as mock_confirmation_request:
|
|
@@ -37,7 +37,7 @@ class TestToolFix:
|
|
|
37
37
|
"""Test that entire tools list is replaced with default tools when loading agent."""
|
|
38
38
|
# Create a mock agent with different tools and working directories
|
|
39
39
|
mock_agent = Agent(
|
|
40
|
-
llm=LLM(model='test/model', api_key='test-key',
|
|
40
|
+
llm=LLM(model='test/model', api_key='test-key', usage_id='test-service'),
|
|
41
41
|
tools=[
|
|
42
42
|
Tool(name='BashTool'),
|
|
43
43
|
Tool(name='FileEditorTool'),
|
|
@@ -182,7 +182,7 @@ class TestLaunchGuiServer:
|
|
|
182
182
|
# Check pull command
|
|
183
183
|
pull_call = mock_run.call_args_list[0]
|
|
184
184
|
pull_cmd = pull_call[0][0]
|
|
185
|
-
assert pull_cmd[0:3] == ['docker', 'pull', 'docker.
|
|
185
|
+
assert pull_cmd[0:3] == ['docker', 'pull', 'docker.openhands.dev/openhands/runtime:latest-nikolaik']
|
|
186
186
|
|
|
187
187
|
# Check run command
|
|
188
188
|
run_call = mock_run.call_args_list[1]
|
|
@@ -26,7 +26,7 @@ def _create_agent(mcp_config=None) -> Agent:
|
|
|
26
26
|
if mcp_config is None:
|
|
27
27
|
mcp_config = {}
|
|
28
28
|
return Agent(
|
|
29
|
-
llm=LLM(model='test-model', api_key='test-key',
|
|
29
|
+
llm=LLM(model='test-model', api_key='test-key', usage_id='test-service'),
|
|
30
30
|
tools=[],
|
|
31
31
|
mcp_config=mcp_config,
|
|
32
32
|
)
|
|
@@ -1828,7 +1828,7 @@ wheels = [
|
|
|
1828
1828
|
|
|
1829
1829
|
[[package]]
|
|
1830
1830
|
name = "openhands"
|
|
1831
|
-
version = "1.0.
|
|
1831
|
+
version = "1.0.3"
|
|
1832
1832
|
source = { editable = "." }
|
|
1833
1833
|
dependencies = [
|
|
1834
1834
|
{ name = "openhands-sdk" },
|
|
@@ -1855,8 +1855,8 @@ dev = [
|
|
|
1855
1855
|
|
|
1856
1856
|
[package.metadata]
|
|
1857
1857
|
requires-dist = [
|
|
1858
|
-
{ name = "openhands-sdk", specifier = "==1.0.
|
|
1859
|
-
{ name = "openhands-tools", specifier = "==1.0.
|
|
1858
|
+
{ name = "openhands-sdk", specifier = "==1.0.0a5" },
|
|
1859
|
+
{ name = "openhands-tools", specifier = "==1.0.0a5" },
|
|
1860
1860
|
{ name = "prompt-toolkit", specifier = ">=3" },
|
|
1861
1861
|
{ name = "typer", specifier = ">=0.17.4" },
|
|
1862
1862
|
]
|
|
@@ -1879,7 +1879,7 @@ dev = [
|
|
|
1879
1879
|
|
|
1880
1880
|
[[package]]
|
|
1881
1881
|
name = "openhands-sdk"
|
|
1882
|
-
version = "1.0.
|
|
1882
|
+
version = "1.0.0a5"
|
|
1883
1883
|
source = { registry = "https://pypi.org/simple" }
|
|
1884
1884
|
dependencies = [
|
|
1885
1885
|
{ name = "fastmcp" },
|
|
@@ -1891,14 +1891,14 @@ dependencies = [
|
|
|
1891
1891
|
{ name = "tenacity" },
|
|
1892
1892
|
{ name = "websockets" },
|
|
1893
1893
|
]
|
|
1894
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1894
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1a/90/d40f6716641a95a61d2042f00855e0eadc0b2558167078324576cc5a3c22/openhands_sdk-1.0.0a5.tar.gz", hash = "sha256:8888d6892d58cf9b11a71fa80086156c0b6c9a0b50df6839c0a9cafffba2338c", size = 152810, upload-time = "2025-10-29T16:19:52.086Z" }
|
|
1895
1895
|
wheels = [
|
|
1896
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1896
|
+
{ url = "https://files.pythonhosted.org/packages/00/6b/d3aa28019163f22f4b589ad818b83e3bea23d0a50b0c51ecc070ffdec139/openhands_sdk-1.0.0a5-py3-none-any.whl", hash = "sha256:db20272b04cf03627f9f7d1e87992078ac4ce15d188955a2962aa9e754d0af03", size = 204063, upload-time = "2025-10-29T16:19:50.684Z" },
|
|
1897
1897
|
]
|
|
1898
1898
|
|
|
1899
1899
|
[[package]]
|
|
1900
1900
|
name = "openhands-tools"
|
|
1901
|
-
version = "1.0.
|
|
1901
|
+
version = "1.0.0a5"
|
|
1902
1902
|
source = { registry = "https://pypi.org/simple" }
|
|
1903
1903
|
dependencies = [
|
|
1904
1904
|
{ name = "bashlex" },
|
|
@@ -1910,9 +1910,9 @@ dependencies = [
|
|
|
1910
1910
|
{ name = "openhands-sdk" },
|
|
1911
1911
|
{ name = "pydantic" },
|
|
1912
1912
|
]
|
|
1913
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1913
|
+
sdist = { url = "https://files.pythonhosted.org/packages/0c/8d/d62bc5e6c986676363692743688f10b6a922fd24dd525e5c6e87bd6fc08e/openhands_tools-1.0.0a5.tar.gz", hash = "sha256:6c67454e612596e95c5151267659ddd3b633a5d4a1b70b348f7f913c62146562", size = 63012, upload-time = "2025-10-29T16:19:53.783Z" }
|
|
1914
1914
|
wheels = [
|
|
1915
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1915
|
+
{ url = "https://files.pythonhosted.org/packages/07/9d/4da48258f0af73d017b61ed3f12786fae4caccc7e7cd97d77ef2bb25f00c/openhands_tools-1.0.0a5-py3-none-any.whl", hash = "sha256:74c27e23e6adc9a0bad00e32448bd4872019ce0786474e8de2fbf2d7c0887e8e", size = 84724, upload-time = "2025-10-29T16:19:52.84Z" },
|
|
1916
1916
|
]
|
|
1917
1917
|
|
|
1918
1918
|
[[package]]
|
|
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
|