diff --git a/openhands/integrations/github/github_service.py b/openhands/integrations/github/github_service.py index 6aa0da7cc0..f1adfed34b 100644 --- a/openhands/integrations/github/github_service.py +++ b/openhands/integrations/github/github_service.py @@ -28,6 +28,17 @@ from openhands.utils.import_utils import get_impl class GitHubService(BaseGitService, GitService): + """Default implementation of GitService for GitHub integration. + + TODO: This doesn't seem a good candidate for the get_impl() pattern. What are the abstract methods we should actually separate and implement here? + This is an extension point in OpenHands that allows applications to customize GitHub + integration behavior. Applications can substitute their own implementation by: + 1. Creating a class that inherits from GitService + 2. Implementing all required methods + 3. Setting server_config.github_service_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + """ BASE_URL = 'https://api.github.com' token: SecretStr = SecretStr('') refresh = False diff --git a/openhands/integrations/gitlab/gitlab_service.py b/openhands/integrations/gitlab/gitlab_service.py index 9ad745c003..5d9f17dd8c 100644 --- a/openhands/integrations/gitlab/gitlab_service.py +++ b/openhands/integrations/gitlab/gitlab_service.py @@ -21,6 +21,17 @@ from openhands.utils.import_utils import get_impl class GitLabService(BaseGitService, GitService): + """Default implementation of GitService for GitLab integration. + + TODO: This doesn't seem a good candidate for the get_impl() pattern. What are the abstract methods we should actually separate and implement here? + This is an extension point in OpenHands that allows applications to customize GitLab + integration behavior. Applications can substitute their own implementation by: + 1. Creating a class that inherits from GitService + 2. Implementing all required methods + 3. Setting server_config.gitlab_service_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + """ BASE_URL = 'https://gitlab.com/api/v4' GRAPHQL_URL = 'https://gitlab.com/api/graphql' token: SecretStr = SecretStr('') diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index 421c428252..858f4291cd 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -90,10 +90,33 @@ def _default_env_vars(sandbox_config: SandboxConfig) -> dict[str, str]: class Runtime(FileEditRuntimeMixin): - """The runtime is how the agent interacts with the external environment. - This includes a bash sandbox, a browser, and filesystem interactions. + """Abstract base class for agent runtime environments. - sid is the session id, which is used to identify the current user session. + This is an extension point in OpenHands that allows applications to customize how + agents interact with the external environment. The runtime provides a sandbox with: + - Bash shell access + - Browser interaction + - Filesystem operations + - Git operations + - Environment variable management + + Applications can substitute their own implementation by: + 1. Creating a class that inherits from Runtime + 2. Implementing all required methods + 3. Setting the runtime name in configuration or using get_runtime_cls() + + The class is instantiated via get_impl() in get_runtime_cls(). + + Built-in implementations include: + - DockerRuntime: Containerized environment using Docker + - E2BRuntime: Secure sandbox using E2B + - RemoteRuntime: Remote execution environment + - ModalRuntime: Scalable cloud environment using Modal + - LocalRuntime: Local execution for development + - DaytonaRuntime: Cloud development environment using Daytona + + Args: + sid: Session ID that uniquely identifies the current user session """ sid: str diff --git a/openhands/server/conversation_manager/conversation_manager.py b/openhands/server/conversation_manager/conversation_manager.py index 8766f06767..e1efb433b8 100644 --- a/openhands/server/conversation_manager/conversation_manager.py +++ b/openhands/server/conversation_manager/conversation_manager.py @@ -21,6 +21,24 @@ class ConversationManager(ABC): This class defines the interface for managing conversations, whether in standalone or clustered mode. It handles the lifecycle of conversations, including creation, attachment, detachment, and cleanup. + + This is an extension point in OpenHands, that applications built on it can use to modify behavior via server configuration, without modifying its code. + Applications can provide their own + implementation by: + 1. Creating a class that inherits from ConversationManager + 2. Implementing all required abstract methods + 3. Setting server_config.conversation_manager_class to the fully qualified name + of the implementation class + + The default implementation is StandaloneConversationManager, which handles + conversations in a single-server deployment. Applications might want to provide + their own implementation for scenarios like: + - Clustered deployments with distributed conversation state + - Custom persistence or caching strategies + - Integration with external conversation management systems + - Enhanced monitoring or logging capabilities + + The implementation class is instantiated via get_impl() in openhands.server.shared.py. """ sio: socketio.AsyncServer diff --git a/openhands/server/conversation_manager/standalone_conversation_manager.py b/openhands/server/conversation_manager/standalone_conversation_manager.py index 838ce7a2cc..09649c9125 100644 --- a/openhands/server/conversation_manager/standalone_conversation_manager.py +++ b/openhands/server/conversation_manager/standalone_conversation_manager.py @@ -38,7 +38,10 @@ UPDATED_AT_CALLBACK_ID = 'updated_at_callback_id' @dataclass class StandaloneConversationManager(ConversationManager): - """Manages conversations in standalone mode (single server instance).""" + """Default implementation of ConversationManager for single-server deployments. + + See ConversationManager for extensibility details. + """ sio: socketio.AsyncServer config: OpenHandsConfig diff --git a/openhands/server/monitoring.py b/openhands/server/monitoring.py index 872eb9ae8d..89c99fe83e 100644 --- a/openhands/server/monitoring.py +++ b/openhands/server/monitoring.py @@ -3,8 +3,15 @@ from openhands.events.event import Event class MonitoringListener: - """ - Allow tracking of application activity for monitoring purposes. + """Abstract base class for monitoring application activity. + + This is an extension point in OpenHands that allows applications to customize how + application activity is monitored. Applications can substitute their own implementation by: + 1. Creating a class that inherits from MonitoringListener + 2. Implementing desired methods (all methods have default no-op implementations) + 3. Setting server_config.monitoring_listener_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. Implementations should be non-disruptive, do not raise or block to perform I/O. """ diff --git a/openhands/server/user_auth/user_auth.py b/openhands/server/user_auth/user_auth.py index 50e5fca273..2b32297b57 100644 --- a/openhands/server/user_auth/user_auth.py +++ b/openhands/server/user_auth/user_auth.py @@ -21,7 +21,16 @@ class AuthType(Enum): class UserAuth(ABC): - """Extensible class encapsulating user Authentication""" + """Abstract base class for user authentication. + + This is an extension point in OpenHands that allows applications to provide their own + authentication mechanisms. Applications can substitute their own implementation by: + 1. Creating a class that inherits from UserAuth + 2. Implementing all required methods + 3. Setting server_config.user_auth_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + """ _settings: Settings | None diff --git a/openhands/storage/conversation/conversation_store.py b/openhands/storage/conversation/conversation_store.py index b28b2fc5be..76059c8263 100644 --- a/openhands/storage/conversation/conversation_store.py +++ b/openhands/storage/conversation/conversation_store.py @@ -12,7 +12,18 @@ from openhands.utils.async_utils import wait_all class ConversationStore(ABC): - """Storage for conversation metadata. May or may not support multiple users depending on the environment.""" + """Abstract base class for conversation metadata storage. + + This is an extension point in OpenHands that allows applications to customize how + conversation metadata is stored. Applications can substitute their own implementation by: + 1. Creating a class that inherits from ConversationStore + 2. Implementing all required methods + 3. Setting server_config.conversation_store_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + + The implementation may or may not support multiple users depending on the environment. + """ @abstractmethod async def save_metadata(self, metadata: ConversationMetadata) -> None: diff --git a/openhands/storage/conversation/conversation_validator.py b/openhands/storage/conversation/conversation_validator.py index a6399089da..84f0f40808 100644 --- a/openhands/storage/conversation/conversation_validator.py +++ b/openhands/storage/conversation/conversation_validator.py @@ -4,7 +4,18 @@ from openhands.utils.import_utils import get_impl class ConversationValidator: - """Storage for conversation metadata. May or may not support multiple users depending on the environment.""" + """Abstract base class for validating conversation access. + + This is an extension point in OpenHands that allows applications to customize how + conversation access is validated. Applications can substitute their own implementation by: + 1. Creating a class that inherits from ConversationValidator + 2. Implementing the validate method + 3. Setting OPENHANDS_CONVERSATION_VALIDATOR_CLS environment variable to the fully qualified name of the class + + The class is instantiated via get_impl() in create_conversation_validator(). + + The default implementation performs no validation and returns None, None. + """ async def validate( self, diff --git a/openhands/storage/secrets/secrets_store.py b/openhands/storage/secrets/secrets_store.py index 6a1f55185b..2683bbe69c 100644 --- a/openhands/storage/secrets/secrets_store.py +++ b/openhands/storage/secrets/secrets_store.py @@ -7,7 +7,18 @@ from openhands.storage.data_models.user_secrets import UserSecrets class SecretsStore(ABC): - """Storage for secrets. May or may not support multiple users depending on the environment.""" + """Abstract base class for storing user secrets. + + This is an extension point in OpenHands that allows applications to customize how + user secrets are stored. Applications can substitute their own implementation by: + 1. Creating a class that inherits from SecretsStore + 2. Implementing all required methods + 3. Setting server_config.secret_store_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + + The implementation may or may not support multiple users depending on the environment. + """ @abstractmethod async def load(self) -> UserSecrets | None: diff --git a/openhands/storage/settings/settings_store.py b/openhands/storage/settings/settings_store.py index 380871f025..05b394d0a9 100644 --- a/openhands/storage/settings/settings_store.py +++ b/openhands/storage/settings/settings_store.py @@ -7,7 +7,18 @@ from openhands.storage.data_models.settings import Settings class SettingsStore(ABC): - """Storage for ConversationInitData. May or may not support multiple users depending on the environment.""" + """Abstract base class for storing user settings. + + This is an extension point in OpenHands that allows applications to customize how + user settings are stored. Applications can substitute their own implementation by: + 1. Creating a class that inherits from SettingsStore + 2. Implementing all required methods + 3. Setting server_config.settings_store_class to the fully qualified name of the class + + The class is instantiated via get_impl() in openhands.server.shared.py. + + The implementation may or may not support multiple users depending on the environment. + """ @abstractmethod async def load(self) -> Settings | None: diff --git a/openhands/utils/README.md b/openhands/utils/README.md new file mode 100644 index 0000000000..634a9b1424 --- /dev/null +++ b/openhands/utils/README.md @@ -0,0 +1,78 @@ +# OpenHands Utilities + +This directory contains various utility functions and classes used throughout OpenHands. + +## Runtime Implementation Substitution + +OpenHands provides an extensibility mechanism through the `get_impl` and `import_from` functions in `import_utils.py`. This mechanism allows applications built on OpenHands to customize behavior by providing their own implementations of OpenHands base classes. + +### How It Works + +1. Base classes define interfaces through abstract methods and properties +2. Default implementations are provided by OpenHands +3. Applications can provide custom implementations by: + - Creating a class that inherits from the base class + - Implementing all required methods + - Configuring OpenHands to use the custom implementation via configuration + +### Example + +```python +# In OpenHands base code: +class ConversationManager: + @abstractmethod + async def attach_to_conversation(self, sid: str) -> Conversation: + """Attach to an existing conversation.""" + +# Default implementation in OpenHands: +class StandaloneConversationManager(ConversationManager): + async def attach_to_conversation(self, sid: str) -> Conversation: + # Single-server implementation + ... + +# In your application: +class ClusteredConversationManager(ConversationManager): + async def attach_to_conversation(self, sid: str) -> Conversation: + # Custom distributed implementation + ... + +# In configuration: +server_config.conversation_manager_class = 'myapp.ClusteredConversationManager' +``` + +### Common Extension Points + +OpenHands provides several components that can be extended: + +1. Server Components: + - `ConversationManager`: Manages conversation lifecycles + - `UserAuth`: Handles user authentication + - `MonitoringListener`: Provides monitoring capabilities + +2. Storage: + - `ConversationStore`: Stores conversation data + - `SettingsStore`: Manages user settings + - `SecretsStore`: Handles sensitive data + +3. Service Integrations: + - GitHub service + - GitLab service + +### Implementation Details + +The mechanism is implemented through two key functions: + +1. `import_from(qual_name: str)`: Imports any Python value from its fully qualified name + ```python + UserAuth = import_from('openhands.server.user_auth.UserAuth') + ``` + +2. `get_impl(cls: type[T], impl_name: str | None) -> type[T]`: Imports and validates a class implementation + ```python + ConversationManagerImpl = get_impl( + ConversationManager, + server_config.conversation_manager_class + ) + ``` + +The `get_impl` function ensures type safety by validating that the imported class is either the same as or a subclass of the specified base class. It also caches results to avoid repeated imports. diff --git a/openhands/utils/import_utils.py b/openhands/utils/import_utils.py index 56680d9fa1..61be22115c 100644 --- a/openhands/utils/import_utils.py +++ b/openhands/utils/import_utils.py @@ -6,7 +6,23 @@ T = TypeVar('T') def import_from(qual_name: str): - """Import the value from the qualified name given""" + """Import a value from its fully qualified name. + + This function is a utility to dynamically import any Python value (class, function, variable) + from its fully qualified name. For example, 'openhands.server.user_auth.UserAuth' would + import the UserAuth class from the openhands.server.user_auth module. + + Args: + qual_name: A fully qualified name in the format 'module.submodule.name' + e.g. 'openhands.server.user_auth.UserAuth' + + Returns: + The imported value (class, function, or variable) + + Example: + >>> UserAuth = import_from('openhands.server.user_auth.UserAuth') + >>> auth = UserAuth() + """ parts = qual_name.split('.') module_name = '.'.join(parts[:-1]) module = importlib.import_module(module_name) @@ -16,7 +32,36 @@ def import_from(qual_name: str): @lru_cache() def get_impl(cls: type[T], impl_name: str | None) -> type[T]: - """Import a named implementation of the specified class""" + """Import and validate a named implementation of a base class. + + This function is an extensibility mechanism in OpenHands that allows runtime substitution + of implementations. It enables applications to customize behavior by providing their own + implementations of OpenHands base classes. + + The function ensures type safety by validating that the imported class is either the same as + or a subclass of the specified base class. + + Args: + cls: The base class that defines the interface + impl_name: Fully qualified name of the implementation class, or None to use the base class + e.g. 'openhands.server.conversation_manager.StandaloneConversationManager' + + Returns: + The implementation class, which is guaranteed to be a subclass of cls + + Example: + >>> # Get default implementation + >>> ConversationManager = get_impl(ConversationManager, None) + >>> # Get custom implementation + >>> CustomManager = get_impl(ConversationManager, 'myapp.CustomConversationManager') + + Common Use Cases: + - Server components (ConversationManager, UserAuth, etc.) + - Storage implementations (ConversationStore, SettingsStore, etc.) + - Service integrations (GitHub, GitLab services) + + The implementation is cached to avoid repeated imports of the same class. + """ if impl_name is None: return cls impl_class = import_from(impl_name)