Skip to content

Commit 6c34e58

Browse files
Fix issue #8419: Document get_impl and import_from (#8420)
Co-authored-by: Engel Nyst <[email protected]>
1 parent b771fb6 commit 6c34e58

File tree

13 files changed

+263
-14
lines changed

13 files changed

+263
-14
lines changed

openhands/integrations/github/github_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828

2929

3030
class GitHubService(BaseGitService, GitService):
31+
"""Default implementation of GitService for GitHub integration.
32+
33+
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?
34+
This is an extension point in OpenHands that allows applications to customize GitHub
35+
integration behavior. Applications can substitute their own implementation by:
36+
1. Creating a class that inherits from GitService
37+
2. Implementing all required methods
38+
3. Setting server_config.github_service_class to the fully qualified name of the class
39+
40+
The class is instantiated via get_impl() in openhands.server.shared.py.
41+
"""
3142
BASE_URL = 'https://api.github.com'
3243
token: SecretStr = SecretStr('')
3344
refresh = False

openhands/integrations/gitlab/gitlab_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121

2222

2323
class GitLabService(BaseGitService, GitService):
24+
"""Default implementation of GitService for GitLab integration.
25+
26+
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?
27+
This is an extension point in OpenHands that allows applications to customize GitLab
28+
integration behavior. Applications can substitute their own implementation by:
29+
1. Creating a class that inherits from GitService
30+
2. Implementing all required methods
31+
3. Setting server_config.gitlab_service_class to the fully qualified name of the class
32+
33+
The class is instantiated via get_impl() in openhands.server.shared.py.
34+
"""
2435
BASE_URL = 'https://gitlab.com/api/v4'
2536
GRAPHQL_URL = 'https://gitlab.com/api/graphql'
2637
token: SecretStr = SecretStr('')

openhands/runtime/base.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,33 @@ def _default_env_vars(sandbox_config: SandboxConfig) -> dict[str, str]:
9090

9191

9292
class Runtime(FileEditRuntimeMixin):
93-
"""The runtime is how the agent interacts with the external environment.
94-
This includes a bash sandbox, a browser, and filesystem interactions.
95-
96-
sid is the session id, which is used to identify the current user session.
93+
"""Abstract base class for agent runtime environments.
94+
95+
This is an extension point in OpenHands that allows applications to customize how
96+
agents interact with the external environment. The runtime provides a sandbox with:
97+
- Bash shell access
98+
- Browser interaction
99+
- Filesystem operations
100+
- Git operations
101+
- Environment variable management
102+
103+
Applications can substitute their own implementation by:
104+
1. Creating a class that inherits from Runtime
105+
2. Implementing all required methods
106+
3. Setting the runtime name in configuration or using get_runtime_cls()
107+
108+
The class is instantiated via get_impl() in get_runtime_cls().
109+
110+
Built-in implementations include:
111+
- DockerRuntime: Containerized environment using Docker
112+
- E2BRuntime: Secure sandbox using E2B
113+
- RemoteRuntime: Remote execution environment
114+
- ModalRuntime: Scalable cloud environment using Modal
115+
- LocalRuntime: Local execution for development
116+
- DaytonaRuntime: Cloud development environment using Daytona
117+
118+
Args:
119+
sid: Session ID that uniquely identifies the current user session
97120
"""
98121

99122
sid: str

openhands/server/conversation_manager/conversation_manager.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ class ConversationManager(ABC):
2121
This class defines the interface for managing conversations, whether in standalone
2222
or clustered mode. It handles the lifecycle of conversations, including creation,
2323
attachment, detachment, and cleanup.
24+
25+
This is an extension point in OpenHands, that applications built on it can use to modify behavior via server configuration, without modifying its code.
26+
Applications can provide their own
27+
implementation by:
28+
1. Creating a class that inherits from ConversationManager
29+
2. Implementing all required abstract methods
30+
3. Setting server_config.conversation_manager_class to the fully qualified name
31+
of the implementation class
32+
33+
The default implementation is StandaloneConversationManager, which handles
34+
conversations in a single-server deployment. Applications might want to provide
35+
their own implementation for scenarios like:
36+
- Clustered deployments with distributed conversation state
37+
- Custom persistence or caching strategies
38+
- Integration with external conversation management systems
39+
- Enhanced monitoring or logging capabilities
40+
41+
The implementation class is instantiated via get_impl() in openhands.server.shared.py.
2442
"""
2543

2644
sio: socketio.AsyncServer

openhands/server/conversation_manager/standalone_conversation_manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838

3939
@dataclass
4040
class StandaloneConversationManager(ConversationManager):
41-
"""Manages conversations in standalone mode (single server instance)."""
41+
"""Default implementation of ConversationManager for single-server deployments.
42+
43+
See ConversationManager for extensibility details.
44+
"""
4245

4346
sio: socketio.AsyncServer
4447
config: OpenHandsConfig

