feat(backend): credential swap to api keys in cred store (#8403)

* feat(backend): credential swap

* ci: formatting

* fix: importing is hard okay

* fix: spelln' is hard

* feat: better credential provider handling

* docs: update the imports locations

* fix: test credentials + formatting

* feat: drop continuous read mode

* fix: lint

* feat: fallback credentials

* feat: charge for credential useage and have a bad backup mechnism

* fix: don't save default credentials + add d_id

* fix: formatting

* feat: basic encryption/decryption

* ref: move files around

* ref: sign all blocks out of their credentials

* ref: update target to match a new, and encrypted future

* wip: llm provider merger

* don't delete `credentials` input on nodes

* fix llm block ci issues

* updated get AICredentials

* fix fix

* insert migration to move integration credentials from `auth.user` metadata to `platform.User.metadata`

* fixed migration

* add migration for existing user integration credentials

* disabled reddit and email block

* fix credential handling in LLM blocks

* add other secret fields to credential scrubber migration

* add other secret fields to credential scrubber migration (vol. 2)

* fix: pr fixes

* fix: mock funciton

* add encrypted values

---------

Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
Co-authored-by: SwiftyOS <craigswift13@gmail.com>
Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com>
Co-authored-by: Aarushi <aarushik93@gmail.com>
This commit is contained in:
Nicholas Tindle
2024-10-30 21:36:38 -05:00
committed by GitHub
parent 26caf1c6af
commit 3aebed61d2
34 changed files with 1269 additions and 341 deletions

View File

@@ -2,6 +2,8 @@ import secrets
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.cache import thread_cached
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
)
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
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 @@ class SupabaseIntegrationCredentialsStore:
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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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 @@ logger = logging.getLogger(__name__)
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 @@ class AIShortformVideoCreatorBlock(Block):
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 @@ class AIShortformVideoCreatorBlock(Block):
"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 @@ class AIShortformVideoCreatorBlock(Block):
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 @@ class AIShortformVideoCreatorBlock(Block):
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 @@ class AIShortformVideoCreatorBlock(Block):
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 @@ class AIShortformVideoCreatorBlock(Block):
}
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 @@ class AIShortformVideoCreatorBlock(Block):
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

View File

