Merge remote-tracking branch 'origin/zamilmajdy/fix-connection-consumption-agent-executor-block' into toran/open-2057-mailerlite-blocks

This commit is contained in:
Toran Bruce Richards
2024-11-19 10:59:46 +00:00
32 changed files with 317 additions and 131 deletions

View File

@@ -1,7 +1,8 @@
import fastapi
from .middleware import auth_middleware
from .models import User
from .models import User, DEFAULT_USER_ID, DEFAULT_EMAIL
from .config import Settings
def requires_user(payload: dict = fastapi.Depends(auth_middleware)) -> User:
@@ -16,8 +17,12 @@ def requires_admin_user(
def verify_user(payload: dict | None, admin_only: bool) -> User:
if not payload:
if Settings.ENABLE_AUTH:
raise fastapi.HTTPException(
status_code=401, detail="Authorization header is missing"
)
# This handles the case when authentication is disabled
payload = {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
payload = {"sub": DEFAULT_USER_ID, "role": "admin"}
user_id = payload.get("sub")

View File

@@ -1,5 +1,8 @@
from dataclasses import dataclass
DEFAULT_USER_ID = "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
DEFAULT_EMAIL = "default@example.com"
# Using dataclass here to avoid adding dependency on pydantic
@dataclass(frozen=True)

View File

@@ -79,7 +79,7 @@ jina_credentials = APIKeyCredentials(
title="Use Credits for Jina",
expires_at=None,
)
unreal_credentials = APIKeyCredentials(#
unreal_credentials = APIKeyCredentials(
id="66f20754-1b81-48e4-91d0-f4f0dd82145f",
provider="unreal",
api_key=SecretStr(settings.secrets.unreal_speech_api_key),
@@ -93,6 +93,14 @@ mailerlite_credentials = APIKeyCredentials(
title="Use Credits for MailerLite",
expires_at=None,
)
open_router_credentials = APIKeyCredentials(
id="b5a0e27d-0c98-4df3-a4b9-10193e1f3c40",
provider="open_router",
api_key=SecretStr(settings.secrets.open_router_api_key),
title="Use Credits for Open Router",
expires_at=None,
)
DEFAULT_CREDENTIALS = [
revid_credentials,
@@ -105,6 +113,7 @@ DEFAULT_CREDENTIALS = [
jina_credentials,
unreal_credentials,
mailerlite_credentials,
open_router_credentials,
]
@@ -154,6 +163,8 @@ class SupabaseIntegrationCredentialsStore:
all_credentials.append(unreal_credentials)
if settings.secrets.mailerlite_api_key:
all_credentials.append(mailerlite_credentials)
if settings.secrets.open_router_api_key:
all_credentials.append(open_router_credentials)
return all_credentials
def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None:

View File

@@ -1209,13 +1209,13 @@ yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pyjwt"
version = "2.9.0"
version = "2.10.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
{file = "PyJWT-2.10.0-py3-none-any.whl", hash = "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15"},
{file = "pyjwt-2.10.0.tar.gz", hash = "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c"},
]
[package.extras]
@@ -1324,29 +1324,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.7.3"
version = "0.7.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
{file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
{file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
{file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
{file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
{file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
{file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
{file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
{file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
{file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"},
{file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"},
{file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"},
{file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"},
{file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"},
{file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"},
{file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"},
{file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"},
{file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"},
{file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"},
{file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"},
{file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"},
]
[[package]]
@@ -1750,4 +1750,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "406a67ad0e7a03e7fa8bc6fb59a071c3a4b2a1ac9eb54a8b8d2eff09b26fe527"
content-hash = "48184ad1281689c7743b8ca23135a647dc52257d54702d88b043fe31fe27ff27"

View File

@@ -12,14 +12,14 @@ expiringdict = "^1.2.2"
google-cloud-logging = "^3.11.3"
pydantic = "^2.9.2"
pydantic-settings = "^2.6.1"
pyjwt = "^2.8.0"
pyjwt = "^2.10.0"
python = ">=3.10,<4.0"
python-dotenv = "^1.0.1"
supabase = "^2.10.0"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.0"
ruff = "^0.7.3"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core"]

View File

@@ -57,6 +57,7 @@ GOOGLE_CLIENT_SECRET=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
OPEN_ROUTER_API_KEY=
# Reddit
REDDIT_CLIENT_ID=

View File

@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
# "ollama": BlockSecret(value=""),
# }
LLMProviderName = Literal["anthropic", "groq", "openai", "ollama"]
LLMProviderName = Literal["anthropic", "groq", "openai", "ollama", "open_router"]
AICredentials = CredentialsMetaInput[LLMProviderName, Literal["api_key"]]
TEST_CREDENTIALS = APIKeyCredentials(
@@ -51,7 +51,7 @@ TEST_CREDENTIALS_INPUT = {
def AICredentialsField() -> AICredentials:
return CredentialsField(
description="API key for the LLM provider.",
provider=["anthropic", "groq", "openai", "ollama"],
provider=["anthropic", "groq", "openai", "ollama", "open_router"],
supported_credential_types={"api_key"},
discriminator="model",
discriminator_mapping={
@@ -108,6 +108,18 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
# Ollama models
OLLAMA_LLAMA3_8B = "llama3"
OLLAMA_LLAMA3_405B = "llama3.1:405b"
# OpenRouter models
GEMINI_FLASH_1_5_8B = "google/gemini-flash-1.5"
GEMINI_FLASH_1_5_EXP = "google/gemini-flash-1.5-exp"
GROK_BETA = "x-ai/grok-beta"
MISTRAL_NEMO = "mistralai/mistral-nemo"
COHERE_COMMAND_R_08_2024 = "cohere/command-r-08-2024"
COHERE_COMMAND_R_PLUS_08_2024 = "cohere/command-r-plus-08-2024"
EVA_QWEN_2_5_32B = "eva-unit-01/eva-qwen-2.5-32b"
DEEPSEEK_CHAT = "deepseek/deepseek-chat"
PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = (
"perplexity/llama-3.1-sonar-large-128k-online"
)
@property
def metadata(self) -> ModelMetadata:
@@ -142,6 +154,17 @@ MODEL_METADATA = {
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192),
LlmModel.GEMINI_FLASH_1_5_8B: ModelMetadata("open_router", 8192),
LlmModel.GEMINI_FLASH_1_5_EXP: ModelMetadata("open_router", 8192),
LlmModel.GROK_BETA: ModelMetadata("open_router", 8192),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 4000),
LlmModel.EVA_QWEN_2_5_32B: ModelMetadata("open_router", 4000),
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 8192),
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata(
"open_router", 8192
),
}
for model in LlmModel:
@@ -354,6 +377,34 @@ class AIStructuredResponseGeneratorBlock(Block):
response.get("prompt_eval_count") or 0,
response.get("eval_count") or 0,
)
elif provider == "open_router":
client = openai.OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=credentials.api_key.get_secret_value(),
)
response = client.chat.completions.create(
extra_headers={
"HTTP-Referer": "https://agpt.co",
"X-Title": "AutoGPT",
},
model=llm_model.value,
messages=prompt, # type: ignore
max_tokens=max_tokens,
)
# If there's no response, raise an error
if not response.choices:
if response:
raise ValueError(f"OpenRouter error: {response}")
else:
raise ValueError("No response from OpenRouter.")
return (
response.choices[0].message.content or "",
response.usage.prompt_tokens if response.usage else 0,
response.usage.completion_tokens if response.usage else 0,
)
else:
raise ValueError(f"Unsupported LLM provider: {provider}")

View File

@@ -94,15 +94,7 @@ class BlockSchema(BaseModel):
@classmethod
def validate_data(cls, data: BlockInput) -> str | None:
"""
Validate the data against the schema.
Returns the validation error message if the data does not match the schema.
"""
try:
jsonschema.validate(data, cls.jsonschema())
return None
except jsonschema.ValidationError as e:
return str(e)
return json.validate_jsonschema(schema=cls.jsonschema(), data=data)
@classmethod
def validate_field(cls, field_name: str, data: BlockInput) -> str | None:

View File

@@ -6,6 +6,7 @@ from autogpt_libs.supabase_integration_credentials_store.store import (
groq_credentials,
ideogram_credentials,
jina_credentials,
open_router_credentials,
openai_credentials,
replicate_credentials,
revid_credentials,
@@ -54,6 +55,15 @@ MODEL_COST: dict[LlmModel, int] = {
LlmModel.LLAMA3_1_8B: 1,
LlmModel.OLLAMA_LLAMA3_8B: 1,
LlmModel.OLLAMA_LLAMA3_405B: 1,
LlmModel.GEMINI_FLASH_1_5_8B: 1,
LlmModel.GEMINI_FLASH_1_5_EXP: 1,
LlmModel.GROK_BETA: 5,
LlmModel.MISTRAL_NEMO: 1,
LlmModel.COHERE_COMMAND_R_08_2024: 1,
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3,
LlmModel.EVA_QWEN_2_5_32B: 1,
LlmModel.DEEPSEEK_CHAT: 2,
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: 1,
}
for model in LlmModel:
@@ -124,6 +134,23 @@ LLM_COST = (
cost_filter={"api_key": None},
),
]
# Open Router Models
+ [
BlockCost(
cost_type=BlockCostType.RUN,
cost_filter={
"model": model,
"credentials": {
"id": open_router_credentials.id,
"provider": open_router_credentials.provider,
"type": open_router_credentials.type,
},
},
cost_amount=cost,
)
for model, cost in MODEL_COST.items()
if MODEL_METADATA[model].provider == "open_router"
]
)
# =============== This is the exhaustive list of cost for each Block =============== #

View File

@@ -1,6 +1,7 @@
import logging
from typing import Optional, cast
from autogpt_libs.auth.models import DEFAULT_USER_ID
from autogpt_libs.supabase_integration_credentials_store.types import (
UserIntegrations,
UserMetadata,
@@ -15,9 +16,6 @@ from backend.util.encryption import JSONCryptor
logger = logging.getLogger(__name__)
DEFAULT_USER_ID = "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
DEFAULT_EMAIL = "default@example.com"
async def get_or_create_user(user_data: dict) -> User:
user_id = user_data.get("sub")

View File

@@ -18,6 +18,7 @@ if TYPE_CHECKING:
from autogpt_libs.utils.cache import thread_cached
from backend.blocks.agent import AgentExecutorBlock
from backend.data import redis
from backend.data.block import Block, BlockData, BlockInput, BlockType, get_block
from backend.data.execution import (
@@ -135,7 +136,6 @@ def execute_node(
logger.error(f"Block {node.block_id} not found.")
return
# Sanity check: validate the execution input.
log_metadata = LogMetadata(
user_id=user_id,
graph_eid=graph_exec_id,
@@ -144,11 +144,20 @@ def execute_node(
node_id=node_id,
block_name=node_block.name,
)
# Sanity check: validate the execution input.
input_data, error = validate_exec(node, data.data, resolve_input=False)
if input_data is None:
log_metadata.error(f"Skip execution, input validation error: {error}")
db_client.upsert_execution_output(node_exec_id, "error", error)
update_execution(ExecutionStatus.FAILED)
return
# Re-shape the input data for agent block.
# AgentExecutorBlock specially separate the node input_data & its input_default.
if isinstance(node_block, AgentExecutorBlock):
input_data = {**node.input_default, "data": input_data}
# Execute the node
input_data_str = json.dumps(input_data)
input_size = len(input_data_str)
@@ -376,31 +385,46 @@ def validate_exec(
if not node_block:
return None, f"Block for {node.block_id} not found."
error_prefix = f"Input data missing for {node_block.name}:"
if isinstance(node_block, AgentExecutorBlock):
# Validate the execution metadata for the agent executor block.
try:
exec_data = AgentExecutorBlock.Input(**node.input_default)
except Exception as e:
return None, f"Input data doesn't match {node_block.name}: {str(e)}"
# Validation input
input_schema = exec_data.input_schema
required_fields = set(input_schema["required"])
input_default = exec_data.data
else:
# Convert non-matching data types to the expected input schema.
for name, data_type in node_block.input_schema.__annotations__.items():
if (value := data.get(name)) and (type(value) is not data_type):
data[name] = convert(value, data_type)
# Validation input
input_schema = node_block.input_schema.jsonschema()
required_fields = node_block.input_schema.get_required_fields()
input_default = node.input_default
# Input data (without default values) should contain all required fields.
error_prefix = f"Input data missing or mismatch for `{node_block.name}`:"
input_fields_from_nodes = {link.sink_name for link in node.input_links}
if not input_fields_from_nodes.issubset(data):
return None, f"{error_prefix} {input_fields_from_nodes - set(data)}"
# Merge input data with default values and resolve dynamic dict/list/object pins.
data = {**node.input_default, **data}
data = {**input_default, **data}
if resolve_input:
data = merge_execution_input(data)
# Input data post-merge should contain all required fields from the schema.
input_fields_from_schema = node_block.input_schema.get_required_fields()
if not input_fields_from_schema.issubset(data):
return None, f"{error_prefix} {input_fields_from_schema - set(data)}"
# Convert non-matching data types to the expected input schema.
for name, data_type in node_block.input_schema.__annotations__.items():
if (value := data.get(name)) and (type(value) is not data_type):
data[name] = convert(value, data_type)
if not required_fields.issubset(data):
return None, f"{error_prefix} {required_fields - set(data)}"
# Last validation: Validate the input values against the schema.
if error := node_block.input_schema.validate_data(data):
error_message = f"Input data doesn't match {node_block.name}: {error}"
if error := json.validate_jsonschema(schema=input_schema, data=data):
error_message = f"{error_prefix} {error}"
logger.error(error_message)
return None, error_message

View File

@@ -1,5 +1,5 @@
import enum
import typing
from typing import Any, List, Optional, Union
import pydantic
@@ -12,11 +12,12 @@ class Methods(enum.Enum):
UNSUBSCRIBE = "unsubscribe"
EXECUTION_EVENT = "execution_event"
ERROR = "error"
HEARTBEAT = "heartbeat"
class WsMessage(pydantic.BaseModel):
method: Methods
data: typing.Dict[str, typing.Any] | list[typing.Any] | None = None
data: Optional[Union[dict[str, Any], list[Any], str]] = None
success: bool | None = None
channel: str | None = None
error: str | None = None
@@ -40,8 +41,8 @@ class CreateGraph(pydantic.BaseModel):
class CreateAPIKeyRequest(pydantic.BaseModel):
name: str
permissions: typing.List[APIKeyPermission]
description: typing.Optional[str] = None
permissions: List[APIKeyPermission]
description: Optional[str] = None
class CreateAPIKeyResponse(pydantic.BaseModel):
@@ -54,4 +55,4 @@ class SetGraphActiveVersion(pydantic.BaseModel):
class UpdatePermissionsRequest(pydantic.BaseModel):
permissions: typing.List[APIKeyPermission]
permissions: List[APIKeyPermission]

View File

@@ -124,7 +124,8 @@ def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput
async def get_user_credits(
user_id: Annotated[str, Depends(get_user_id)]
) -> dict[str, int]:
return {"credits": await _user_credit_model.get_or_refill_credit(user_id)}
# Credits can go negative, so ensure it's at least 0 for user to see.
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}
########################################################

View File

@@ -1,18 +1,11 @@
from autogpt_libs.auth.middleware import auth_middleware
from fastapi import Depends, HTTPException
from autogpt_libs.auth.depends import requires_user
from autogpt_libs.auth.models import User
from fastapi import Depends
from backend.data.user import DEFAULT_USER_ID
from backend.util.settings import Settings
settings = Settings()
def get_user_id(payload: dict = Depends(auth_middleware)) -> str:
if not payload:
# This handles the case when authentication is disabled
return DEFAULT_USER_ID
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found in token")
return user_id
def get_user_id(user: User = Depends(requires_user)) -> str:
return user.user_id

View File

@@ -53,25 +53,25 @@ async def event_broadcaster(manager: ConnectionManager):
async def authenticate_websocket(websocket: WebSocket) -> str:
if settings.config.enable_auth:
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=4001, reason="Missing authentication token")
return ""
try:
payload = parse_jwt_token(token)
user_id = payload.get("sub")
if not user_id:
await websocket.close(code=4002, reason="Invalid token")
return ""
return user_id
except ValueError:
await websocket.close(code=4003, reason="Invalid token")
return ""
else:
if not settings.config.enable_auth:
return DEFAULT_USER_ID
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=4001, reason="Missing authentication token")
return ""
try:
payload = parse_jwt_token(token)
user_id = payload.get("sub")
if not user_id:
await websocket.close(code=4002, reason="Invalid token")
return ""
return user_id
except ValueError:
await websocket.close(code=4003, reason="Invalid token")
return ""
async def handle_subscribe(
websocket: WebSocket, manager: ConnectionManager, message: WsMessage
@@ -138,6 +138,13 @@ async def websocket_router(
while True:
data = await websocket.receive_text()
message = WsMessage.model_validate_json(data)
if message.method == Methods.HEARTBEAT:
await websocket.send_json(
{"method": Methods.HEARTBEAT.value, "data": "pong", "success": True}
)
continue
if message.method == Methods.SUBSCRIBE:
await handle_subscribe(websocket, manager, message)

View File

@@ -3,14 +3,6 @@ import pathlib
import sys
def get_secrets_path() -> pathlib.Path:
return get_data_path() / "secrets"
def get_config_path() -> pathlib.Path:
return get_data_path()
def get_frontend_path() -> pathlib.Path:
if getattr(sys, "frozen", False):
# The application is frozen

View File

@@ -1,6 +1,7 @@
import json
from typing import Any, Type, TypeVar, overload
import jsonschema
from fastapi.encoders import jsonable_encoder
from .type import type_match
@@ -30,3 +31,15 @@ def loads(data: str, *args, target_type: Type[T] | None = None, **kwargs) -> Any
if target_type:
return type_match(parsed, target_type)
return parsed
def validate_jsonschema(schema: dict[str, Any], data: dict[str, Any]) -> str | None:
"""
Validate the data against the schema.
Returns the validation error message if the data does not match the schema.
"""
try:
jsonschema.validate(data, schema)
return None
except jsonschema.ValidationError as e:
return str(e)

View File

@@ -11,7 +11,7 @@ from pydantic_settings import (
SettingsConfigDict,
)
from backend.util.data import get_data_path, get_secrets_path
from backend.util.data import get_data_path
T = TypeVar("T", bound=BaseSettings)
@@ -238,6 +238,7 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
openai_api_key: str = Field(default="", description="OpenAI API key")
anthropic_api_key: str = Field(default="", description="Anthropic API key")
groq_api_key: str = Field(default="", description="Groq API key")
open_router_api_key: str = Field(default="", description="Open Router API Key")
reddit_client_id: str = Field(default="", description="Reddit client ID")
reddit_client_secret: str = Field(default="", description="Reddit client secret")
@@ -273,7 +274,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
# Add more secret fields as needed
model_config = SettingsConfigDict(
secrets_dir=get_secrets_path(),
env_file=".env",
env_file_encoding="utf-8",
extra="allow",
@@ -300,11 +300,3 @@ class Settings(BaseModel):
with open(config_path, "w") as f:
json.dump(config_to_save, f, indent=2)
self.config.clear_updates()
# Save updated secrets to individual files
secrets_dir = get_secrets_path()
for key in self.secrets.updated_fields:
secret_file = os.path.join(secrets_dir, key)
with open(secret_file, "w") as f:
f.write(str(getattr(self.secrets, key)))
self.secrets.clear_updates()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "autogpt-platform-backend"
version = "0.3.0"
version = "0.3.1"
description = "A platform for building AI-powered agentic workflows"
authors = ["AutoGPT <info@agpt.co>"]
readme = "README.md"

View File

@@ -2,7 +2,7 @@
FROM node:21-alpine AS base
WORKDIR /app
COPY autogpt_platform/frontend/package.json autogpt_platform/frontend/yarn.lock ./
RUN yarn install --frozen-lockfile
RUN --mount=type=cache,target=/usr/local/share/.cache yarn install --frozen-lockfile
# Dev stage
FROM base AS dev
@@ -16,17 +16,23 @@ FROM base AS build
COPY autogpt_platform/frontend/ .
RUN yarn build
# Prod stage
# Prod stage - based on NextJS reference Dockerfile https://github.com/vercel/next.js/blob/64271354533ed16da51be5dce85f0dbd15f17517/examples/with-docker/Dockerfile
FROM node:21-alpine AS prod
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/package.json /app/yarn.lock ./
RUN yarn install --frozen-lockfile
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/next.config.mjs ./next.config.mjs
USER nextjs
EXPOSE 3000
CMD ["yarn", "start"]
CMD ["node", "server.js"]

View File

@@ -14,6 +14,7 @@ const nextConfig = {
},
];
},
output: "standalone",
// TODO: Re-enable TypeScript checks once current issues are resolved
typescript: {
ignoreBuildErrors: true,

View File

@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -73,6 +73,7 @@ export default function PrivatePage() {
"7f7b0654-c36b-4565-8fa7-9a52575dfae2", // D-ID
"7f26de70-ba0d-494e-ba76-238e65e7b45f", // Jina
"66f20754-1b81-48e4-91d0-f4f0dd82145f", // Unreal Speech
"b5a0e27d-0c98-4df3-a4b9-10193e1f3c40", // Open Router
],
[],
);

View File

@@ -145,7 +145,7 @@ export const AgentImportForm: React.FC<
);
if (
!["name", "description", "nodes", "links"].every(
(key) => !!obj[key],
(key) => key in obj && obj[key] != null,
)
) {
throw new Error(

View File

@@ -205,7 +205,10 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
{beautifyString(block.name).replace(/ Block$/, "")}
</span>
<span className="block break-words text-xs font-normal text-gray-500">
{block.description}
{/* Cap description at 100 characters max */}
{block.description?.length > 100
? block.description.slice(0, 100) + "..."
: block.description}
</span>
</div>
<div

View File

@@ -60,6 +60,7 @@ export const providerIcons: Record<
ollama: fallbackIcon,
openai: fallbackIcon,
openweathermap: fallbackIcon,
open_router: fallbackIcon,
pinecone: fallbackIcon,
replicate: fallbackIcon,
revid: fallbackIcon,

View File

@@ -34,6 +34,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
ollama: "Ollama",
openai: "OpenAI",
openweathermap: "OpenWeatherMap",
open_router: "Open Router",
pinecone: "Pinecone",
replicate: "Replicate",
revid: "Rev.ID",

View File

@@ -105,10 +105,11 @@ export const FlowInfo: React.FC<
</DropdownMenu>
)}
<Link
className={buttonVariants({ variant: "outline" })}
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon />
<Pencil2Icon className="mr-2" />
Open in Builder
</Link>
<Button
variant="outline"
@@ -126,7 +127,7 @@ export const FlowInfo: React.FC<
)
}
>
<ExitIcon />
<ExitIcon className="mr-2" /> Export
</Button>
<Button variant="outline" onClick={() => setIsDeleteModalOpen(true)}>
<Trash2Icon className="h-full" />

View File

@@ -47,10 +47,10 @@ export const FlowRunInfo: React.FC<
</Button>
)}
<Link
className={buttonVariants({ variant: "outline" })}
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
<Pencil2Icon className="mr-2" /> Open in Builder
</Link>
</div>
</CardHeader>

View File

@@ -28,6 +28,10 @@ export default class BaseAutoGPTServerAPI {
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private supabaseClient: SupabaseClient | null = null;
heartbeatInterval: number | null = null;
readonly HEARTBEAT_INTERVAL = 30000; // 30 seconds
readonly HEARTBEAT_TIMEOUT = 10000; // 10 seconds
heartbeatTimeoutId: number | null = null;
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
@@ -324,34 +328,84 @@ export default class BaseAutoGPTServerAPI {
}
}
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = window.setInterval(() => {
if (this.webSocket?.readyState === WebSocket.OPEN) {
this.webSocket.send(
JSON.stringify({
method: "heartbeat",
data: "ping",
success: true,
}),
);
this.heartbeatTimeoutId = window.setTimeout(() => {
console.log("Heartbeat timeout - reconnecting");
this.webSocket?.close();
this.connectWebSocket();
}, this.HEARTBEAT_TIMEOUT);
}
}, this.HEARTBEAT_INTERVAL);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
handleHeartbeatResponse() {
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.debug("WebSocket connection established");
console.log("WebSocket connection established");
this.startHeartbeat(); // Start heartbeat when connection opens
resolve();
};
this.webSocket.onclose = (event) => {
console.debug("WebSocket connection closed", event);
console.log("WebSocket connection closed", event);
this.stopHeartbeat(); // Stop heartbeat when connection closes
this.webSocket = null;
// Attempt to reconnect after a delay
setTimeout(() => this.connectWebSocket(), 1000);
};
this.webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
this.stopHeartbeat(); // Stop heartbeat on error
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
if (message.method == "execution_event") {
// Handle heartbeat response
if (message.method === "heartbeat" && message.data === "pong") {
this.handleHeartbeatResponse();
return;
}
if (message.method === "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
@@ -367,6 +421,7 @@ export default class BaseAutoGPTServerAPI {
}
disconnectWebSocket() {
this.stopHeartbeat(); // Stop heartbeat when disconnecting
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.close();
}
@@ -423,6 +478,7 @@ type GraphCreateRequestBody =
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
execution_event: NodeExecutionResult;
heartbeat: "ping" | "pong";
};
type WebsocketMessage = {

View File

@@ -112,6 +112,7 @@ export const PROVIDER_NAMES = {
OLLAMA: "ollama",
OPENAI: "openai",
OPENWEATHERMAP: "openweathermap",
OPEN_ROUTER: "open_router",
PINECONE: "pinecone",
REPLICATE: "replicate",
REVID: "revid",

View File

@@ -2,8 +2,12 @@
ENV_PATH=$(poetry env info --path)
if [ -d "$ENV_PATH" ]; then
rm -rf $ENV_PATH
echo "Removed the poetry environment at $ENV_PATH."
if [ -e delete ]; then
rm -rf "$ENV_PATH" || { echo "Please manually remove $ENV_PATH"; exit 1; }
else
echo "Press ENTER to remove $ENV_PATH"
read && { rm -r "$ENV_PATH" && echo "Removed the poetry environment at $ENV_PATH."; } || { echo "Please manually remove $ENV_PATH."; exit 1; }
fi
else
echo "No poetry environment found."
fi