import uuid from types import MappingProxyType from typing import Any from openhands.core.config.mcp_config import MCPConfig from openhands.core.logger import openhands_logger as logger from openhands.events.action.message import MessageAction from openhands.experiments.experiment_manager import ExperimentManagerImpl from openhands.integrations.provider import ( CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA, PROVIDER_TOKEN_TYPE, ProviderToken, ) from openhands.integrations.service_types import ProviderType from openhands.server.data_models.agent_loop_info import AgentLoopInfo from openhands.server.session.conversation_init_data import ConversationInitData from openhands.server.shared import ( ConversationStoreImpl, SecretsStoreImpl, SettingsStoreImpl, config, conversation_manager, server_config, ) from openhands.server.types import AppMode, LLMAuthenticationError, MissingSettingsError from openhands.storage.data_models.conversation_metadata import ( ConversationMetadata, ConversationTrigger, ) from openhands.storage.data_models.user_secrets import UserSecrets from openhands.utils.conversation_summary import get_default_conversation_title async def initialize_conversation( user_id: str | None, conversation_id: str | None, selected_repository: str | None, selected_branch: str | None, conversation_trigger: ConversationTrigger = ConversationTrigger.GUI, git_provider: ProviderType | None = None, ) -> ConversationMetadata | None: if conversation_id is None: conversation_id = uuid.uuid4().hex conversation_store = await ConversationStoreImpl.get_instance(config, user_id) if not await conversation_store.exists(conversation_id): logger.info( f'New conversation ID: {conversation_id}', extra={'user_id': user_id, 'session_id': conversation_id}, ) conversation_title = get_default_conversation_title(conversation_id) logger.info(f'Saving metadata for conversation {conversation_id}') convo_metadata = ConversationMetadata( trigger=conversation_trigger, conversation_id=conversation_id, title=conversation_title, user_id=user_id, selected_repository=selected_repository, selected_branch=selected_branch, git_provider=git_provider, ) await conversation_store.save_metadata(convo_metadata) return convo_metadata try: convo_metadata = await conversation_store.get_metadata(conversation_id) return convo_metadata except Exception: pass return None async def start_conversation( user_id: str | None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None, custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA | None, initial_user_msg: str | None, image_urls: list[str] | None, replay_json: str | None, conversation_id: str, convo_metadata: ConversationMetadata, conversation_instructions: str | None, mcp_config: MCPConfig | None = None, ) -> AgentLoopInfo: logger.info( 'Creating conversation', extra={ 'signal': 'create_conversation', 'user_id': user_id, 'trigger': convo_metadata.trigger, }, ) logger.info('Loading settings') settings_store = await SettingsStoreImpl.get_instance(config, user_id) settings = await settings_store.load() logger.info('Settings loaded') session_init_args: dict[str, Any] = {} if settings: session_init_args = {**settings.__dict__, **session_init_args} # We could use litellm.check_valid_key for a more accurate check, # but that would run a tiny inference. if ( not settings.llm_api_key or settings.llm_api_key.get_secret_value().isspace() ): logger.warning(f'Missing api key for model {settings.llm_model}') raise LLMAuthenticationError( 'Error authenticating with the LLM provider. Please check your API key' ) else: logger.warning('Settings not present, not starting conversation') raise MissingSettingsError('Settings not found') session_init_args['git_provider_tokens'] = git_provider_tokens session_init_args['selected_repository'] = convo_metadata.selected_repository session_init_args['custom_secrets'] = custom_secrets session_init_args['selected_branch'] = convo_metadata.selected_branch session_init_args['git_provider'] = convo_metadata.git_provider session_init_args['conversation_instructions'] = conversation_instructions if mcp_config: session_init_args['mcp_config'] = mcp_config conversation_init_data = ConversationInitData(**session_init_args) conversation_init_data = ExperimentManagerImpl.run_conversation_variant_test( user_id, conversation_id, conversation_init_data ) logger.info( f'Starting agent loop for conversation {conversation_id}', extra={'user_id': user_id, 'session_id': conversation_id}, ) initial_message_action = None if initial_user_msg or image_urls: initial_message_action = MessageAction( content=initial_user_msg or '', image_urls=image_urls or [], ) agent_loop_info = await conversation_manager.maybe_start_agent_loop( conversation_id, conversation_init_data, user_id, initial_user_msg=initial_message_action, replay_json=replay_json, ) logger.info(f'Finished initializing conversation {agent_loop_info.conversation_id}') return agent_loop_info async def create_new_conversation( user_id: str | None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None, custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA | None, selected_repository: str | None, selected_branch: str | None, initial_user_msg: str | None, image_urls: list[str] | None, replay_json: str | None, conversation_instructions: str | None = None, conversation_trigger: ConversationTrigger = ConversationTrigger.GUI, git_provider: ProviderType | None = None, conversation_id: str | None = None, mcp_config: MCPConfig | None = None, ) -> AgentLoopInfo: conversation_metadata = await initialize_conversation( user_id, conversation_id, selected_repository, selected_branch, conversation_trigger, git_provider, ) if not conversation_metadata: raise Exception('Failed to initialize conversation') return await start_conversation( user_id, git_provider_tokens, custom_secrets, initial_user_msg, image_urls, replay_json, conversation_metadata.conversation_id, conversation_metadata, conversation_instructions, mcp_config, ) def create_provider_tokens_object( providers_set: list[ProviderType], ) -> PROVIDER_TOKEN_TYPE: """Create provider tokens object for the given providers.""" provider_information: dict[ProviderType, ProviderToken] = {} for provider in providers_set: provider_information[provider] = ProviderToken(token=None, user_id=None) return MappingProxyType(provider_information) async def setup_init_conversation_settings( user_id: str | None, conversation_id: str, providers_set: list[ProviderType] ) -> ConversationInitData: """Set up conversation initialization data with provider tokens. Args: user_id: The user ID conversation_id: The conversation ID providers_set: List of provider types to set up tokens for Returns: ConversationInitData with provider tokens configured """ settings_store = await SettingsStoreImpl.get_instance(config, user_id) settings = await settings_store.load() secrets_store = await SecretsStoreImpl.get_instance(config, user_id) user_secrets: UserSecrets | None = await secrets_store.load() if not settings: from socketio.exceptions import ConnectionRefusedError raise ConnectionRefusedError( 'Settings not found', {'msg_id': 'CONFIGURATION$SETTINGS_NOT_FOUND'} ) session_init_args: dict = {} session_init_args = {**settings.__dict__, **session_init_args} git_provider_tokens = create_provider_tokens_object(providers_set) logger.info(f'Git provider scaffold: {git_provider_tokens}') if server_config.app_mode != AppMode.SAAS and user_secrets: git_provider_tokens = user_secrets.provider_tokens session_init_args['git_provider_tokens'] = git_provider_tokens if user_secrets: session_init_args['custom_secrets'] = user_secrets.custom_secrets conversation_init_data = ConversationInitData(**session_init_args) # We should recreate the same experiment conditions when restarting a conversation return ExperimentManagerImpl.run_conversation_variant_test( user_id, conversation_id, conversation_init_data )