Fix issue #8419: Document get_impl and import_from (#8420)

Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
OpenHands 2025-06-04 09:23:53 +08:00 committed by GitHub
parent b771fb6e32
commit 6c34e5850b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 262 additions and 13 deletions

View File

@ -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

View File

@ -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('')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.
"""

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

@ -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:

78
openhands/utils/README.md Normal file
View File

@ -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.

View File

@ -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)