@@ -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 @@ class ReadDiscordMessagesBlock(Block):
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 @@ class ReadDiscordMessagesBlock(Block):
},
)
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 @@ class ReadDiscordMessagesBlock(Block):
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 @@ class ReadDiscordMessagesBlock(Block):
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 @@ class SendDiscordMessageBlock(Block):
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 @@ class SendDiscordMessageBlock(Block):
"""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,
)

View File

@@ -43,6 +43,7 @@ class SendEmailBlock(Block):
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},

View File

@@ -79,12 +79,32 @@ class GmailReadBlock(Block):
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 @@ class GmailReadBlock(Block):
"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"},

View File

@@ -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 @@ class GoogleMapsSearchBlock(Block):
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 @@ class GoogleMapsSearchBlock(Block):
}
]
},
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 @@ class GoogleMapsSearchBlock(Block):
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):

View File

@@ -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 @@ class IdeogramModelBlock(Block):
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 @@ class IdeogramModelBlock(Block):
"style_type": StyleType.AUTO,
"negative_prompt": None,
"color_palette_name": ColorPalettePreset.NONE,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_output=[
(
@@ -153,14 +173,17 @@ class IdeogramModelBlock(Block):
"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 @@ class IdeogramModelBlock(Block):
# 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 @@ class IdeogramModelBlock(Block):
def run_model(
self,
api_key: str,
api_key: SecretStr,
model_name: str,
prompt: str,
seed: Optional[int],
@@ -193,7 +216,10 @@ class IdeogramModelBlock(Block):
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 @@ class IdeogramModelBlock(Block):
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:

View File

@@ -3,7 +3,10 @@ import logging
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 @@ import openai
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 AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
@staticmethod
def llm_call(
api_key: str,
credentials: APIKeyCredentials,
llm_model: LlmModel,
prompt: list[dict],
json_format: bool,
@@ -234,7 +262,7 @@ class AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
# 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 @@ class AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
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 @@ class AIStructuredResponseGeneratorBlock(Block):
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 AITextGeneratorBlock(Block):
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 @@ class AITextGeneratorBlock(Block):
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 AITextSummarizerBlock(Block):
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 @@ class AITextSummarizerBlock(Block):
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 @@ class AITextSummarizerBlock(Block):
},
)
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 @@ class AITextSummarizerBlock(Block):
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 @@ class AITextSummarizerBlock(Block):
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 @@ class AITextSummarizerBlock(Block):
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 AIConversationBlock(Block):
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 @@ class AIConversationBlock(Block):
{"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 @@ class AIConversationBlock(Block):
},
)
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 AIListGeneratorBlock(Block):
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 @@ class AIListGeneratorBlock(Block):
"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 @@ class AIListGeneratorBlock(Block):
("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 @@ class AIListGeneratorBlock(Block):
@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 @@ class AIListGeneratorBlock(Block):
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 @@ class AIListGeneratorBlock(Block):
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}")

View File

@@ -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 PublishToMediumBlock(Block):
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 @@ class PublishToMediumBlock(Block):
"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 @@ class PublishToMediumBlock(Block):
}
}
},
test_credentials=TEST_CREDENTIALS,
)
def create_post(
self,
api_key,
api_key: SecretStr,
author_id,
title,
content,
@@ -120,7 +145,7 @@ class PublishToMediumBlock(Block):
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 @@ class PublishToMediumBlock(Block):
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,

View File

@@ -70,6 +70,7 @@ class GetRedditPostsBlock(Block):
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},

View File

@@ -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 @@ class ReplicateFluxAdvancedModelBlock(Block):
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 @@ class ReplicateFluxAdvancedModelBlock(Block):
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 @@ class ReplicateFluxAdvancedModelBlock(Block):
# 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 @@ class ReplicateFluxAdvancedModelBlock(Block):
def run_model(
self,
api_key,
api_key: SecretStr,
model_name,
prompt,
seed,
@@ -170,7 +194,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
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(

View File

@@ -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 @@ class ExtractWebsiteContentBlock(Block, GetRequest):
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 @@ class GetWeatherInformationBlock(Block, GetRequest):
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 @@ class GetWeatherInformationBlock(Block, GetRequest):
"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)

View File

@@ -2,15 +2,36 @@ import time
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 @@ class CreateTalkingAvatarVideoBlock(Block):
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 @@ class CreateTalkingAvatarVideoBlock(Block):
"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 @@ class CreateTalkingAvatarVideoBlock(Block):
"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

View File

@@ -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 UnrealTextToSpeechBlock(Block):
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 @@ class UnrealTextToSpeechBlock(Block):
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 @@ class UnrealTextToSpeechBlock(Block):
"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 @@ class UnrealTextToSpeechBlock(Block):
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,
)

View File

@@ -4,11 +4,22 @@ from enum import Enum
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 @@ from backend.blocks.llm import (
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 @@ class BlockCost(BaseModel):
)
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 @@ BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = {
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,
}
},
)
],
}

View File

@@ -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)},
)

View File

@@ -16,7 +16,12 @@ from backend.data.execution import (
)
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 @@ R = TypeVar("R")
class DatabaseManager(AppService):
def __init__(self):
super().__init__()
self.use_db = True
@@ -79,6 +83,8 @@ class DatabaseManager(AppService):
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)

View File

@@ -19,7 +19,7 @@ from backend.data import execution as execution_db
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 @@ class AgentServer(AppService):
async def lifespan(self, _: FastAPI):
await db.connect()
await block.initialize_blocks()
await migrate_and_encrypt_user_integrations()
yield
await db.disconnect()

View File

@@ -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())

View File

@@ -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")

View File

@@ -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 $$;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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"

View File

@@ -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"

View File

@@ -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[]

View File

@@ -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[]

View File

@@ -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<string, React.FC<{ className?: string }>> = {
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]

View File

@@ -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";
// Get keys from CredentialsProviderName type
const CREDENTIALS_PROVIDER_NAMES = Object.values(
PROVIDER_NAMES,
) as CredentialsProviderName[];
// --8<-- [start:CredentialsProviderNames]
const CREDENTIALS_PROVIDER_NAMES = [
"github",
"google",
"notion",
"jina",
"pinecone",
] as const;
export type CredentialsProviderName =
(typeof CREDENTIALS_PROVIDER_NAMES)[number];
const providerDisplayNames: Record<CredentialsProviderName, string> = {
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: [] },
);
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),
},
}));
});
});
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]);

View File

@@ -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<CredentialsType>;
};
// --8<-- [end:BlockIOCredentialsSubSchema]
export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
type: "null";

View File

@@ -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="
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=="

View File

@@ -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="
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=="