mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge remote-tracking branch 'origin/zamilmajdy/fix-connection-consumption-agent-executor-block' into toran/open-2057-mailerlite-blocks
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
48
autogpt_platform/autogpt_libs/poetry.lock
generated
48
autogpt_platform/autogpt_libs/poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -57,6 +57,7 @@ GOOGLE_CLIENT_SECRET=
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GROQ_API_KEY=
|
||||
OPEN_ROUTER_API_KEY=
|
||||
|
||||
# Reddit
|
||||
REDDIT_CLIENT_ID=
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 =============== #
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
########################################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -14,6 +14,7 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
output: "standalone",
|
||||
// TODO: Re-enable TypeScript checks once current issues are resolved
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -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
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,7 @@ export const providerIcons: Record<
|
||||
ollama: fallbackIcon,
|
||||
openai: fallbackIcon,
|
||||
openweathermap: fallbackIcon,
|
||||
open_router: fallbackIcon,
|
||||
pinecone: fallbackIcon,
|
||||
replicate: fallbackIcon,
|
||||
revid: fallbackIcon,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -112,6 +112,7 @@ export const PROVIDER_NAMES = {
|
||||
OLLAMA: "ollama",
|
||||
OPENAI: "openai",
|
||||
OPENWEATHERMAP: "openweathermap",
|
||||
OPEN_ROUTER: "open_router",
|
||||
PINECONE: "pinecone",
|
||||
REPLICATE: "replicate",
|
||||
REVID: "revid",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user