openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastmcp.mcp_config import MCPConfig
|
|
6
|
+
from openhands_cli.locations import MCP_CONFIG_FILE, PERSISTENCE_DIR
|
|
7
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
8
|
+
|
|
9
|
+
from openhands.sdk import Agent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MCPScreen:
|
|
13
|
+
"""
|
|
14
|
+
MCP Screen
|
|
15
|
+
|
|
16
|
+
1. Display information about setting up MCP
|
|
17
|
+
2. See existing servers that are setup
|
|
18
|
+
3. Debug additional servers passed via mcp.json
|
|
19
|
+
4. Identify servers waiting to sync on session restart
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# ---------- server spec handlers ----------
|
|
23
|
+
|
|
24
|
+
def _check_server_specs_are_equal(
|
|
25
|
+
self, first_server_spec, second_server_spec
|
|
26
|
+
) -> bool:
|
|
27
|
+
first_stringified_server_spec = json.dumps(first_server_spec, sort_keys=True)
|
|
28
|
+
second_stringified_server_spec = json.dumps(second_server_spec, sort_keys=True)
|
|
29
|
+
return first_stringified_server_spec == second_stringified_server_spec
|
|
30
|
+
|
|
31
|
+
def _check_mcp_config_status(self) -> dict:
|
|
32
|
+
"""Check the status of the MCP configuration file and return information about it."""
|
|
33
|
+
config_path = Path(PERSISTENCE_DIR) / MCP_CONFIG_FILE
|
|
34
|
+
|
|
35
|
+
if not config_path.exists():
|
|
36
|
+
return {
|
|
37
|
+
'exists': False,
|
|
38
|
+
'valid': False,
|
|
39
|
+
'servers': {},
|
|
40
|
+
'message': f'MCP configuration file not found at ~/.openhands/{MCP_CONFIG_FILE}',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
mcp_config = MCPConfig.from_file(config_path)
|
|
45
|
+
servers = mcp_config.to_dict().get('mcpServers', {})
|
|
46
|
+
return {
|
|
47
|
+
'exists': True,
|
|
48
|
+
'valid': True,
|
|
49
|
+
'servers': servers,
|
|
50
|
+
'message': f'Valid MCP configuration found with {len(servers)} server(s)',
|
|
51
|
+
}
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return {
|
|
54
|
+
'exists': True,
|
|
55
|
+
'valid': False,
|
|
56
|
+
'servers': {},
|
|
57
|
+
'message': f'Invalid MCP configuration file: {str(e)}',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ---------- TUI helpers ----------
|
|
61
|
+
|
|
62
|
+
def _get_mcp_server_diff(
|
|
63
|
+
self,
|
|
64
|
+
current: dict[str, Any],
|
|
65
|
+
incoming: dict[str, Any],
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Display a diff-style view:
|
|
69
|
+
|
|
70
|
+
- Always show the MCP servers the agent is *currently* configured with
|
|
71
|
+
- If there are incoming servers (from ~/.openhands/mcp.json),
|
|
72
|
+
clearly show which ones are NEW (not in current) and which ones are CHANGED
|
|
73
|
+
(same name but different config). Unchanged servers are not repeated.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
print_formatted_text(HTML('<white>Current Agent MCP Servers:</white>'))
|
|
77
|
+
if current:
|
|
78
|
+
for name, cfg in current.items():
|
|
79
|
+
self._render_server_summary(name, cfg, indent=2)
|
|
80
|
+
else:
|
|
81
|
+
print_formatted_text(
|
|
82
|
+
HTML(' <yellow>None configured on the current agent.</yellow>')
|
|
83
|
+
)
|
|
84
|
+
print_formatted_text('')
|
|
85
|
+
|
|
86
|
+
# If no incoming, we're done
|
|
87
|
+
if not incoming:
|
|
88
|
+
print_formatted_text(
|
|
89
|
+
HTML('<grey>No incoming servers detected for next restart.</grey>')
|
|
90
|
+
)
|
|
91
|
+
print_formatted_text('')
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Compare names and configs
|
|
95
|
+
current_names = set(current.keys())
|
|
96
|
+
incoming_names = set(incoming.keys())
|
|
97
|
+
new_servers = sorted(incoming_names - current_names)
|
|
98
|
+
|
|
99
|
+
overriden_servers = []
|
|
100
|
+
for name in sorted(incoming_names & current_names):
|
|
101
|
+
if not self._check_server_specs_are_equal(current[name], incoming[name]):
|
|
102
|
+
overriden_servers.append(name)
|
|
103
|
+
|
|
104
|
+
# Display incoming section header
|
|
105
|
+
print_formatted_text(
|
|
106
|
+
HTML(
|
|
107
|
+
'<white>Incoming Servers on Restart (from ~/.openhands/mcp.json):</white>'
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not new_servers and not overriden_servers:
|
|
112
|
+
print_formatted_text(
|
|
113
|
+
HTML(
|
|
114
|
+
' <grey>All configured servers match the current agent configuration.</grey>'
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
print_formatted_text('')
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if new_servers:
|
|
121
|
+
print_formatted_text(HTML(' <green>New servers (will be added):</green>'))
|
|
122
|
+
for name in new_servers:
|
|
123
|
+
self._render_server_summary(name, incoming[name], indent=4)
|
|
124
|
+
|
|
125
|
+
if overriden_servers:
|
|
126
|
+
print_formatted_text(
|
|
127
|
+
HTML(' <yellow>Updated servers (configuration will change):</yellow>')
|
|
128
|
+
)
|
|
129
|
+
for name in overriden_servers:
|
|
130
|
+
print_formatted_text(HTML(f' <white>• {name}</white>'))
|
|
131
|
+
print_formatted_text(HTML(' <grey>Current:</grey>'))
|
|
132
|
+
self._render_server_summary(None, current[name], indent=8)
|
|
133
|
+
print_formatted_text(HTML(' <grey>Incoming:</grey>'))
|
|
134
|
+
self._render_server_summary(None, incoming[name], indent=8)
|
|
135
|
+
|
|
136
|
+
print_formatted_text('')
|
|
137
|
+
|
|
138
|
+
def _render_server_summary(
|
|
139
|
+
self, server_name: str | None, server_spec: dict[str, Any], indent: int = 2
|
|
140
|
+
) -> None:
|
|
141
|
+
pad = ' ' * indent
|
|
142
|
+
|
|
143
|
+
if server_name:
|
|
144
|
+
print_formatted_text(HTML(f'{pad}<white>• {server_name}</white>'))
|
|
145
|
+
|
|
146
|
+
if isinstance(server_spec, dict):
|
|
147
|
+
if 'command' in server_spec:
|
|
148
|
+
cmd = server_spec.get('command', '')
|
|
149
|
+
args = server_spec.get('args', [])
|
|
150
|
+
args_str = ' '.join(args) if args else ''
|
|
151
|
+
print_formatted_text(HTML(f'{pad} <grey>Type: Command-based</grey>'))
|
|
152
|
+
if cmd or args_str:
|
|
153
|
+
print_formatted_text(
|
|
154
|
+
HTML(f'{pad} <grey>Command: {cmd} {args_str}</grey>')
|
|
155
|
+
)
|
|
156
|
+
elif 'url' in server_spec:
|
|
157
|
+
url = server_spec.get('url', '')
|
|
158
|
+
auth = server_spec.get('auth', 'none')
|
|
159
|
+
print_formatted_text(HTML(f'{pad} <grey>Type: URL-based</grey>'))
|
|
160
|
+
if url:
|
|
161
|
+
print_formatted_text(HTML(f'{pad} <grey>URL: {url}</grey>'))
|
|
162
|
+
print_formatted_text(HTML(f'{pad} <grey>Auth: {auth}</grey>'))
|
|
163
|
+
|
|
164
|
+
def _display_information_header(self) -> None:
|
|
165
|
+
print_formatted_text(
|
|
166
|
+
HTML('<gold>MCP (Model Context Protocol) Configuration</gold>')
|
|
167
|
+
)
|
|
168
|
+
print_formatted_text('')
|
|
169
|
+
print_formatted_text(HTML('<white>To get started:</white>'))
|
|
170
|
+
print_formatted_text(
|
|
171
|
+
HTML(
|
|
172
|
+
' 1. Create the configuration file: <cyan>~/.openhands/mcp.json</cyan>'
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
print_formatted_text(
|
|
176
|
+
HTML(
|
|
177
|
+
' 2. Add your MCP server configurations '
|
|
178
|
+
'<cyan>https://gofastmcp.com/clients/client#configuration-format</cyan>'
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
print_formatted_text(
|
|
182
|
+
HTML(' 3. Restart your OpenHands session to load the new configuration')
|
|
183
|
+
)
|
|
184
|
+
print_formatted_text('')
|
|
185
|
+
|
|
186
|
+
# ---------- status + display entrypoint ----------
|
|
187
|
+
|
|
188
|
+
def display_mcp_info(self, existing_agent: Agent) -> None:
|
|
189
|
+
"""Display comprehensive MCP configuration information."""
|
|
190
|
+
|
|
191
|
+
self._display_information_header()
|
|
192
|
+
|
|
193
|
+
# Always determine current & incoming first
|
|
194
|
+
status = self._check_mcp_config_status()
|
|
195
|
+
incoming_servers = status.get('servers', {}) if status.get('valid') else {}
|
|
196
|
+
current_servers = existing_agent.mcp_config.get('mcpServers', {})
|
|
197
|
+
|
|
198
|
+
# Show file status
|
|
199
|
+
if not status['exists']:
|
|
200
|
+
print_formatted_text(
|
|
201
|
+
HTML('<yellow>Status: Configuration file not found</yellow>')
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
elif not status['valid']:
|
|
205
|
+
print_formatted_text(HTML(f'<red>Status: {status["message"]}</red>'))
|
|
206
|
+
print_formatted_text('')
|
|
207
|
+
print_formatted_text(
|
|
208
|
+
HTML('<white>Please check your configuration file format.</white>')
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
print_formatted_text(HTML(f'<green>Status: {status["message"]}</green>'))
|
|
212
|
+
|
|
213
|
+
print_formatted_text('')
|
|
214
|
+
|
|
215
|
+
# Always show the agent's current servers
|
|
216
|
+
# Then show incoming (deduped and changes highlighted)
|
|
217
|
+
self._get_mcp_server_diff(current_servers, incoming_servers)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
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
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
7
|
+
from prompt_toolkit.shortcuts import print_container
|
|
8
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
9
|
+
|
|
10
|
+
from openhands_cli.llm_utils import get_llm_metadata
|
|
11
|
+
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
12
|
+
from openhands_cli.pt_style import COLOR_GREY
|
|
13
|
+
from openhands_cli.tui.settings.store import AgentStore
|
|
14
|
+
from openhands_cli.tui.utils import StepCounter
|
|
15
|
+
from openhands_cli.user_actions.settings_action import (
|
|
16
|
+
SettingsType,
|
|
17
|
+
choose_llm_model,
|
|
18
|
+
choose_llm_provider,
|
|
19
|
+
choose_memory_condensation,
|
|
20
|
+
prompt_api_key,
|
|
21
|
+
prompt_base_url,
|
|
22
|
+
prompt_custom_model,
|
|
23
|
+
save_settings_confirmation,
|
|
24
|
+
settings_type_confirmation,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SettingsScreen:
|
|
29
|
+
def __init__(self, conversation: BaseConversation | None = None):
|
|
30
|
+
self.file_store = LocalFileStore(PERSISTENCE_DIR)
|
|
31
|
+
self.agent_store = AgentStore()
|
|
32
|
+
self.conversation = conversation
|
|
33
|
+
|
|
34
|
+
def display_settings(self) -> None:
|
|
35
|
+
agent_spec = self.agent_store.load()
|
|
36
|
+
if not agent_spec:
|
|
37
|
+
return
|
|
38
|
+
assert self.conversation is not None, (
|
|
39
|
+
'Conversation must be set to display settings.'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
llm = agent_spec.llm
|
|
43
|
+
advanced_llm_settings = True if llm.base_url else False
|
|
44
|
+
|
|
45
|
+
# Prepare labels and values based on settings
|
|
46
|
+
labels_and_values = []
|
|
47
|
+
if not advanced_llm_settings:
|
|
48
|
+
# Attempt to determine provider, fallback if not directly available
|
|
49
|
+
provider = llm.model.split('/')[0] if '/' in llm.model else 'Unknown'
|
|
50
|
+
|
|
51
|
+
labels_and_values.extend(
|
|
52
|
+
[
|
|
53
|
+
(' LLM Provider', str(provider)),
|
|
54
|
+
(' LLM Model', str(llm.model)),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
labels_and_values.extend(
|
|
59
|
+
[
|
|
60
|
+
(' Custom Model', llm.model),
|
|
61
|
+
(' Base URL', llm.base_url),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
labels_and_values.extend(
|
|
65
|
+
[
|
|
66
|
+
(' API Key', '********' if llm.api_key else 'Not Set'),
|
|
67
|
+
(
|
|
68
|
+
' Confirmation Mode',
|
|
69
|
+
'Enabled'
|
|
70
|
+
if self.conversation.is_confirmation_mode_active
|
|
71
|
+
else 'Disabled',
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
' Memory Condensation',
|
|
75
|
+
'Enabled' if agent_spec.condenser else 'Disabled',
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
' Configuration File',
|
|
79
|
+
os.path.join(PERSISTENCE_DIR, AGENT_SETTINGS_PATH),
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Calculate max widths for alignment
|
|
85
|
+
# Ensure values are strings for len() calculation
|
|
86
|
+
str_labels_and_values = [
|
|
87
|
+
(label, str(value)) for label, value in labels_and_values
|
|
88
|
+
]
|
|
89
|
+
max_label_width = (
|
|
90
|
+
max(len(label) for label, _ in str_labels_and_values)
|
|
91
|
+
if str_labels_and_values
|
|
92
|
+
else 0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Construct the summary text with aligned columns
|
|
96
|
+
settings_lines = [
|
|
97
|
+
f'{label + ":":<{max_label_width + 1}} {value:<}' # Changed value alignment to left (<)
|
|
98
|
+
for label, value in str_labels_and_values
|
|
99
|
+
]
|
|
100
|
+
settings_text = '\n'.join(settings_lines)
|
|
101
|
+
|
|
102
|
+
container = Frame(
|
|
103
|
+
TextArea(
|
|
104
|
+
text=settings_text,
|
|
105
|
+
read_only=True,
|
|
106
|
+
style=COLOR_GREY,
|
|
107
|
+
wrap_lines=True,
|
|
108
|
+
),
|
|
109
|
+
title='Settings',
|
|
110
|
+
style=f'fg:{COLOR_GREY}',
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
print_container(container)
|
|
114
|
+
|
|
115
|
+
self.configure_settings()
|
|
116
|
+
|
|
117
|
+
def configure_settings(self, first_time=False):
|
|
118
|
+
try:
|
|
119
|
+
settings_type = settings_type_confirmation(first_time=first_time)
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if settings_type == SettingsType.BASIC:
|
|
124
|
+
self.handle_basic_settings()
|
|
125
|
+
elif settings_type == SettingsType.ADVANCED:
|
|
126
|
+
self.handle_advanced_settings()
|
|
127
|
+
|
|
128
|
+
def handle_basic_settings(self):
|
|
129
|
+
step_counter = StepCounter(3)
|
|
130
|
+
try:
|
|
131
|
+
provider = choose_llm_provider(step_counter, escapable=True)
|
|
132
|
+
llm_model = choose_llm_model(step_counter, provider, escapable=True)
|
|
133
|
+
api_key = prompt_api_key(
|
|
134
|
+
step_counter,
|
|
135
|
+
provider,
|
|
136
|
+
self.conversation.state.agent.llm.api_key
|
|
137
|
+
if self.conversation
|
|
138
|
+
else None,
|
|
139
|
+
escapable=True,
|
|
140
|
+
)
|
|
141
|
+
save_settings_confirmation()
|
|
142
|
+
except KeyboardInterrupt:
|
|
143
|
+
print_formatted_text(HTML('\n<red>Cancelled settings change.</red>'))
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Store the collected settings for persistence
|
|
147
|
+
self._save_llm_settings(f'{provider}/{llm_model}', api_key)
|
|
148
|
+
|
|
149
|
+
def handle_advanced_settings(self, escapable=True):
|
|
150
|
+
"""Handle advanced settings configuration with clean step-by-step flow."""
|
|
151
|
+
step_counter = StepCounter(4)
|
|
152
|
+
try:
|
|
153
|
+
custom_model = prompt_custom_model(step_counter)
|
|
154
|
+
base_url = prompt_base_url(step_counter)
|
|
155
|
+
api_key = prompt_api_key(
|
|
156
|
+
step_counter,
|
|
157
|
+
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
|
|
158
|
+
self.conversation.agent.llm.api_key if self.conversation else None,
|
|
159
|
+
escapable=escapable,
|
|
160
|
+
)
|
|
161
|
+
memory_condensation = choose_memory_condensation(step_counter)
|
|
162
|
+
|
|
163
|
+
# Confirm save
|
|
164
|
+
save_settings_confirmation()
|
|
165
|
+
except KeyboardInterrupt:
|
|
166
|
+
print_formatted_text(HTML('\n<red>Cancelled settings change.</red>'))
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Store the collected settings for persistence
|
|
170
|
+
self._save_advanced_settings(
|
|
171
|
+
custom_model, base_url, api_key, memory_condensation
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _save_llm_settings(self, model, api_key, base_url: str | None = None) -> None:
|
|
175
|
+
llm = LLM(
|
|
176
|
+
model=model,
|
|
177
|
+
api_key=api_key,
|
|
178
|
+
base_url=base_url,
|
|
179
|
+
service_id='agent',
|
|
180
|
+
metadata=get_llm_metadata(model_name=model, llm_type='agent'),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
agent = self.agent_store.load()
|
|
184
|
+
if not agent:
|
|
185
|
+
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
186
|
+
|
|
187
|
+
agent = agent.model_copy(update={'llm': llm})
|
|
188
|
+
self.agent_store.save(agent)
|
|
189
|
+
|
|
190
|
+
def _save_advanced_settings(
|
|
191
|
+
self, custom_model: str, base_url: str, api_key: str, memory_condensation: bool
|
|
192
|
+
):
|
|
193
|
+
self._save_llm_settings(custom_model, api_key, base_url=base_url)
|
|
194
|
+
|
|
195
|
+
agent_spec = self.agent_store.load()
|
|
196
|
+
if not agent_spec:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if not memory_condensation:
|
|
200
|
+
agent_spec.model_copy(update={'condenser': None})
|
|
201
|
+
|
|
202
|
+
self.agent_store.save(agent_spec)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# openhands_cli/settings/store.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastmcp.mcp_config import MCPConfig
|
|
8
|
+
from openhands_cli.llm_utils import get_llm_metadata
|
|
9
|
+
from openhands_cli.locations import (
|
|
10
|
+
AGENT_SETTINGS_PATH,
|
|
11
|
+
MCP_CONFIG_FILE,
|
|
12
|
+
PERSISTENCE_DIR,
|
|
13
|
+
WORK_DIR,
|
|
14
|
+
)
|
|
15
|
+
from prompt_toolkit import HTML, print_formatted_text
|
|
16
|
+
|
|
17
|
+
from openhands.sdk import Agent, AgentContext, LocalFileStore
|
|
18
|
+
from openhands.sdk.context.condenser import LLMSummarizingCondenser
|
|
19
|
+
from openhands.tools.preset.default import get_default_tools
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentStore:
|
|
23
|
+
"""Single source of truth for persisting/retrieving AgentSpec."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self.file_store = LocalFileStore(root=PERSISTENCE_DIR)
|
|
27
|
+
|
|
28
|
+
def load_mcp_configuration(self) -> dict[str, Any]:
|
|
29
|
+
try:
|
|
30
|
+
mcp_config_path = Path(self.file_store.root) / MCP_CONFIG_FILE
|
|
31
|
+
mcp_config = MCPConfig.from_file(mcp_config_path)
|
|
32
|
+
return mcp_config.to_dict()['mcpServers']
|
|
33
|
+
except Exception:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
def load(self, session_id: str | None = None) -> Agent | None:
|
|
37
|
+
try:
|
|
38
|
+
str_spec = self.file_store.read(AGENT_SETTINGS_PATH)
|
|
39
|
+
agent = Agent.model_validate_json(str_spec)
|
|
40
|
+
|
|
41
|
+
# Update tools with most recent working directory
|
|
42
|
+
updated_tools = get_default_tools(enable_browser=False)
|
|
43
|
+
|
|
44
|
+
agent_context = AgentContext(
|
|
45
|
+
system_message_suffix=f'You current working directory is: {WORK_DIR}',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
additional_mcp_config = self.load_mcp_configuration()
|
|
49
|
+
mcp_config: dict = agent.mcp_config.copy().get('mcpServers', {})
|
|
50
|
+
mcp_config.update(additional_mcp_config)
|
|
51
|
+
|
|
52
|
+
# Update LLM metadata with current information
|
|
53
|
+
agent_llm_metadata = get_llm_metadata(
|
|
54
|
+
model_name=agent.llm.model, llm_type='agent', session_id=session_id
|
|
55
|
+
)
|
|
56
|
+
updated_llm = agent.llm.model_copy(update={'metadata': agent_llm_metadata})
|
|
57
|
+
|
|
58
|
+
condenser_updates = {}
|
|
59
|
+
if agent.condenser and isinstance(agent.condenser, LLMSummarizingCondenser):
|
|
60
|
+
condenser_updates['llm'] = agent.condenser.llm.model_copy(
|
|
61
|
+
update={
|
|
62
|
+
'metadata': get_llm_metadata(
|
|
63
|
+
model_name=agent.condenser.llm.model,
|
|
64
|
+
llm_type='condenser',
|
|
65
|
+
session_id=session_id,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
agent = agent.model_copy(
|
|
71
|
+
update={
|
|
72
|
+
'llm': updated_llm,
|
|
73
|
+
'tools': updated_tools,
|
|
74
|
+
'mcp_config': {'mcpServers': mcp_config} if mcp_config else {},
|
|
75
|
+
'agent_context': agent_context,
|
|
76
|
+
'condenser': agent.condenser.model_copy(update=condenser_updates)
|
|
77
|
+
if agent.condenser
|
|
78
|
+
else None,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return agent
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
return None
|
|
85
|
+
except Exception:
|
|
86
|
+
print_formatted_text(
|
|
87
|
+
HTML('\n<red>Agent configuration file is corrupted!</red>')
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def save(self, agent: Agent) -> None:
|
|
92
|
+
serialized_spec = agent.model_dump_json(context={'expose_secrets': True})
|
|
93
|
+
self.file_store.write(AGENT_SETTINGS_PATH, serialized_spec)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Status display components for OpenHands CLI TUI."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from openhands.sdk import BaseConversation
|
|
6
|
+
from prompt_toolkit import print_formatted_text
|
|
7
|
+
from prompt_toolkit.formatted_text import HTML
|
|
8
|
+
from prompt_toolkit.shortcuts import print_container
|
|
9
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def display_status(
|
|
13
|
+
conversation: BaseConversation,
|
|
14
|
+
session_start_time: datetime,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Display detailed conversation status including metrics and uptime.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
conversation: The conversation to display status for
|
|
20
|
+
session_start_time: The session start time for uptime calculation
|
|
21
|
+
"""
|
|
22
|
+
# Get conversation stats
|
|
23
|
+
stats = conversation.conversation_stats.get_combined_metrics()
|
|
24
|
+
|
|
25
|
+
# Calculate uptime from session start time
|
|
26
|
+
now = datetime.now()
|
|
27
|
+
diff = now - session_start_time
|
|
28
|
+
|
|
29
|
+
# Format as hours, minutes, seconds
|
|
30
|
+
total_seconds = int(diff.total_seconds())
|
|
31
|
+
hours = total_seconds // 3600
|
|
32
|
+
minutes = (total_seconds % 3600) // 60
|
|
33
|
+
seconds = total_seconds % 60
|
|
34
|
+
uptime_str = f"{hours}h {minutes}m {seconds}s"
|
|
35
|
+
|
|
36
|
+
# Display conversation ID and uptime
|
|
37
|
+
print_formatted_text(HTML(f'<grey>Conversation ID: {conversation.id}</grey>'))
|
|
38
|
+
print_formatted_text(HTML(f'<grey>Uptime: {uptime_str}</grey>'))
|
|
39
|
+
print_formatted_text('')
|
|
40
|
+
|
|
41
|
+
# Calculate token metrics
|
|
42
|
+
token_usage = stats.accumulated_token_usage
|
|
43
|
+
total_input_tokens = token_usage.prompt_tokens if token_usage else 0
|
|
44
|
+
total_output_tokens = token_usage.completion_tokens if token_usage else 0
|
|
45
|
+
cache_hits = token_usage.cache_read_tokens if token_usage else 0
|
|
46
|
+
cache_writes = token_usage.cache_write_tokens if token_usage else 0
|
|
47
|
+
total_tokens = total_input_tokens + total_output_tokens
|
|
48
|
+
total_cost = stats.accumulated_cost
|
|
49
|
+
|
|
50
|
+
# Use prompt_toolkit containers for formatted display
|
|
51
|
+
_display_usage_metrics_container(
|
|
52
|
+
total_cost,
|
|
53
|
+
total_input_tokens,
|
|
54
|
+
total_output_tokens,
|
|
55
|
+
cache_hits,
|
|
56
|
+
cache_writes,
|
|
57
|
+
total_tokens
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _display_usage_metrics_container(
|
|
62
|
+
total_cost: float,
|
|
63
|
+
total_input_tokens: int,
|
|
64
|
+
total_output_tokens: int,
|
|
65
|
+
cache_hits: int,
|
|
66
|
+
cache_writes: int,
|
|
67
|
+
total_tokens: int
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Display usage metrics using prompt_toolkit containers."""
|
|
70
|
+
# Format values with proper formatting
|
|
71
|
+
cost_str = f'${total_cost:.6f}'
|
|
72
|
+
input_tokens_str = f'{total_input_tokens:,}'
|
|
73
|
+
cache_read_str = f'{cache_hits:,}'
|
|
74
|
+
cache_write_str = f'{cache_writes:,}'
|
|
75
|
+
output_tokens_str = f'{total_output_tokens:,}'
|
|
76
|
+
total_tokens_str = f'{total_tokens:,}'
|
|
77
|
+
|
|
78
|
+
labels_and_values = [
|
|
79
|
+
(' Total Cost (USD):', cost_str),
|
|
80
|
+
('', ''),
|
|
81
|
+
(' Total Input Tokens:', input_tokens_str),
|
|
82
|
+
(' Cache Hits:', cache_read_str),
|
|
83
|
+
(' Cache Writes:', cache_write_str),
|
|
84
|
+
(' Total Output Tokens:', output_tokens_str),
|
|
85
|
+
('', ''),
|
|
86
|
+
(' Total Tokens:', total_tokens_str),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Calculate max widths for alignment
|
|
90
|
+
max_label_width = max(len(label) for label, _ in labels_and_values)
|
|
91
|
+
max_value_width = max(len(value) for _, value in labels_and_values)
|
|
92
|
+
|
|
93
|
+
# Construct the summary text with aligned columns
|
|
94
|
+
summary_lines = [
|
|
95
|
+
f'{label:<{max_label_width}} {value:<{max_value_width}}'
|
|
96
|
+
for label, value in labels_and_values
|
|
97
|
+
]
|
|
98
|
+
summary_text = '\n'.join(summary_lines)
|
|
99
|
+
|
|
100
|
+
container = Frame(
|
|
101
|
+
TextArea(
|
|
102
|
+
text=summary_text,
|
|
103
|
+
read_only=True,
|
|
104
|
+
wrap_lines=True,
|
|
105
|
+
),
|
|
106
|
+
title='Usage Metrics',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
print_container(container)
|