Skip to content

Fix issue #8419: Document get_impl and import_from #8420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions openhands/integrations/github/github_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@


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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize GitHubService was among these classes. It seems to break the pattern quite a bit, such as this or this. We could refactor it. I'm not even sure if it's reimplemented or what is reimplemented, that is, what are the abstract methods we should actually separate and implement here?

"""
BASE_URL = 'https://api.github.com'
token: SecretStr = SecretStr('')
refresh = False
Expand Down
10 changes: 10 additions & 0 deletions openhands/integrations/gitlab/gitlab_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@


class GitLabService(BaseGitService, GitService):
"""Default implementation of GitService for GitLab integration.

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('')
Expand Down
31 changes: 27 additions & 4 deletions openhands/runtime/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,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.

sid is the session id, which is used to identify the current user session.
"""Abstract base class for agent runtime environments.

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
Expand Down
18 changes: 18 additions & 0 deletions openhands/server/conversation_manager/conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@

@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: AppConfig
Expand Down
11 changes: 9 additions & 2 deletions openhands/server/monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@


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.
"""
Expand Down
11 changes: 10 additions & 1 deletion openhands/server/user_auth/user_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 12 additions & 1 deletion openhands/storage/conversation/conversation_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@


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:
Expand Down
13 changes: 12 additions & 1 deletion openhands/storage/conversation/conversation_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@


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, conversation_id: str, cookies_str: str
Expand Down
13 changes: 12 additions & 1 deletion openhands/storage/secrets/secrets_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@


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:
Expand Down
13 changes: 12 additions & 1 deletion openhands/storage/settings/settings_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@


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:
Expand Down
78 changes: 78 additions & 0 deletions openhands/utils/README.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 47 additions & 2 deletions openhands/utils/import_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@


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)
Expand All @@ -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)
Expand Down
Loading