diff --git a/autogpt_platform/backend/backend/blocks/github/issues.py b/autogpt_platform/backend/backend/blocks/github/issues.py index 97a4694340e4..45161897a399 100644 --- a/autogpt_platform/backend/backend/blocks/github/issues.py +++ b/autogpt_platform/backend/backend/blocks/github/issues.py @@ -13,6 +13,7 @@ ) +# --8<-- [start:GithubCommentBlock] class GithubCommentBlock(Block): class Input(BlockSchema): credentials: GithubCredentialsInput = GithubCredentialsField("repo") @@ -104,6 +105,9 @@ def run( yield "error", f"Failed to post comment: {str(e)}" +# --8<-- [end:GithubCommentBlock] + + class GithubMakeIssueBlock(Block): class Input(BlockSchema): credentials: GithubCredentialsInput = GithubCredentialsField("repo") diff --git a/autogpt_platform/backend/backend/blocks/google/_auth.py b/autogpt_platform/backend/backend/blocks/google/_auth.py index 207861847d8e..5a63d1bf45d6 100644 --- a/autogpt_platform/backend/backend/blocks/google/_auth.py +++ b/autogpt_platform/backend/backend/blocks/google/_auth.py @@ -6,11 +6,12 @@ from backend.data.model import CredentialsField, CredentialsMetaInput from backend.util.settings import Secrets +# --8<-- [start:GoogleOAuthIsConfigured] secrets = Secrets() GOOGLE_OAUTH_IS_CONFIGURED = bool( secrets.google_client_id and secrets.google_client_secret ) - +# --8<-- [end: GoogleOAuthIsConfigured] GoogleCredentials = OAuth2Credentials GoogleCredentialsInput = CredentialsMetaInput[Literal["google"], Literal["oauth2"]] diff --git a/autogpt_platform/backend/backend/integrations/oauth/__init__.py b/autogpt_platform/backend/backend/integrations/oauth/__init__.py index 3ce18050a008..6b70d592d52d 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/__init__.py +++ b/autogpt_platform/backend/backend/integrations/oauth/__init__.py @@ -3,6 +3,7 @@ from .google import GoogleOAuthHandler from .notion import NotionOAuthHandler +# --8<-- [start:HANDLERS_BY_NAMEExample] HANDLERS_BY_NAME: dict[str, type[BaseOAuthHandler]] = { handler.PROVIDER_NAME: handler for handler in [ @@ -11,5 +12,6 @@ NotionOAuthHandler, ] } +# --8<-- [end: HANDLERS_BY_NAMEExample] __all__ = ["HANDLERS_BY_NAME"] diff --git a/autogpt_platform/backend/backend/integrations/oauth/base.py b/autogpt_platform/backend/backend/integrations/oauth/base.py index bafb457aa646..5743b3e9e4e2 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/base.py +++ b/autogpt_platform/backend/backend/integrations/oauth/base.py @@ -6,26 +6,36 @@ class BaseOAuthHandler(ABC): + # --8<-- [start: BaseOAuthHandler1] PROVIDER_NAME: ClassVar[str] DEFAULT_SCOPES: ClassVar[list[str]] = [] + # --8<-- [end: BaseOAuthHandler1] @abstractmethod + # --8<-- [start: BaseOAuthHandler2] def __init__(self, client_id: str, client_secret: str, redirect_uri: str): ... + # --8<-- [end: BaseOAuthHandler2] @abstractmethod + # --8<-- [start: BaseOAuthHandler3] def get_login_url(self, scopes: list[str], state: str) -> str: + # --8<-- [end: BaseOAuthHandler3] """Constructs a login URL that the user can be redirected to""" ... @abstractmethod + # --8<-- [start: BaseOAuthHandler4] def exchange_code_for_tokens( self, code: str, scopes: list[str] ) -> OAuth2Credentials: + # --8<-- [end: BaseOAuthHandler4] """Exchanges the acquired authorization code from login for a set of tokens""" ... @abstractmethod + # --8<-- [start: BaseOAuthHandler5] def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + # --8<-- [end: BaseOAuthHandler5] """Implements the token refresh mechanism""" ... diff --git a/autogpt_platform/backend/backend/integrations/oauth/github.py b/autogpt_platform/backend/backend/integrations/oauth/github.py index 9741a96beb9e..41186855708d 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/github.py +++ b/autogpt_platform/backend/backend/integrations/oauth/github.py @@ -8,6 +8,7 @@ from .base import BaseOAuthHandler +# --8<-- [start:GithubOAuthHandler] class GitHubOAuthHandler(BaseOAuthHandler): """ Based on the documentation at: @@ -119,3 +120,6 @@ def _request_username(self, access_token: str) -> str | None: # Get the login (username) return response.json().get("login") + + +# --8<-- [end:GithubOAuthHandler] diff --git a/autogpt_platform/backend/backend/integrations/oauth/google.py b/autogpt_platform/backend/backend/integrations/oauth/google.py index 033f548c9477..2760452b0bd9 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/google.py +++ b/autogpt_platform/backend/backend/integrations/oauth/google.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) +# --8<-- [start:GoogleOAuthHandler] class GoogleOAuthHandler(BaseOAuthHandler): """ Based on the documentation at https://developers.google.com/identity/protocols/oauth2/web-server @@ -27,6 +28,8 @@ class GoogleOAuthHandler(BaseOAuthHandler): "openid", ] + # --8<-- [end: GoogleOAuthHandler] + def __init__(self, client_id: str, client_secret: str, redirect_uri: str): self.client_id = client_id self.client_secret = client_secret diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index c5cfdc660250..e12ddcc82f15 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -177,10 +177,12 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): ) # OAuth server credentials for integrations + # --8<-- [start: OAuthServerCredentialsExample] github_client_id: str = Field(default="", description="GitHub OAuth client ID") github_client_secret: str = Field( default="", description="GitHub OAuth client secret" ) + # --8<-- [end: OAuthServerCredentialsExample] google_client_id: str = Field(default="", description="Google OAuth client ID") google_client_secret: str = Field( default="", description="Google OAuth client secret" diff --git a/docs/content/server/new_blocks.md b/docs/content/server/new_blocks.md index ae14f0bc387c..f2835c5e5cbc 100644 --- a/docs/content/server/new_blocks.md +++ b/docs/content/server/new_blocks.md @@ -115,6 +115,7 @@ for a service that we already have OAuth2 support for. Implementing the block itself is relatively simple. On top of the instructions above, you're going to add a `credentials` parameter to the `Input` model and the `run` method: + ```python from autogpt_libs.supabase_integration_credentials_store.types import ( APIKeyCredentials, @@ -191,10 +192,12 @@ class BlockWithAPIKeyAndOAuth(Block): ) -> BlockOutput: ... ``` + The credentials will be automagically injected by the executor in the back end. The `APIKeyCredentials` and `OAuth2Credentials` models are defined [here](https://github.com/Significant-Gravitas/AutoGPT/blob/master/rnd/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py). To use them in e.g. an API request, you can either access the token directly: + ```python # credentials: APIKeyCredentials response = requests.post( @@ -212,7 +215,9 @@ response = requests.post( }, ) ``` + or use the shortcut `credentials.bearer()`: + ```python # credentials: APIKeyCredentials | OAuth2Credentials response = requests.post( @@ -227,25 +232,64 @@ To add support for a new OAuth2-authenticated service, you'll need to add an `OA All our existing handlers and the base class can be found [here][OAuth2 handlers]. Every handler must implement the following parts of the [`BaseOAuthHandler`] interface: -- `PROVIDER_NAME` -- `__init__(client_id, client_secret, redirect_uri)` -- `get_login_url(scopes, state)` -- `exchange_code_for_tokens(code)` -- `_refresh_tokens(credentials)` + +```python +--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler1" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler2" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler3" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler4" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/base.py:BaseOAuthHandler5" +``` As you can see, this is modeled after the standard OAuth2 flow. Aside from implementing the `OAuthHandler` itself, adding a handler into the system requires two more things: -- Adding the handler class to `HANDLERS_BY_NAME` [here](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/__init__.py) -- Adding `{provider}_client_id` and `{provider}_client_secret` to the application's `Secrets` [here](https://github.com/Significant-Gravitas/AutoGPT/blob/e3f35d79c7e9fc6ee0cabefcb73e0fad15a0ce2d/autogpt_platform/backend/backend/util/settings.py#L132) + +- Adding the handler class to `HANDLERS_BY_NAME` under [`integrations/oauth/__init__.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/__init__.py) + +```python title="autogpt_platform/backend/backend/integrations/oauth/__init__.py" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/__init__.py:HANDLERS_BY_NAMEExample" +``` + +- Adding `{provider}_client_id` and `{provider}_client_secret` to the application's `Secrets` under [`util/settings.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/util/settings.py) + +```python title="autogpt_platform/backend/backend/util/settings.py" +--8<-- "autogpt_platform/backend/backend/util/settings.py:OAuthServerCredentialsExample" +``` [OAuth2 handlers]: https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/integrations/oauth [`BaseOAuthHandler`]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/base.py #### Example: GitHub integration + - GitHub blocks with API key + OAuth2 support: [`blocks/github`](https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/blocks/github/) + +```python title="blocks/github/issues.py" +--8<-- "autogpt_platform/backend/backend/blocks/github/issues.py:GithubCommentBlock" +``` + - GitHub OAuth2 handler: [`integrations/oauth/github.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/github.py) +```python title="blocks/github/github.py" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/github.py:GithubOAuthHandler" +``` + +#### Example: Google integration + +- Google OAuth2 handler: [`integrations/oauth/google.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/google.py) + +```python title="integrations/oauth/google.py" +--8<-- "autogpt_platform/backend/backend/integrations/oauth/google.py:GoogleOAuthHandler" +``` + +You can see that google has defined a `DEFAULT_SCOPES` variable, this is used to set the scopes that are requested no matter what the user asks for. + +```python title="blocks/google/_auth.py" +--8<-- "autogpt_platform/backend/backend/blocks/google/_auth.py:GoogleOAuthIsConfigured" +``` + +You can also see that `GOOGLE_OAUTH_IS_CONFIGURED` is used to disable the blocks that require OAuth if the oauth is not configured. This is in the `__init__` method of each block. This is because there is no api key fallback for google blocks so we need to make sure that the oauth is configured before we allow the user to use the blocks. + ## Key Points to Remember - **Unique ID**: Give your block a unique ID in the **init** method. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 28dc686c4379..d3edc1218799 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -136,8 +136,8 @@ markdown_extensions: - pymdownx.mark - pymdownx.smartsymbols - pymdownx.snippets: - auto_append: - - includes/abbreviations.md + base_path: ['.','../'] + check_paths: true - pymdownx.superfences: custom_fences: - name: mermaid