diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/store.py b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/store.py index 6a4bb354fc87..e84b375c58bc 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/store.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/store.py @@ -2,6 +2,8 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING +from pydantic import SecretStr + if TYPE_CHECKING: from redis import Redis from backend.executor.database import DatabaseManager @@ -10,13 +12,77 @@ from autogpt_libs.utils.synchronize import RedisKeyedMutex from .types import ( + APIKeyCredentials, Credentials, OAuth2Credentials, OAuthState, - UserMetadata, - UserMetadataRaw, + UserIntegrations, +) + +from backend.util.settings import Settings + +settings = Settings() + +revid_credentials = APIKeyCredentials( + id="fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", + provider="revid", + api_key=SecretStr(settings.secrets.revid_api_key), + title="Use Credits for Revid", + expires_at=None, +) +ideogram_credentials = APIKeyCredentials( + id="760f84fc-b270-42de-91f6-08efe1b512d0", + provider="ideogram", + api_key=SecretStr(settings.secrets.ideogram_api_key), + title="Use Credits for Ideogram", + expires_at=None, +) +replicate_credentials = APIKeyCredentials( + id="6b9fc200-4726-4973-86c9-cd526f5ce5db", + provider="replicate", + api_key=SecretStr(settings.secrets.replicate_api_key), + title="Use Credits for Replicate", + expires_at=None, +) +openai_credentials = APIKeyCredentials( + id="53c25cb8-e3ee-465c-a4d1-e75a4c899c2a", + provider="llm", + api_key=SecretStr(settings.secrets.openai_api_key), + title="Use Credits for OpenAI", + expires_at=None, +) +anthropic_credentials = APIKeyCredentials( + id="24e5d942-d9e3-4798-8151-90143ee55629", + provider="llm", + api_key=SecretStr(settings.secrets.anthropic_api_key), + title="Use Credits for Anthropic", + expires_at=None, +) +groq_credentials = APIKeyCredentials( + id="4ec22295-8f97-4dd1-b42b-2c6957a02545", + provider="llm", + api_key=SecretStr(settings.secrets.groq_api_key), + title="Use Credits for Groq", + expires_at=None, +) +did_credentials = APIKeyCredentials( + id="7f7b0654-c36b-4565-8fa7-9a52575dfae2", + provider="d_id", + api_key=SecretStr(settings.secrets.did_api_key), + title="Use Credits for D-ID", + expires_at=None, ) +DEFAULT_CREDENTIALS = [ + revid_credentials, + ideogram_credentials, + replicate_credentials, + openai_credentials, + anthropic_credentials, + groq_credentials, + did_credentials, +] + class SupabaseIntegrationCredentialsStore: def __init__(self, redis: "Redis"): @@ -27,10 +93,11 @@ def __init__(self, redis: "Redis"): def db_manager(self) -> "DatabaseManager": from backend.executor.database import DatabaseManager from backend.util.service import get_service_client + return get_service_client(DatabaseManager) def add_creds(self, user_id: str, credentials: Credentials) -> None: - with self.locked_user_metadata(user_id): + with self.locked_user_integrations(user_id): if self.get_creds_by_id(user_id, credentials.id): raise ValueError( f"Can not re-create existing credentials #{credentials.id} " @@ -41,10 +108,23 @@ def add_creds(self, user_id: str, credentials: Credentials) -> None: ) def get_all_creds(self, user_id: str) -> list[Credentials]: - user_metadata = self._get_user_metadata(user_id) - return UserMetadata.model_validate( - user_metadata.model_dump() - ).integration_credentials + users_credentials = self._get_user_integrations(user_id).credentials + all_credentials = users_credentials + if settings.secrets.revid_api_key: + all_credentials.append(revid_credentials) + if settings.secrets.ideogram_api_key: + all_credentials.append(ideogram_credentials) + if settings.secrets.groq_api_key: + all_credentials.append(groq_credentials) + if settings.secrets.replicate_api_key: + all_credentials.append(replicate_credentials) + if settings.secrets.openai_api_key: + all_credentials.append(openai_credentials) + if settings.secrets.anthropic_api_key: + all_credentials.append(anthropic_credentials) + if settings.secrets.did_api_key: + all_credentials.append(did_credentials) + return all_credentials def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None: all_credentials = self.get_all_creds(user_id) @@ -59,7 +139,7 @@ def get_authorized_providers(self, user_id: str) -> list[str]: return list(set(c.provider for c in credentials)) def update_creds(self, user_id: str, updated: Credentials) -> None: - with self.locked_user_metadata(user_id): + with self.locked_user_integrations(user_id): current = self.get_creds_by_id(user_id, updated.id) if not current: raise ValueError( @@ -93,7 +173,7 @@ def update_creds(self, user_id: str, updated: Credentials) -> None: self._set_user_integration_creds(user_id, updated_credentials_list) def delete_creds_by_id(self, user_id: str, credentials_id: str) -> None: - with self.locked_user_metadata(user_id): + with self.locked_user_integrations(user_id): filtered_credentials = [ c for c in self.get_all_creds(user_id) if c.id != credentials_id ] @@ -110,14 +190,14 @@ def store_state_token(self, user_id: str, provider: str, scopes: list[str]) -> s scopes=scopes, ) - with self.locked_user_metadata(user_id): - user_metadata = self._get_user_metadata(user_id) - oauth_states = user_metadata.integration_oauth_states - oauth_states.append(state.model_dump()) - user_metadata.integration_oauth_states = oauth_states + with self.locked_user_integrations(user_id): + user_integrations = self._get_user_integrations(user_id) + oauth_states = user_integrations.oauth_states + oauth_states.append(state) + user_integrations.oauth_states = oauth_states - self.db_manager.update_user_metadata( - user_id=user_id, metadata=user_metadata + self.db_manager.update_user_integrations( + user_id=user_id, data=user_integrations ) return token @@ -132,39 +212,39 @@ def get_any_valid_scopes_from_state_token( IS TO CHECK IF THE USER HAS GIVEN PERMISSIONS TO THE APPLICATION BEFORE EXCHANGING THE CODE FOR TOKENS. """ - user_metadata = self._get_user_metadata(user_id) - oauth_states = user_metadata.integration_oauth_states + user_integrations = self._get_user_integrations(user_id) + oauth_states = user_integrations.oauth_states now = datetime.now(timezone.utc) valid_state = next( ( state for state in oauth_states - if state["token"] == token - and state["provider"] == provider - and state["expires_at"] > now.timestamp() + if state.token == token + and state.provider == provider + and state.expires_at > now.timestamp() ), None, ) if valid_state: - return valid_state.get("scopes", []) + return valid_state.scopes return [] def verify_state_token(self, user_id: str, token: str, provider: str) -> bool: - with self.locked_user_metadata(user_id): - user_metadata = self._get_user_metadata(user_id) - oauth_states = user_metadata.integration_oauth_states + with self.locked_user_integrations(user_id): + user_integrations = self._get_user_integrations(user_id) + oauth_states = user_integrations.oauth_states now = datetime.now(timezone.utc) valid_state = next( ( state for state in oauth_states - if state["token"] == token - and state["provider"] == provider - and state["expires_at"] > now.timestamp() + if state.token == token + and state.provider == provider + and state.expires_at > now.timestamp() ), None, ) @@ -172,8 +252,8 @@ def verify_state_token(self, user_id: str, token: str, provider: str) -> bool: if valid_state: # Remove the used state oauth_states.remove(valid_state) - user_metadata.integration_oauth_states = oauth_states - self.db_manager.update_user_metadata(user_id, user_metadata) + user_integrations.oauth_states = oauth_states + self.db_manager.update_user_integrations(user_id, user_integrations) return True return False @@ -181,14 +261,18 @@ def verify_state_token(self, user_id: str, token: str, provider: str) -> bool: def _set_user_integration_creds( self, user_id: str, credentials: list[Credentials] ) -> None: - raw_metadata = self._get_user_metadata(user_id) - raw_metadata.integration_credentials = [c.model_dump() for c in credentials] - self.db_manager.update_user_metadata(user_id, raw_metadata) + integrations = self._get_user_integrations(user_id) + # Remove default credentials from the list + credentials = [c for c in credentials if c not in DEFAULT_CREDENTIALS] + integrations.credentials = credentials + self.db_manager.update_user_integrations(user_id, integrations) - def _get_user_metadata(self, user_id: str) -> UserMetadataRaw: - metadata: UserMetadataRaw = self.db_manager.get_user_metadata(user_id=user_id) - return metadata + def _get_user_integrations(self, user_id: str) -> UserIntegrations: + integrations: UserIntegrations = self.db_manager.get_user_integrations( + user_id=user_id + ) + return integrations - def locked_user_metadata(self, user_id: str): - key = (self.db_manager, f"user:{user_id}", "metadata") + def locked_user_integrations(self, user_id: str): + key = (self.db_manager, f"user:{user_id}", "integrations") return self.locks.locked(key) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py index 0f973bb52484..384dfdc79729 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py @@ -65,6 +65,11 @@ class UserMetadata(BaseModel): integration_oauth_states: list[OAuthState] = Field(default_factory=list) -class UserMetadataRaw(BaseModel): - integration_credentials: list[dict] = Field(default_factory=list) - integration_oauth_states: list[dict] = Field(default_factory=list) +class UserMetadataRaw(TypedDict, total=False): + integration_credentials: list[dict] + integration_oauth_states: list[dict] + + +class UserIntegrations(BaseModel): + credentials: list[Credentials] = Field(default_factory=list) + oauth_states: list[OAuthState] = Field(default_factory=list) diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index 0ec84ca83e66..2b7648ea84c9 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -7,6 +7,9 @@ PRISMA_SCHEMA="postgres/schema.prisma" BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] +# generate using `from cryptography.fernet import Fernet;Fernet.generate_key().decode()` +ENCRYPTION_KEY='dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw=' + REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=password diff --git a/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py b/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py index 3fe92950c199..ff1c46f2a5c0 100644 --- a/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py +++ b/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py @@ -1,11 +1,28 @@ import logging import time from enum import Enum +from typing import Literal import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="revid", + api_key=SecretStr("mock-revid-api-key"), + title="Mock Revid API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class AudioTrack(str, Enum): @@ -119,10 +136,13 @@ class VisualMediaType(str, Enum): class AIShortformVideoCreatorBlock(Block): class Input(BlockSchema): - api_key: BlockSecret = SecretField( - key="revid_api_key", - description="Your revid.ai API key", - placeholder="Enter your revid.ai API key", + credentials: CredentialsMetaInput[Literal["revid"], Literal["api_key"]] = ( + CredentialsField( + provider="revid", + supported_credential_types={"api_key"}, + description="The revid.ai integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", + ) ) script: str = SchemaField( description="""1. Use short and punctuated sentences\n\n2. Use linebreaks to create a new clip\n\n3. Text outside of brackets is spoken by the AI, and [text between brackets] will be used to guide the visual generation. For example, [close-up of a cat] will show a close-up of a cat.""", @@ -168,7 +188,7 @@ def __init__(self): input_schema=AIShortformVideoCreatorBlock.Input, output_schema=AIShortformVideoCreatorBlock.Output, test_input={ - "api_key": "test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, "script": "[close-up of a cat] Meow!", "ratio": "9 / 16", "resolution": "720p", @@ -190,6 +210,7 @@ def __init__(self): "create_video": lambda api_key, payload: {"pid": "test_pid"}, "wait_for_video": lambda api_key, pid, webhook_token, max_wait_time=1000: "https://example.com/video.mp4", }, + test_credentials=TEST_CREDENTIALS, ) def create_webhook(self): @@ -200,9 +221,9 @@ def create_webhook(self): webhook_data = response.json() return webhook_data["uuid"], f"https://webhook.site/{webhook_data['uuid']}" - def create_video(self, api_key: str, payload: dict) -> dict: + def create_video(self, api_key: SecretStr, payload: dict) -> dict: url = "https://www.revid.ai/api/public/v2/render" - headers = {"key": api_key} + headers = {"key": api_key.get_secret_value()} response = requests.post(url, json=payload, headers=headers) logger.debug( f"API Response Status Code: {response.status_code}, Content: {response.text}" @@ -210,15 +231,19 @@ def create_video(self, api_key: str, payload: dict) -> dict: response.raise_for_status() return response.json() - def check_video_status(self, api_key: str, pid: str) -> dict: + def check_video_status(self, api_key: SecretStr, pid: str) -> dict: url = f"https://www.revid.ai/api/public/v2/status?pid={pid}" - headers = {"key": api_key} + headers = {"key": api_key.get_secret_value()} response = requests.get(url, headers=headers) response.raise_for_status() return response.json() def wait_for_video( - self, api_key: str, pid: str, webhook_token: str, max_wait_time: int = 1000 + self, + api_key: SecretStr, + pid: str, + webhook_token: str, + max_wait_time: int = 1000, ) -> str: start_time = time.time() while time.time() - start_time < max_wait_time: @@ -240,7 +265,9 @@ def wait_for_video( logger.error("Video creation timed out") raise TimeoutError("Video creation timed out") - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: # Create a new Webhook.site URL webhook_token, webhook_url = self.create_webhook() logger.debug(f"Webhook URL: {webhook_url}") @@ -279,7 +306,7 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: } logger.debug("Creating video...") - response = self.create_video(input_data.api_key.get_secret_value(), payload) + response = self.create_video(credentials.api_key, payload) pid = response.get("pid") if not pid: @@ -291,8 +318,6 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: logger.debug( f"Video created with project ID: {pid}. Waiting for completion..." ) - video_url = self.wait_for_video( - input_data.api_key.get_secret_value(), pid, webhook_token - ) + video_url = self.wait_for_video(credentials.api_key, pid, webhook_token) logger.debug(f"Video ready: {video_url}") yield "video_url", video_url diff --git a/autogpt_platform/backend/backend/blocks/discord.py b/autogpt_platform/backend/backend/blocks/discord.py index e5414cd32727..69d3c3bc744c 100644 --- a/autogpt_platform/backend/backend/blocks/discord.py +++ b/autogpt_platform/backend/backend/blocks/discord.py @@ -1,20 +1,43 @@ import asyncio +from typing import Literal import aiohttp import discord +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +DiscordCredentials = CredentialsMetaInput[Literal["discord"], Literal["api_key"]] + + +def DiscordCredentialsField() -> DiscordCredentials: + return CredentialsField( + description="Discord bot token", + provider="discord", + supported_credential_types={"api_key"}, + ) + + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="discord", + api_key=SecretStr("test_api_key"), + title="Mock Discord API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class ReadDiscordMessagesBlock(Block): class Input(BlockSchema): - discord_bot_token: BlockSecret = SecretField( - key="discord_bot_token", description="Discord bot token" - ) - continuous_read: bool = SchemaField( - description="Whether to continuously read messages", default=True - ) + credentials: DiscordCredentials = DiscordCredentialsField() class Output(BlockSchema): message_content: str = SchemaField( @@ -34,7 +57,11 @@ def __init__(self): output_schema=ReadDiscordMessagesBlock.Output, # Assign output schema description="Reads messages from a Discord channel using a bot token.", categories={BlockCategory.SOCIAL}, - test_input={"discord_bot_token": "test_token", "continuous_read": False}, + test_input={ + "continuous_read": False, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, test_output=[ ( "message_content", @@ -48,7 +75,7 @@ def __init__(self): }, ) - async def run_bot(self, token: str): + async def run_bot(self, token: SecretStr): intents = discord.Intents.default() intents.message_content = True @@ -81,19 +108,20 @@ async def on_message(message): await client.close() - await client.start(token) + await client.start(token.get_secret_value()) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: while True: - for output_name, output_value in self.__run(input_data): + for output_name, output_value in self.__run(input_data, credentials): yield output_name, output_value - if not input_data.continuous_read: - break + break - def __run(self, input_data: Input) -> BlockOutput: + def __run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput: try: loop = asyncio.get_event_loop() - future = self.run_bot(input_data.discord_bot_token.get_secret_value()) + future = self.run_bot(credentials.api_key) # If it's a Future (mock), set the result if isinstance(future, asyncio.Future): @@ -132,9 +160,7 @@ def __run(self, input_data: Input) -> BlockOutput: class SendDiscordMessageBlock(Block): class Input(BlockSchema): - discord_bot_token: BlockSecret = SecretField( - key="discord_bot_token", description="Discord bot token" - ) + credentials: DiscordCredentials = DiscordCredentialsField() message_content: str = SchemaField( description="The content of the message received" ) @@ -155,14 +181,15 @@ def __init__(self): description="Sends a message to a Discord channel using a bot token.", categories={BlockCategory.SOCIAL}, test_input={ - "discord_bot_token": "YOUR_DISCORD_BOT_TOKEN", "channel_name": "general", "message_content": "Hello, Discord!", + "credentials": TEST_CREDENTIALS_INPUT, }, test_output=[("status", "Message sent")], test_mock={ "send_message": lambda token, channel_name, message_content: asyncio.Future() }, + test_credentials=TEST_CREDENTIALS, ) async def send_message(self, token: str, channel_name: str, message_content: str): @@ -192,11 +219,13 @@ def chunk_message(self, message: str, limit: int = 2000) -> list: """Splits a message into chunks not exceeding the Discord limit.""" return [message[i : i + limit] for i in range(0, len(message), limit)] - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: try: loop = asyncio.get_event_loop() future = self.send_message( - input_data.discord_bot_token.get_secret_value(), + credentials.api_key.get_secret_value(), input_data.channel_name, input_data.message_content, ) diff --git a/autogpt_platform/backend/backend/blocks/email_block.py b/autogpt_platform/backend/backend/blocks/email_block.py index 79accb6d7d35..dd63dbbcf6df 100644 --- a/autogpt_platform/backend/backend/blocks/email_block.py +++ b/autogpt_platform/backend/backend/blocks/email_block.py @@ -43,6 +43,7 @@ class Output(BlockSchema): def __init__(self): super().__init__( + disabled=True, id="4335878a-394e-4e67-adf2-919877ff49ae", description="This block sends an email using the provided SMTP credentials.", categories={BlockCategory.OUTPUT}, diff --git a/autogpt_platform/backend/backend/blocks/google/gmail.py b/autogpt_platform/backend/backend/blocks/google/gmail.py index beb96f343904..d0168e4a82be 100644 --- a/autogpt_platform/backend/backend/blocks/google/gmail.py +++ b/autogpt_platform/backend/backend/blocks/google/gmail.py @@ -79,12 +79,32 @@ def __init__(self): test_credentials=TEST_CREDENTIALS, test_output=[ ( - "result", + "email", + { + "id": "1", + "subject": "Test Email", + "snippet": "This is a test email", + "from_": "test@example.com", + "to": "recipient@example.com", + "date": "2024-01-01", + "body": "This is a test email", + "sizeEstimate": 100, + "attachments": [], + }, + ), + ( + "emails", [ { "id": "1", "subject": "Test Email", "snippet": "This is a test email", + "from_": "test@example.com", + "to": "recipient@example.com", + "date": "2024-01-01", + "body": "This is a test email", + "sizeEstimate": 100, + "attachments": [], } ], ), @@ -95,6 +115,12 @@ def __init__(self): "id": "1", "subject": "Test Email", "snippet": "This is a test email", + "from_": "test@example.com", + "to": "recipient@example.com", + "date": "2024-01-01", + "body": "This is a test email", + "sizeEstimate": 100, + "attachments": [], } ], "_send_email": lambda *args, **kwargs: {"id": "1", "status": "sent"}, diff --git a/autogpt_platform/backend/backend/blocks/google_maps.py b/autogpt_platform/backend/backend/blocks/google_maps.py index 3be57b93e818..97d91c83705e 100644 --- a/autogpt_platform/backend/backend/blocks/google_maps.py +++ b/autogpt_platform/backend/backend/blocks/google_maps.py @@ -1,8 +1,25 @@ +from typing import Literal + import googlemaps -from pydantic import BaseModel +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import BaseModel, SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="google_maps", + api_key=SecretStr("mock-google-maps-api-key"), + title="Mock Google Maps API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class Place(BaseModel): @@ -16,8 +33,11 @@ class Place(BaseModel): class GoogleMapsSearchBlock(Block): class Input(BlockSchema): - api_key: BlockSecret = SecretField( - key="google_maps_api_key", + credentials: CredentialsMetaInput[ + Literal["google_maps"], Literal["api_key"] + ] = CredentialsField( + provider="google_maps", + supported_credential_types={"api_key"}, description="Google Maps API Key", ) query: str = SchemaField( @@ -49,7 +69,7 @@ def __init__(self): input_schema=GoogleMapsSearchBlock.Input, output_schema=GoogleMapsSearchBlock.Output, test_input={ - "api_key": "your_test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, "query": "restaurants in new york", "radius": 5000, "max_results": 5, @@ -79,11 +99,14 @@ def __init__(self): } ] }, + test_credentials=TEST_CREDENTIALS, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: places = self.search_places( - input_data.api_key.get_secret_value(), + credentials.api_key, input_data.query, input_data.radius, input_data.max_results, @@ -91,8 +114,8 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: for place in places: yield "place", place - def search_places(self, api_key, query, radius, max_results): - client = googlemaps.Client(key=api_key) + def search_places(self, api_key: SecretStr, query, radius, max_results): + client = googlemaps.Client(key=api_key.get_secret_value()) return self._search_places(client, query, radius, max_results) def _search_places(self, client, query, radius, max_results): diff --git a/autogpt_platform/backend/backend/blocks/ideogram.py b/autogpt_platform/backend/backend/blocks/ideogram.py index 6818a25371e2..e00988d14eb2 100644 --- a/autogpt_platform/backend/backend/blocks/ideogram.py +++ b/autogpt_platform/backend/backend/blocks/ideogram.py @@ -1,10 +1,26 @@ from enum import Enum -from typing import Any, Dict, Optional +from typing import Any, Dict, Literal, Optional import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="ideogram", + api_key=SecretStr("mock-ideogram-api-key"), + title="Mock Ideogram API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class IdeogramModelName(str, Enum): @@ -62,9 +78,13 @@ class UpscaleOption(str, Enum): class IdeogramModelBlock(Block): class Input(BlockSchema): - api_key: BlockSecret = SecretField( - key="ideogram_api_key", - description="Ideogram API Key", + + credentials: CredentialsMetaInput[Literal["ideogram"], Literal["api_key"]] = ( + CredentialsField( + provider="ideogram", + supported_credential_types={"api_key"}, + description="The Ideogram integration can be used with any API key with sufficient permissions for the blocks it is used on.", + ) ) prompt: str = SchemaField( description="Text prompt for image generation", @@ -132,7 +152,6 @@ def __init__(self): input_schema=IdeogramModelBlock.Input, output_schema=IdeogramModelBlock.Output, test_input={ - "api_key": "test_api_key", "ideogram_model_name": IdeogramModelName.V2, "prompt": "A futuristic cityscape at sunset", "aspect_ratio": AspectRatio.ASPECT_1_1, @@ -142,6 +161,7 @@ def __init__(self): "style_type": StyleType.AUTO, "negative_prompt": None, "color_palette_name": ColorPalettePreset.NONE, + "credentials": TEST_CREDENTIALS_INPUT, }, test_output=[ ( @@ -153,14 +173,17 @@ def __init__(self): "run_model": lambda api_key, model_name, prompt, seed, aspect_ratio, magic_prompt_option, style_type, negative_prompt, color_palette_name: "https://ideogram.ai/api/images/test-generated-image-url.png", "upscale_image": lambda api_key, image_url: "https://ideogram.ai/api/images/test-upscaled-image-url.png", }, + test_credentials=TEST_CREDENTIALS, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: seed = input_data.seed # Step 1: Generate the image result = self.run_model( - api_key=input_data.api_key.get_secret_value(), + api_key=credentials.api_key, model_name=input_data.ideogram_model_name.value, prompt=input_data.prompt, seed=seed, @@ -174,7 +197,7 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: # Step 2: Upscale the image if requested if input_data.upscale == UpscaleOption.AI_UPSCALE: result = self.upscale_image( - api_key=input_data.api_key.get_secret_value(), + api_key=credentials.api_key, image_url=result, ) @@ -182,7 +205,7 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: def run_model( self, - api_key: str, + api_key: SecretStr, model_name: str, prompt: str, seed: Optional[int], @@ -193,7 +216,10 @@ def run_model( color_palette_name: str, ): url = "https://api.ideogram.ai/generate" - headers = {"Api-Key": api_key, "Content-Type": "application/json"} + headers = { + "Api-Key": api_key.get_secret_value(), + "Content-Type": "application/json", + } data: Dict[str, Any] = { "image_request": { @@ -221,10 +247,10 @@ def run_model( except requests.exceptions.RequestException as e: raise Exception(f"Failed to fetch image: {str(e)}") - def upscale_image(self, api_key: str, image_url: str): + def upscale_image(self, api_key: SecretStr, image_url: str): url = "https://api.ideogram.ai/upscale" headers = { - "Api-Key": api_key, + "Api-Key": api_key.get_secret_value(), } try: diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index 1366429a542d..d6373347c0e9 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -3,7 +3,10 @@ from enum import Enum, EnumMeta from json import JSONDecodeError from types import MappingProxyType -from typing import TYPE_CHECKING, Any, List, NamedTuple +from typing import TYPE_CHECKING, Any, List, Literal, NamedTuple + +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr if TYPE_CHECKING: from enum import _EnumMemberT @@ -14,20 +17,44 @@ from groq import Groq from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField from backend.util import json from backend.util.settings import BehaveAs, Settings logger = logging.getLogger(__name__) -LlmApiKeys = { - "openai": BlockSecret("openai_api_key"), - "anthropic": BlockSecret("anthropic_api_key"), - "groq": BlockSecret("groq_api_key"), - "ollama": BlockSecret(value=""), +# LlmApiKeys = { +# "openai": BlockSecret("openai_api_key"), +# "anthropic": BlockSecret("anthropic_api_key"), +# "groq": BlockSecret("groq_api_key"), +# "ollama": BlockSecret(value=""), +# } + +AICredentials = CredentialsMetaInput[Literal["llm"], Literal["api_key"]] + +TEST_CREDENTIALS = APIKeyCredentials( + id="ed55ac19-356e-4243-a6cb-bc599e9b716f", + provider="llm", + api_key=SecretStr("mock-openai-api-key"), + title="Mock OpenAI API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.title, } +def AICredentialsField() -> AICredentials: + return CredentialsField( + description="API key for the LLM provider.", + provider="llm", + supported_credential_types={"api_key"}, + ) + + class ModelMetadata(NamedTuple): provider: str context_window: int @@ -149,7 +176,7 @@ class Input(BlockSchema): description="The language model to use for answering the prompt.", advanced=False, ) - api_key: BlockSecret = SecretField(value="") + credentials: AICredentials = AICredentialsField() sys_prompt: str = SchemaField( title="System Prompt", default="", @@ -188,13 +215,14 @@ def __init__(self): output_schema=AIStructuredResponseGeneratorBlock.Output, test_input={ "model": LlmModel.GPT4_TURBO, - "api_key": "fake-api", + "credentials": TEST_CREDENTIALS_INPUT, "expected_format": { "key1": "value1", "key2": "value2", }, "prompt": "User prompt", }, + test_credentials=TEST_CREDENTIALS, test_output=("response", {"key1": "key1Value", "key2": "key2Value"}), test_mock={ "llm_call": lambda *args, **kwargs: ( @@ -212,7 +240,7 @@ def __init__(self): @staticmethod def llm_call( - api_key: str, + credentials: APIKeyCredentials, llm_model: LlmModel, prompt: list[dict], json_format: bool, @@ -234,7 +262,7 @@ def llm_call( provider = llm_model.metadata.provider if provider == "openai": - openai.api_key = api_key + oai_client = openai.OpenAI(api_key=credentials.api_key.get_secret_value()) response_format = None if llm_model in [LlmModel.O1_MINI, LlmModel.O1_PREVIEW]: @@ -247,7 +275,7 @@ def llm_call( elif json_format: response_format = {"type": "json_object"} - response = openai.chat.completions.create( + response = oai_client.chat.completions.create( model=llm_model.value, messages=prompt, # type: ignore response_format=response_format, # type: ignore @@ -274,7 +302,7 @@ def llm_call( # If the role is the same as the last one, combine the content messages[-1]["content"] += "\n" + p["content"] - client = anthropic.Anthropic(api_key=api_key) + client = anthropic.Anthropic(api_key=credentials.api_key.get_secret_value()) try: resp = client.messages.create( model=llm_model.value, @@ -293,7 +321,7 @@ def llm_call( logger.error(error_message) raise ValueError(error_message) elif provider == "groq": - client = Groq(api_key=api_key) + client = Groq(api_key=credentials.api_key.get_secret_value()) response_format = {"type": "json_object"} if json_format else None response = client.chat.completions.create( model=llm_model.value, @@ -322,7 +350,9 @@ def llm_call( else: raise ValueError(f"Unsupported LLM provider: {provider}") - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: logger.debug(f"Calling LLM with input data: {input_data}") prompt = [p.model_dump() for p in input_data.conversation_history] @@ -371,15 +401,11 @@ def parse_response(resp: str) -> tuple[dict[str, Any], str | None]: logger.info(f"LLM request: {prompt}") retry_prompt = "" llm_model = input_data.model - api_key = ( - input_data.api_key.get_secret_value() - or LlmApiKeys[llm_model.metadata.provider].get_secret_value() - ) for retry_count in range(input_data.retry): try: response_text, input_token, output_token = self.llm_call( - api_key=api_key, + credentials=credentials, llm_model=llm_model, prompt=prompt, json_format=bool(input_data.expected_format), @@ -451,7 +477,7 @@ class Input(BlockSchema): description="The language model to use for answering the prompt.", advanced=False, ) - api_key: BlockSecret = SecretField(value="") + credentials: AICredentials = AICredentialsField() sys_prompt: str = SchemaField( title="System Prompt", default="", @@ -484,7 +510,11 @@ def __init__(self): categories={BlockCategory.AI}, input_schema=AITextGeneratorBlock.Input, output_schema=AITextGeneratorBlock.Output, - test_input={"prompt": "User prompt"}, + test_input={ + "prompt": "User prompt", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, test_output=("response", "Response text"), test_mock={"llm_call": lambda *args, **kwargs: "Response text"}, ) @@ -531,7 +561,7 @@ class Input(BlockSchema): default=SummaryStyle.CONCISE, description="The style of the summary to generate.", ) - api_key: BlockSecret = SecretField(value="") + credentials: AICredentials = AICredentialsField() # TODO: Make this dynamic max_tokens: int = SchemaField( title="Max Tokens", @@ -557,10 +587,14 @@ def __init__(self): categories={BlockCategory.AI, BlockCategory.TEXT}, input_schema=AITextSummarizerBlock.Input, output_schema=AITextSummarizerBlock.Output, - test_input={"text": "Lorem ipsum..." * 100}, + test_input={ + "text": "Lorem ipsum..." * 100, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, test_output=("summary", "Final summary of a long text"), test_mock={ - "llm_call": lambda input_data: ( + "llm_call": lambda input_data, credentials: ( {"final_summary": "Final summary of a long text"} if "final_summary" in input_data.expected_format else {"summary": "Summary of a chunk of text"} @@ -568,21 +602,23 @@ def __init__(self): }, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: - for output in self._run(input_data): + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + for output in self._run(input_data, credentials): yield output - def _run(self, input_data: Input) -> BlockOutput: + def _run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput: chunks = self._split_text( input_data.text, input_data.max_tokens, input_data.chunk_overlap ) summaries = [] for chunk in chunks: - chunk_summary = self._summarize_chunk(chunk, input_data) + chunk_summary = self._summarize_chunk(chunk, input_data, credentials) summaries.append(chunk_summary) - final_summary = self._combine_summaries(summaries, input_data) + final_summary = self._combine_summaries(summaries, input_data, credentials) yield "summary", final_summary @staticmethod @@ -597,27 +633,36 @@ def _split_text(text: str, max_tokens: int, overlap: int) -> list[str]: return chunks - def llm_call(self, input_data: AIStructuredResponseGeneratorBlock.Input) -> dict: + def llm_call( + self, + input_data: AIStructuredResponseGeneratorBlock.Input, + credentials: APIKeyCredentials, + ) -> dict: block = AIStructuredResponseGeneratorBlock() - response = block.run_once(input_data, "response") + response = block.run_once(input_data, "response", credentials=credentials) self.merge_stats(block.execution_stats) return response - def _summarize_chunk(self, chunk: str, input_data: Input) -> str: + def _summarize_chunk( + self, chunk: str, input_data: Input, credentials: APIKeyCredentials + ) -> str: prompt = f"Summarize the following text in a {input_data.style} form. Focus your summary on the topic of `{input_data.focus}` if present, otherwise just provide a general summary:\n\n```{chunk}```" llm_response = self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt=prompt, - api_key=input_data.api_key, + credentials=input_data.credentials, model=input_data.model, expected_format={"summary": "The summary of the given text."}, - ) + ), + credentials=credentials, ) return llm_response["summary"] - def _combine_summaries(self, summaries: list[str], input_data: Input) -> str: + def _combine_summaries( + self, summaries: list[str], input_data: Input, credentials: APIKeyCredentials + ) -> str: combined_text = "\n\n".join(summaries) if len(combined_text.split()) <= input_data.max_tokens: @@ -626,12 +671,13 @@ def _combine_summaries(self, summaries: list[str], input_data: Input) -> str: llm_response = self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt=prompt, - api_key=input_data.api_key, + credentials=input_data.credentials, model=input_data.model, expected_format={ "final_summary": "The final summary of all provided summaries." }, - ) + ), + credentials=credentials, ) return llm_response["final_summary"] @@ -640,11 +686,12 @@ def _combine_summaries(self, summaries: list[str], input_data: Input) -> str: return self._run( AITextSummarizerBlock.Input( text=combined_text, - api_key=input_data.api_key, + credentials=input_data.credentials, model=input_data.model, max_tokens=input_data.max_tokens, chunk_overlap=input_data.chunk_overlap, - ) + ), + credentials=credentials, ).send(None)[ 1 ] # Get the first yielded value @@ -660,9 +707,7 @@ class Input(BlockSchema): default=LlmModel.GPT4_TURBO, description="The language model to use for the conversation.", ) - api_key: BlockSecret = SecretField( - value="", description="API key for the chosen language model provider." - ) + credentials: AICredentials = AICredentialsField() max_tokens: int | None = SchemaField( advanced=True, default=None, @@ -693,8 +738,9 @@ def __init__(self): {"role": "user", "content": "Where was it played?"}, ], "model": LlmModel.GPT4_TURBO, - "api_key": "test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, }, + test_credentials=TEST_CREDENTIALS, test_output=( "response", "The 2020 World Series was played at Globe Life Field in Arlington, Texas.", @@ -704,22 +750,29 @@ def __init__(self): }, ) - def llm_call(self, input_data: AIStructuredResponseGeneratorBlock.Input) -> str: + def llm_call( + self, + input_data: AIStructuredResponseGeneratorBlock.Input, + credentials: APIKeyCredentials, + ) -> str: block = AIStructuredResponseGeneratorBlock() - response = block.run_once(input_data, "response") + response = block.run_once(input_data, "response", credentials=credentials) self.merge_stats(block.execution_stats) return response["response"] - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: response = self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt="", - api_key=input_data.api_key, + credentials=input_data.credentials, model=input_data.model, conversation_history=input_data.messages, max_tokens=input_data.max_tokens, expected_format={}, - ) + ), + credentials=credentials, ) yield "response", response @@ -745,7 +798,7 @@ class Input(BlockSchema): description="The language model to use for generating the list.", advanced=True, ) - api_key: BlockSecret = SecretField(value="") + credentials: AICredentials = AICredentialsField() max_retries: int = SchemaField( default=3, description="Maximum number of retries for generating a valid list.", @@ -785,9 +838,10 @@ def __init__(self): "fictional worlds." ), "model": LlmModel.GPT4_TURBO, - "api_key": "test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, "max_retries": 3, }, + test_credentials=TEST_CREDENTIALS, test_output=[ ( "generated_list", @@ -800,7 +854,7 @@ def __init__(self): ("list_item", "Draknos"), ], test_mock={ - "llm_call": lambda input_data: { + "llm_call": lambda input_data, credentials: { "response": "['Zylora Prime', 'Kharon-9', 'Vortexia', 'Oceara', 'Draknos']" }, }, @@ -809,9 +863,10 @@ def __init__(self): @staticmethod def llm_call( input_data: AIStructuredResponseGeneratorBlock.Input, + credentials: APIKeyCredentials, ) -> dict[str, str]: llm_block = AIStructuredResponseGeneratorBlock() - response = llm_block.run_once(input_data, "response") + response = llm_block.run_once(input_data, "response", credentials=credentials) return response @staticmethod @@ -833,14 +888,13 @@ def string_to_list(string): logger.error(f"Failed to convert string to list: {e}") raise ValueError("Invalid list format. Could not convert to list.") - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: logger.debug(f"Starting AIListGeneratorBlock.run with input data: {input_data}") # Check for API key - api_key_check = ( - input_data.api_key.get_secret_value() - or LlmApiKeys[input_data.model.metadata.provider].get_secret_value() - ) + api_key_check = credentials.api_key.get_secret_value() if not api_key_check: raise ValueError("No LLM API key provided.") @@ -904,10 +958,11 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: AIStructuredResponseGeneratorBlock.Input( sys_prompt=sys_prompt, prompt=prompt, - api_key=input_data.api_key, + credentials=input_data.credentials, model=input_data.model, expected_format={}, # Do not use structured response - ) + ), + credentials=credentials, ) logger.debug(f"LLM response: {llm_response}") diff --git a/autogpt_platform/backend/backend/blocks/medium.py b/autogpt_platform/backend/backend/blocks/medium.py index 1d85e0978082..546536827e37 100644 --- a/autogpt_platform/backend/backend/blocks/medium.py +++ b/autogpt_platform/backend/backend/blocks/medium.py @@ -1,10 +1,32 @@ from enum import Enum -from typing import List +from typing import List, Literal import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import ( + BlockSecret, + CredentialsField, + CredentialsMetaInput, + SchemaField, + SecretField, +) + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="medium", + api_key=SecretStr("mock-medium-api-key"), + title="Mock Medium API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class PublishToMediumStatus(str, Enum): @@ -55,10 +77,12 @@ class Input(BlockSchema): description="Whether to notify followers that the user has published", placeholder="False", ) - api_key: BlockSecret = SecretField( - key="medium_api_key", - description="""The API key for the Medium integration. You can get this from https://medium.com/me/settings/security and scrolling down to "integration Tokens".""", - placeholder="Enter your Medium API key", + credentials: CredentialsMetaInput[Literal["medium"], Literal["api_key"]] = ( + CredentialsField( + provider="medium", + supported_credential_types={"api_key"}, + description="The Medium integration can be used with any API key with sufficient permissions for the blocks it is used on.", + ) ) class Output(BlockSchema): @@ -87,7 +111,7 @@ def __init__(self): "license": "all-rights-reserved", "notify_followers": False, "publish_status": PublishToMediumStatus.DRAFT.value, - "api_key": "your_test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, }, test_output=[ ("post_id", "e6f36a"), @@ -104,11 +128,12 @@ def __init__(self): } } }, + test_credentials=TEST_CREDENTIALS, ) def create_post( self, - api_key, + api_key: SecretStr, author_id, title, content, @@ -120,7 +145,7 @@ def create_post( notify_followers, ): headers = { - "Authorization": f"Bearer {api_key}", + "Authorization": f"Bearer {api_key.get_secret_value()}", "Content-Type": "application/json", "Accept": "application/json", } @@ -144,9 +169,11 @@ def create_post( return response.json() - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: response = self.create_post( - input_data.api_key.get_secret_value(), + credentials.api_key, input_data.author_id.get_secret_value(), input_data.title, input_data.content, diff --git a/autogpt_platform/backend/backend/blocks/reddit.py b/autogpt_platform/backend/backend/blocks/reddit.py index 9e4f3f3aca0b..cb04bf26c78a 100644 --- a/autogpt_platform/backend/backend/blocks/reddit.py +++ b/autogpt_platform/backend/backend/blocks/reddit.py @@ -70,6 +70,7 @@ class Output(BlockSchema): def __init__(self): super().__init__( + disabled=True, id="c6731acb-4285-4ee1-bc9b-03d0766c370f", description="This block fetches Reddit posts from a defined subreddit name.", categories={BlockCategory.SOCIAL}, diff --git a/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py b/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py index 38abc8da20f0..a7c3c1d8bccf 100644 --- a/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py +++ b/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py @@ -1,10 +1,27 @@ import os from enum import Enum +from typing import Literal import replicate +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="replicate", + api_key=SecretStr("mock-replicate-api-key"), + title="Mock Replicate API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} # Model name enum @@ -32,9 +49,13 @@ class ImageType(str, Enum): class ReplicateFluxAdvancedModelBlock(Block): class Input(BlockSchema): - api_key: BlockSecret = SecretField( - key="replicate_api_key", - description="Replicate API Key", + credentials: CredentialsMetaInput[Literal["replicate"], Literal["api_key"]] = ( + CredentialsField( + provider="replicate", + supported_credential_types={"api_key"}, + description="The Replicate integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", + ) ) prompt: str = SchemaField( description="Text prompt for image generation", @@ -110,7 +131,7 @@ def __init__(self): input_schema=ReplicateFluxAdvancedModelBlock.Input, output_schema=ReplicateFluxAdvancedModelBlock.Output, test_input={ - "api_key": "test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, "replicate_model_name": ReplicateFluxModelName.FLUX_SCHNELL, "prompt": "A beautiful landscape painting of a serene lake at sunrise", "seed": None, @@ -131,9 +152,12 @@ def __init__(self): test_mock={ "run_model": lambda api_key, model_name, prompt, seed, steps, guidance, interval, aspect_ratio, output_format, output_quality, safety_tolerance: "https://replicate.com/output/generated-image-url.jpg", }, + test_credentials=TEST_CREDENTIALS, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: # If the seed is not provided, generate a random seed seed = input_data.seed if seed is None: @@ -141,7 +165,7 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: # Run the model using the provided inputs result = self.run_model( - api_key=input_data.api_key.get_secret_value(), + api_key=credentials.api_key, model_name=input_data.replicate_model_name.api_name, prompt=input_data.prompt, seed=seed, @@ -157,7 +181,7 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: def run_model( self, - api_key, + api_key: SecretStr, model_name, prompt, seed, @@ -170,7 +194,7 @@ def run_model( safety_tolerance, ): # Initialize Replicate client with the API key - client = replicate.Client(api_token=api_key) + client = replicate.Client(api_token=api_key.get_secret_value()) # Run the model with additional parameters output = client.run( diff --git a/autogpt_platform/backend/backend/blocks/search.py b/autogpt_platform/backend/backend/blocks/search.py index 27a4322ce6a7..5759573c6110 100644 --- a/autogpt_platform/backend/backend/blocks/search.py +++ b/autogpt_platform/backend/backend/blocks/search.py @@ -1,10 +1,12 @@ -from typing import Any +from typing import Any, Literal from urllib.parse import quote import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField class GetRequest: @@ -120,12 +122,34 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "content", content +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="openweathermap", + api_key=SecretStr("mock-openweathermap-api-key"), + title="Mock OpenWeatherMap API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} + + class GetWeatherInformationBlock(Block, GetRequest): class Input(BlockSchema): location: str = SchemaField( description="Location to get weather information for" ) - api_key: BlockSecret = SecretField(key="openweathermap_api_key") + credentials: CredentialsMetaInput[ + Literal["openweathermap"], Literal["api_key"] + ] = CredentialsField( + provider="openweathermap", + supported_credential_types={"api_key"}, + description="The OpenWeatherMap integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", + ) use_celsius: bool = SchemaField( default=True, description="Whether to use Celsius or Fahrenheit for temperature", @@ -151,8 +175,8 @@ def __init__(self): description="Retrieves weather information for a specified location using OpenWeatherMap API.", test_input={ "location": "New York", - "api_key": "YOUR_API_KEY", "use_celsius": True, + "credentials": TEST_CREDENTIALS_INPUT, }, test_output=[ ("temperature", "21.66"), @@ -165,11 +189,14 @@ def __init__(self): "weather": [{"description": "overcast clouds"}], } }, + test_credentials=TEST_CREDENTIALS, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: units = "metric" if input_data.use_celsius else "imperial" - api_key = input_data.api_key.get_secret_value() + api_key = credentials.api_key location = input_data.location url = f"http://api.openweathermap.org/data/2.5/weather?q={quote(location)}&appid={api_key}&units={units}" weather_data = self.get_request(url, json=True) diff --git a/autogpt_platform/backend/backend/blocks/talking_head.py b/autogpt_platform/backend/backend/blocks/talking_head.py index f4497d85ffab..56b726f02e81 100644 --- a/autogpt_platform/backend/backend/blocks/talking_head.py +++ b/autogpt_platform/backend/backend/blocks/talking_head.py @@ -2,15 +2,36 @@ from typing import Literal import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="d_id", + api_key=SecretStr("mock-d-id-api-key"), + title="Mock D-ID API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class CreateTalkingAvatarVideoBlock(Block): class Input(BlockSchema): - api_key: BlockSecret = SecretField( - key="did_api_key", description="D-ID API Key" + credentials: CredentialsMetaInput[Literal["d_id"], Literal["api_key"]] = ( + CredentialsField( + provider="d_id", + supported_credential_types={"api_key"}, + description="The D-ID integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", + ) ) script_input: str = SchemaField( description="The text input for the script", @@ -58,7 +79,7 @@ def __init__(self): input_schema=CreateTalkingAvatarVideoBlock.Input, output_schema=CreateTalkingAvatarVideoBlock.Output, test_input={ - "api_key": "your_test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, "script_input": "Welcome to AutoGPT", "voice_id": "en-US-JennyNeural", "presenter_id": "amy-Aq6OmGZnMt", @@ -86,27 +107,33 @@ def __init__(self): "result_url": "https://d-id.com/api/clips/abcd1234-5678-efgh-ijkl-mnopqrstuvwx/video", }, }, + test_credentials=TEST_CREDENTIALS, ) - def create_clip(self, api_key: str, payload: dict) -> dict: + def create_clip(self, api_key: SecretStr, payload: dict) -> dict: url = "https://api.d-id.com/clips" headers = { "accept": "application/json", "content-type": "application/json", - "authorization": f"Basic {api_key}", + "authorization": f"Basic {api_key.get_secret_value()}", } response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return response.json() - def get_clip_status(self, api_key: str, clip_id: str) -> dict: + def get_clip_status(self, api_key: SecretStr, clip_id: str) -> dict: url = f"https://api.d-id.com/clips/{clip_id}" - headers = {"accept": "application/json", "authorization": f"Basic {api_key}"} + headers = { + "accept": "application/json", + "authorization": f"Basic {api_key.get_secret_value()}", + } response = requests.get(url, headers=headers) response.raise_for_status() return response.json() - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: # Create the clip payload = { "script": { @@ -125,14 +152,12 @@ def run(self, input_data: Input, **kwargs) -> BlockOutput: "driver_id": input_data.driver_id, } - response = self.create_clip(input_data.api_key.get_secret_value(), payload) + response = self.create_clip(credentials.api_key, payload) clip_id = response["id"] # Poll for clip status for _ in range(input_data.max_polling_attempts): - status_response = self.get_clip_status( - input_data.api_key.get_secret_value(), clip_id - ) + status_response = self.get_clip_status(credentials.api_key, clip_id) if status_response["status"] == "done": yield "video_url", status_response["result_url"] return diff --git a/autogpt_platform/backend/backend/blocks/text_to_speech_block.py b/autogpt_platform/backend/backend/blocks/text_to_speech_block.py index 41412763407f..3881dc25342c 100644 --- a/autogpt_platform/backend/backend/blocks/text_to_speech_block.py +++ b/autogpt_platform/backend/backend/blocks/text_to_speech_block.py @@ -1,9 +1,25 @@ -from typing import Any +from typing import Any, Literal import requests +from autogpt_libs.supabase_integration_credentials_store.types import APIKeyCredentials +from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import BlockSecret, SchemaField, SecretField +from backend.data.model import CredentialsField, CredentialsMetaInput, SchemaField + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="unreal_speech", + api_key=SecretStr("mock-unreal-speech-api-key"), + title="Mock Unreal Speech API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} class UnrealTextToSpeechBlock(Block): @@ -17,8 +33,13 @@ class Input(BlockSchema): placeholder="Scarlett", default="Scarlett", ) - api_key: BlockSecret = SecretField( - key="unreal_speech_api_key", description="Your Unreal Speech API key" + credentials: CredentialsMetaInput[ + Literal["unreal_speech"], Literal["api_key"] + ] = CredentialsField( + provider="unreal_speech", + supported_credential_types={"api_key"}, + description="The Unreal Speech integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", ) class Output(BlockSchema): @@ -35,7 +56,7 @@ def __init__(self): test_input={ "text": "This is a test of the text to speech API.", "voice_id": "Scarlett", - "api_key": "test_api_key", + "credentials": TEST_CREDENTIALS_INPUT, }, test_output=[("mp3_url", "https://example.com/test.mp3")], test_mock={ @@ -43,15 +64,16 @@ def __init__(self): "OutputUri": "https://example.com/test.mp3" } }, + test_credentials=TEST_CREDENTIALS, ) @staticmethod def call_unreal_speech_api( - api_key: str, text: str, voice_id: str + api_key: SecretStr, text: str, voice_id: str ) -> dict[str, Any]: url = "https://api.v7.unrealspeech.com/speech" headers = { - "Authorization": f"Bearer {api_key}", + "Authorization": f"Bearer {api_key.get_secret_value()}", "Content-Type": "application/json", } data = { @@ -67,9 +89,11 @@ def call_unreal_speech_api( response.raise_for_status() return response.json() - def run(self, input_data: Input, **kwargs) -> BlockOutput: + def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: api_response = self.call_unreal_speech_api( - input_data.api_key.get_secret_value(), + credentials.api_key, input_data.text, input_data.voice_id, ) diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index 5581a7854226..805d8022f3bd 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -4,11 +4,22 @@ from typing import Any, Optional, Type import prisma.errors +from autogpt_libs.supabase_integration_credentials_store.store import ( + anthropic_credentials, + did_credentials, + groq_credentials, + ideogram_credentials, + openai_credentials, + replicate_credentials, + revid_credentials, +) from prisma import Json from prisma.enums import UserBlockCreditType from prisma.models import UserBlockCredit from pydantic import BaseModel +from backend.blocks.ai_shortform_video_block import AIShortformVideoCreatorBlock +from backend.blocks.ideogram import IdeogramModelBlock from backend.blocks.llm import ( MODEL_METADATA, AIConversationBlock, @@ -17,6 +28,7 @@ AITextSummarizerBlock, LlmModel, ) +from backend.blocks.replicate_flux_advanced import ReplicateFluxAdvancedModelBlock from backend.blocks.search import ExtractWebsiteContentBlock, SearchTheWebBlock from backend.blocks.talking_head import CreateTalkingAvatarVideoBlock from backend.data.block import Block, BlockInput, get_block @@ -49,23 +61,70 @@ def __init__( ) -llm_cost = [ - BlockCost( - cost_type=BlockCostType.RUN, - cost_filter={ - "model": model, - "api_key": None, # Running LLM with user own API key is free. - }, - cost_amount=metadata.cost_factor, - ) - for model, metadata in MODEL_METADATA.items() -] + [ - BlockCost( - # Default cost is running LlmModel.GPT4O. - cost_amount=MODEL_METADATA[LlmModel.GPT4O].cost_factor, - cost_filter={"api_key": None}, - ), -] +llm_cost = ( + [ + BlockCost( + cost_type=BlockCostType.RUN, + cost_filter={ + "model": model, + "api_key": None, # Running LLM with user own API key is free. + }, + cost_amount=metadata.cost_factor, + ) + for model, metadata in MODEL_METADATA.items() + ] + + [ + BlockCost( + cost_type=BlockCostType.RUN, + cost_filter={ + "model": model, + "credentials": { + "id": anthropic_credentials.id, + "provider": anthropic_credentials.provider, + "type": anthropic_credentials.type, + }, + }, + cost_amount=metadata.cost_factor, + ) + for model, metadata in MODEL_METADATA.items() + if metadata.provider == "anthropic" + ] + + [ + BlockCost( + cost_type=BlockCostType.RUN, + cost_filter={ + "model": model, + "credentials": { + "id": openai_credentials.id, + "provider": openai_credentials.provider, + "type": openai_credentials.type, + }, + }, + cost_amount=metadata.cost_factor, + ) + for model, metadata in MODEL_METADATA.items() + if metadata.provider == "openai" + ] + + [ + BlockCost( + cost_type=BlockCostType.RUN, + cost_filter={ + "model": model, + "credentials": {"id": groq_credentials.id}, + }, + cost_amount=metadata.cost_factor, + ) + for model, metadata in MODEL_METADATA.items() + if metadata.provider == "groq" + ] + + [ + BlockCost( + # Default cost is running LlmModel.GPT4O. + cost_amount=MODEL_METADATA[LlmModel.GPT4O].cost_factor, + cost_filter={"api_key": None}, + ), + ] +) BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = { AIConversationBlock: llm_cost, @@ -73,12 +132,57 @@ def __init__( AIStructuredResponseGeneratorBlock: llm_cost, AITextSummarizerBlock: llm_cost, CreateTalkingAvatarVideoBlock: [ - BlockCost(cost_amount=15, cost_filter={"api_key": None}) + BlockCost( + cost_amount=15, + cost_filter={ + "credentials": { + "id": did_credentials.id, + "provider": did_credentials.provider, + "type": did_credentials.type, + } + }, + ) ], SearchTheWebBlock: [BlockCost(cost_amount=1)], ExtractWebsiteContentBlock: [ BlockCost(cost_amount=1, cost_filter={"raw_content": False}) ], + IdeogramModelBlock: [ + BlockCost( + cost_amount=1, + cost_filter={ + "credentials": { + "id": ideogram_credentials.id, + "provider": ideogram_credentials.provider, + "type": ideogram_credentials.type, + } + }, + ) + ], + AIShortformVideoCreatorBlock: [ + BlockCost( + cost_amount=10, + cost_filter={ + "credentials": { + "id": revid_credentials.id, + "provider": revid_credentials.provider, + "type": revid_credentials.type, + } + }, + ) + ], + ReplicateFluxAdvancedModelBlock: [ + BlockCost( + cost_amount=10, + cost_filter={ + "credentials": { + "id": replicate_credentials.id, + "provider": replicate_credentials.provider, + "type": replicate_credentials.type, + } + }, + ) + ], } diff --git a/autogpt_platform/backend/backend/data/user.py b/autogpt_platform/backend/backend/data/user.py index 477b3bae6526..bb7458dc7b3a 100644 --- a/autogpt_platform/backend/backend/data/user.py +++ b/autogpt_platform/backend/backend/data/user.py @@ -1,11 +1,19 @@ -from typing import Optional +import logging +from typing import Optional, cast -from autogpt_libs.supabase_integration_credentials_store.types import UserMetadataRaw +from autogpt_libs.supabase_integration_credentials_store.types import ( + UserIntegrations, + UserMetadata, + UserMetadataRaw, +) from fastapi import HTTPException from prisma import Json from prisma.models import User from backend.data.db import prisma +from backend.util.encryption import JSONCryptor + +logger = logging.getLogger(__name__) DEFAULT_USER_ID = "3e53486c-cf57-477e-ba2a-cb02dc828e1a" DEFAULT_EMAIL = "default@example.com" @@ -50,19 +58,79 @@ async def create_default_user() -> Optional[User]: return User.model_validate(user) -async def get_user_metadata(user_id: str) -> UserMetadataRaw: +async def get_user_metadata(user_id: str) -> UserMetadata: user = await User.prisma().find_unique_or_raise( where={"id": user_id}, ) - return ( - UserMetadataRaw.model_validate(user.metadata) - if user.metadata - else UserMetadataRaw() - ) + metadata = cast(UserMetadataRaw, user.metadata) + return UserMetadata.model_validate(metadata) -async def update_user_metadata(user_id: str, metadata: UserMetadataRaw): + +async def update_user_metadata(user_id: str, metadata: UserMetadata): await User.prisma().update( where={"id": user_id}, data={"metadata": Json(metadata.model_dump())}, ) + + +async def get_user_integrations(user_id: str) -> UserIntegrations: + user = await User.prisma().find_unique_or_raise( + where={"id": user_id}, + ) + + encrypted_integrations = user.integrations + if not encrypted_integrations: + return UserIntegrations() + else: + return UserIntegrations.model_validate( + JSONCryptor().decrypt(encrypted_integrations) + ) + + +async def update_user_integrations(user_id: str, data: UserIntegrations): + encrypted_data = JSONCryptor().encrypt(data.model_dump()) + await User.prisma().update( + where={"id": user_id}, + data={"integrations": encrypted_data}, + ) + + +async def migrate_and_encrypt_user_integrations(): + """Migrate integration credentials and OAuth states from metadata to integrations column.""" + users = await User.prisma().find_many( + where={ + "metadata": { + "path": ["integration_credentials"], + "not": Json({"a": "yolo"}), # bogus value works to check if key exists + } # type: ignore + } + ) + logger.info(f"Migrating integration credentials for {len(users)} users") + + for user in users: + raw_metadata = cast(UserMetadataRaw, user.metadata) + metadata = UserMetadata.model_validate(raw_metadata) + + # Get existing integrations data + integrations = await get_user_integrations(user_id=user.id) + + # Copy credentials and oauth states from metadata if they exist + if metadata.integration_credentials and not integrations.credentials: + integrations.credentials = metadata.integration_credentials + if metadata.integration_oauth_states: + integrations.oauth_states = metadata.integration_oauth_states + + # Save to integrations column + await update_user_integrations(user_id=user.id, data=integrations) + + # Remove from metadata + raw_metadata = dict(raw_metadata) + raw_metadata.pop("integration_credentials", None) + raw_metadata.pop("integration_oauth_states", None) + + # Update metadata without integration data + await User.prisma().update( + where={"id": user.id}, + data={"metadata": Json(raw_metadata)}, + ) diff --git a/autogpt_platform/backend/backend/executor/database.py b/autogpt_platform/backend/backend/executor/database.py index 0d33c28460c1..ecc9a12fc780 100644 --- a/autogpt_platform/backend/backend/executor/database.py +++ b/autogpt_platform/backend/backend/executor/database.py @@ -16,7 +16,12 @@ ) from backend.data.graph import get_graph, get_node from backend.data.queue import RedisExecutionEventBus -from backend.data.user import get_user_metadata, update_user_metadata +from backend.data.user import ( + get_user_integrations, + get_user_metadata, + update_user_integrations, + update_user_metadata, +) from backend.util.service import AppService, expose from backend.util.settings import Config @@ -25,7 +30,6 @@ class DatabaseManager(AppService): - def __init__(self): super().__init__() self.use_db = True @@ -79,6 +83,8 @@ def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> R: exposed_run_and_wait(user_credit_model.spend_credits), ) - # User + User Metadata + # User + User Metadata + User Integrations get_user_metadata = exposed_run_and_wait(get_user_metadata) update_user_metadata = exposed_run_and_wait(update_user_metadata) + get_user_integrations = exposed_run_and_wait(get_user_integrations) + update_user_integrations = exposed_run_and_wait(update_user_integrations) diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 8c3ed3dcba14..0d91e9c622f5 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -19,7 +19,7 @@ from backend.data import graph as graph_db from backend.data.block import BlockInput, CompletedBlockOutput from backend.data.credit import get_block_costs, get_user_credit_model -from backend.data.user import get_or_create_user +from backend.data.user import get_or_create_user, migrate_and_encrypt_user_integrations from backend.executor import ExecutionManager, ExecutionScheduler from backend.server.model import CreateGraph, SetGraphActiveVersion from backend.util.service import AppService, get_service_client @@ -47,6 +47,7 @@ def get_port(cls) -> int: async def lifespan(self, _: FastAPI): await db.connect() await block.initialize_blocks() + await migrate_and_encrypt_user_integrations() yield await db.disconnect() diff --git a/autogpt_platform/backend/backend/util/encryption.py b/autogpt_platform/backend/backend/util/encryption.py new file mode 100644 index 000000000000..c8ba8b5edc50 --- /dev/null +++ b/autogpt_platform/backend/backend/util/encryption.py @@ -0,0 +1,34 @@ +import json +from typing import Optional + +from cryptography.fernet import Fernet + +from backend.util.settings import Settings + +ENCRYPTION_KEY = Settings().secrets.encryption_key + + +class JSONCryptor: + def __init__(self, key: Optional[str] = None): + # Use provided key or get from environment + self.key = key or ENCRYPTION_KEY + if not self.key: + raise ValueError( + "Encryption key must be provided or set in ENCRYPTION_KEY environment variable" + ) + self.fernet = Fernet( + self.key.encode() if isinstance(self.key, str) else self.key + ) + + def encrypt(self, data: dict) -> str: + """Encrypt dictionary data to string""" + json_str = json.dumps(data) + encrypted = self.fernet.encrypt(json_str.encode()) + return encrypted.decode() + + def decrypt(self, encrypted_str: str) -> dict: + """Decrypt string to dictionary""" + if not encrypted_str: + return {} + decrypted = self.fernet.decrypt(encrypted_str.encode()) + return json.loads(decrypted.decode()) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 6cca5e5469ff..7b7e5e30af32 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -208,6 +208,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): default="", description="Supabase service role key" ) + encryption_key: str = Field(default="", description="Encryption key") + # OAuth server credentials for integrations # --8<-- [start:OAuthServerCredentialsExample] github_client_id: str = Field(default="", description="GitHub OAuth client ID") diff --git a/autogpt_platform/backend/migrations/20241030014950_move_integration_creds_to_platform.User/migration.sql b/autogpt_platform/backend/migrations/20241030014950_move_integration_creds_to_platform.User/migration.sql new file mode 100644 index 000000000000..f2b2269b181e --- /dev/null +++ b/autogpt_platform/backend/migrations/20241030014950_move_integration_creds_to_platform.User/migration.sql @@ -0,0 +1,40 @@ +-- Migrate integration credentials from auth.user.raw_user_meta_data to platform.User.metadata +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'auth' + AND table_name = 'users' + ) THEN + -- First update User metadata for users that have integration_credentials + WITH users_with_creds AS ( + SELECT + id, + raw_user_meta_data->'integration_credentials' as integration_credentials, + raw_user_meta_data + FROM auth.users + WHERE raw_user_meta_data ? 'integration_credentials' + ) + UPDATE "User" u + SET metadata = COALESCE( + CASE + -- If User.metadata already has .integration_credentials, leave it + WHEN u.metadata ? 'integration_credentials' THEN u.metadata + -- If User.metadata exists but has no .integration_credentials, add it + WHEN u.metadata IS NOT NULL AND u.metadata::text != '' THEN + (u.metadata || jsonb_build_object('integration_credentials', uwc.integration_credentials)) + -- If User.metadata is NULL, set it + ELSE jsonb_build_object('integration_credentials', uwc.integration_credentials) + END, + '{}'::jsonb + ) + FROM users_with_creds uwc + WHERE u.id = uwc.id::text; + + -- Finally remove integration_credentials from auth.users + UPDATE auth.users + SET raw_user_meta_data = raw_user_meta_data - 'integration_credentials' + WHERE raw_user_meta_data ? 'integration_credentials'; + END IF; +END $$; diff --git a/autogpt_platform/backend/migrations/20241030061705_encrypt_user_metadata/migration.sql b/autogpt_platform/backend/migrations/20241030061705_encrypt_user_metadata/migration.sql new file mode 100644 index 000000000000..977f5db33e91 --- /dev/null +++ b/autogpt_platform/backend/migrations/20241030061705_encrypt_user_metadata/migration.sql @@ -0,0 +1,16 @@ +-- Make User.metadata column consistent and add integrations column for encrypted credentials + +-- First update all records to have empty JSON object +UPDATE "User" +SET "metadata" = '{}'::jsonb +WHERE "metadata" IS NULL; + +-- Then make it required +ALTER TABLE "User" +ALTER COLUMN "metadata" SET DEFAULT '{}'::jsonb, +ALTER COLUMN "metadata" SET NOT NULL, +-- and add integrations column (which will be encrypted JSON) +ADD COLUMN "integrations" TEXT NOT NULL DEFAULT ''; + +-- Encrypting the credentials and moving them from metadata to integrations +-- will be handled in the backend. diff --git a/autogpt_platform/backend/migrations/20241030063332_drop_all_credentials_from_constant_input/migration.sql b/autogpt_platform/backend/migrations/20241030063332_drop_all_credentials_from_constant_input/migration.sql new file mode 100644 index 000000000000..4880f8aab0cb --- /dev/null +++ b/autogpt_platform/backend/migrations/20241030063332_drop_all_credentials_from_constant_input/migration.sql @@ -0,0 +1,59 @@ +-- Function to clean sensitive data from JSON +CREATE OR REPLACE FUNCTION clean_sensitive_json(data jsonb) +RETURNS jsonb AS $$ +DECLARE + result jsonb := data; +BEGIN + -- If the JSON contains api_key directly + IF result ? 'api_key' THEN + result = result - 'api_key'; + END IF; + + -- If the JSON contains discord_bot_token + IF result ? 'discord_bot_token' THEN + result = result - 'discord_bot_token'; + END IF; + + -- If the JSON contains creds + IF result ? 'creds' THEN + result = result - 'creds'; + END IF; + + -- If the JSON contains smtp credentials + IF result ? 'smtp_username' THEN + result = result - 'smtp_username'; + END IF; + + IF result ? 'smtp_password' THEN + result = result - 'smtp_password'; + END IF; + + -- If the JSON contains OAuth credentials + IF result ? 'client_id' THEN + result = result - 'client_id'; + END IF; + + IF result ? 'client_secret' THEN + result = result - 'client_secret'; + END IF; + + -- If the JSON contains username/password + IF result ? 'username' THEN + result = result - 'username'; + END IF; + + IF result ? 'password' THEN + result = result - 'password'; + END IF; + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- Update the table using the function +UPDATE "AgentNode" +SET "constantInput" = clean_sensitive_json("constantInput"::jsonb)::json +WHERE "constantInput"::jsonb ?| array['api_key', 'discord_bot_token', 'creds', 'smtp_username', 'smtp_password', 'client_id', 'client_secret', 'username', 'password']; + +-- Drop the function after use +DROP FUNCTION clean_sensitive_json; diff --git a/autogpt_platform/backend/poetry.lock b/autogpt_platform/backend/poetry.lock index ef370a12a128..edf27890f686 100644 --- a/autogpt_platform/backend/poetry.lock +++ b/autogpt_platform/backend/poetry.lock @@ -294,12 +294,12 @@ develop = true [package.dependencies] colorama = "^0.4.6" expiringdict = "^1.2.2" -google-cloud-logging = "^3.8.0" -pydantic = "^2.8.2" -pydantic-settings = "^2.5.2" +google-cloud-logging = "^3.11.3" +pydantic = "^2.9.2" +pydantic-settings = "^2.6.0" pyjwt = "^2.8.0" python-dotenv = "^1.0.1" -supabase = "^2.7.2" +supabase = "^2.9.1" [package.source] type = "directory" @@ -373,6 +373,85 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -512,6 +591,55 @@ files = [ python-dateutil = "*" pytz = ">2021.1" +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "deprecated" version = "1.2.14" @@ -949,13 +1077,13 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-logging" -version = "3.11.2" +version = "3.11.3" description = "Stackdriver Logging API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google_cloud_logging-3.11.2-py2.py3-none-any.whl", hash = "sha256:0a755f04f184fbe77ad608258dc283a032485ebb4d0e2b2501964059ee9c898f"}, - {file = "google_cloud_logging-3.11.2.tar.gz", hash = "sha256:4897441c2b74f6eda9181c23a8817223b6145943314a821d64b729d30766cb2b"}, + {file = "google_cloud_logging-3.11.3-py2.py3-none-any.whl", hash = "sha256:b8ec23f2998f76a58f8492db26a0f4151dd500425c3f08448586b85972f3c494"}, + {file = "google_cloud_logging-3.11.3.tar.gz", hash = "sha256:0a73cd94118875387d4535371d9e9426861edef8e44fba1261e86782d5b8d54f"}, ] [package.dependencies] @@ -1860,8 +1988,8 @@ python-dateutil = ">=2.5.3" tqdm = ">=4.64.1" typing-extensions = ">=3.7.4" urllib3 = [ - {version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, {version = ">=1.26.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, + {version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, ] [package.extras] @@ -1944,20 +2072,20 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "postgrest" -version = "0.16.11" +version = "0.17.2" description = "PostgREST client for Python. This library provides an ORM interface to PostgREST." optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "postgrest-0.16.11-py3-none-any.whl", hash = "sha256:22fb6b817ace1f68aa648fd4ce0f56d2786c9260fa4ed2cb9046191231a682b8"}, - {file = "postgrest-0.16.11.tar.gz", hash = "sha256:10af51b4c39e288ad7df2db92d6a61fb3c4683131b40561f473e3de116e83fa5"}, + {file = "postgrest-0.17.2-py3-none-any.whl", hash = "sha256:f7c4f448e5a5e2d4c1dcf192edae9d1007c4261e9a6fb5116783a0046846ece2"}, + {file = "postgrest-0.17.2.tar.gz", hash = "sha256:445cd4e4a191e279492549df0c4e827d32f9d01d0852599bb8a6efb0f07fcf78"}, ] [package.dependencies] deprecation = ">=2.1.0,<3.0.0" -httpx = {version = ">=0.24,<0.28", extras = ["http2"]} +httpx = {version = ">=0.26,<0.28", extras = ["http2"]} pydantic = ">=1.9,<3.0" -strenum = ">=0.4.9,<0.5.0" +strenum = {version = ">=0.4.9,<0.5.0", markers = "python_version < \"3.11\""} [[package]] name = "praw" @@ -2131,6 +2259,17 @@ files = [ {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.9.2" @@ -2146,8 +2285,8 @@ files = [ annotated-types = ">=0.6.0" pydantic-core = "2.23.4" typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, ] [package.extras] @@ -2257,13 +2396,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.5.2" +version = "2.6.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, - {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, + {file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"}, + {file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"}, ] [package.dependencies] @@ -2880,17 +3019,17 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "storage3" -version = "0.7.7" +version = "0.8.2" description = "Supabase Storage client for Python." optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "storage3-0.7.7-py3-none-any.whl", hash = "sha256:ed80a2546cd0b5c22e2c30ea71096db6c99268daf2958c603488e7d72efb8426"}, - {file = "storage3-0.7.7.tar.gz", hash = "sha256:9fba680cf761d139ad764f43f0e91c245d1ce1af2cc3afe716652f835f48f83e"}, + {file = "storage3-0.8.2-py3-none-any.whl", hash = "sha256:f2e995b18c77a2a9265d1a33047d43e4d6abb11eb3ca5067959f68281c305de3"}, + {file = "storage3-0.8.2.tar.gz", hash = "sha256:db05d3fe8fb73bd30c814c4c4749664f37a5dfc78b629e8c058ef558c2b89f5a"}, ] [package.dependencies] -httpx = {version = ">=0.24,<0.28", extras = ["http2"]} +httpx = {version = ">=0.26,<0.28", extras = ["http2"]} python-dateutil = ">=2.8.2,<3.0.0" typing-extensions = ">=4.2.0,<5.0.0" @@ -2912,36 +3051,36 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] [[package]] name = "supabase" -version = "2.7.4" +version = "2.9.1" description = "Supabase client for Python." optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "supabase-2.7.4-py3-none-any.whl", hash = "sha256:01815fbc30cac753933d4a44a2529fd13cb7634b56c705c65b12a02c8e75982b"}, - {file = "supabase-2.7.4.tar.gz", hash = "sha256:5a979c7711b3c5ce688514fa0afc015780522569494e1a9a9d25d03b7c3d654b"}, + {file = "supabase-2.9.1-py3-none-any.whl", hash = "sha256:a96f857a465712cb551679c1df66ba772c834f861756ce4aa2aa4cb703f6aeb7"}, + {file = "supabase-2.9.1.tar.gz", hash = "sha256:51fce39c9eb50573126dabb342541ec5e1f13e7476938768f4b0ccfdb8c522cd"}, ] [package.dependencies] -gotrue = ">=1.3,<3.0" -httpx = ">=0.24,<0.28" -postgrest = ">=0.14,<0.17.0" +gotrue = ">=2.9.0,<3.0.0" +httpx = ">=0.26,<0.28" +postgrest = ">=0.17.0,<0.18.0" realtime = ">=2.0.0,<3.0.0" -storage3 = ">=0.5.3,<0.8.0" -supafunc = ">=0.3.1,<0.6.0" +storage3 = ">=0.8.0,<0.9.0" +supafunc = ">=0.6.0,<0.7.0" [[package]] name = "supafunc" -version = "0.5.1" +version = "0.6.2" description = "Library for Supabase Functions" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "supafunc-0.5.1-py3-none-any.whl", hash = "sha256:b05e99a2b41270211a3f90ec843c04c5f27a5618f2d2d2eb8e07f41eb962a910"}, - {file = "supafunc-0.5.1.tar.gz", hash = "sha256:1ae9dce6bd935939c561650e86abb676af9665ecf5d4ffc1c7ec3c4932c84334"}, + {file = "supafunc-0.6.2-py3-none-any.whl", hash = "sha256:101b30616b0a1ce8cf938eca1df362fa4cf1deacb0271f53ebbd674190fb0da5"}, + {file = "supafunc-0.6.2.tar.gz", hash = "sha256:c7dfa20db7182f7fe4ae436e94e05c06cd7ed98d697fed75d68c7b9792822adc"}, ] [package.dependencies] -httpx = {version = ">=0.24,<0.28", extras = ["http2"]} +httpx = {version = ">=0.26,<0.28", extras = ["http2"]} [[package]] name = "tenacity" @@ -3741,4 +3880,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "2df083d46526768cb97098b290978598e5eff9c728fa4c28e2502ce556fc894a" +content-hash = "00069717c4818aa24b164e3c00a104d559c2fb16c531f60bccfcf5a69fb553c8" diff --git a/autogpt_platform/backend/pyproject.toml b/autogpt_platform/backend/pyproject.toml index 8f04c85427c2..ce5d2e665beb 100644 --- a/autogpt_platform/backend/pyproject.toml +++ b/autogpt_platform/backend/pyproject.toml @@ -46,6 +46,7 @@ youtube-transcript-api = "^0.6.2" googlemaps = "^4.10.0" replicate = "^0.34.1" pinecone = "^5.3.1" +cryptography = "^43.0.3" [tool.poetry.group.dev.dependencies] poethepoet = "^0.29.0" httpx = "^0.27.0" diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index b316e226d202..7b16a5e652f4 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -12,12 +12,13 @@ generator client { // User model to mirror Auth provider users model User { - id String @id // This should match the Supabase user ID - email String @unique - name String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - metadata Json? + id String @id // This should match the Supabase user ID + email String @unique + name String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + metadata Json @default("{}") + integrations String @default("") // Relations AgentGraphs AgentGraph[] diff --git a/autogpt_platform/backend/target.prisma b/autogpt_platform/backend/target.prisma index 7c378b5a6fb7..195cde655114 100644 --- a/autogpt_platform/backend/target.prisma +++ b/autogpt_platform/backend/target.prisma @@ -21,7 +21,7 @@ model User { name String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - metadata Json? @default("{}") + metadata String @default("") // Relations Agents Agent[] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx index 252eb6fd704f..8b7e3a36e9d2 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx @@ -7,9 +7,12 @@ import useCredentials from "@/hooks/useCredentials"; import { zodResolver } from "@hookform/resolvers/zod"; import AutoGPTServerAPI from "@/lib/autogpt-server-api"; import { NotionLogoIcon } from "@radix-ui/react-icons"; -import { FaGithub, FaGoogle, FaKey } from "react-icons/fa"; +import { FaDiscord, FaGithub, FaGoogle, FaMedium, FaKey } from "react-icons/fa"; import { FC, useMemo, useState } from "react"; -import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types"; +import { + CredentialsMetaInput, + CredentialsProviderName, +} from "@/lib/autogpt-server-api/types"; import { IconKey, IconKeyPlus, IconUserPlus } from "@/components/ui/icons"; import { Dialog, @@ -36,13 +39,29 @@ import { SelectValue, } from "@/components/ui/select"; +const fallbackIcon = FaKey; + // --8<-- [start:ProviderIconsEmbed] -export const providerIcons: Record> = { +export const providerIcons: Record< + CredentialsProviderName, + React.FC<{ className?: string }> +> = { github: FaGithub, google: FaGoogle, notion: NotionLogoIcon, - jina: FaKey, - pinecone: FaKey, + discord: FaDiscord, + d_id: fallbackIcon, + google_maps: FaGoogle, + jina: fallbackIcon, + ideogram: fallbackIcon, + llm: fallbackIcon, + medium: FaMedium, + openai: fallbackIcon, + openweathermap: fallbackIcon, + pinecone: fallbackIcon, + replicate: fallbackIcon, + revid: fallbackIcon, + unreal_speech: fallbackIcon, }; // --8<-- [end:ProviderIconsEmbed] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 40ad438b929b..cf79fcf9d0e2 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -2,6 +2,8 @@ import AutoGPTServerAPI, { APIKeyCredentials, CredentialsDeleteResponse, CredentialsMetaResponse, + CredentialsProviderName, + PROVIDER_NAMES, } from "@/lib/autogpt-server-api"; import { createContext, @@ -11,25 +13,30 @@ import { useState, } from "react"; -// --8<-- [start:CredentialsProviderNames] -const CREDENTIALS_PROVIDER_NAMES = [ - "github", - "google", - "notion", - "jina", - "pinecone", -] as const; - -export type CredentialsProviderName = - (typeof CREDENTIALS_PROVIDER_NAMES)[number]; +// Get keys from CredentialsProviderName type +const CREDENTIALS_PROVIDER_NAMES = Object.values( + PROVIDER_NAMES, +) as CredentialsProviderName[]; +// --8<-- [start:CredentialsProviderNames] const providerDisplayNames: Record = { + discord: "Discord", + d_id: "D-ID", github: "GitHub", google: "Google", - notion: "Notion", + google_maps: "Google Maps", + ideogram: "Ideogram", jina: "Jina", + medium: "Medium", + llm: "LLM", + notion: "Notion", + openai: "OpenAI", + openweathermap: "OpenWeatherMap", pinecone: "Pinecone", -}; + replicate: "Replicate", + revid: "Rev.ID", + unreal_speech: "Unreal Speech", +} as const; // --8<-- [end:CredentialsProviderNames] type APIKeyCredentialsCreatable = Omit< @@ -162,41 +169,43 @@ export default function CredentialsProvider({ api.isAuthenticated().then((isAuthenticated) => { if (!isAuthenticated) return; - CREDENTIALS_PROVIDER_NAMES.forEach((provider) => { - api.listCredentials(provider).then((response) => { - const { oauthCreds, apiKeys } = response.reduce<{ - oauthCreds: CredentialsMetaResponse[]; - apiKeys: CredentialsMetaResponse[]; - }>( - (acc, cred) => { - if (cred.type === "oauth2") { - acc.oauthCreds.push(cred); - } else if (cred.type === "api_key") { - acc.apiKeys.push(cred); - } - return acc; - }, - { oauthCreds: [], apiKeys: [] }, - ); - - setProviders((prev) => ({ - ...prev, - [provider]: { - provider, - providerName: providerDisplayNames[provider], - savedApiKeys: apiKeys, - savedOAuthCredentials: oauthCreds, - oAuthCallback: (code: string, state_token: string) => - oAuthCallback(provider, code, state_token), - createAPIKeyCredentials: ( - credentials: APIKeyCredentialsCreatable, - ) => createAPIKeyCredentials(provider, credentials), - deleteCredentials: (id: string) => - deleteCredentials(provider, id), - }, - })); - }); - }); + CREDENTIALS_PROVIDER_NAMES.forEach( + (provider: CredentialsProviderName) => { + api.listCredentials(provider).then((response) => { + const { oauthCreds, apiKeys } = response.reduce<{ + oauthCreds: CredentialsMetaResponse[]; + apiKeys: CredentialsMetaResponse[]; + }>( + (acc, cred) => { + if (cred.type === "oauth2") { + acc.oauthCreds.push(cred); + } else if (cred.type === "api_key") { + acc.apiKeys.push(cred); + } + return acc; + }, + { oauthCreds: [], apiKeys: [] }, + ); + + setProviders((prev) => ({ + ...prev, + [provider]: { + provider, + providerName: providerDisplayNames[provider], + savedApiKeys: apiKeys, + savedOAuthCredentials: oauthCreds, + oAuthCallback: (code: string, state_token: string) => + oAuthCallback(provider, code, state_token), + createAPIKeyCredentials: ( + credentials: APIKeyCredentialsCreatable, + ) => createAPIKeyCredentials(provider, credentials), + deleteCredentials: (id: string) => + deleteCredentials(provider, id), + }, + })); + }); + }, + ); }); }, [api, createAPIKeyCredentials, deleteCredentials, oAuthCallback]); diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index d7ae2a0a8331..d6cf1cc0a09f 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -95,12 +95,34 @@ export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & { export type CredentialsType = "api_key" | "oauth2"; // --8<-- [start:BlockIOCredentialsSubSchema] +export const PROVIDER_NAMES = { + D_ID: "d_id", + DISCORD: "discord", + GITHUB: "github", + GOOGLE: "google", + GOOGLE_MAPS: "google_maps", + IDEOGRAM: "ideogram", + JINA: "jina", + LLM: "llm", + MEDIUM: "medium", + NOTION: "notion", + OPENAI: "openai", + OPENWEATHERMAP: "openweathermap", + PINECONE: "pinecone", + REPLICATE: "replicate", + REVID: "revid", + UNREAL_SPEECH: "unreal_speech", +} as const; +// --8<-- [end:BlockIOCredentialsSubSchema] + +export type CredentialsProviderName = + (typeof PROVIDER_NAMES)[keyof typeof PROVIDER_NAMES]; + export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & { - credentials_provider: "github" | "google" | "notion" | "jina" | "pinecone"; + credentials_provider: CredentialsProviderName; credentials_scopes?: string[]; credentials_types: Array; }; -// --8<-- [end:BlockIOCredentialsSubSchema] export type BlockIONullSubSchema = BlockIOSubSchemaMeta & { type: "null"; diff --git a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml index 4e48378e891d..1c837db541fa 100644 --- a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml @@ -122,4 +122,5 @@ secrets: REDIS_PASSWORD: "AgBKMJoMuj4Aze7QZFm0mmR+7FJ/1Shc/fvFMc1yv1WcyT12ngDlSdmw6eW6PaAxnrzTRZbjGNxDVONS/8g86OvEEe+OiZjI7iaGxipGkxeKMzHPbHgQt97gKRT0wEQ8K6d67gD72YZDpVmYKMOWlMDIWl64404O1Xq4FJeBQQiB57MpP5VBX0Haxe+piYfyCcli/V9mZqLb8rzutl+IovCzd3z+rpJ2EC9kgCWjGzH0Kaylmrg86ZFFSQScTcv+UQ6/7y2WldVJPohMFEOFbxUXEThzkPxy7rryNNDrQ2M704a+/ixAqhQ9nJmaAfMNdFgp4T0oEQlsTPBEsXwCt3yzqbdAm+eAohe2X60d+trNsHdMGEzgWDFtTLEjCdKml9a7GJMJsZsf2Qb1AnvdwlLFWm9jm8X+x9YXrHvakso+zvRCB1uvVEB+77ys4y0flBXDheFOTsS7cnGfumexGV/0IrJPBujVJM1q6J1ilPGTYqWVpSznl4taCPvkGjFtsKj1JHlc1FMkyV9vmkHfMfC/YuYYzMpKcfMQlUh22gpth69ENhN3DNUUEH3m5Ea4hzG5lsiCJ9XFJyJ4RSqUU3U58zy18ONEzlX1qNb26oqTSe2j5+29JpTAOkmcRyMBH0WHhB1Us5vgYjN0WNKY4EKLO53kxJDJIKiquEb1mWAmy9yzft+LhroqpyhAUtTvh5MLVs1CCpUX2Q==" DATABASE_URL: "AgBiuPoCatLyHm2T4JojAjXxjd59gDazf2eSPGFjtagTe/ue6crSW9oios4+kzDhSoK+t6CVqBKtRZRK2pzeKJ0xNsEQbCPU8xGrymUS2HsBuadSKz6opJzsrF7V8cFsiZWl8aoJqV1QR2pbRf/o6ws/g2PiXnbykDPwViamQlN+iZXzYA35h8QPYgXLkdWXzqII6cnpxdkgDQGFuZxkKTm6yqX4tKwCT5GcpNNV333IxX33ljZQDwBJENxAGs43wH8KOhSeVq5uGArJz04teagn6zAxVhP6ZnoK77FCCCHzgQ5eupigBxWnLXYSuC0652hcmCWnVTy+eJzGAWvCwdTk45xZ2fyvxj6uTc5DG9Pqk1U5SlLr9C9yvou9Qwd30M4q/Sj9t4WtH4wMIuCHRp5uaTzDHdW+XHIhflRIPJD4XTXvotsbawCgpulwowrtWXtiDZmUJ1IOw12tXnBROk62lglfeb4y0zCc1snpBQeAJd1GWrksZ/j+VRTl6wJFCPfnQot1g6qccBah4Uiz266o2aybcbZH6nIu/hCrRX8QSFrZQZODJoLGZH1XDjYEPX/LHVaCRsQiBiFuWZbYqcU4RhmOiM/KKTimBsl0lzlAMEz8ITL0sLJnjJleqdqPuDp0IAkAZCjHK9cshJIv5Kxp+m9TFRSSscCRSFeVCqROaxZsYKpfdb0JaHFWaZ/h8Is=" SENTRY_DSN: "AgB9i02k9BgaIXF0p9Qyyeo0PRa9bd3UiPBWQ3V4Jn19Vy5XAzKfYvqP8t+vafN2ffY+wCk1FlhYzdIuFjh3oRvdKvtwGEBZk6nLFiUrw/GSum0ueR2OzEy+AwGFXA9FstD0KCMJvyehSv9xRm9kqLOC4Xb/5lOWwTNF3AKqkEMEeKrOWx4OLXG6MLdR7OicY45BCE5WvcV2PizDaN5w3J72eUxFP0HjXit/aW/gK32IJME0RxeuQZ5TnPKTNrooYPR0eWXd2PgYshFjQ2ARy/OsvOrD10y8tQ3M5qx/HNWLC/r0lEu2np+9iUIAE1ufSwjmNSyi4V8usdZWq7xnf3vuKlSgmveqKkLbwQUWj1BpLNIjUvyY+1Rk1rxup/WCgaw+xOZd6sR/qTIjILv5GuzpU0AiwEm7sgl2pmpFXq6n6QjNOfZoPBTL73f4bpXNJ3EyMYDbPxOtGDz91B+bDtOsMr1DNWQslKkk3EIilm/l0+NuLKxf/e2HwM3sB15mkQqVZBdbiVOr7B27cR9xAnr296KE/BU6E9dp/fl+IgcaonMpTsE61pCLHWxQXNBO5X078/zhmaXBQyEBNQ5SPDr9u3pHWrrLkBtXwldZvgmLMMVFMAzrVVkJB4lC9sZj0pXPhda0/BsA4xcGRELj/PizwSr+kb3lDumNMqzEap5ZjEGCBpeeIVSo19v+RoEDw0AFmyxfYx2+91HsgiEqjEUg+J6yDmjAoRpOD1wRZOnnpR8ufMiqdBteCG8B5SXkhgto1WtDyOMVlX2wbmBFVetv2nAbMIA/l4E/Yv8HXiJsTqAkeYc5Qak6/SMGnZTw7Q==" - SUPABASE_SERVICE_ROLE_KEY: "AgBKJDPEiTQUYLY0B/NKaAkxH7whrGuxQVtRdz9mEr/Bx06n1Yu1Zm4/oEQp3OvYerRvQWuv1k3//jf3eiya4ZW9+ZntfPdQWL9/tzq+/spevFtiEvuQ8uuUhtNOU4IGt27KTTlhCfeHKte8jtLQ/lwcrSrfPZ1T8Gy8PXdsAgakGUauEs2oHuX2XUaPE9UFF4HRAhmjPZ9e7u7Zfgcj8D+otjrwNVC5ZXFM7/ha0roeZHdpTvOcemKjxhiZA0FmdkXgOCPihNxlz0sKcupCEte6ocnSkpL3pBflBsa9+NLz7kLCUQPeOCExkMTndyqWk1kFci6j35cBP6PQlHfWPdo8OFCdG+3EfvEt5+4PQ08d7nXRZqowBiQdE2/e8qA/dZc8cJ7ecpza+9Qf/pNIl+9Ix0EFvmB6rpbO9w5Ptw2yMOAebVl7qV+A65GelvcPWROK5Vfftwx8KT2sn9ldVOYy+C7OafgOm8qaL7mEMePSwJy68MKpnMm/TceE7xxZ2sMSWEl9FMn4QXEawD4LQrJHKpum6XyUG2FlMkogHMikOEbJzz1ICAcHB6OWJXo3wU+fK8jkI4/UYioFSfF9MQXaC8bUGc4NT6T62KvjnrdkmOHG7HcN4UNQ7yBa/fP1pM7peYQdwAKr4Oxl4v3i96uKRdCuIimYiWpcxklkQhmSsMLKFMZGDDvv1BNtL6oxK4ZaEyWoorEyjkd2UrSpbP6cvyVMVWbTl1/BD350NRn6OYTpGIXwmAhqVWuuyt8kfLLU6Ot2MYcq/i16qvc3dA5XizLKHY95X8R+DlUCEzawF75Sx++eMPKU/o5rxhZNRjvffIcXtw1Hy/uqVCqilmSt33RqUsehOxQUqHBIW7L7sAik/L+hgTiE3MwN8XSfGncDB/bNweJh3ZSXsdwZD2bleEY1nWpsXVhcJDNcdW3YscJbsyTCGQcOb0zEQaxSmfLAd3rIa+SAZpFEOsD8A5kZjI4QpmqPkhNHeF8=" \ No newline at end of file + SUPABASE_SERVICE_ROLE_KEY: "AgBKJDPEiTQUYLY0B/NKaAkxH7whrGuxQVtRdz9mEr/Bx06n1Yu1Zm4/oEQp3OvYerRvQWuv1k3//jf3eiya4ZW9+ZntfPdQWL9/tzq+/spevFtiEvuQ8uuUhtNOU4IGt27KTTlhCfeHKte8jtLQ/lwcrSrfPZ1T8Gy8PXdsAgakGUauEs2oHuX2XUaPE9UFF4HRAhmjPZ9e7u7Zfgcj8D+otjrwNVC5ZXFM7/ha0roeZHdpTvOcemKjxhiZA0FmdkXgOCPihNxlz0sKcupCEte6ocnSkpL3pBflBsa9+NLz7kLCUQPeOCExkMTndyqWk1kFci6j35cBP6PQlHfWPdo8OFCdG+3EfvEt5+4PQ08d7nXRZqowBiQdE2/e8qA/dZc8cJ7ecpza+9Qf/pNIl+9Ix0EFvmB6rpbO9w5Ptw2yMOAebVl7qV+A65GelvcPWROK5Vfftwx8KT2sn9ldVOYy+C7OafgOm8qaL7mEMePSwJy68MKpnMm/TceE7xxZ2sMSWEl9FMn4QXEawD4LQrJHKpum6XyUG2FlMkogHMikOEbJzz1ICAcHB6OWJXo3wU+fK8jkI4/UYioFSfF9MQXaC8bUGc4NT6T62KvjnrdkmOHG7HcN4UNQ7yBa/fP1pM7peYQdwAKr4Oxl4v3i96uKRdCuIimYiWpcxklkQhmSsMLKFMZGDDvv1BNtL6oxK4ZaEyWoorEyjkd2UrSpbP6cvyVMVWbTl1/BD350NRn6OYTpGIXwmAhqVWuuyt8kfLLU6Ot2MYcq/i16qvc3dA5XizLKHY95X8R+DlUCEzawF75Sx++eMPKU/o5rxhZNRjvffIcXtw1Hy/uqVCqilmSt33RqUsehOxQUqHBIW7L7sAik/L+hgTiE3MwN8XSfGncDB/bNweJh3ZSXsdwZD2bleEY1nWpsXVhcJDNcdW3YscJbsyTCGQcOb0zEQaxSmfLAd3rIa+SAZpFEOsD8A5kZjI4QpmqPkhNHeF8=" + ENCRYPTION_KEY: "AgAlH+nrWFAwm3DxUjlKTdjNeqJjs2ozS7VcIv7it9HmV7LYVntyWgaOVch0JJe6RKQ1U+xXD7Y0jSywk9iTPQe1R429q9uNk3Jnukd/U8UiD0WoFcvte7+ZhESFb5jyZqNEYHCYUDhQyi+Xkm2ha2PQG0hMFrLSabjok9YVO0lU6zroyJpPKs7WpoWaBlfOpqwCfDShKj50gpY5q/xgENkzDX83nKB+WX8BauGqw9GNFKcSZA4ZANHMLoJpqxhNwDqHJg0P9cUd59QfyrftNbr5xwpG+z0Qz/WehC4EuPj2eBn34GF+C02F7T7m6IqQd3x03gh5cyFUP37iQ/KY+CKif+slJMxC86pBVstGSvqAqX+43g/y2P3sQHTMKB+yXsnjkRPIeSAohqCntKDv4CfF1deVLLP5oVikFJOHAPdzVPDawex8hClzxmtVBa1loe44lEDnwHAQwZ+CGYhK6UxdnxEZWpu0om+SqWdiPor9rfY2U6ek6AymMjAci4pZAFgUNbv4saWQo8pXKyyhYfJ1jzwsnAl+tk72HnidVFOkoWkRiWDCiJV4ZwQNnJoKzq+8lE88GbFIc5aa+6a/+W092yWPgRoSwXy6gzDuWmHo6pfStVDxmS8c8e9pSBDyCFX6gN1Qqb1CIXRNcVq+vspwcLrYbs53i7lPUEpVatCIwPmOXvNqzr9C8zstUG5Znjt/p3KAEnRNnD7RmbKxLo6RUIHbn4hsicb74I8bjMHgVoqjAXT0pNCFN5JtPw==" \ No newline at end of file diff --git a/autogpt_platform/infra/helm/autogpt-server/values.prod.yaml b/autogpt_platform/infra/helm/autogpt-server/values.prod.yaml index ff1ffdc07058..4fbf020879bf 100644 --- a/autogpt_platform/infra/helm/autogpt-server/values.prod.yaml +++ b/autogpt_platform/infra/helm/autogpt-server/values.prod.yaml @@ -123,4 +123,5 @@ secrets: SUPABASE_JWT_SECRET: "AgAYMdZyP+UhxIdTx6qyRzq9xf1dT7S+DFEC8KSPEFydX9+hAdJVTpprOlgLnqSbfSDmbqcFnCH+aK/6rdRx3HI3v41FogyCNFFxTrfxq1Esk8VuaVh8XrO2xKPd4iGBPZaTrenKlgt89aGdjPJzgl+NlZ5+/BXd95P2uX39DDGr9GJdO14zBt69O+L+Yt7kdd3ZMBjWYibZAzf+YaNIx/M7jjzGLYvxtywMVTrR+6e6GkGQSt5CzBpgk1b6ugPVtFs7PqmMtUqXMQjlrW2u7WVZRWeXO93ukc/TtjO2XUY9JfrgibMf0H81NDDTAAQBNqaDk0LdXsPUo9QGnyeQZTsfAOaeM6lTxX9qCYjneN6pxe60U1BKLURpordRdBs3peAedNJ95GC75qcdSkZE2agjwJvXKs8yy2Ig5eiU/80W27IWPMSLWhMSSf4ixyfkNWM4EfWL45bXlVGvtYaeyqByb0QU1g+II3AukIyO1qOS572y0sGseEv/UlfU2NDBLFejeBZaz4s/20lSyLhP3v1Y9aTs8qWIGl67syFKZoCwPRxwip2v7wIDnlDYXtlxMpQUWDnSUX16zQiVALD3izeDYkd1RViBgdYT/G0tp6lBeV1vnF8tBEGWIl3GJFV0okUflAQ9NIrdC5+BlcQDD08Jn0oGjyje7KE/BfvB1lHT7K+h9rr8B/U8zBSaAe+KFjA8pcjHqXgi4Zx3ayTXdAddyFZd0YqONohEAvXB+BLLdYJVNNXjBFwY62XQ6ojD2ZYWz4m/Wo+/zG0Zm5s/v2VS8UT5qe2Wjs3oGHKIJc6Eo3hVwLefcb7V" SENTRY_DSN: "AgA+X2HkF9b3+p13JS4vG7VY+8p7su6qJ7smoPKqYh44Vpb7J5Eu9ksPNQkcFTDDPT8jAylqsHWUdI0A8u20+a4lqqGkmPN5tCgyBgAL1pIyvPUQjYUbL7A5lTQKlRLJJ+05h5XbkRU7cWR+G4yDUCDj2HcThne0CNDUbDao9D67ekSLUtp6/d0KO45Efao4MLuqISnypPUBGHmAdWGr2z/w7ItXjvUKt3RpH6pSCrGzjlKPKhenKdTsk/NX4Z+ew/JBbHiDQjKCdj0UlXFWH7Q4axaFy0T8tsqf/UN7n/QTalYE+v28isxrHvoR6h7kZETQV/gl0y7DdmTCi8/A1j1+e/9zUx6HvK+C/qGMsKMdNgaaVNSdfFp/yfMgXTUn4HGAls4gjVKSSRaIAbBq32NdKkIvRfocuAGsxInwbrDXLR0nzbHG/U/QhlvfL2gfqKRIVRJtEh99VW/KMMeXZUWR9dNt9gfTMtyzL7eta4oEV+g7sdO/9VjDn5wtic2/7eAxgA7wTEoDA8m0whpHH4VcPLHUfKLTHnRXVu6bykAfBgfEKhJBS8DghvPyu73qL5MREuYkGya4n0RQ73h5ja7mYwI0lsefQszP9Fz1lR+757dhJ6+/E7nNnOE/ShD/8xE0V54pd2IvrRoJmcOsIOZ5w+xWfmN8OyLn7wuEpqEuMHEoisLF9RSp2V5iKbB+fFB4o5P1/VqkNPEFBe0jA4K8DAGX+VdChMpjAI47wF22aj+jmTRf+EY+5l+aEvjyU0G7oUPVzzG8rYa6p+v56zeVsmU4SHIDO75J1cH7tnYDeOxk9fAYZgNplS4gKHVT0w==" SUPABASE_SERVICE_ROLE_KEY: "AgCADpjXfdpTDruyK2F4GRNT/Kl+yaI87mQdXDHQo3jOC3gWoOiRlXg70JG3jIi2cWjAXwU8ySjpT87aJdRwsMToeMD78zr0FbOSB2abx7OPTij8zWFSzhIo4cLoEkvLxZO9HXwQc959Cxh5oBcn6WBhJ5XMUxNWALIem9+Lb5Eu1CwxSF0EDrl3znx3Iqw/zUqnAgS+Ob4AAiJwXNO641ja7dAKYkb2NJ/KCBgmSXAaPfxQByuNkGP4iwmQuxhhJQ/N+LRVCu03J6NLPVw22feKKtZxAAroMDn5wPhRdmzBawqbRsejiCb0JNL2yd574CDN5xzsDur/RYkCpTrMWzgnN3F1VcYMuB9FwYazKU3XqviOYtP8Ca4sUQChHQEOFP8n3Nt0Z17zo1NtgRt8IBpXpDeZFgDZU6Zy8EtpHHn05KT8YqyLDms2LfJhduiuyndbZgeIfr7IcxbU4aBafh+J/tfN7Tlj5NFYxFImKQ0NFg5z6W9zKKkfFMo9WUcOOXgwg8+g6xeZUX9g3rNpMBNf2bt0UfNqSIBeAmUZVKHuEqneFONbgtqOP2NKsKsSfvCsnpKgAndv+eL627qWAuDzywWuoAcxsF/Kvo/fQnv1a+7abCr1Qhf61u3DBriGp6TAVhQ9z7iGqvkuviELt97NKekeevCgdjwWpk78iKBCmxJobBTErdX2Xhrqfc5AHteoUBYv3TS7N8ZcOmfVmZc2ulgLLQZZ5hK30w9FFu28bu01ArfKcSp3U21keaC/cGHBNdUWgAbg3wIH+3y3vU2MRHI6T6sFrRsNgJH6b8S+HcOInTsoaLFiRv7SYxGYliV47AEukv4G2G+9XO4i4y9P90u5i7KM+J5FRlR6sfiISPozGHUBe9EAKYQcqaSSGP7FWsyNl6DGq/pDkG8IJYqNr21Sl9N1cdhK/Hdd4J80q05A9f3AyzHjtU4YVcvz4TCKr1FJLugBUsz120cA8FxGXweIQRWCzGvSeGA=" - REDIS_PASSWORD: "AgB7eiUuFQO88vVMI28xfmJsA2QzEb71r3NyDJ/KTNsjqn7ai1KpjVaaTDyr4Xzo1wOhwwwxlhIoeBwf26wPiraJtkjRU9z9Aotvy0u8SXFm05ObhMjJoY2dBvW6ga3KNaunWoTx5e6NbYPGRIgNtRBVN4PH5Lf7Ou5SZBjJBaVWgIT1x71tB2eD2XksOw2mrfaF0WODsQxXDOaF9BJ4Gn7yIT0Nh76Okn9uhesQxvojaqlAIeAKXyrZJwAH5qL3D772rYsISmbHC0bCBgx4dbbtvsr4YgiR387ri7KGfrEqoFH/jzUp5cwsJNyBpWG1n2O0QXYgbMIsmJP6rdD+KTZkLGBz0wgq/JySCZM9hj54dYtLE7LMmpZn7//EKZk7zsV1u9oSciQisWcJqW8El+IMOAZilqSR2NjpI4cb0xR7/gTLLQF33+wnZwbbHghbDwTowkzOZ0i7qt73YkR8MKrlLhLcCGHjhyb50xr1DJl9mVUoyHXvFOj2tQO/273sMNdKpJvNFi9EEhdirzbcuphnaRm5xXYF1CHKtXUp6EvdxgHqEuoGwh5Kt8dtGMJfSJ40LsARZXCFU7CC6g/faPq93K5QB/bwlOdABeOVF/odqXZQAADX3TQwIPMH36XuqwNggWQ8Igy5o1d3Hi84jVChmjid/Wk8DREmkntzDy+4Jxzqx1rPSThyoOvopirY8VA=" \ No newline at end of file + REDIS_PASSWORD: "AgB7eiUuFQO88vVMI28xfmJsA2QzEb71r3NyDJ/KTNsjqn7ai1KpjVaaTDyr4Xzo1wOhwwwxlhIoeBwf26wPiraJtkjRU9z9Aotvy0u8SXFm05ObhMjJoY2dBvW6ga3KNaunWoTx5e6NbYPGRIgNtRBVN4PH5Lf7Ou5SZBjJBaVWgIT1x71tB2eD2XksOw2mrfaF0WODsQxXDOaF9BJ4Gn7yIT0Nh76Okn9uhesQxvojaqlAIeAKXyrZJwAH5qL3D772rYsISmbHC0bCBgx4dbbtvsr4YgiR387ri7KGfrEqoFH/jzUp5cwsJNyBpWG1n2O0QXYgbMIsmJP6rdD+KTZkLGBz0wgq/JySCZM9hj54dYtLE7LMmpZn7//EKZk7zsV1u9oSciQisWcJqW8El+IMOAZilqSR2NjpI4cb0xR7/gTLLQF33+wnZwbbHghbDwTowkzOZ0i7qt73YkR8MKrlLhLcCGHjhyb50xr1DJl9mVUoyHXvFOj2tQO/273sMNdKpJvNFi9EEhdirzbcuphnaRm5xXYF1CHKtXUp6EvdxgHqEuoGwh5Kt8dtGMJfSJ40LsARZXCFU7CC6g/faPq93K5QB/bwlOdABeOVF/odqXZQAADX3TQwIPMH36XuqwNggWQ8Igy5o1d3Hi84jVChmjid/Wk8DREmkntzDy+4Jxzqx1rPSThyoOvopirY8VA=" + ENCRYPTION_KEY: "AgCmQ6fVBbbkm884bZyenUPhrbVBb3+sjOFwekFeeptorwVNDTqfOXtoWzl+W+t13tCqHDn8EqEgOGUPSJutAxfiyKcPo9IFDaMskUzaTGUSx/XUmQzHrI9tKP6doSk8V1Vmwm5PRk1elpMP90aG+TtjG1BLFU8JIozEFEvqcmdB9apnXacBV26vP2Gk709DqAYaCkGpXGfoCKDWNjrF/68W8a6UOaRC/+qHuoWXwi03rY16RdPIRRryHICrpp+l6zJToZFHboSV3UpENjJ4tWUW2Qd8pzE5+ZGiZaHcmqp0WtyBmN8d+7m1q2t5RjJ7DTuVmr+XgS5Eb28aHshZK4gdzlqEsYZSMSwxzTgKrDIPCxPqz+TDPA31fQyR1FfwNxgHztXGluUGPAVxtYVij/CGAkQpISHIjR4FR776lMFkddBaVQfbIyXQhnhEeaV9Swxr1EXG9/Nj6q3n13WmCsMVUZYTk147UCeaTdV6Ec4DRDoTVk4uVgGNsjgRS0wLdnW6naPKndOEz8XpjRtnPz1xNeyLW8SF9DAAoOV+zOrfzbzFaFfXozcExFs2OIDa+D9z57j0wi1Nh8qUoFIFVSjIJ5rOztDlDEd/15P5Zvfm5t/GIJNsIUoCEepirqEnQMqs4/J2ZkhL77qPk4QuvY6AMAvx8pOIh/4Z0Fwf2vJtKhN1lNvq9i7NFoiuiaWSzH/LWcsX8U2ibZ+Do5OspJuFIsP6E1v1Mm9hQdmbEp4XRpb4eBvy2L1a5K9vVw==" \ No newline at end of file