mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
parent
b771fb6e32
commit
6c34e5850b
@ -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
|
||||
|
||||
@ -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('')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
78
openhands/utils/README.md
Normal 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.
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user