openhands/server/monitoring.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33

44

55
class MonitoringListener:
6-
"""
7-
Allow tracking of application activity for monitoring purposes.
6+
"""Abstract base class for monitoring application activity.
7+
8+
This is an extension point in OpenHands that allows applications to customize how
9+
application activity is monitored. Applications can substitute their own implementation by:
10+
1. Creating a class that inherits from MonitoringListener
11+
2. Implementing desired methods (all methods have default no-op implementations)
12+
3. Setting server_config.monitoring_listener_class to the fully qualified name of the class
13+
14+
The class is instantiated via get_impl() in openhands.server.shared.py.
815
916
Implementations should be non-disruptive, do not raise or block to perform I/O.
1017
"""

openhands/server/user_auth/user_auth.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ class AuthType(Enum):
2121

2222

2323
class UserAuth(ABC):
24-
"""Extensible class encapsulating user Authentication"""
24+
"""Abstract base class for user authentication.
25+
26+
This is an extension point in OpenHands that allows applications to provide their own
27+
authentication mechanisms. Applications can substitute their own implementation by:
28+
1. Creating a class that inherits from UserAuth
29+
2. Implementing all required methods
30+
3. Setting server_config.user_auth_class to the fully qualified name of the class
31+
32+
The class is instantiated via get_impl() in openhands.server.shared.py.
33+
"""
2534

2635
_settings: Settings | None
2736

openhands/storage/conversation/conversation_store.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@
1212

1313

1414
class ConversationStore(ABC):
15-
"""Storage for conversation metadata. May or may not support multiple users depending on the environment."""
15+
"""Abstract base class for conversation metadata storage.
16+
17+
This is an extension point in OpenHands that allows applications to customize how
18+
conversation metadata is stored. Applications can substitute their own implementation by:
19+
1. Creating a class that inherits from ConversationStore
20+
2. Implementing all required methods
21+
3. Setting server_config.conversation_store_class to the fully qualified name of the class
22+
23+
The class is instantiated via get_impl() in openhands.server.shared.py.
24+
25+
The implementation may or may not support multiple users depending on the environment.
26+
"""
1627

1728
@abstractmethod
1829
async def save_metadata(self, metadata: ConversationMetadata) -> None:

openhands/storage/conversation/conversation_validator.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@
44

55

66
class ConversationValidator:
7-
"""Storage for conversation metadata. May or may not support multiple users depending on the environment."""
7+
"""Abstract base class for validating conversation access.
8+
9+
This is an extension point in OpenHands that allows applications to customize how
10+
conversation access is validated. Applications can substitute their own implementation by:
11+
1. Creating a class that inherits from ConversationValidator
12+
2. Implementing the validate method
13+
3. Setting OPENHANDS_CONVERSATION_VALIDATOR_CLS environment variable to the fully qualified name of the class
14+
15+
The class is instantiated via get_impl() in create_conversation_validator().
16+
17+
The default implementation performs no validation and returns None, None.
18+
"""
819

920
async def validate(
1021
self,

openhands/storage/secrets/secrets_store.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@
77

88

99
class SecretsStore(ABC):
10-
"""Storage for secrets. May or may not support multiple users depending on the environment."""
10+
"""Abstract base class for storing user secrets.
11+
12+
This is an extension point in OpenHands that allows applications to customize how
13+
user secrets are stored. Applications can substitute their own implementation by:
14+
1. Creating a class that inherits from SecretsStore
15+
2. Implementing all required methods
16+
3. Setting server_config.secret_store_class to the fully qualified name of the class
17+
18+
The class is instantiated via get_impl() in openhands.server.shared.py.
19+
20+
The implementation may or may not support multiple users depending on the environment.
21+
"""
1122

1223
@abstractmethod
1324
async def load(self) -> UserSecrets | None:

openhands/storage/settings/settings_store.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@
77

88

99
class SettingsStore(ABC):
10-
"""Storage for ConversationInitData. May or may not support multiple users depending on the environment."""
10+
"""Abstract base class for storing user settings.
11+
12+
This is an extension point in OpenHands that allows applications to customize how
13+
user settings are stored. Applications can substitute their own implementation by:
14+
1. Creating a class that inherits from SettingsStore
15+
2. Implementing all required methods
16+
3. Setting server_config.settings_store_class to the fully qualified name of the class
17+
18+
The class is instantiated via get_impl() in openhands.server.shared.py.
19+
20+
The implementation may or may not support multiple users depending on the environment.
21+
"""
1122

1223
@abstractmethod
1324
async def load(self) -> Settings | None:

openhands/utils/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# OpenHands Utilities
2+
3+
This directory contains various utility functions and classes used throughout OpenHands.
4+
5+
## Runtime Implementation Substitution
6+
7+
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.
8+
9+
### How It Works
10+
11+
1. Base classes define interfaces through abstract methods and properties
12+
2. Default implementations are provided by OpenHands
13+
3. Applications can provide custom implementations by:
14+
- Creating a class that inherits from the base class
15+
- Implementing all required methods
16+
- Configuring OpenHands to use the custom implementation via configuration
17+
18+
### Example
19+
20+
```python
21+
# In OpenHands base code:
22+
class ConversationManager:
23+
@abstractmethod
24+
async def attach_to_conversation(self, sid: str) -> Conversation:
25+
"""Attach to an existing conversation."""
26+
27+
# Default implementation in OpenHands:
28+
class StandaloneConversationManager(ConversationManager):
29+
async def attach_to_conversation(self, sid: str) -> Conversation:
30+
# Single-server implementation
31+
...
32+
33+
# In your application:
34+
class ClusteredConversationManager(ConversationManager):
35+
async def attach_to_conversation(self, sid: str) -> Conversation:
36+
# Custom distributed implementation
37+
...
38+
39+
# In configuration:
40+
server_config.conversation_manager_class = 'myapp.ClusteredConversationManager'
41+
```
42+
43+
### Common Extension Points
44+
45+
OpenHands provides several components that can be extended:
46+
47+
1. Server Components:
48+
- `ConversationManager`: Manages conversation lifecycles
49+
- `UserAuth`: Handles user authentication
50+
- `MonitoringListener`: Provides monitoring capabilities
51+
52+
2. Storage:
53+
- `ConversationStore`: Stores conversation data
54+
- `SettingsStore`: Manages user settings
55+
- `SecretsStore`: Handles sensitive data
56+
57+
3. Service Integrations:
58+
- GitHub service
59+
- GitLab service
60+
61+
### Implementation Details
62+
63+
The mechanism is implemented through two key functions:
64+
65+
1. `import_from(qual_name: str)`: Imports any Python value from its fully qualified name
66+
```python
67+
UserAuth = import_from('openhands.server.user_auth.UserAuth')
68+
```
69+
70+
2. `get_impl(cls: type[T], impl_name: str | None) -> type[T]`: Imports and validates a class implementation
71+
```python
72+
ConversationManagerImpl = get_impl(
73+
ConversationManager,
74+
server_config.conversation_manager_class
75+
)
76+
```
77+
78+
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.

openhands/utils/import_utils.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,23 @@
66

77

88
def import_from(qual_name: str):
9-
"""Import the value from the qualified name given"""
9+
"""Import a value from its fully qualified name.
10+
11+
This function is a utility to dynamically import any Python value (class, function, variable)
12+
from its fully qualified name. For example, 'openhands.server.user_auth.UserAuth' would
13+
import the UserAuth class from the openhands.server.user_auth module.
14+
15+
Args:
16+
qual_name: A fully qualified name in the format 'module.submodule.name'
17+
e.g. 'openhands.server.user_auth.UserAuth'
18+
19+
Returns:
20+
The imported value (class, function, or variable)
21+
22+
Example:
23+
>>> UserAuth = import_from('openhands.server.user_auth.UserAuth')
24+
>>> auth = UserAuth()
25+
"""
1026
parts = qual_name.split('.')
1127
module_name = '.'.join(parts[:-1])
1228
module = importlib.import_module(module_name)
@@ -16,7 +32,36 @@ def import_from(qual_name: str):
1632

1733
@lru_cache()
1834
def get_impl(cls: type[T], impl_name: str | None) -> type[T]:
19-
"""Import a named implementation of the specified class"""
35+
"""Import and validate a named implementation of a base class.
36+
37+
This function is an extensibility mechanism in OpenHands that allows runtime substitution
38+
of implementations. It enables applications to customize behavior by providing their own
39+
implementations of OpenHands base classes.
40+
41+
The function ensures type safety by validating that the imported class is either the same as
42+
or a subclass of the specified base class.
43+
44+
Args:
45+
cls: The base class that defines the interface
46+
impl_name: Fully qualified name of the implementation class, or None to use the base class
47+
e.g. 'openhands.server.conversation_manager.StandaloneConversationManager'
48+
49+
Returns:
50+
The implementation class, which is guaranteed to be a subclass of cls
51+
52+
Example:
53+
>>> # Get default implementation
54+
>>> ConversationManager = get_impl(ConversationManager, None)
55+
>>> # Get custom implementation
56+
>>> CustomManager = get_impl(ConversationManager, 'myapp.CustomConversationManager')
57+
58+
Common Use Cases:
59+
- Server components (ConversationManager, UserAuth, etc.)
60+
- Storage implementations (ConversationStore, SettingsStore, etc.)
61+
- Service integrations (GitHub, GitLab services)
62+
63+
The implementation is cached to avoid repeated imports of the same class.
64+
"""
2065
if impl_name is None:
2166
return cls
2267
impl_class = import_from(impl_name)

0 commit comments

Comments
 (0)