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 3 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
17 changes: 17 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,23 @@ 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 a key extension point in OpenHands. 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,16 @@

@dataclass
class StandaloneConversationManager(ConversationManager):
"""Manages conversations in standalone mode (single server instance)."""
"""Manages conversations in standalone mode (single server instance).

This is the default implementation of ConversationManager, designed for single-server deployments.
Applications can substitute their own implementation (e.g., a clustered manager) by:
1. Creating a class that inherits from ConversationManager
2. Implementing all required methods
3. Setting server_config.conversation_manager_class to the fully qualified name of the class

The class is instantiated via get_impl() in openhands.server.shared.py.
"""

sio: socketio.AsyncServer
config: AppConfig
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