from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.completion import FuzzyWordCompleter
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.widgets import Frame, TextArea
from pydantic import SecretStr
from openhands.cli.tui import (
COLOR_GREY,
UserCancelledError,
cli_confirm,
kb_cancel,
)
from openhands.cli.utils import (
VERIFIED_ANTHROPIC_MODELS,
VERIFIED_OPENAI_MODELS,
VERIFIED_PROVIDERS,
organize_models_and_providers,
)
from openhands.controller.agent import Agent
from openhands.core.config import OpenHandsConfig
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import OH_DEFAULT_AGENT
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
LLMSummarizingCondenserConfig,
)
from openhands.storage.data_models.settings import Settings
from openhands.storage.settings.file_settings_store import FileSettingsStore
from openhands.utils.llm import get_supported_llm_models
def display_settings(config: OpenHandsConfig) -> None:
llm_config = config.get_llm_config()
advanced_llm_settings = True if llm_config.base_url else False
# Prepare labels and values based on settings
labels_and_values = []
if not advanced_llm_settings:
# Attempt to determine provider, fallback if not directly available
provider = getattr(
llm_config,
'provider',
llm_config.model.split('/')[0] if '/' in llm_config.model else 'Unknown',
)
labels_and_values.extend(
[
(' LLM Provider', str(provider)),
(' LLM Model', str(llm_config.model)),
(' API Key', '********' if llm_config.api_key else 'Not Set'),
]
)
else:
labels_and_values.extend(
[
(' Custom Model', str(llm_config.model)),
(' Base URL', str(llm_config.base_url)),
(' API Key', '********' if llm_config.api_key else 'Not Set'),
]
)
# Common settings
labels_and_values.extend(
[
(' Agent', str(config.default_agent)),
(
' Confirmation Mode',
'Enabled' if config.security.confirmation_mode else 'Disabled',
),
(
' Memory Condensation',
'Enabled' if config.enable_default_condenser else 'Disabled',
),
]
)
# Calculate max widths for alignment
# Ensure values are strings for len() calculation
str_labels_and_values = [(label, str(value)) for label, value in labels_and_values]
max_label_width = (
max(len(label) for label, _ in str_labels_and_values)
if str_labels_and_values
else 0
)
# Construct the summary text with aligned columns
settings_lines = [
f'{label + ":":<{max_label_width + 1}} {value:<}' # Changed value alignment to left (<)
for label, value in str_labels_and_values
]
settings_text = '\n'.join(settings_lines)
container = Frame(
TextArea(
text=settings_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Settings',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
async def get_validated_input(
session: PromptSession,
prompt_text: str,
completer=None,
validator=None,
error_message: str = 'Input cannot be empty',
) -> str:
session.completer = completer
value = None
while True:
value = await session.prompt_async(prompt_text)
if validator:
is_valid = validator(value)
if not is_valid:
print_formatted_text('')
print_formatted_text(HTML(f'{error_message}: {value}'))
print_formatted_text('')
continue
elif not value:
print_formatted_text('')
print_formatted_text(HTML(f'{error_message}'))
print_formatted_text('')
continue
break
return value
def save_settings_confirmation() -> bool:
return (
cli_confirm(
'\nSave new settings? (They will take effect after restart)',
['Yes, save', 'No, discard'],
)
== 0
)
async def modify_llm_settings_basic(
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
model_list = get_supported_llm_models(config)
organized_models = organize_models_and_providers(model_list)
provider_list = list(organized_models.keys())
verified_providers = [p for p in VERIFIED_PROVIDERS if p in provider_list]
provider_list = [p for p in provider_list if p not in verified_providers]
provider_list = verified_providers + provider_list
provider_completer = FuzzyWordCompleter(provider_list)
session = PromptSession(key_bindings=kb_cancel())
# Set default provider - use the first available provider from the list
provider = provider_list[0] if provider_list else 'openai'
model = None
api_key = None
try:
# Show the default provider but allow changing it
print_formatted_text(
HTML(f'\nDefault provider: {provider}')
)
change_provider = (
cli_confirm(
'Do you want to use a different provider?',
[f'Use {provider}', 'Select another provider'],
)
== 1
)
if change_provider:
# Define a validator function that prints an error message
def provider_validator(x):
is_valid = x in organized_models
if not is_valid:
print_formatted_text(
HTML('Invalid provider selected: {}'.format(x))
)
return is_valid
provider = await get_validated_input(
session,
'(Step 1/3) Select LLM Provider (TAB for options, CTRL-c to cancel): ',
completer=provider_completer,
validator=provider_validator,
error_message='Invalid provider selected',
)
# Make sure the provider exists in organized_models
if provider not in organized_models:
# If the provider doesn't exist, use the first available provider
provider = (
next(iter(organized_models.keys())) if organized_models else 'openai'
)
provider_models = organized_models[provider]['models']
if provider == 'openai':
provider_models = [
m for m in provider_models if m not in VERIFIED_OPENAI_MODELS
]
provider_models = VERIFIED_OPENAI_MODELS + provider_models
if provider == 'anthropic':
provider_models = [
m for m in provider_models if m not in VERIFIED_ANTHROPIC_MODELS
]
provider_models = VERIFIED_ANTHROPIC_MODELS + provider_models
# Set default model to the first model in the list
default_model = provider_models[0] if provider_models else 'gpt-4'
# Show the default model but allow changing it
print_formatted_text(
HTML(f'\nDefault model: {default_model}')
)
change_model = (
cli_confirm(
'Do you want to use a different model?',
[f'Use {default_model}', 'Select another model'],
)
== 1
)
if change_model:
model_completer = FuzzyWordCompleter(provider_models)
# Define a validator function that prints an error message
def model_validator(x):
is_valid = x in provider_models
if not is_valid:
print_formatted_text(
HTML(
f'Invalid model selected for provider {provider}: {x}'
)
)
return is_valid
model = await get_validated_input(
session,
'(Step 2/3) Select LLM Model (TAB for options, CTRL-c to cancel): ',
completer=model_completer,
validator=model_validator,
error_message=f'Invalid model selected for provider {provider}',
)
else:
# Use the default model
model = default_model
api_key = await get_validated_input(
session,
'(Step 3/3) Enter API Key (CTRL-c to cancel): ',
error_message='API Key cannot be empty',
)
except (
UserCancelledError,
KeyboardInterrupt,
EOFError,
):
return # Return on exception
# The try-except block above ensures we either have valid inputs or we've already returned
# No need to check for None values here
save_settings = save_settings_confirmation()
if not save_settings:
return
llm_config = config.get_llm_config()
llm_config.model = f'{provider}{organized_models[provider]["separator"]}{model}'
llm_config.api_key = SecretStr(api_key)
llm_config.base_url = None
config.set_llm_config(llm_config)
config.default_agent = OH_DEFAULT_AGENT
config.enable_default_condenser = True
agent_config = config.get_agent_config(config.default_agent)
agent_config.condenser = LLMSummarizingCondenserConfig(
llm_config=llm_config,
type='llm',
)
config.set_agent_config(agent_config, config.default_agent)
settings = await settings_store.load()
if not settings:
settings = Settings()
settings.llm_model = f'{provider}{organized_models[provider]["separator"]}{model}'
settings.llm_api_key = SecretStr(api_key)
settings.llm_base_url = None
settings.agent = OH_DEFAULT_AGENT
settings.enable_default_condenser = True
await settings_store.store(settings)
async def modify_llm_settings_advanced(
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
session = PromptSession(key_bindings=kb_cancel())
custom_model = None
base_url = None
api_key = None
agent = None
try:
custom_model = await get_validated_input(
session,
'(Step 1/6) Custom Model (CTRL-c to cancel): ',
error_message='Custom Model cannot be empty',
)
base_url = await get_validated_input(
session,
'(Step 2/6) Base URL (CTRL-c to cancel): ',
error_message='Base URL cannot be empty',
)
api_key = await get_validated_input(
session,
'(Step 3/6) API Key (CTRL-c to cancel): ',
error_message='API Key cannot be empty',
)
agent_list = Agent.list_agents()
agent_completer = FuzzyWordCompleter(agent_list)
agent = await get_validated_input(
session,
'(Step 4/6) Agent (TAB for options, CTRL-c to cancel): ',
completer=agent_completer,
validator=lambda x: x in agent_list,
error_message='Invalid agent selected',
)
enable_confirmation_mode = (
cli_confirm(
question='(Step 5/6) Confirmation Mode (CTRL-c to cancel):',
choices=['Enable', 'Disable'],
)
== 0
)
enable_memory_condensation = (
cli_confirm(
question='(Step 6/6) Memory Condensation (CTRL-c to cancel):',
choices=['Enable', 'Disable'],
)
== 0
)
except (
UserCancelledError,
KeyboardInterrupt,
EOFError,
):
return # Return on exception
# The try-except block above ensures we either have valid inputs or we've already returned
# No need to check for None values here
save_settings = save_settings_confirmation()
if not save_settings:
return
llm_config = config.get_llm_config()
llm_config.model = custom_model
llm_config.base_url = base_url
llm_config.api_key = SecretStr(api_key)
config.set_llm_config(llm_config)
config.default_agent = agent
config.security.confirmation_mode = enable_confirmation_mode
agent_config = config.get_agent_config(config.default_agent)
if enable_memory_condensation:
agent_config.condenser = LLMSummarizingCondenserConfig(
llm_config=llm_config,
type='llm',
)
else:
agent_config.condenser = NoOpCondenserConfig(type='noop')
config.set_agent_config(agent_config)
settings = await settings_store.load()
if not settings:
settings = Settings()
settings.llm_model = custom_model
settings.llm_api_key = SecretStr(api_key)
settings.llm_base_url = base_url
settings.agent = agent
settings.confirmation_mode = enable_confirmation_mode
settings.enable_default_condenser = enable_memory_condensation
await settings_store.store(settings)