Merge branch 'dev' into codex/fix-400-error-with-non-default-voices-in-unreal-tts

This commit is contained in:
Toran Bruce Richards
2025-06-24 21:25:39 +01:00
committed by GitHub
352 changed files with 13676 additions and 9228 deletions

View File

@@ -55,6 +55,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate API client
run: pnpm generate:api-client
- name: Run tsc check
run: pnpm type-check

6
.gitignore vendored
View File

@@ -165,7 +165,7 @@ package-lock.json
# Allow for locally private items
# private
pri*
pri*
# ignore
ig*
.github_access_token
@@ -176,3 +176,7 @@ autogpt_platform/backend/settings.py
*.ign.*
.test-contents
.claude/settings.local.json
# Auto generated client
autogpt_platform/frontend/src/api/__generated__

View File

@@ -32,6 +32,7 @@ poetry run test
poetry run pytest path/to/test_file.py::test_function_name
# Lint and format
# prefer format if you want to just "fix" it and only get the errors that can't be autofixed
poetry run format # Black + isort
poetry run lint # ruff
```
@@ -77,6 +78,7 @@ npm run type-check
- **Queue System**: RabbitMQ for async task processing
- **Execution Engine**: Separate executor service processes agent workflows
- **Authentication**: JWT-based with Supabase integration
- **Security**: Cache protection middleware prevents sensitive data caching in browsers/proxies
### Frontend Architecture
- **Framework**: Next.js App Router with React Server Components
@@ -129,4 +131,15 @@ Key models (defined in `/backend/schema.prisma`):
1. Components go in `/frontend/src/components/`
2. Use existing UI components from `/frontend/src/components/ui/`
3. Add Storybook stories for new components
4. Test with Playwright if user-facing
4. Test with Playwright if user-facing
### Security Implementation
**Cache Protection Middleware:**
- Located in `/backend/backend/server/middleware/security.py`
- Default behavior: Disables caching for ALL endpoints with `Cache-Control: no-store, no-cache, must-revalidate, private`
- Uses an allow list approach - only explicitly permitted paths can be cached
- Cacheable paths include: static assets (`/static/*`, `/_next/static/*`), health checks, public store pages, documentation
- Prevents sensitive data (auth tokens, API keys, user data) from being cached by browsers/proxies
- To allow caching for a new endpoint, add it to `CACHEABLE_PATHS` in the middleware
- Applied to both main API server and external API applications

View File

@@ -62,6 +62,12 @@ To run the AutoGPT Platform, follow these steps:
pnpm i
```
Generate the API client (this step is required before running the frontend):
```
pnpm generate:api-client
```
Then start the frontend application in development mode:
```
@@ -164,3 +170,27 @@ To persist data for PostgreSQL and Redis, you can modify the `docker-compose.yml
3. Save the file and run `docker compose up -d` to apply the changes.
This configuration will create named volumes for PostgreSQL and Redis, ensuring that your data persists across container restarts.
### API Client Generation
The platform includes scripts for generating and managing the API client:
- `pnpm fetch:openapi`: Fetches the OpenAPI specification from the backend service (requires backend to be running on port 8006)
- `pnpm generate:api-client`: Generates the TypeScript API client from the OpenAPI specification using Orval
- `pnpm generate:api-all`: Runs both fetch and generate commands in sequence
#### Manual API Client Updates
If you need to update the API client after making changes to the backend API:
1. Ensure the backend services are running:
```
docker compose up -d
```
2. Generate the updated API client:
```
pnpm generate:api-all
```
This will fetch the latest OpenAPI specification and regenerate the TypeScript client code.

View File

@@ -31,4 +31,5 @@ class APIKeyManager:
"""Verify if a provided API key matches the stored hash."""
if not provided_key.startswith(self.PREFIX):
return False
return hashlib.sha256(provided_key.encode()).hexdigest() == stored_hash
provided_hash = hashlib.sha256(provided_key.encode()).hexdigest()
return secrets.compare_digest(provided_hash, stored_hash)

View File

@@ -1,5 +1,6 @@
import inspect
import logging
import secrets
from typing import Any, Callable, Optional
from fastapi import HTTPException, Request, Security
@@ -93,7 +94,11 @@ class APIKeyValidator:
self.error_message = error_message
async def default_validator(self, api_key: str) -> bool:
return api_key == self.expected_token
if not self.expected_token:
raise ValueError(
"Expected Token Required to be set when uisng API Key Validator default validation"
)
return secrets.compare_digest(api_key, self.expected_token)
async def __call__(
self, request: Request, api_key: str = Security(APIKeyHeader)

View File

@@ -1,15 +1,15 @@
from contextlib import contextmanager
from threading import Lock
import asyncio
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any
from expiringdict import ExpiringDict
if TYPE_CHECKING:
from redis import Redis
from redis.lock import Lock as RedisLock
from redis.asyncio import Redis as AsyncRedis
from redis.asyncio.lock import Lock as AsyncRedisLock
class RedisKeyedMutex:
class AsyncRedisKeyedMutex:
"""
This class provides a mutex that can be locked and unlocked by a specific key,
using Redis as a distributed locking provider.
@@ -17,41 +17,45 @@ class RedisKeyedMutex:
in case the key is not unlocked for a specified duration, to prevent memory leaks.
"""
def __init__(self, redis: "Redis", timeout: int | None = 60):
def __init__(self, redis: "AsyncRedis", timeout: int | None = 60):
self.redis = redis
self.timeout = timeout
self.locks: dict[Any, "RedisLock"] = ExpiringDict(
self.locks: dict[Any, "AsyncRedisLock"] = ExpiringDict(
max_len=6000, max_age_seconds=self.timeout
)
self.locks_lock = Lock()
self.locks_lock = asyncio.Lock()
@contextmanager
def locked(self, key: Any):
lock = self.acquire(key)
@asynccontextmanager
async def locked(self, key: Any):
lock = await self.acquire(key)
try:
yield
finally:
if lock.locked() and lock.owned():
lock.release()
if (await lock.locked()) and (await lock.owned()):
await lock.release()
def acquire(self, key: Any) -> "RedisLock":
async def acquire(self, key: Any) -> "AsyncRedisLock":
"""Acquires and returns a lock with the given key"""
with self.locks_lock:
async with self.locks_lock:
if key not in self.locks:
self.locks[key] = self.redis.lock(
str(key), self.timeout, thread_local=False
)
lock = self.locks[key]
lock.acquire()
await lock.acquire()
return lock
def release(self, key: Any):
if (lock := self.locks.get(key)) and lock.locked() and lock.owned():
lock.release()
async def release(self, key: Any):
if (
(lock := self.locks.get(key))
and (await lock.locked())
and (await lock.owned())
):
await lock.release()
def release_all_locks(self):
async def release_all_locks(self):
"""Call this on process termination to ensure all locks are released"""
self.locks_lock.acquire(blocking=False)
for lock in self.locks.values():
if lock.locked() and lock.owned():
lock.release()
async with self.locks_lock:
for lock in self.locks.values():
if (await lock.locked()) and (await lock.owned()):
await lock.release()

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -177,7 +177,7 @@ files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""}
markers = {main = "python_version == \"3.10\"", dev = "python_full_version < \"3.11.3\""}
[[package]]
name = "attrs"
@@ -323,6 +323,21 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "click"
version = "8.2.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"},
{file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
@@ -375,7 +390,7 @@ description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -399,6 +414,27 @@ files = [
[package.extras]
tests = ["coverage", "coveralls", "dill", "mock", "nose"]
[[package]]
name = "fastapi"
version = "0.115.12"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"},
{file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.47.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "frozenlist"
version = "1.4.1"
@@ -895,6 +931,47 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "launchdarkly-eventsource"
version = "1.2.4"
description = "LaunchDarkly SSE Client"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "launchdarkly_eventsource-1.2.4-py3-none-any.whl", hash = "sha256:048ef8c4440d0d8219778661ee4d4b5e12aa6ed2c29a3004417ede44c2386e8c"},
{file = "launchdarkly_eventsource-1.2.4.tar.gz", hash = "sha256:b8b9342681f55e1d35c56243431cbbaca4eb9812d6785f8de204af322104e066"},
]
[package.dependencies]
urllib3 = ">=1.26.0,<3"
[[package]]
name = "launchdarkly-server-sdk"
version = "9.11.1"
description = "LaunchDarkly SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "launchdarkly_server_sdk-9.11.1-py3-none-any.whl", hash = "sha256:128569cebf666dd115cc0ba03c48ff75f6acc9788301a7e2c3a54d06107e445a"},
{file = "launchdarkly_server_sdk-9.11.1.tar.gz", hash = "sha256:150e29656cb8c506d1967f3c59e62b69310d345ec27217640a6146dd1db5d250"},
]
[package.dependencies]
certifi = ">=2018.4.16"
expiringdict = ">=1.1.4"
launchdarkly-eventsource = ">=1.2.4,<2.0.0"
pyRFC3339 = ">=1.0"
semver = ">=2.10.2"
urllib3 = ">=1.26.0,<3"
[package.extras]
consul = ["python-consul (>=1.0.1)"]
dynamodb = ["boto3 (>=1.9.71)"]
redis = ["redis (>=2.10.5)"]
test-filesource = ["pyyaml (>=5.3.1)", "watchdog (>=3.0.0)"]
[[package]]
name = "multidict"
version = "6.1.0"
@@ -1412,6 +1489,18 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pyrfc3339"
version = "2.0.1"
description = "Generate and parse RFC 3339 timestamps"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pyRFC3339-2.0.1-py3-none-any.whl", hash = "sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d"},
{file = "pyrfc3339-2.0.1.tar.gz", hash = "sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b"},
]
[[package]]
name = "pytest"
version = "8.3.3"
@@ -1604,6 +1693,18 @@ files = [
{file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"},
]
[[package]]
name = "semver"
version = "3.0.4"
description = "Python helper for Semantic Versioning (https://semver.org)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"},
{file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"},
]
[[package]]
name = "six"
version = "1.16.0"
@@ -1628,6 +1729,24 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "starlette"
version = "0.46.2"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"},
{file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"},
]
[package.dependencies]
anyio = ">=3.6.2,<5"
[package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
[[package]]
name = "storage3"
version = "0.11.0"
@@ -1704,7 +1823,7 @@ description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
@@ -1755,6 +1874,26 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.34.3"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"},
{file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "websockets"
version = "12.0"
@@ -2037,4 +2176,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "78ebf65cdef769cfbe92fe204f01e32d219cca9ee5a6ca9e657aa0630be63802"
content-hash = "d92143928a88ca3a56ac200c335910eafac938940022fed8bd0d17c95040b54f"

View File

@@ -17,6 +17,9 @@ pyjwt = "^2.10.1"
pytest-asyncio = "^0.26.0"
pytest-mock = "^3.14.0"
supabase = "^2.15.1"
launchdarkly-server-sdk = "^9.11.1"
fastapi = "^0.115.12"
uvicorn = "^0.34.3"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.1"

View File

@@ -13,7 +13,6 @@ PRISMA_SCHEMA="postgres/schema.prisma"
# EXECUTOR
NUM_GRAPH_WORKERS=10
NUM_NODE_WORKERS=3
BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"]

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
from typing import Any, Optional
@@ -61,37 +62,78 @@ class AgentExecutorBlock(Block):
categories={BlockCategory.AGENT},
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
from backend.data.execution import ExecutionEventType
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
from backend.executor import utils as execution_utils
event_bus = execution_utils.get_execution_event_bus()
graph_exec = execution_utils.add_graph_execution(
graph_exec = await execution_utils.add_graph_execution(
graph_id=input_data.graph_id,
graph_version=input_data.graph_version,
user_id=input_data.user_id,
inputs=input_data.inputs,
node_credentials_input_map=input_data.node_credentials_input_map,
use_db_query=False,
)
log_id = f"Graph #{input_data.graph_id}-V{input_data.graph_version}, exec-id: {graph_exec.id}"
try:
async for name, data in self._run(
graph_id=input_data.graph_id,
graph_version=input_data.graph_version,
graph_exec_id=graph_exec.id,
user_id=input_data.user_id,
):
yield name, data
except asyncio.CancelledError:
logger.warning(
f"Execution of graph {input_data.graph_id} version {input_data.graph_version} was cancelled."
)
await execution_utils.stop_graph_execution(
graph_exec.id, use_db_query=False
)
except Exception as e:
logger.error(
f"Execution of graph {input_data.graph_id} version {input_data.graph_version} failed: {e}, stopping execution."
)
await execution_utils.stop_graph_execution(
graph_exec.id, use_db_query=False
)
raise
async def _run(
self,
graph_id: str,
graph_version: int,
graph_exec_id: str,
user_id: str,
) -> BlockOutput:
from backend.data.execution import ExecutionEventType
from backend.executor import utils as execution_utils
event_bus = execution_utils.get_async_execution_event_bus()
log_id = f"Graph #{graph_id}-V{graph_version}, exec-id: {graph_exec_id}"
logger.info(f"Starting execution of {log_id}")
for event in event_bus.listen(
user_id=graph_exec.user_id,
graph_id=graph_exec.graph_id,
graph_exec_id=graph_exec.id,
async for event in event_bus.listen(
user_id=user_id,
graph_id=graph_id,
graph_exec_id=graph_exec_id,
):
if event.status not in [
ExecutionStatus.COMPLETED,
ExecutionStatus.TERMINATED,
ExecutionStatus.FAILED,
]:
logger.debug(
f"Execution {log_id} received event {event.event_type} with status {event.status}"
)
continue
if event.event_type == ExecutionEventType.GRAPH_EXEC_UPDATE:
if event.status in [
ExecutionStatus.COMPLETED,
ExecutionStatus.TERMINATED,
ExecutionStatus.FAILED,
]:
logger.info(f"Execution {log_id} ended with status {event.status}")
break
else:
continue
# If the graph execution is COMPLETED, TERMINATED, or FAILED,
# we can stop listening for further events.
break
logger.debug(
f"Execution {log_id} produced input {event.input_data} output {event.output_data}"

View File

@@ -165,7 +165,7 @@ class AIImageGeneratorBlock(Block):
},
)
def _run_client(
async def _run_client(
self, credentials: APIKeyCredentials, model_name: str, input_params: dict
):
try:
@@ -173,7 +173,7 @@ class AIImageGeneratorBlock(Block):
client = ReplicateClient(api_token=credentials.api_key.get_secret_value())
# Run the model with input parameters
output = client.run(model_name, input=input_params, wait=False)
output = await client.async_run(model_name, input=input_params, wait=False)
# Process output
if isinstance(output, list) and len(output) > 0:
@@ -195,7 +195,7 @@ class AIImageGeneratorBlock(Block):
except Exception as e:
raise RuntimeError(f"Unexpected error during model execution: {e}")
def generate_image(self, input_data: Input, credentials: APIKeyCredentials):
async def generate_image(self, input_data: Input, credentials: APIKeyCredentials):
try:
# Handle style-based prompt modification for models without native style support
modified_prompt = input_data.prompt
@@ -213,7 +213,7 @@ class AIImageGeneratorBlock(Block):
"steps": 40,
"cfg_scale": 7.0,
}
output = self._run_client(
output = await self._run_client(
credentials,
"stability-ai/stable-diffusion-3.5-medium",
input_params,
@@ -231,7 +231,7 @@ class AIImageGeneratorBlock(Block):
"output_format": "jpg", # Set to jpg for Flux models
"output_quality": 90,
}
output = self._run_client(
output = await self._run_client(
credentials, "black-forest-labs/flux-1.1-pro", input_params
)
return output
@@ -246,7 +246,7 @@ class AIImageGeneratorBlock(Block):
"output_format": "jpg",
"output_quality": 90,
}
output = self._run_client(
output = await self._run_client(
credentials, "black-forest-labs/flux-1.1-pro-ultra", input_params
)
return output
@@ -257,7 +257,7 @@ class AIImageGeneratorBlock(Block):
"size": SIZE_TO_RECRAFT_DIMENSIONS[input_data.size],
"style": input_data.style.value,
}
output = self._run_client(
output = await self._run_client(
credentials, "recraft-ai/recraft-v3", input_params
)
return output
@@ -296,9 +296,9 @@ class AIImageGeneratorBlock(Block):
style_text = style_map.get(style, "")
return f"{style_text} of" if style_text else ""
def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):
async def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):
try:
url = self.generate_image(input_data, credentials)
url = await self.generate_image(input_data, credentials)
if url:
yield "image_url", url
else:

View File

@@ -1,5 +1,5 @@
import asyncio
import logging
import time
from enum import Enum
from typing import Literal
@@ -142,7 +142,7 @@ class AIMusicGeneratorBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
max_retries = 3
@@ -154,7 +154,7 @@ class AIMusicGeneratorBlock(Block):
logger.debug(
f"[AIMusicGeneratorBlock] - Running model (attempt {attempt + 1})"
)
result = self.run_model(
result = await self.run_model(
api_key=credentials.api_key,
music_gen_model_version=input_data.music_gen_model_version,
prompt=input_data.prompt,
@@ -176,13 +176,13 @@ class AIMusicGeneratorBlock(Block):
last_error = f"Unexpected error: {str(e)}"
logger.error(f"[AIMusicGeneratorBlock] - Error: {last_error}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
await asyncio.sleep(retry_delay)
continue
# If we've exhausted all retries, yield the error
yield "error", f"Failed after {max_retries} attempts. Last error: {last_error}"
def run_model(
async def run_model(
self,
api_key: SecretStr,
music_gen_model_version: MusicGenModelVersion,
@@ -199,7 +199,7 @@ class AIMusicGeneratorBlock(Block):
client = ReplicateClient(api_token=api_key.get_secret_value())
# Run the model with parameters
output = client.run(
output = await client.async_run(
"meta/musicgen:671ac645ce5e552cc63a54a2bbff63fcf798043055d2dac5fc9e36a837eedcfb",
input={
"prompt": prompt,

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
import time
from enum import Enum
@@ -13,7 +14,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -216,29 +217,29 @@ class AIShortformVideoCreatorBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def create_webhook(self):
async def create_webhook(self):
url = "https://webhook.site/token"
headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = requests.post(url, headers=headers)
response = await Requests().post(url, headers=headers)
webhook_data = response.json()
return webhook_data["uuid"], f"https://webhook.site/{webhook_data['uuid']}"
def create_video(self, api_key: SecretStr, payload: dict) -> dict:
async def create_video(self, api_key: SecretStr, payload: dict) -> dict:
url = "https://www.revid.ai/api/public/v2/render"
headers = {"key": api_key.get_secret_value()}
response = requests.post(url, json=payload, headers=headers)
response = await Requests().post(url, json=payload, headers=headers)
logger.debug(
f"API Response Status Code: {response.status_code}, Content: {response.text}"
f"API Response Status Code: {response.status}, Content: {response.text}"
)
return response.json()
def check_video_status(self, api_key: SecretStr, pid: str) -> dict:
async 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.get_secret_value()}
response = requests.get(url, headers=headers)
response = await Requests().get(url, headers=headers)
return response.json()
def wait_for_video(
async def wait_for_video(
self,
api_key: SecretStr,
pid: str,
@@ -247,7 +248,7 @@ class AIShortformVideoCreatorBlock(Block):
) -> str:
start_time = time.time()
while time.time() - start_time < max_wait_time:
status = self.check_video_status(api_key, pid)
status = await self.check_video_status(api_key, pid)
logger.debug(f"Video status: {status}")
if status.get("status") == "ready" and "videoUrl" in status:
@@ -260,16 +261,16 @@ class AIShortformVideoCreatorBlock(Block):
logger.error(f"Video creation failed: {status.get('message')}")
raise ValueError(f"Video creation failed: {status.get('message')}")
time.sleep(10)
await asyncio.sleep(10)
logger.error("Video creation timed out")
raise TimeoutError("Video creation timed out")
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
# Create a new Webhook.site URL
webhook_token, webhook_url = self.create_webhook()
webhook_token, webhook_url = await self.create_webhook()
logger.debug(f"Webhook URL: {webhook_url}")
audio_url = input_data.background_music.audio_url
@@ -306,7 +307,7 @@ class AIShortformVideoCreatorBlock(Block):
}
logger.debug("Creating video...")
response = self.create_video(credentials.api_key, payload)
response = await self.create_video(credentials.api_key, payload)
pid = response.get("pid")
if not pid:
@@ -318,6 +319,8 @@ class AIShortformVideoCreatorBlock(Block):
logger.debug(
f"Video created with project ID: {pid}. Waiting for completion..."
)
video_url = self.wait_for_video(credentials.api_key, pid, webhook_token)
video_url = await self.wait_for_video(
credentials.api_key, pid, webhook_token
)
logger.debug(f"Video ready: {video_url}")
yield "video_url", video_url

View File

@@ -27,14 +27,15 @@ class ApolloClient:
def _get_headers(self) -> dict[str, str]:
return {"x-api-key": self.credentials.api_key.get_secret_value()}
def search_people(self, query: SearchPeopleRequest) -> List[Contact]:
async def search_people(self, query: SearchPeopleRequest) -> List[Contact]:
"""Search for people in Apollo"""
response = self.requests.get(
response = await self.requests.post(
f"{self.API_URL}/mixed_people/search",
headers=self._get_headers(),
params=query.model_dump(exclude={"credentials", "max_results"}),
json=query.model_dump(exclude={"max_results"}),
)
parsed_response = SearchPeopleResponse(**response.json())
data = response.json()
parsed_response = SearchPeopleResponse(**data)
if parsed_response.pagination.total_entries == 0:
return []
@@ -52,27 +53,29 @@ class ApolloClient:
and len(parsed_response.people) > 0
):
query.page += 1
response = self.requests.get(
response = await self.requests.post(
f"{self.API_URL}/mixed_people/search",
headers=self._get_headers(),
params=query.model_dump(exclude={"credentials", "max_results"}),
json=query.model_dump(exclude={"max_results"}),
)
parsed_response = SearchPeopleResponse(**response.json())
data = response.json()
parsed_response = SearchPeopleResponse(**data)
people.extend(parsed_response.people[: query.max_results - len(people)])
logger.info(f"Found {len(people)} people")
return people[: query.max_results] if query.max_results else people
def search_organizations(
async def search_organizations(
self, query: SearchOrganizationsRequest
) -> List[Organization]:
"""Search for organizations in Apollo"""
response = self.requests.get(
response = await self.requests.post(
f"{self.API_URL}/mixed_companies/search",
headers=self._get_headers(),
params=query.model_dump(exclude={"credentials", "max_results"}),
json=query.model_dump(exclude={"max_results"}),
)
parsed_response = SearchOrganizationsResponse(**response.json())
data = response.json()
parsed_response = SearchOrganizationsResponse(**data)
if parsed_response.pagination.total_entries == 0:
return []
@@ -90,12 +93,13 @@ class ApolloClient:
and len(parsed_response.organizations) > 0
):
query.page += 1
response = self.requests.get(
response = await self.requests.post(
f"{self.API_URL}/mixed_companies/search",
headers=self._get_headers(),
params=query.model_dump(exclude={"credentials", "max_results"}),
json=query.model_dump(exclude={"max_results"}),
)
parsed_response = SearchOrganizationsResponse(**response.json())
data = response.json()
parsed_response = SearchOrganizationsResponse(**data)
organizations.extend(
parsed_response.organizations[
: query.max_results - len(organizations)

View File

@@ -1,17 +1,31 @@
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel as OriginalBaseModel
from pydantic import ConfigDict
from backend.data.model import SchemaField
class BaseModel(OriginalBaseModel):
def model_dump(self, *args, exclude: set[str] | None = None, **kwargs):
if exclude is None:
exclude = set("credentials")
else:
exclude.add("credentials")
kwargs.setdefault("exclude_none", True)
kwargs.setdefault("exclude_unset", True)
kwargs.setdefault("exclude_defaults", True)
return super().model_dump(*args, exclude=exclude, **kwargs)
class PrimaryPhone(BaseModel):
"""A primary phone in Apollo"""
number: str
source: str
sanitized_number: str
number: str = ""
source: str = ""
sanitized_number: str = ""
class SenorityLevels(str, Enum):
@@ -42,88 +56,88 @@ class ContactEmailStatuses(str, Enum):
class RuleConfigStatus(BaseModel):
"""A rule config status in Apollo"""
_id: str
created_at: str
rule_action_config_id: str
rule_config_id: str
status_cd: str
updated_at: str
id: str
key: str
_id: str = ""
created_at: str = ""
rule_action_config_id: str = ""
rule_config_id: str = ""
status_cd: str = ""
updated_at: str = ""
id: str = ""
key: str = ""
class ContactCampaignStatus(BaseModel):
"""A contact campaign status in Apollo"""
id: str
emailer_campaign_id: str
send_email_from_user_id: str
inactive_reason: str
status: str
added_at: str
added_by_user_id: str
finished_at: str
paused_at: str
auto_unpause_at: str
send_email_from_email_address: str
send_email_from_email_account_id: str
manually_set_unpause: str
failure_reason: str
current_step_id: str
in_response_to_emailer_message_id: str
cc_emails: str
bcc_emails: str
to_emails: str
id: str = ""
emailer_campaign_id: str = ""
send_email_from_user_id: str = ""
inactive_reason: str = ""
status: str = ""
added_at: str = ""
added_by_user_id: str = ""
finished_at: str = ""
paused_at: str = ""
auto_unpause_at: str = ""
send_email_from_email_address: str = ""
send_email_from_email_account_id: str = ""
manually_set_unpause: str = ""
failure_reason: str = ""
current_step_id: str = ""
in_response_to_emailer_message_id: str = ""
cc_emails: str = ""
bcc_emails: str = ""
to_emails: str = ""
class Account(BaseModel):
"""An account in Apollo"""
id: str
name: str
website_url: str
blog_url: str
angellist_url: str
linkedin_url: str
twitter_url: str
facebook_url: str
primary_phone: PrimaryPhone
id: str = ""
name: str = ""
website_url: str = ""
blog_url: str = ""
angellist_url: str = ""
linkedin_url: str = ""
twitter_url: str = ""
facebook_url: str = ""
primary_phone: PrimaryPhone = PrimaryPhone()
languages: list[str]
alexa_ranking: int
phone: str
linkedin_uid: str
founded_year: int
publicly_traded_symbol: str
publicly_traded_exchange: str
logo_url: str
chrunchbase_url: str
primary_domain: str
domain: str
team_id: str
organization_id: str
account_stage_id: str
source: str
original_source: str
creator_id: str
owner_id: str
created_at: str
phone_status: str
hubspot_id: str
salesforce_id: str
crm_owner_id: str
parent_account_id: str
sanitized_phone: str
alexa_ranking: int = 0
phone: str = ""
linkedin_uid: str = ""
founded_year: int = 0
publicly_traded_symbol: str = ""
publicly_traded_exchange: str = ""
logo_url: str = ""
chrunchbase_url: str = ""
primary_domain: str = ""
domain: str = ""
team_id: str = ""
organization_id: str = ""
account_stage_id: str = ""
source: str = ""
original_source: str = ""
creator_id: str = ""
owner_id: str = ""
created_at: str = ""
phone_status: str = ""
hubspot_id: str = ""
salesforce_id: str = ""
crm_owner_id: str = ""
parent_account_id: str = ""
sanitized_phone: str = ""
# no listed type on the API docs
account_playbook_statues: list[Any]
account_rule_config_statuses: list[RuleConfigStatus]
existence_level: str
label_ids: list[str]
account_playbook_statues: list[Any] = []
account_rule_config_statuses: list[RuleConfigStatus] = []
existence_level: str = ""
label_ids: list[str] = []
typed_custom_fields: Any
custom_field_errors: Any
modality: str
source_display_name: str
salesforce_record_id: str
crm_record_url: str
modality: str = ""
source_display_name: str = ""
salesforce_record_id: str = ""
crm_record_url: str = ""
class ContactEmail(BaseModel):
@@ -205,7 +219,7 @@ class Pagination(BaseModel):
class DialerFlags(BaseModel):
"""A dialer flags in Apollo"""
country_name: str
country_name: str = ""
country_enabled: bool
high_risk_calling_enabled: bool
potential_high_risk_number: bool

View File

@@ -201,19 +201,17 @@ To find IDs, identify the values for organization_id when you call this endpoint
)
@staticmethod
def search_organizations(
async def search_organizations(
query: SearchOrganizationsRequest, credentials: ApolloCredentials
) -> list[Organization]:
client = ApolloClient(credentials)
return client.search_organizations(query)
return await client.search_organizations(query)
def run(
async def run(
self, input_data: Input, *, credentials: ApolloCredentials, **kwargs
) -> BlockOutput:
query = SearchOrganizationsRequest(
**input_data.model_dump(exclude={"credentials"})
)
organizations = self.search_organizations(query, credentials)
query = SearchOrganizationsRequest(**input_data.model_dump())
organizations = await self.search_organizations(query, credentials)
for organization in organizations:
yield "organization", organization
yield "organizations", organizations

View File

@@ -107,6 +107,7 @@ class SearchPeopleBlock(Block):
default_factory=list,
)
person: Contact = SchemaField(
title="Person",
description="Each found person, one at a time",
)
error: str = SchemaField(
@@ -373,13 +374,13 @@ class SearchPeopleBlock(Block):
)
@staticmethod
def search_people(
async def search_people(
query: SearchPeopleRequest, credentials: ApolloCredentials
) -> list[Contact]:
client = ApolloClient(credentials)
return client.search_people(query)
return await client.search_people(query)
def run(
async def run(
self,
input_data: Input,
*,
@@ -387,8 +388,8 @@ class SearchPeopleBlock(Block):
**kwargs,
) -> BlockOutput:
query = SearchPeopleRequest(**input_data.model_dump(exclude={"credentials"}))
people = self.search_people(query, credentials)
query = SearchPeopleRequest(**input_data.model_dump())
people = await self.search_people(query, credentials)
for person in people:
yield "person", person
yield "people", people

View File

@@ -30,14 +30,14 @@ class FileStoreBlock(Block):
static_output=True,
)
def run(
async def run(
self,
input_data: Input,
*,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
file_path = store_media_file(
file_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.file_in,
return_content=False,
@@ -84,7 +84,7 @@ class StoreValueBlock(Block):
static_output=True,
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.data or input_data.input
@@ -110,7 +110,7 @@ class PrintToConsoleBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text
yield "status", "printed"
@@ -151,7 +151,7 @@ class FindInDictionaryBlock(Block):
categories={BlockCategory.BASIC},
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
obj = input_data.input
key = input_data.key
@@ -241,7 +241,7 @@ class AddToDictionaryBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
updated_dict = input_data.dictionary.copy()
if input_data.value is not None and input_data.key:
@@ -319,7 +319,7 @@ class AddToListBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
entries_added = input_data.entries.copy()
if input_data.entry:
entries_added.append(input_data.entry)
@@ -366,7 +366,7 @@ class FindInListBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
yield "index", input_data.list.index(input_data.value)
yield "found", True
@@ -396,7 +396,7 @@ class NoteBlock(Block):
block_type=BlockType.NOTE,
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text
@@ -442,7 +442,7 @@ class CreateDictionaryBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# The values are already validated by Pydantic schema
yield "dictionary", input_data.values
@@ -456,6 +456,11 @@ class CreateListBlock(Block):
description="A list of values to be combined into a new list.",
placeholder="e.g., ['Alice', 25, True]",
)
max_size: int | None = SchemaField(
default=None,
description="Maximum size of the list. If provided, the list will be yielded in chunks of this size.",
advanced=True,
)
class Output(BlockSchema):
list: List[Any] = SchemaField(
@@ -490,10 +495,11 @@ class CreateListBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# The values are already validated by Pydantic schema
yield "list", input_data.values
max_size = input_data.max_size or len(input_data.values)
for i in range(0, len(input_data.values), max_size):
yield "list", input_data.values[i : i + max_size]
except Exception as e:
yield "error", f"Failed to create list: {str(e)}"
@@ -525,7 +531,7 @@ class UniversalTypeConverterBlock(Block):
output_schema=UniversalTypeConverterBlock.Output,
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
converted_value = convert(
input_data.value,

View File

@@ -38,7 +38,7 @@ class BlockInstallationBlock(Block):
disabled=True,
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
code = input_data.code
if search := re.search(r"class (\w+)\(Block\):", code):
@@ -64,7 +64,7 @@ class BlockInstallationBlock(Block):
from backend.util.test import execute_block_test
execute_block_test(block)
await execute_block_test(block)
yield "success", "Block installed successfully."
except Exception as e:
os.remove(file_path)

View File

@@ -70,7 +70,7 @@ class ConditionBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
operator = input_data.operator
value1 = input_data.value1
@@ -163,7 +163,7 @@ class IfInputMatchesBlock(Block):
},
{
"input": 10,
"value": None,
"value": "None",
"yes_value": "Yes",
"no_value": "No",
},
@@ -180,7 +180,7 @@ class IfInputMatchesBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
if input_data.input == input_data.value or input_data.input is input_data.value:
yield "result", True
yield "yes_output", input_data.yes_value

View File

@@ -1,7 +1,7 @@
from enum import Enum
from typing import Literal
from e2b_code_interpreter import Sandbox
from e2b_code_interpreter import AsyncSandbox
from pydantic import SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
@@ -123,7 +123,7 @@ class CodeExecutionBlock(Block):
},
)
def execute_code(
async def execute_code(
self,
code: str,
language: ProgrammingLanguage,
@@ -135,21 +135,21 @@ class CodeExecutionBlock(Block):
try:
sandbox = None
if template_id:
sandbox = Sandbox(
sandbox = await AsyncSandbox.create(
template=template_id, api_key=api_key, timeout=timeout
)
else:
sandbox = Sandbox(api_key=api_key, timeout=timeout)
sandbox = await AsyncSandbox.create(api_key=api_key, timeout=timeout)
if not sandbox:
raise Exception("Sandbox not created")
# Running setup commands
for cmd in setup_commands:
sandbox.commands.run(cmd)
await sandbox.commands.run(cmd)
# Executing the code
execution = sandbox.run_code(
execution = await sandbox.run_code(
code,
language=language.value,
on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error
@@ -167,11 +167,11 @@ class CodeExecutionBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
response, stdout_logs, stderr_logs = self.execute_code(
response, stdout_logs, stderr_logs = await self.execute_code(
input_data.code,
input_data.language,
input_data.setup_commands,
@@ -278,11 +278,11 @@ class InstantiationBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
sandbox_id, response, stdout_logs, stderr_logs = self.execute_code(
sandbox_id, response, stdout_logs, stderr_logs = await self.execute_code(
input_data.setup_code,
input_data.language,
input_data.setup_commands,
@@ -303,7 +303,7 @@ class InstantiationBlock(Block):
except Exception as e:
yield "error", str(e)
def execute_code(
async def execute_code(
self,
code: str,
language: ProgrammingLanguage,
@@ -315,21 +315,21 @@ class InstantiationBlock(Block):
try:
sandbox = None
if template_id:
sandbox = Sandbox(
sandbox = await AsyncSandbox.create(
template=template_id, api_key=api_key, timeout=timeout
)
else:
sandbox = Sandbox(api_key=api_key, timeout=timeout)
sandbox = await AsyncSandbox.create(api_key=api_key, timeout=timeout)
if not sandbox:
raise Exception("Sandbox not created")
# Running setup commands
for cmd in setup_commands:
sandbox.commands.run(cmd)
await sandbox.commands.run(cmd)
# Executing the code
execution = sandbox.run_code(
execution = await sandbox.run_code(
code,
language=language.value,
on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error
@@ -409,7 +409,7 @@ class StepExecutionBlock(Block):
},
)
def execute_step_code(
async def execute_step_code(
self,
sandbox_id: str,
code: str,
@@ -417,12 +417,12 @@ class StepExecutionBlock(Block):
api_key: str,
):
try:
sandbox = Sandbox.connect(sandbox_id=sandbox_id, api_key=api_key)
sandbox = await AsyncSandbox.connect(sandbox_id=sandbox_id, api_key=api_key)
if not sandbox:
raise Exception("Sandbox not found")
# Executing the code
execution = sandbox.run_code(code, language=language.value)
execution = await sandbox.run_code(code, language=language.value)
if execution.error:
raise Exception(execution.error)
@@ -436,11 +436,11 @@ class StepExecutionBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
response, stdout_logs, stderr_logs = self.execute_step_code(
response, stdout_logs, stderr_logs = await self.execute_step_code(
input_data.sandbox_id,
input_data.step_code,
input_data.language,

View File

@@ -49,7 +49,7 @@ class CodeExtractionBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
# List of supported programming languages with mapped aliases
language_aliases = {
"html": ["html", "htm"],

View File

@@ -56,5 +56,5 @@ class CompassAITriggerBlock(Block):
# ],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "transcription", input_data.payload.transcription

View File

@@ -30,7 +30,7 @@ class WordCharacterCountBlock(Block):
test_output=[("word_count", 4), ("character_count", 19)],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
text = input_data.text
word_count = len(text.split())

View File

@@ -69,7 +69,7 @@ class ReadCsvBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
import csv
from io import StringIO

View File

@@ -34,6 +34,6 @@ This is a "quoted" string.""",
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
decoded_text = codecs.decode(input_data.text, "unicode_escape")
yield "decoded_text", decoded_text

View File

@@ -1,4 +1,3 @@
import asyncio
from typing import Literal
import aiohttp
@@ -74,7 +73,11 @@ class ReadDiscordMessagesBlock(Block):
("username", "test_user"),
],
test_mock={
"run_bot": lambda token: asyncio.Future() # Create a Future object for mocking
"run_bot": lambda token: {
"output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
"channel_name": "general",
"username": "test_user",
}
},
)
@@ -106,37 +109,24 @@ class ReadDiscordMessagesBlock(Block):
if attachment.filename.endswith((".txt", ".py")):
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as response:
file_content = await response.text()
file_content = response.text()
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
await client.close()
await client.start(token.get_secret_value())
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
while True:
for output_name, output_value in self.__run(input_data, credentials):
yield output_name, output_value
break
async for output_name, output_value in self.__run(input_data, credentials):
yield output_name, output_value
def __run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput:
async def __run(
self, input_data: Input, credentials: APIKeyCredentials
) -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.run_bot(credentials.api_key)
# If it's a Future (mock), set the result
if isinstance(future, asyncio.Future):
future.set_result(
{
"output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
"channel_name": "general",
"username": "test_user",
}
)
result = loop.run_until_complete(future)
result = await self.run_bot(credentials.api_key)
# For testing purposes, use the mocked result
if isinstance(result, dict):
@@ -190,7 +180,7 @@ class SendDiscordMessageBlock(Block):
},
test_output=[("status", "Message sent")],
test_mock={
"send_message": lambda token, channel_name, message_content: asyncio.Future()
"send_message": lambda token, channel_name, message_content: "Message sent"
},
test_credentials=TEST_CREDENTIALS,
)
@@ -222,23 +212,16 @@ 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(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.send_message(
result = await self.send_message(
credentials.api_key.get_secret_value(),
input_data.channel_name,
input_data.message_content,
)
# If it's a Future (mock), set the result
if isinstance(future, asyncio.Future):
future.set_result("Message sent")
result = loop.run_until_complete(future)
# For testing purposes, use the mocked result
if isinstance(result, str):
self.output_data = result

View File

@@ -121,7 +121,7 @@ class SendEmailBlock(Block):
return "Email sent successfully"
def run(
async def run(
self, input_data: Input, *, credentials: SMTPCredentials, **kwargs
) -> BlockOutput:
yield "status", self.send_email(

View File

@@ -9,7 +9,7 @@ from backend.blocks.exa._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class ContentRetrievalSettings(BaseModel):
@@ -62,7 +62,7 @@ class ExaContentsBlock(Block):
output_schema=ExaContentsBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/contents"
@@ -79,8 +79,7 @@ class ExaContentsBlock(Block):
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
response = await Requests().post(url, headers=headers, json=payload)
data = response.json()
yield "results", data.get("results", [])
except Exception as e:

View File

@@ -9,7 +9,7 @@ from backend.blocks.exa._auth import (
from backend.blocks.exa.helpers import ContentSettings
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class ExaSearchBlock(Block):
@@ -91,7 +91,7 @@ class ExaSearchBlock(Block):
output_schema=ExaSearchBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/search"
@@ -136,8 +136,7 @@ class ExaSearchBlock(Block):
payload[api_field] = value
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
response = await Requests().post(url, headers=headers, json=payload)
data = response.json()
# Extract just the results array from the response
yield "results", data.get("results", [])

View File

@@ -8,7 +8,7 @@ from backend.blocks.exa._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
from .helpers import ContentSettings
@@ -78,7 +78,7 @@ class ExaFindSimilarBlock(Block):
output_schema=ExaFindSimilarBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: ExaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.exa.ai/findSimilar"
@@ -120,8 +120,7 @@ class ExaFindSimilarBlock(Block):
payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z")
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
response = await Requests().post(url, headers=headers, json=payload)
data = response.json()
yield "results", data.get("results", [])
except Exception as e:

View File

@@ -1,10 +1,8 @@
import asyncio
import logging
import time
from enum import Enum
from typing import Any
import httpx
from backend.blocks.fal._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
@@ -14,6 +12,7 @@ from backend.blocks.fal._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import ClientResponseError, Requests
logger = logging.getLogger(__name__)
@@ -66,35 +65,37 @@ class AIVideoGeneratorBlock(Block):
)
def _get_headers(self, api_key: str) -> dict[str, str]:
"""Get headers for FAL API requests."""
"""Get headers for FAL API Requests."""
return {
"Authorization": f"Key {api_key}",
"Content-Type": "application/json",
}
def _submit_request(
async def _submit_request(
self, url: str, headers: dict[str, str], data: dict[str, Any]
) -> dict[str, Any]:
"""Submit a request to the FAL API."""
try:
response = httpx.post(url, headers=headers, json=data)
response.raise_for_status()
response = await Requests().post(url, headers=headers, json=data)
return response.json()
except httpx.HTTPError as e:
except ClientResponseError as e:
logger.error(f"FAL API request failed: {str(e)}")
raise RuntimeError(f"Failed to submit request: {str(e)}")
def _poll_status(self, status_url: str, headers: dict[str, str]) -> dict[str, Any]:
async def _poll_status(
self, status_url: str, headers: dict[str, str]
) -> dict[str, Any]:
"""Poll the status endpoint until completion or failure."""
try:
response = httpx.get(status_url, headers=headers)
response.raise_for_status()
response = await Requests().get(status_url, headers=headers)
return response.json()
except httpx.HTTPError as e:
except ClientResponseError as e:
logger.error(f"Failed to get status: {str(e)}")
raise RuntimeError(f"Failed to get status: {str(e)}")
def generate_video(self, input_data: Input, credentials: FalCredentials) -> str:
async def generate_video(
self, input_data: Input, credentials: FalCredentials
) -> str:
"""Generate video using the specified FAL model."""
base_url = "https://queue.fal.run"
api_key = credentials.api_key.get_secret_value()
@@ -110,8 +111,9 @@ class AIVideoGeneratorBlock(Block):
try:
# Submit request to queue
submit_response = httpx.post(submit_url, headers=headers, json=submit_data)
submit_response.raise_for_status()
submit_response = await Requests().post(
submit_url, headers=headers, json=submit_data
)
request_data = submit_response.json()
# Get request_id and urls from initial response
@@ -122,14 +124,23 @@ class AIVideoGeneratorBlock(Block):
if not all([request_id, status_url, result_url]):
raise ValueError("Missing required data in submission response")
# Ensure status_url is a string
if not isinstance(status_url, str):
raise ValueError("Invalid status URL format")
# Ensure result_url is a string
if not isinstance(result_url, str):
raise ValueError("Invalid result URL format")
# Poll for status with exponential backoff
max_attempts = 30
attempt = 0
base_wait_time = 5
while attempt < max_attempts:
status_response = httpx.get(f"{status_url}?logs=1", headers=headers)
status_response.raise_for_status()
status_response = await Requests().get(
f"{status_url}?logs=1", headers=headers
)
status_data = status_response.json()
# Process new logs only
@@ -152,8 +163,7 @@ class AIVideoGeneratorBlock(Block):
status = status_data.get("status")
if status == "COMPLETED":
# Get the final result
result_response = httpx.get(result_url, headers=headers)
result_response.raise_for_status()
result_response = await Requests().get(result_url, headers=headers)
result_data = result_response.json()
if "video" not in result_data or not isinstance(
@@ -162,8 +172,8 @@ class AIVideoGeneratorBlock(Block):
raise ValueError("Invalid response format - missing video data")
video_url = result_data["video"].get("url")
if not video_url:
raise ValueError("No video URL in response")
if not video_url or not isinstance(video_url, str):
raise ValueError("No valid video URL in response")
return video_url
@@ -183,19 +193,19 @@ class AIVideoGeneratorBlock(Block):
logger.info(f"[FAL Generation] Status: Unknown status: {status}")
wait_time = min(base_wait_time * (2**attempt), 60) # Cap at 60 seconds
time.sleep(wait_time)
await asyncio.sleep(wait_time)
attempt += 1
raise RuntimeError("Maximum polling attempts reached")
except httpx.HTTPError as e:
except ClientResponseError as e:
raise RuntimeError(f"API request failed: {str(e)}")
def run(
async def run(
self, input_data: Input, *, credentials: FalCredentials, **kwargs
) -> BlockOutput:
try:
video_url = self.generate_video(input_data, credentials)
video_url = await self.generate_video(input_data, credentials)
yield "video_url", video_url
except Exception as e:
error_message = str(e)

View File

@@ -123,14 +123,14 @@ class AIImageEditorBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
result = self.run_model(
result = await self.run_model(
api_key=credentials.api_key,
model_name=input_data.model.api_name,
prompt=input_data.prompt,
@@ -140,7 +140,7 @@ class AIImageEditorBlock(Block):
)
yield "output_image", result
def run_model(
async def run_model(
self,
api_key: SecretStr,
model_name: str,
@@ -157,7 +157,7 @@ class AIImageEditorBlock(Block):
**({"seed": seed} if seed is not None else {}),
}
output: FileOutput | list[FileOutput] = client.run( # type: ignore
output: FileOutput | list[FileOutput] = await client.async_run( # type: ignore
model_name,
input=input_params,
wait=False,

View File

@@ -46,6 +46,6 @@ class GenericWebhookTriggerBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "constants", input_data.constants
yield "payload", input_data.payload

View File

@@ -129,7 +129,7 @@ class GithubCreateCheckRunBlock(Block):
)
@staticmethod
def create_check_run(
async def create_check_run(
credentials: GithubCredentials,
repo_url: str,
name: str,
@@ -172,7 +172,7 @@ class GithubCreateCheckRunBlock(Block):
data.output = output_data
check_runs_url = f"{repo_url}/check-runs"
response = api.post(
response = await api.post(
check_runs_url, data=data.model_dump_json(exclude_none=True)
)
result = response.json()
@@ -183,7 +183,7 @@ class GithubCreateCheckRunBlock(Block):
"status": result["status"],
}
def run(
async def run(
self,
input_data: Input,
*,
@@ -191,7 +191,7 @@ class GithubCreateCheckRunBlock(Block):
**kwargs,
) -> BlockOutput:
try:
result = self.create_check_run(
result = await self.create_check_run(
credentials=credentials,
repo_url=input_data.repo_url,
name=input_data.name,
@@ -292,7 +292,7 @@ class GithubUpdateCheckRunBlock(Block):
)
@staticmethod
def update_check_run(
async def update_check_run(
credentials: GithubCredentials,
repo_url: str,
check_run_id: int,
@@ -325,7 +325,7 @@ class GithubUpdateCheckRunBlock(Block):
data.output = output_data
check_run_url = f"{repo_url}/check-runs/{check_run_id}"
response = api.patch(
response = await api.patch(
check_run_url, data=data.model_dump_json(exclude_none=True)
)
result = response.json()
@@ -337,7 +337,7 @@ class GithubUpdateCheckRunBlock(Block):
"conclusion": result.get("conclusion"),
}
def run(
async def run(
self,
input_data: Input,
*,
@@ -345,7 +345,7 @@ class GithubUpdateCheckRunBlock(Block):
**kwargs,
) -> BlockOutput:
try:
result = self.update_check_run(
result = await self.update_check_run(
credentials=credentials,
repo_url=input_data.repo_url,
check_run_id=input_data.check_run_id,

View File

@@ -80,7 +80,7 @@ class GithubCommentBlock(Block):
)
@staticmethod
def post_comment(
async def post_comment(
credentials: GithubCredentials, issue_url: str, body_text: str
) -> tuple[int, str]:
api = get_api(credentials)
@@ -88,18 +88,18 @@ class GithubCommentBlock(Block):
if "pull" in issue_url:
issue_url = issue_url.replace("pull", "issues")
comments_url = issue_url + "/comments"
response = api.post(comments_url, json=data)
response = await api.post(comments_url, json=data)
comment = response.json()
return comment["id"], comment["html_url"]
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
id, url = self.post_comment(
id, url = await self.post_comment(
credentials,
input_data.issue_url,
input_data.comment,
@@ -171,7 +171,7 @@ class GithubUpdateCommentBlock(Block):
)
@staticmethod
def update_comment(
async def update_comment(
credentials: GithubCredentials, comment_url: str, body_text: str
) -> tuple[int, str]:
api = get_api(credentials, convert_urls=False)
@@ -179,11 +179,11 @@ class GithubUpdateCommentBlock(Block):
url = convert_comment_url_to_api_endpoint(comment_url)
logger.info(url)
response = api.patch(url, json=data)
response = await api.patch(url, json=data)
comment = response.json()
return comment["id"], comment["html_url"]
def run(
async def run(
self,
input_data: Input,
*,
@@ -209,7 +209,7 @@ class GithubUpdateCommentBlock(Block):
raise ValueError(
"Must provide either comment_url or comment_id and issue_url"
)
id, url = self.update_comment(
id, url = await self.update_comment(
credentials,
input_data.comment_url,
input_data.comment,
@@ -288,7 +288,7 @@ class GithubListCommentsBlock(Block):
)
@staticmethod
def list_comments(
async def list_comments(
credentials: GithubCredentials, issue_url: str
) -> list[Output.CommentItem]:
parsed_url = urlparse(issue_url)
@@ -305,7 +305,7 @@ class GithubListCommentsBlock(Block):
# Set convert_urls=False since we're already providing an API URL
api = get_api(credentials, convert_urls=False)
response = api.get(api_url)
response = await api.get(api_url)
comments = response.json()
parsed_comments: list[GithubListCommentsBlock.Output.CommentItem] = [
{
@@ -318,18 +318,19 @@ class GithubListCommentsBlock(Block):
]
return parsed_comments
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
comments = self.list_comments(
comments = await self.list_comments(
credentials,
input_data.issue_url,
)
yield from (("comment", comment) for comment in comments)
for comment in comments:
yield "comment", comment
yield "comments", comments
@@ -381,24 +382,24 @@ class GithubMakeIssueBlock(Block):
)
@staticmethod
def create_issue(
async def create_issue(
credentials: GithubCredentials, repo_url: str, title: str, body: str
) -> tuple[int, str]:
api = get_api(credentials)
data = {"title": title, "body": body}
issues_url = repo_url + "/issues"
response = api.post(issues_url, json=data)
response = await api.post(issues_url, json=data)
issue = response.json()
return issue["number"], issue["html_url"]
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
number, url = self.create_issue(
number, url = await self.create_issue(
credentials,
input_data.repo_url,
input_data.title,
@@ -451,25 +452,25 @@ class GithubReadIssueBlock(Block):
)
@staticmethod
def read_issue(
async def read_issue(
credentials: GithubCredentials, issue_url: str
) -> tuple[str, str, str]:
api = get_api(credentials)
response = api.get(issue_url)
response = await api.get(issue_url)
data = response.json()
title = data.get("title", "No title found")
body = data.get("body", "No body content found")
user = data.get("user", {}).get("login", "No user found")
return title, body, user
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
title, body, user = self.read_issue(
title, body, user = await self.read_issue(
credentials,
input_data.issue_url,
)
@@ -531,30 +532,30 @@ class GithubListIssuesBlock(Block):
)
@staticmethod
def list_issues(
async def list_issues(
credentials: GithubCredentials, repo_url: str
) -> list[Output.IssueItem]:
api = get_api(credentials)
issues_url = repo_url + "/issues"
response = api.get(issues_url)
response = await api.get(issues_url)
data = response.json()
issues: list[GithubListIssuesBlock.Output.IssueItem] = [
{"title": issue["title"], "url": issue["html_url"]} for issue in data
]
return issues
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
issues = self.list_issues(
for issue in await self.list_issues(
credentials,
input_data.repo_url,
)
yield from (("issue", issue) for issue in issues)
):
yield "issue", issue
class GithubAddLabelBlock(Block):
@@ -593,21 +594,23 @@ class GithubAddLabelBlock(Block):
)
@staticmethod
def add_label(credentials: GithubCredentials, issue_url: str, label: str) -> str:
async def add_label(
credentials: GithubCredentials, issue_url: str, label: str
) -> str:
api = get_api(credentials)
data = {"labels": [label]}
labels_url = issue_url + "/labels"
api.post(labels_url, json=data)
await api.post(labels_url, json=data)
return "Label added successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.add_label(
status = await self.add_label(
credentials,
input_data.issue_url,
input_data.label,
@@ -653,20 +656,22 @@ class GithubRemoveLabelBlock(Block):
)
@staticmethod
def remove_label(credentials: GithubCredentials, issue_url: str, label: str) -> str:
async def remove_label(
credentials: GithubCredentials, issue_url: str, label: str
) -> str:
api = get_api(credentials)
label_url = issue_url + f"/labels/{label}"
api.delete(label_url)
await api.delete(label_url)
return "Label removed successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.remove_label(
status = await self.remove_label(
credentials,
input_data.issue_url,
input_data.label,
@@ -714,7 +719,7 @@ class GithubAssignIssueBlock(Block):
)
@staticmethod
def assign_issue(
async def assign_issue(
credentials: GithubCredentials,
issue_url: str,
assignee: str,
@@ -722,17 +727,17 @@ class GithubAssignIssueBlock(Block):
api = get_api(credentials)
assignees_url = issue_url + "/assignees"
data = {"assignees": [assignee]}
api.post(assignees_url, json=data)
await api.post(assignees_url, json=data)
return "Issue assigned successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.assign_issue(
status = await self.assign_issue(
credentials,
input_data.issue_url,
input_data.assignee,
@@ -780,7 +785,7 @@ class GithubUnassignIssueBlock(Block):
)
@staticmethod
def unassign_issue(
async def unassign_issue(
credentials: GithubCredentials,
issue_url: str,
assignee: str,
@@ -788,17 +793,17 @@ class GithubUnassignIssueBlock(Block):
api = get_api(credentials)
assignees_url = issue_url + "/assignees"
data = {"assignees": [assignee]}
api.delete(assignees_url, json=data)
await api.delete(assignees_url, json=data)
return "Issue unassigned successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.unassign_issue(
status = await self.unassign_issue(
credentials,
input_data.issue_url,
input_data.assignee,

View File

@@ -65,28 +65,31 @@ class GithubListPullRequestsBlock(Block):
)
@staticmethod
def list_prs(credentials: GithubCredentials, repo_url: str) -> list[Output.PRItem]:
async def list_prs(
credentials: GithubCredentials, repo_url: str
) -> list[Output.PRItem]:
api = get_api(credentials)
pulls_url = repo_url + "/pulls"
response = api.get(pulls_url)
response = await api.get(pulls_url)
data = response.json()
pull_requests: list[GithubListPullRequestsBlock.Output.PRItem] = [
{"title": pr["title"], "url": pr["html_url"]} for pr in data
]
return pull_requests
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
pull_requests = self.list_prs(
pull_requests = await self.list_prs(
credentials,
input_data.repo_url,
)
yield from (("pull_request", pr) for pr in pull_requests)
for pr in pull_requests:
yield "pull_request", pr
class GithubMakePullRequestBlock(Block):
@@ -153,7 +156,7 @@ class GithubMakePullRequestBlock(Block):
)
@staticmethod
def create_pr(
async def create_pr(
credentials: GithubCredentials,
repo_url: str,
title: str,
@@ -164,11 +167,11 @@ class GithubMakePullRequestBlock(Block):
api = get_api(credentials)
pulls_url = repo_url + "/pulls"
data = {"title": title, "body": body, "head": head, "base": base}
response = api.post(pulls_url, json=data)
response = await api.post(pulls_url, json=data)
pr_data = response.json()
return pr_data["number"], pr_data["html_url"]
def run(
async def run(
self,
input_data: Input,
*,
@@ -176,7 +179,7 @@ class GithubMakePullRequestBlock(Block):
**kwargs,
) -> BlockOutput:
try:
number, url = self.create_pr(
number, url = await self.create_pr(
credentials,
input_data.repo_url,
input_data.title,
@@ -242,39 +245,39 @@ class GithubReadPullRequestBlock(Block):
)
@staticmethod
def read_pr(credentials: GithubCredentials, pr_url: str) -> tuple[str, str, str]:
async def read_pr(
credentials: GithubCredentials, pr_url: str
) -> tuple[str, str, str]:
api = get_api(credentials)
# Adjust the URL to access the issue endpoint for PR metadata
issue_url = pr_url.replace("/pull/", "/issues/")
response = api.get(issue_url)
response = await api.get(issue_url)
data = response.json()
title = data.get("title", "No title found")
body = data.get("body", "No body content found")
author = data.get("user", {}).get("login", "No user found")
author = data.get("user", {}).get("login", "Unknown author")
return title, body, author
@staticmethod
def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str:
async def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str:
api = get_api(credentials)
files_url = prepare_pr_api_url(pr_url=pr_url, path="files")
response = api.get(files_url)
response = await api.get(files_url)
files = response.json()
changes = []
for file in files:
filename = file.get("filename")
patch = file.get("patch")
if filename and patch:
changes.append(f"File: {filename}\n{patch}")
return "\n\n".join(changes)
filename = file.get("filename", "")
status = file.get("status", "")
changes.append(f"{filename}: {status}")
return "\n".join(changes)
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
title, body, author = self.read_pr(
title, body, author = await self.read_pr(
credentials,
input_data.pr_url,
)
@@ -283,7 +286,7 @@ class GithubReadPullRequestBlock(Block):
yield "author", author
if input_data.include_pr_changes:
changes = self.read_pr_changes(
changes = await self.read_pr_changes(
credentials,
input_data.pr_url,
)
@@ -330,16 +333,16 @@ class GithubAssignPRReviewerBlock(Block):
)
@staticmethod
def assign_reviewer(
async def assign_reviewer(
credentials: GithubCredentials, pr_url: str, reviewer: str
) -> str:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
data = {"reviewers": [reviewer]}
api.post(reviewers_url, json=data)
await api.post(reviewers_url, json=data)
return "Reviewer assigned successfully"
def run(
async def run(
self,
input_data: Input,
*,
@@ -347,7 +350,7 @@ class GithubAssignPRReviewerBlock(Block):
**kwargs,
) -> BlockOutput:
try:
status = self.assign_reviewer(
status = await self.assign_reviewer(
credentials,
input_data.pr_url,
input_data.reviewer,
@@ -397,16 +400,16 @@ class GithubUnassignPRReviewerBlock(Block):
)
@staticmethod
def unassign_reviewer(
async def unassign_reviewer(
credentials: GithubCredentials, pr_url: str, reviewer: str
) -> str:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
data = {"reviewers": [reviewer]}
api.delete(reviewers_url, json=data)
await api.delete(reviewers_url, json=data)
return "Reviewer unassigned successfully"
def run(
async def run(
self,
input_data: Input,
*,
@@ -414,7 +417,7 @@ class GithubUnassignPRReviewerBlock(Block):
**kwargs,
) -> BlockOutput:
try:
status = self.unassign_reviewer(
status = await self.unassign_reviewer(
credentials,
input_data.pr_url,
input_data.reviewer,
@@ -477,12 +480,12 @@ class GithubListPRReviewersBlock(Block):
)
@staticmethod
def list_reviewers(
async def list_reviewers(
credentials: GithubCredentials, pr_url: str
) -> list[Output.ReviewerItem]:
api = get_api(credentials)
reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers")
response = api.get(reviewers_url)
response = await api.get(reviewers_url)
data = response.json()
reviewers: list[GithubListPRReviewersBlock.Output.ReviewerItem] = [
{"username": reviewer["login"], "url": reviewer["html_url"]}
@@ -490,18 +493,18 @@ class GithubListPRReviewersBlock(Block):
]
return reviewers
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
reviewers = self.list_reviewers(
for reviewer in await self.list_reviewers(
credentials,
input_data.pr_url,
)
yield from (("reviewer", reviewer) for reviewer in reviewers)
):
yield "reviewer", reviewer
def prepare_pr_api_url(pr_url: str, path: str) -> str:

View File

@@ -65,12 +65,12 @@ class GithubListTagsBlock(Block):
)
@staticmethod
def list_tags(
async def list_tags(
credentials: GithubCredentials, repo_url: str
) -> list[Output.TagItem]:
api = get_api(credentials)
tags_url = repo_url + "/tags"
response = api.get(tags_url)
response = await api.get(tags_url)
data = response.json()
repo_path = repo_url.replace("https://github.com/", "")
tags: list[GithubListTagsBlock.Output.TagItem] = [
@@ -82,18 +82,19 @@ class GithubListTagsBlock(Block):
]
return tags
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
tags = self.list_tags(
tags = await self.list_tags(
credentials,
input_data.repo_url,
)
yield from (("tag", tag) for tag in tags)
for tag in tags:
yield "tag", tag
class GithubListBranchesBlock(Block):
@@ -147,12 +148,12 @@ class GithubListBranchesBlock(Block):
)
@staticmethod
def list_branches(
async def list_branches(
credentials: GithubCredentials, repo_url: str
) -> list[Output.BranchItem]:
api = get_api(credentials)
branches_url = repo_url + "/branches"
response = api.get(branches_url)
response = await api.get(branches_url)
data = response.json()
repo_path = repo_url.replace("https://github.com/", "")
branches: list[GithubListBranchesBlock.Output.BranchItem] = [
@@ -164,18 +165,19 @@ class GithubListBranchesBlock(Block):
]
return branches
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
branches = self.list_branches(
branches = await self.list_branches(
credentials,
input_data.repo_url,
)
yield from (("branch", branch) for branch in branches)
for branch in branches:
yield "branch", branch
class GithubListDiscussionsBlock(Block):
@@ -234,7 +236,7 @@ class GithubListDiscussionsBlock(Block):
)
@staticmethod
def list_discussions(
async def list_discussions(
credentials: GithubCredentials, repo_url: str, num_discussions: int
) -> list[Output.DiscussionItem]:
api = get_api(credentials)
@@ -254,7 +256,7 @@ class GithubListDiscussionsBlock(Block):
}
"""
variables = {"owner": owner, "repo": repo, "num": num_discussions}
response = api.post(
response = await api.post(
"https://api.github.com/graphql",
json={"query": query, "variables": variables},
)
@@ -265,17 +267,20 @@ class GithubListDiscussionsBlock(Block):
]
return discussions
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
discussions = self.list_discussions(
credentials, input_data.repo_url, input_data.num_discussions
discussions = await self.list_discussions(
credentials,
input_data.repo_url,
input_data.num_discussions,
)
yield from (("discussion", discussion) for discussion in discussions)
for discussion in discussions:
yield "discussion", discussion
class GithubListReleasesBlock(Block):
@@ -329,30 +334,31 @@ class GithubListReleasesBlock(Block):
)
@staticmethod
def list_releases(
async def list_releases(
credentials: GithubCredentials, repo_url: str
) -> list[Output.ReleaseItem]:
api = get_api(credentials)
releases_url = repo_url + "/releases"
response = api.get(releases_url)
response = await api.get(releases_url)
data = response.json()
releases: list[GithubListReleasesBlock.Output.ReleaseItem] = [
{"name": release["name"], "url": release["html_url"]} for release in data
]
return releases
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
releases = self.list_releases(
releases = await self.list_releases(
credentials,
input_data.repo_url,
)
yield from (("release", release) for release in releases)
for release in releases:
yield "release", release
class GithubReadFileBlock(Block):
@@ -405,40 +411,40 @@ class GithubReadFileBlock(Block):
)
@staticmethod
def read_file(
async def read_file(
credentials: GithubCredentials, repo_url: str, file_path: str, branch: str
) -> tuple[str, int]:
api = get_api(credentials)
content_url = repo_url + f"/contents/{file_path}?ref={branch}"
response = api.get(content_url)
content = response.json()
response = await api.get(content_url)
data = response.json()
if isinstance(content, list):
if isinstance(data, list):
# Multiple entries of different types exist at this path
if not (file := next((f for f in content if f["type"] == "file"), None)):
if not (file := next((f for f in data if f["type"] == "file"), None)):
raise TypeError("Not a file")
content = file
data = file
if content["type"] != "file":
if data["type"] != "file":
raise TypeError("Not a file")
return content["content"], content["size"]
return data["content"], data["size"]
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
raw_content, size = self.read_file(
content, size = await self.read_file(
credentials,
input_data.repo_url,
input_data.file_path.lstrip("/"),
input_data.file_path,
input_data.branch,
)
yield "raw_content", raw_content
yield "text_content", base64.b64decode(raw_content).decode("utf-8")
yield "raw_content", content
yield "text_content", base64.b64decode(content).decode("utf-8")
yield "size", size
@@ -515,52 +521,55 @@ class GithubReadFolderBlock(Block):
)
@staticmethod
def read_folder(
async def read_folder(
credentials: GithubCredentials, repo_url: str, folder_path: str, branch: str
) -> tuple[list[Output.FileEntry], list[Output.DirEntry]]:
api = get_api(credentials)
contents_url = repo_url + f"/contents/{folder_path}?ref={branch}"
response = api.get(contents_url)
content = response.json()
response = await api.get(contents_url)
data = response.json()
if not isinstance(content, list):
if not isinstance(data, list):
raise TypeError("Not a folder")
files = [
files: list[GithubReadFolderBlock.Output.FileEntry] = [
GithubReadFolderBlock.Output.FileEntry(
name=entry["name"],
path=entry["path"],
size=entry["size"],
)
for entry in content
for entry in data
if entry["type"] == "file"
]
dirs = [
dirs: list[GithubReadFolderBlock.Output.DirEntry] = [
GithubReadFolderBlock.Output.DirEntry(
name=entry["name"],
path=entry["path"],
)
for entry in content
for entry in data
if entry["type"] == "dir"
]
return files, dirs
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
files, dirs = self.read_folder(
files, dirs = await self.read_folder(
credentials,
input_data.repo_url,
input_data.folder_path.lstrip("/"),
input_data.branch,
)
yield from (("file", file) for file in files)
yield from (("dir", dir) for dir in dirs)
for file in files:
yield "file", file
for dir in dirs:
yield "dir", dir
class GithubMakeBranchBlock(Block):
@@ -606,32 +615,35 @@ class GithubMakeBranchBlock(Block):
)
@staticmethod
def create_branch(
async def create_branch(
credentials: GithubCredentials,
repo_url: str,
new_branch: str,
source_branch: str,
) -> str:
api = get_api(credentials)
# Get the SHA of the source branch
ref_url = repo_url + f"/git/refs/heads/{source_branch}"
response = api.get(ref_url)
sha = response.json()["object"]["sha"]
response = await api.get(ref_url)
data = response.json()
sha = data["object"]["sha"]
# Create the new branch
create_ref_url = repo_url + "/git/refs"
data = {"ref": f"refs/heads/{new_branch}", "sha": sha}
response = api.post(create_ref_url, json=data)
new_ref_url = repo_url + "/git/refs"
data = {
"ref": f"refs/heads/{new_branch}",
"sha": sha,
}
response = await api.post(new_ref_url, json=data)
return "Branch created successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.create_branch(
status = await self.create_branch(
credentials,
input_data.repo_url,
input_data.new_branch,
@@ -678,22 +690,22 @@ class GithubDeleteBranchBlock(Block):
)
@staticmethod
def delete_branch(
async def delete_branch(
credentials: GithubCredentials, repo_url: str, branch: str
) -> str:
api = get_api(credentials)
ref_url = repo_url + f"/git/refs/heads/{branch}"
api.delete(ref_url)
await api.delete(ref_url)
return "Branch deleted successfully"
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
status = self.delete_branch(
status = await self.delete_branch(
credentials,
input_data.repo_url,
input_data.branch,
@@ -761,7 +773,7 @@ class GithubCreateFileBlock(Block):
)
@staticmethod
def create_file(
async def create_file(
credentials: GithubCredentials,
repo_url: str,
file_path: str,
@@ -770,23 +782,18 @@ class GithubCreateFileBlock(Block):
commit_message: str,
) -> tuple[str, str]:
api = get_api(credentials)
# Convert content to base64
content_bytes = content.encode("utf-8")
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
# Create the file using the GitHub API
contents_url = f"{repo_url}/contents/{file_path}"
contents_url = repo_url + f"/contents/{file_path}"
content_base64 = base64.b64encode(content.encode()).decode()
data = {
"message": commit_message,
"content": content_base64,
"branch": branch,
}
response = api.put(contents_url, json=data)
result = response.json()
response = await api.put(contents_url, json=data)
data = response.json()
return data["content"]["html_url"], data["commit"]["sha"]
return result["content"]["html_url"], result["commit"]["sha"]
def run(
async def run(
self,
input_data: Input,
*,
@@ -794,7 +801,7 @@ class GithubCreateFileBlock(Block):
**kwargs,
) -> BlockOutput:
try:
url, sha = self.create_file(
url, sha = await self.create_file(
credentials,
input_data.repo_url,
input_data.file_path,
@@ -866,7 +873,7 @@ class GithubUpdateFileBlock(Block):
)
@staticmethod
def update_file(
async def update_file(
credentials: GithubCredentials,
repo_url: str,
file_path: str,
@@ -875,30 +882,24 @@ class GithubUpdateFileBlock(Block):
commit_message: str,
) -> tuple[str, str]:
api = get_api(credentials)
# First get the current file to get its SHA
contents_url = f"{repo_url}/contents/{file_path}"
contents_url = repo_url + f"/contents/{file_path}"
params = {"ref": branch}
response = api.get(contents_url, params=params)
current_file = response.json()
response = await api.get(contents_url, params=params)
data = response.json()
# Convert new content to base64
content_bytes = content.encode("utf-8")
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
# Update the file
content_base64 = base64.b64encode(content.encode()).decode()
data = {
"message": commit_message,
"content": content_base64,
"sha": current_file["sha"],
"sha": data["sha"],
"branch": branch,
}
response = api.put(contents_url, json=data)
result = response.json()
response = await api.put(contents_url, json=data)
data = response.json()
return data["content"]["html_url"], data["commit"]["sha"]
return result["content"]["html_url"], result["commit"]["sha"]
def run(
async def run(
self,
input_data: Input,
*,
@@ -906,7 +907,7 @@ class GithubUpdateFileBlock(Block):
**kwargs,
) -> BlockOutput:
try:
url, sha = self.update_file(
url, sha = await self.update_file(
credentials,
input_data.repo_url,
input_data.file_path,
@@ -981,7 +982,7 @@ class GithubCreateRepositoryBlock(Block):
)
@staticmethod
def create_repository(
async def create_repository(
credentials: GithubCredentials,
name: str,
description: str,
@@ -989,24 +990,19 @@ class GithubCreateRepositoryBlock(Block):
auto_init: bool,
gitignore_template: str,
) -> tuple[str, str]:
api = get_api(credentials, convert_urls=False) # Disable URL conversion
api = get_api(credentials)
data = {
"name": name,
"description": description,
"private": private,
"auto_init": auto_init,
"gitignore_template": gitignore_template,
}
response = await api.post("https://api.github.com/user/repos", json=data)
data = response.json()
return data["html_url"], data["clone_url"]
if gitignore_template:
data["gitignore_template"] = gitignore_template
# Create repository using the user endpoint
response = api.post("https://api.github.com/user/repos", json=data)
result = response.json()
return result["html_url"], result["clone_url"]
def run(
async def run(
self,
input_data: Input,
*,
@@ -1014,7 +1010,7 @@ class GithubCreateRepositoryBlock(Block):
**kwargs,
) -> BlockOutput:
try:
url, clone_url = self.create_repository(
url, clone_url = await self.create_repository(
credentials,
input_data.name,
input_data.description,
@@ -1081,17 +1077,13 @@ class GithubListStargazersBlock(Block):
)
@staticmethod
def list_stargazers(
async def list_stargazers(
credentials: GithubCredentials, repo_url: str
) -> list[Output.StargazerItem]:
api = get_api(credentials)
# Add /stargazers to the repo URL to get stargazers endpoint
stargazers_url = f"{repo_url}/stargazers"
# Set accept header to get starred_at timestamp
headers = {"Accept": "application/vnd.github.star+json"}
response = api.get(stargazers_url, headers=headers)
stargazers_url = repo_url + "/stargazers"
response = await api.get(stargazers_url)
data = response.json()
stargazers: list[GithubListStargazersBlock.Output.StargazerItem] = [
{
"username": stargazer["login"],
@@ -1101,18 +1093,16 @@ class GithubListStargazersBlock(Block):
]
return stargazers
def run(
async def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
stargazers = self.list_stargazers(
credentials,
input_data.repo_url,
)
yield from (("stargazer", stargazer) for stargazer in stargazers)
except Exception as e:
yield "error", str(e)
stargazers = await self.list_stargazers(
credentials,
input_data.repo_url,
)
for stargazer in stargazers:
yield "stargazer", stargazer

View File

@@ -115,7 +115,7 @@ class GithubCreateStatusBlock(Block):
)
@staticmethod
def create_status(
async def create_status(
credentials: GithubFineGrainedAPICredentials,
repo_url: str,
sha: str,
@@ -144,7 +144,9 @@ class GithubCreateStatusBlock(Block):
data.description = description
status_url = f"{repo_url}/statuses/{sha}"
response = api.post(status_url, data=data.model_dump_json(exclude_none=True))
response = await api.post(
status_url, data=data.model_dump_json(exclude_none=True)
)
result = response.json()
return {
@@ -158,7 +160,7 @@ class GithubCreateStatusBlock(Block):
"updated_at": result["updated_at"],
}
def run(
async def run(
self,
input_data: Input,
*,
@@ -166,7 +168,7 @@ class GithubCreateStatusBlock(Block):
**kwargs,
) -> BlockOutput:
try:
result = self.create_status(
result = await self.create_status(
credentials=credentials,
repo_url=input_data.repo_url,
sha=input_data.sha,

View File

@@ -53,7 +53,7 @@ class GitHubTriggerBase:
description="Error message if the payload could not be processed"
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "payload", input_data.payload
yield "triggered_by_user", input_data.payload["sender"]
@@ -148,8 +148,9 @@ class GithubPullRequestTriggerBlock(GitHubTriggerBase, Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
yield from super().run(input_data, **kwargs)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
yield "event", input_data.payload["action"]
yield "number", input_data.payload["number"]
yield "pull_request", input_data.payload["pull_request"]

View File

@@ -1,3 +1,4 @@
import asyncio
import enum
import uuid
from datetime import datetime, timedelta, timezone
@@ -168,7 +169,7 @@ class GoogleCalendarReadEventsBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
try:
@@ -180,7 +181,8 @@ class GoogleCalendarReadEventsBlock(Block):
)
# Call Google Calendar API
result = self._read_calendar(
result = await asyncio.to_thread(
self._read_calendar,
service=service,
calendarId=input_data.calendar_id,
time_min=input_data.start_time.isoformat(),
@@ -477,12 +479,13 @@ class GoogleCalendarCreateEventBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
try:
service = self._build_service(credentials, **kwargs)
# Create event body
# Get start and end times based on the timing option
if input_data.timing.discriminator == "exact_timing":
start_datetime = input_data.timing.start_datetime
@@ -543,7 +546,8 @@ class GoogleCalendarCreateEventBlock(Block):
event_body["recurrence"] = [rule]
# Create the event
result = self._create_event(
result = await asyncio.to_thread(
self._create_event,
service=service,
calendar_id=input_data.calendar_id,
event_body=event_body,
@@ -551,8 +555,9 @@ class GoogleCalendarCreateEventBlock(Block):
conference_data_version=1 if input_data.add_google_meet else 0,
)
yield "event_id", result.get("id", "")
yield "event_link", result.get("htmlLink", "")
yield "event_id", result["id"]
yield "event_link", result["htmlLink"]
except Exception as e:
yield "error", str(e)

View File

@@ -1,3 +1,4 @@
import asyncio
import base64
from email.utils import parseaddr
from typing import List
@@ -128,11 +129,13 @@ class GmailReadBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = self._build_service(credentials, **kwargs)
messages = self._read_emails(service, input_data.query, input_data.max_results)
service = GmailReadBlock._build_service(credentials, **kwargs)
messages = await asyncio.to_thread(
self._read_emails, service, input_data.query, input_data.max_results
)
for email in messages:
yield "email", email
yield "emails", messages
@@ -286,14 +289,18 @@ class GmailSendBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = GmailReadBlock._build_service(credentials, **kwargs)
send_result = self._send_email(
service, input_data.to, input_data.subject, input_data.body
result = await asyncio.to_thread(
self._send_email,
service,
input_data.to,
input_data.subject,
input_data.body,
)
yield "result", send_result
yield "result", result
def _send_email(self, service, to: str, subject: str, body: str) -> dict:
if not to or not subject or not body:
@@ -358,12 +365,12 @@ class GmailListLabelsBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = GmailReadBlock._build_service(credentials, **kwargs)
labels = self._list_labels(service)
yield "result", labels
result = await asyncio.to_thread(self._list_labels, service)
yield "result", result
def _list_labels(self, service) -> list[dict]:
results = service.users().labels().list(userId="me").execute()
@@ -419,11 +426,13 @@ class GmailAddLabelBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = GmailReadBlock._build_service(credentials, **kwargs)
result = self._add_label(service, input_data.message_id, input_data.label_name)
result = await asyncio.to_thread(
self._add_label, service, input_data.message_id, input_data.label_name
)
yield "result", result
def _add_label(self, service, message_id: str, label_name: str) -> dict:
@@ -502,12 +511,12 @@ class GmailRemoveLabelBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = GmailReadBlock._build_service(credentials, **kwargs)
result = self._remove_label(
service, input_data.message_id, input_data.label_name
result = await asyncio.to_thread(
self._remove_label, service, input_data.message_id, input_data.label_name
)
yield "result", result

View File

@@ -1,3 +1,5 @@
import asyncio
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
@@ -68,11 +70,13 @@ class GoogleSheetsReadBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = self._build_service(credentials, **kwargs)
data = self._read_sheet(service, input_data.spreadsheet_id, input_data.range)
data = await asyncio.to_thread(
self._read_sheet, service, input_data.spreadsheet_id, input_data.range
)
yield "result", data
@staticmethod
@@ -157,11 +161,12 @@ class GoogleSheetsWriteBlock(Block):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
) -> BlockOutput:
service = GoogleSheetsReadBlock._build_service(credentials, **kwargs)
result = self._write_sheet(
result = await asyncio.to_thread(
self._write_sheet,
service,
input_data.spreadsheet_id,
input_data.range,

View File

@@ -103,7 +103,7 @@ class GoogleMapsSearchBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
places = self.search_places(

View File

@@ -1,14 +1,17 @@
from typing import Any, Optional
from backend.util.request import requests
from backend.util.request import Requests
class GetRequest:
@classmethod
def get_request(
async def get_request(
cls, url: str, headers: Optional[dict] = None, json: bool = False
) -> Any:
if headers is None:
headers = {}
response = requests.get(url, headers=headers)
return response.json() if json else response.text
response = await Requests().get(url, headers=headers)
if json:
return response.json()
else:
return response.text()

View File

@@ -1,10 +1,10 @@
import json
import logging
from enum import Enum
from io import BufferedReader
from io import BytesIO
from pathlib import Path
from requests.exceptions import HTTPError, RequestException
import aiofiles
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
@@ -14,7 +14,7 @@ from backend.util.file import (
get_mime_type,
store_media_file,
)
from backend.util.request import requests
from backend.util.request import Requests
logger = logging.getLogger(name=__name__)
@@ -77,54 +77,64 @@ class SendWebRequestBlock(Block):
)
@staticmethod
def _prepare_files(
async def _prepare_files(
graph_exec_id: str,
files_name: str,
files: list[MediaFileType],
) -> tuple[list[tuple[str, tuple[str, BufferedReader, str]]], list[BufferedReader]]:
"""Convert the `files` mapping into the structure expected by `requests`.
Returns a tuple of (**files_payload**, **open_handles**) so we can close handles later.
) -> list[tuple[str, tuple[str, BytesIO, str]]]:
"""
files_payload: list[tuple[str, tuple[str, BufferedReader, str]]] = []
open_handles: list[BufferedReader] = []
Prepare files for the request by storing them and reading their content.
Returns a list of tuples in the format:
(files_name, (filename, BytesIO, mime_type))
"""
files_payload: list[tuple[str, tuple[str, BytesIO, str]]] = []
for media in files:
# Normalise to a list so we can repeat the same key
rel_path = store_media_file(graph_exec_id, media, return_content=False)
rel_path = await store_media_file(
graph_exec_id, media, return_content=False
)
abs_path = get_exec_file_path(graph_exec_id, rel_path)
try:
handle = open(abs_path, "rb")
except Exception as e:
for h in open_handles:
try:
h.close()
except Exception:
pass
raise RuntimeError(f"Failed to open file '{abs_path}': {e}") from e
async with aiofiles.open(abs_path, "rb") as f:
content = await f.read()
handle = BytesIO(content)
mime = get_mime_type(abs_path)
files_payload.append((files_name, (Path(abs_path).name, handle, mime)))
open_handles.append(handle)
mime = get_mime_type(abs_path)
files_payload.append((files_name, (Path(abs_path).name, handle, mime)))
return files_payload
return files_payload, open_handles
def run(self, input_data: Input, *, graph_exec_id: str, **kwargs) -> BlockOutput:
async def run(
self, input_data: Input, *, graph_exec_id: str, **kwargs
) -> BlockOutput:
# ─── Parse/normalise body ────────────────────────────────────
body = input_data.body
if isinstance(body, str):
try:
body = json.loads(body)
except json.JSONDecodeError:
# plain text treat as formfield value instead
# Validate JSON string length to prevent DoS attacks
if len(body) > 10_000_000: # 10MB limit
raise ValueError("JSON body too large")
parsed_body = json.loads(body)
# Validate that parsed JSON is safe (basic object/array/primitive types)
if (
isinstance(parsed_body, (dict, list, str, int, float, bool))
or parsed_body is None
):
body = parsed_body
else:
# Unexpected type, treat as plain text
input_data.json_format = False
except (json.JSONDecodeError, ValueError):
# Invalid JSON or too large treat as formfield value instead
input_data.json_format = False
# ─── Prepare files (if any) ──────────────────────────────────
use_files = bool(input_data.files)
files_payload: list[tuple[str, tuple[str, BufferedReader, str]]] = []
open_handles: list[BufferedReader] = []
files_payload: list[tuple[str, tuple[str, BytesIO, str]]] = []
if use_files:
files_payload, open_handles = self._prepare_files(
files_payload = await self._prepare_files(
graph_exec_id, input_data.files_name, input_data.files
)
@@ -135,47 +145,27 @@ class SendWebRequestBlock(Block):
)
# ─── Execute request ─────────────────────────────────────────
try:
response = requests.request(
input_data.method.value,
input_data.url,
headers=input_data.headers,
files=files_payload if use_files else None,
# * If files → multipart ⇒ pass formfields via data=
data=body if not input_data.json_format else None,
# * Else, choose JSON vs urlencoded based on flag
json=body if (input_data.json_format and not use_files) else None,
)
response = await Requests().request(
input_data.method.value,
input_data.url,
headers=input_data.headers,
files=files_payload if use_files else None,
# * If files → multipart ⇒ pass formfields via data=
data=body if not input_data.json_format else None,
# * Else, choose JSON vs urlencoded based on flag
json=body if (input_data.json_format and not use_files) else None,
)
# Decide how to parse the response
if input_data.json_format or response.headers.get(
"content-type", ""
).startswith("application/json"):
result = (
None
if (response.status_code == 204 or not response.content.strip())
else response.json()
)
else:
result = response.text
# Decide how to parse the response
if response.headers.get("content-type", "").startswith("application/json"):
result = None if response.status == 204 else response.json()
else:
result = response.text()
# Yield according to status code bucket
if 200 <= response.status_code < 300:
yield "response", result
elif 400 <= response.status_code < 500:
yield "client_error", result
else:
yield "server_error", result
except HTTPError as e:
yield "error", f"HTTP error: {str(e)}"
except RequestException as e:
yield "error", f"Request error: {str(e)}"
except Exception as e:
yield "error", str(e)
finally:
for h in open_handles:
try:
h.close()
except Exception:
pass
# Yield according to status code bucket
if 200 <= response.status < 300:
yield "response", result
elif 400 <= response.status < 500:
yield "client_error", result
else:
yield "server_error", result

View File

@@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class HubSpotCompanyBlock(Block):
@@ -35,7 +35,7 @@ class HubSpotCompanyBlock(Block):
output_schema=HubSpotCompanyBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs
) -> BlockOutput:
base_url = "https://api.hubapi.com/crm/v3/objects/companies"
@@ -45,7 +45,7 @@ class HubSpotCompanyBlock(Block):
}
if input_data.operation == "create":
response = requests.post(
response = await Requests().post(
base_url, headers=headers, json={"properties": input_data.company_data}
)
result = response.json()
@@ -67,14 +67,16 @@ class HubSpotCompanyBlock(Block):
}
]
}
response = requests.post(search_url, headers=headers, json=search_data)
result = response.json()
yield "company", result.get("results", [{}])[0]
search_response = await Requests().post(
search_url, headers=headers, json=search_data
)
search_result = search_response.json()
yield "search_company", search_result.get("results", [{}])[0]
yield "status", "retrieved"
elif input_data.operation == "update":
# First get company ID by domain
search_response = requests.post(
search_response = await Requests().post(
f"{base_url}/search",
headers=headers,
json={
@@ -91,10 +93,11 @@ class HubSpotCompanyBlock(Block):
]
},
)
company_id = search_response.json().get("results", [{}])[0].get("id")
search_result = search_response.json()
company_id = search_result.get("results", [{}])[0].get("id")
if company_id:
response = requests.patch(
response = await Requests().patch(
f"{base_url}/{company_id}",
headers=headers,
json={"properties": input_data.company_data},

View File

@@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class HubSpotContactBlock(Block):
@@ -35,7 +35,7 @@ class HubSpotContactBlock(Block):
output_schema=HubSpotContactBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs
) -> BlockOutput:
base_url = "https://api.hubapi.com/crm/v3/objects/contacts"
@@ -45,7 +45,7 @@ class HubSpotContactBlock(Block):
}
if input_data.operation == "create":
response = requests.post(
response = await Requests().post(
base_url, headers=headers, json={"properties": input_data.contact_data}
)
result = response.json()
@@ -53,7 +53,6 @@ class HubSpotContactBlock(Block):
yield "status", "created"
elif input_data.operation == "get":
# Search for contact by email
search_url = f"{base_url}/search"
search_data = {
"filterGroups": [
@@ -68,13 +67,15 @@ class HubSpotContactBlock(Block):
}
]
}
response = requests.post(search_url, headers=headers, json=search_data)
response = await Requests().post(
search_url, headers=headers, json=search_data
)
result = response.json()
yield "contact", result.get("results", [{}])[0]
yield "status", "retrieved"
elif input_data.operation == "update":
search_response = requests.post(
search_response = await Requests().post(
f"{base_url}/search",
headers=headers,
json={
@@ -91,10 +92,11 @@ class HubSpotContactBlock(Block):
]
},
)
contact_id = search_response.json().get("results", [{}])[0].get("id")
search_result = search_response.json()
contact_id = search_result.get("results", [{}])[0].get("id")
if contact_id:
response = requests.patch(
response = await Requests().patch(
f"{base_url}/{contact_id}",
headers=headers,
json={"properties": input_data.contact_data},

View File

@@ -7,7 +7,7 @@ from backend.blocks.hubspot._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class HubSpotEngagementBlock(Block):
@@ -42,7 +42,7 @@ class HubSpotEngagementBlock(Block):
output_schema=HubSpotEngagementBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs
) -> BlockOutput:
base_url = "https://api.hubapi.com"
@@ -66,7 +66,9 @@ class HubSpotEngagementBlock(Block):
}
}
response = requests.post(email_url, headers=headers, json=email_data)
response = await Requests().post(
email_url, headers=headers, json=email_data
)
result = response.json()
yield "result", result
yield "status", "email_sent"
@@ -80,7 +82,9 @@ class HubSpotEngagementBlock(Block):
params = {"limit": 100, "after": from_date.isoformat()}
response = requests.get(engagement_url, headers=headers, params=params)
response = await Requests().get(
engagement_url, headers=headers, params=params
)
engagements = response.json()
# Process engagement metrics

View File

@@ -12,7 +12,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -196,13 +196,13 @@ class IdeogramModelBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
seed = input_data.seed
# Step 1: Generate the image
result = self.run_model(
result = await self.run_model(
api_key=credentials.api_key,
model_name=input_data.ideogram_model_name.value,
prompt=input_data.prompt,
@@ -217,14 +217,14 @@ class IdeogramModelBlock(Block):
# Step 2: Upscale the image if requested
if input_data.upscale == UpscaleOption.AI_UPSCALE:
result = self.upscale_image(
result = await self.upscale_image(
api_key=credentials.api_key,
image_url=result,
)
yield "result", result
def run_model(
async def run_model(
self,
api_key: SecretStr,
model_name: str,
@@ -267,12 +267,12 @@ class IdeogramModelBlock(Block):
}
try:
response = requests.post(url, json=data, headers=headers)
response = await Requests().post(url, headers=headers, json=data)
return response.json()["data"][0]["url"]
except RequestException as e:
raise Exception(f"Failed to fetch image: {str(e)}")
def upscale_image(self, api_key: SecretStr, image_url: str):
async def upscale_image(self, api_key: SecretStr, image_url: str):
url = "https://api.ideogram.ai/upscale"
headers = {
"Api-Key": api_key.get_secret_value(),
@@ -280,21 +280,22 @@ class IdeogramModelBlock(Block):
try:
# Step 1: Download the image from the provided URL
image_response = requests.get(image_url)
response = await Requests().get(image_url)
image_content = response.content
# Step 2: Send the downloaded image to the upscale API
files = {
"image_file": ("image.png", image_response.content, "image/png"),
"image_file": ("image.png", image_content, "image/png"),
}
response = requests.post(
response = await Requests().post(
url,
headers=headers,
data={"image_request": "{}"},
files=files,
)
return response.json()["data"][0]["url"]
return (response.json())["data"][0]["url"]
except RequestException as e:
raise Exception(f"Failed to upscale image: {str(e)}")

View File

@@ -95,7 +95,7 @@ class AgentInputBlock(Block):
}
)
def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
if input_data.value is not None:
yield "result", input_data.value
@@ -186,7 +186,7 @@ class AgentOutputBlock(Block):
static_output=True,
)
def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
"""
Attempts to format the recorded_value using the fmt_string if provided.
If formatting fails or no fmt_string is given, returns the original recorded_value.
@@ -436,7 +436,7 @@ class AgentFileInputBlock(AgentInputBlock):
],
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -446,7 +446,7 @@ class AgentFileInputBlock(AgentInputBlock):
if not input_data.value:
return
file_path = store_media_file(
file_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.value,
return_content=False,

View File

@@ -53,7 +53,7 @@ class StepThroughItemsBlock(Block):
test_mock={},
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
for data in [input_data.items, input_data.items_object, input_data.items_str]:
if not data:
continue

View File

@@ -5,7 +5,7 @@ from backend.blocks.jina._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class JinaChunkingBlock(Block):
@@ -35,7 +35,7 @@ class JinaChunkingBlock(Block):
output_schema=JinaChunkingBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
url = "https://segment.jina.ai/"
@@ -55,7 +55,7 @@ class JinaChunkingBlock(Block):
"max_chunk_length": str(input_data.max_chunk_length),
}
response = requests.post(url, headers=headers, json=data)
response = await Requests().post(url, headers=headers, json=data)
result = response.json()
all_chunks.extend(result.get("chunks", []))

View File

@@ -5,7 +5,7 @@ from backend.blocks.jina._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
class JinaEmbeddingBlock(Block):
@@ -29,7 +29,7 @@ class JinaEmbeddingBlock(Block):
output_schema=JinaEmbeddingBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
url = "https://api.jina.ai/v1/embeddings"
@@ -38,6 +38,6 @@ class JinaEmbeddingBlock(Block):
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
}
data = {"input": input_data.texts, "model": input_data.model}
response = requests.post(url, headers=headers, json=data)
response = await Requests().post(url, headers=headers, json=data)
embeddings = [e["embedding"] for e in response.json()["data"]]
yield "embeddings", embeddings

View File

@@ -1,7 +1,5 @@
from urllib.parse import quote
import requests
from backend.blocks.jina._auth import (
JinaCredentials,
JinaCredentialsField,
@@ -9,6 +7,7 @@ from backend.blocks.jina._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import Requests
class FactCheckerBlock(Block):
@@ -35,7 +34,7 @@ class FactCheckerBlock(Block):
output_schema=FactCheckerBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
encoded_statement = quote(input_data.statement)
@@ -46,8 +45,7 @@ class FactCheckerBlock(Block):
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
}
response = requests.get(url, headers=headers)
response.raise_for_status()
response = await Requests().get(url, headers=headers)
data = response.json()
if "data" in data:

View File

@@ -39,7 +39,7 @@ class SearchTheWebBlock(Block, GetRequest):
test_mock={"get_request": lambda *args, **kwargs: "search content"},
)
def run(
async def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
# Encode the search query
@@ -51,7 +51,7 @@ class SearchTheWebBlock(Block, GetRequest):
# Prepend the Jina Search URL to the encoded query
jina_search_url = f"https://s.jina.ai/{encoded_query}"
results = self.get_request(jina_search_url, headers=headers, json=False)
results = await self.get_request(jina_search_url, headers=headers, json=False)
# Output the search results
yield "results", results
@@ -90,7 +90,7 @@ class ExtractWebsiteContentBlock(Block, GetRequest):
test_mock={"get_request": lambda *args, **kwargs: "scraped content"},
)
def run(
async def run(
self, input_data: Input, *, credentials: JinaCredentials, **kwargs
) -> BlockOutput:
if input_data.raw_content:
@@ -103,5 +103,5 @@ class ExtractWebsiteContentBlock(Block, GetRequest):
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
}
content = self.get_request(url, json=False, headers=headers)
content = await self.get_request(url, json=False, headers=headers)
yield "content", content

View File

@@ -48,7 +48,7 @@ class LinearClient:
raise_for_status=False,
)
def _execute_graphql_request(
async def _execute_graphql_request(
self, query: str, variables: dict | None = None
) -> Any:
"""
@@ -65,19 +65,18 @@ class LinearClient:
if variables:
payload["variables"] = variables
response = self._requests.post(self.API_URL, json=payload)
response = await self._requests.post(self.API_URL, json=payload)
if not response.ok:
try:
error_data = response.json()
error_message = error_data.get("errors", [{}])[0].get("message", "")
except json.JSONDecodeError:
error_message = response.text
error_message = response.text()
raise LinearAPIException(
f"Linear API request failed ({response.status_code}): {error_message}",
response.status_code,
f"Linear API request failed ({response.status}): {error_message}",
response.status,
)
response_data = response.json()
@@ -88,12 +87,12 @@ class LinearClient:
]
raise LinearAPIException(
f"Linear API returned errors: {', '.join(error_messages)}",
response.status_code,
response.status,
)
return response_data["data"]
def query(self, query: str, variables: Optional[dict] = None) -> dict:
async def query(self, query: str, variables: Optional[dict] = None) -> dict:
"""Executes a GraphQL query.
Args:
@@ -103,9 +102,9 @@ class LinearClient:
Returns:
The response data.
"""
return self._execute_graphql_request(query, variables)
return await self._execute_graphql_request(query, variables)
def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict:
async def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict:
"""Executes a GraphQL mutation.
Args:
@@ -115,9 +114,11 @@ class LinearClient:
Returns:
The response data.
"""
return self._execute_graphql_request(mutation, variables)
return await self._execute_graphql_request(mutation, variables)
def try_create_comment(self, issue_id: str, comment: str) -> CreateCommentResponse:
async def try_create_comment(
self, issue_id: str, comment: str
) -> CreateCommentResponse:
try:
mutation = """
mutation CommentCreate($input: CommentCreateInput!) {
@@ -138,13 +139,13 @@ class LinearClient:
}
}
added_comment = self.mutate(mutation, variables)
added_comment = await self.mutate(mutation, variables)
# Select the commentCreate field from the mutation response
return CreateCommentResponse(**added_comment["commentCreate"])
except LinearAPIException as e:
raise e
def try_get_team_by_name(self, team_name: str) -> str:
async def try_get_team_by_name(self, team_name: str) -> str:
try:
query = """
query GetTeamId($searchTerm: String!) {
@@ -167,12 +168,12 @@ class LinearClient:
"searchTerm": team_name,
}
team_id = self.query(query, variables)
team_id = await self.query(query, variables)
return team_id["teams"]["nodes"][0]["id"]
except LinearAPIException as e:
raise e
def try_create_issue(
async def try_create_issue(
self,
team_id: str,
title: str,
@@ -211,12 +212,12 @@ class LinearClient:
if priority:
variables["input"]["priority"] = priority
added_issue = self.mutate(mutation, variables)
added_issue = await self.mutate(mutation, variables)
return CreateIssueResponse(**added_issue["issueCreate"])
except LinearAPIException as e:
raise e
def try_search_projects(self, term: str) -> list[Project]:
async def try_search_projects(self, term: str) -> list[Project]:
try:
query = """
query SearchProjects($term: String!, $includeComments: Boolean!) {
@@ -238,14 +239,14 @@ class LinearClient:
"includeComments": True,
}
projects = self.query(query, variables)
projects = await self.query(query, variables)
return [
Project(**project) for project in projects["searchProjects"]["nodes"]
]
except LinearAPIException as e:
raise e
def try_search_issues(self, term: str) -> list[Issue]:
async def try_search_issues(self, term: str) -> list[Issue]:
try:
query = """
query SearchIssues($term: String!, $includeComments: Boolean!) {
@@ -266,7 +267,7 @@ class LinearClient:
"includeComments": True,
}
issues = self.query(query, variables)
issues = await self.query(query, variables)
return [Issue(**issue) for issue in issues["searchIssues"]["nodes"]]
except LinearAPIException as e:
raise e

View File

@@ -54,21 +54,21 @@ class LinearCreateCommentBlock(Block):
)
@staticmethod
def create_comment(
async def create_comment(
credentials: LinearCredentials, issue_id: str, comment: str
) -> tuple[str, str]:
client = LinearClient(credentials=credentials)
response: CreateCommentResponse = client.try_create_comment(
response: CreateCommentResponse = await client.try_create_comment(
issue_id=issue_id, comment=comment
)
return response.comment.id, response.comment.body
def run(
async def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the comment creation"""
try:
comment_id, comment_body = self.create_comment(
comment_id, comment_body = await self.create_comment(
credentials=credentials,
issue_id=input_data.issue_id,
comment=input_data.comment,

View File

@@ -67,7 +67,7 @@ class LinearCreateIssueBlock(Block):
)
@staticmethod
def create_issue(
async def create_issue(
credentials: LinearCredentials,
team_name: str,
title: str,
@@ -76,15 +76,15 @@ class LinearCreateIssueBlock(Block):
project_name: str | None = None,
) -> tuple[str, str]:
client = LinearClient(credentials=credentials)
team_id = client.try_get_team_by_name(team_name=team_name)
team_id = await client.try_get_team_by_name(team_name=team_name)
project_id: str | None = None
if project_name:
projects = client.try_search_projects(term=project_name)
projects = await client.try_search_projects(term=project_name)
if projects:
project_id = projects[0].id
else:
raise LinearAPIException("Project not found", status_code=404)
response: CreateIssueResponse = client.try_create_issue(
response: CreateIssueResponse = await client.try_create_issue(
team_id=team_id,
title=title,
description=description,
@@ -93,12 +93,12 @@ class LinearCreateIssueBlock(Block):
)
return response.issue.identifier, response.issue.title
def run(
async def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the issue creation"""
try:
issue_id, issue_title = self.create_issue(
issue_id, issue_title = await self.create_issue(
credentials=credentials,
team_name=input_data.team_name,
title=input_data.title,
@@ -168,20 +168,22 @@ class LinearSearchIssuesBlock(Block):
)
@staticmethod
def search_issues(
async def search_issues(
credentials: LinearCredentials,
term: str,
) -> list[Issue]:
client = LinearClient(credentials=credentials)
response: list[Issue] = client.try_search_issues(term=term)
response: list[Issue] = await client.try_search_issues(term=term)
return response
def run(
async def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the issue search"""
try:
issues = self.search_issues(credentials=credentials, term=input_data.term)
issues = await self.search_issues(
credentials=credentials, term=input_data.term
)
yield "issues", issues
except LinearAPIException as e:
yield "error", str(e)

View File

@@ -69,20 +69,20 @@ class LinearSearchProjectsBlock(Block):
)
@staticmethod
def search_projects(
async def search_projects(
credentials: LinearCredentials,
term: str,
) -> list[Project]:
client = LinearClient(credentials=credentials)
response: list[Project] = client.try_search_projects(term=term)
response: list[Project] = await client.try_search_projects(term=term)
return response
def run(
async def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the project search"""
try:
projects = self.search_projects(
projects = await self.search_projects(
credentials=credentials,
term=input_data.term,
)

View File

@@ -3,14 +3,13 @@ import logging
from abc import ABC
from enum import Enum, EnumMeta
from json import JSONDecodeError
from types import MappingProxyType
from typing import Any, Iterable, List, Literal, NamedTuple, Optional
import anthropic
import ollama
import openai
from anthropic.types import ToolParam
from groq import Groq
from groq import AsyncGroq
from pydantic import BaseModel, SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
@@ -24,7 +23,6 @@ from backend.data.model import (
from backend.integrations.providers import ProviderName
from backend.util import json
from backend.util.logging import TruncatedLogger
from backend.util.settings import BehaveAs, Settings
from backend.util.text import TextFormatter
logger = TruncatedLogger(logging.getLogger(__name__), "[LLM-Block]")
@@ -73,20 +71,7 @@ class ModelMetadata(NamedTuple):
class LlmModelMeta(EnumMeta):
@property
def __members__(self) -> MappingProxyType:
if Settings().config.behave_as == BehaveAs.LOCAL:
members = super().__members__
return MappingProxyType(members)
else:
removed_providers = ["ollama"]
existing_members = super().__members__
members = {
name: member
for name, member in existing_members.items()
if LlmModel[name].provider not in removed_providers
}
return MappingProxyType(members)
pass
class LlmModel(str, Enum, metaclass=LlmModelMeta):
@@ -328,7 +313,7 @@ def estimate_token_count(prompt_messages: list[dict]) -> int:
return int(estimated_tokens * 1.2)
def llm_call(
async def llm_call(
credentials: APIKeyCredentials,
llm_model: LlmModel,
prompt: list[dict],
@@ -363,14 +348,14 @@ def llm_call(
# Calculate available tokens based on context window and input length
estimated_input_tokens = estimate_token_count(prompt)
context_window = llm_model.context_window
model_max_output = llm_model.max_output_tokens or 4096
model_max_output = llm_model.max_output_tokens or int(2**15)
user_max = max_tokens or model_max_output
available_tokens = max(context_window - estimated_input_tokens, 0)
max_tokens = max(min(available_tokens, model_max_output, user_max), 0)
max_tokens = max(min(available_tokens, model_max_output, user_max), 1)
if provider == "openai":
tools_param = tools if tools else openai.NOT_GIVEN
oai_client = openai.OpenAI(api_key=credentials.api_key.get_secret_value())
oai_client = openai.AsyncOpenAI(api_key=credentials.api_key.get_secret_value())
response_format = None
if llm_model in [LlmModel.O1_MINI, LlmModel.O1_PREVIEW]:
@@ -383,7 +368,7 @@ def llm_call(
elif json_format:
response_format = {"type": "json_object"}
response = oai_client.chat.completions.create(
response = await oai_client.chat.completions.create(
model=llm_model.value,
messages=prompt, # type: ignore
response_format=response_format, # type: ignore
@@ -439,9 +424,11 @@ def llm_call(
messages.append({"role": p["role"], "content": p["content"]})
last_role = p["role"]
client = anthropic.Anthropic(api_key=credentials.api_key.get_secret_value())
client = anthropic.AsyncAnthropic(
api_key=credentials.api_key.get_secret_value()
)
try:
resp = client.messages.create(
resp = await client.messages.create(
model=llm_model.value,
system=sysprompt,
messages=messages,
@@ -495,9 +482,9 @@ def llm_call(
if tools:
raise ValueError("Groq does not support tools.")
client = Groq(api_key=credentials.api_key.get_secret_value())
client = AsyncGroq(api_key=credentials.api_key.get_secret_value())
response_format = {"type": "json_object"} if json_format else None
response = client.chat.completions.create(
response = await client.chat.completions.create(
model=llm_model.value,
messages=prompt, # type: ignore
response_format=response_format, # type: ignore
@@ -515,10 +502,10 @@ def llm_call(
if tools:
raise ValueError("Ollama does not support tools.")
client = ollama.Client(host=ollama_host)
client = ollama.AsyncClient(host=ollama_host)
sys_messages = [p["content"] for p in prompt if p["role"] == "system"]
usr_messages = [p["content"] for p in prompt if p["role"] != "system"]
response = client.generate(
response = await client.generate(
model=llm_model.value,
prompt=f"{sys_messages}\n\n{usr_messages}",
stream=False,
@@ -534,12 +521,12 @@ def llm_call(
)
elif provider == "open_router":
tools_param = tools if tools else openai.NOT_GIVEN
client = openai.OpenAI(
client = openai.AsyncOpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=credentials.api_key.get_secret_value(),
)
response = client.chat.completions.create(
response = await client.chat.completions.create(
extra_headers={
"HTTP-Referer": "https://agpt.co",
"X-Title": "AutoGPT",
@@ -581,12 +568,12 @@ def llm_call(
)
elif provider == "llama_api":
tools_param = tools if tools else openai.NOT_GIVEN
client = openai.OpenAI(
client = openai.AsyncOpenAI(
base_url="https://api.llama.com/compat/v1/",
api_key=credentials.api_key.get_secret_value(),
)
response = client.chat.completions.create(
response = await client.chat.completions.create(
extra_headers={
"HTTP-Referer": "https://agpt.co",
"X-Title": "AutoGPT",
@@ -676,6 +663,11 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
description="Expected format of the response. If provided, the response will be validated against this format. "
"The keys should be the expected fields in the response, and the values should be the description of the field.",
)
list_result: bool = SchemaField(
title="List Result",
default=False,
description="Whether the response should be a list of objects in the expected format.",
)
model: LlmModel = SchemaField(
title="LLM Model",
default=LlmModel.GPT4O,
@@ -715,7 +707,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
)
class Output(BlockSchema):
response: dict[str, Any] = SchemaField(
response: dict[str, Any] | list[dict[str, Any]] = SchemaField(
description="The response object generated by the language model."
)
prompt: list = SchemaField(description="The prompt sent to the language model.")
@@ -759,7 +751,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
},
)
def llm_call(
async def llm_call(
self,
credentials: APIKeyCredentials,
llm_model: LlmModel,
@@ -774,7 +766,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
so that it can be mocked withing the block testing framework.
"""
self.prompt = prompt
return llm_call(
return await llm_call(
credentials=credentials,
llm_model=llm_model,
prompt=prompt,
@@ -784,7 +776,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
ollama_host=ollama_host,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
logger.debug(f"Calling LLM with input data: {input_data}")
@@ -806,13 +798,22 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
expected_format = [
f'"{k}": "{v}"' for k, v in input_data.expected_format.items()
]
format_prompt = ",\n ".join(expected_format)
if input_data.list_result:
format_prompt = (
f'"results": [\n {{\n {", ".join(expected_format)}\n }}\n]'
)
else:
format_prompt = "\n ".join(expected_format)
sys_prompt = trim_prompt(
f"""
|Reply strictly only in the following JSON format:
|{{
| {format_prompt}
|}}
|
|Ensure the response is valid JSON. Do not include any additional text outside of the JSON.
|If you cannot provide all the keys, provide an empty string for the values you cannot answer.
"""
)
prompt.append({"role": "system", "content": sys_prompt})
@@ -820,17 +821,16 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
if input_data.prompt:
prompt.append({"role": "user", "content": input_data.prompt})
def parse_response(resp: str) -> tuple[dict[str, Any], str | None]:
def validate_response(parsed: object) -> str | None:
try:
parsed = json.loads(resp)
if not isinstance(parsed, dict):
return {}, f"Expected a dictionary, but got {type(parsed)}"
return f"Expected a dictionary, but got {type(parsed)}"
miss_keys = set(input_data.expected_format.keys()) - set(parsed.keys())
if miss_keys:
return parsed, f"Missing keys: {miss_keys}"
return parsed, None
return f"Missing keys: {miss_keys}"
return None
except JSONDecodeError as e:
return {}, f"JSON decode error: {e}"
return f"JSON decode error: {e}"
logger.info(f"LLM request: {prompt}")
retry_prompt = ""
@@ -838,7 +838,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
for retry_count in range(input_data.retry):
try:
llm_response = self.llm_call(
llm_response = await self.llm_call(
credentials=credentials,
llm_model=llm_model,
prompt=prompt,
@@ -856,18 +856,29 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
logger.info(f"LLM attempt-{retry_count} response: {response_text}")
if input_data.expected_format:
parsed_dict, parsed_error = parse_response(response_text)
if not parsed_error:
yield "response", {
k: (
json.loads(v)
if isinstance(v, str)
and v.startswith("[")
and v.endswith("]")
else (", ".join(v) if isinstance(v, list) else v)
response_obj = json.loads(response_text)
if input_data.list_result and isinstance(response_obj, dict):
if "results" in response_obj:
response_obj = response_obj.get("results", [])
elif len(response_obj) == 1:
response_obj = list(response_obj.values())
response_error = "\n".join(
[
validation_error
for response_item in (
response_obj
if isinstance(response_obj, list)
else [response_obj]
)
for k, v in parsed_dict.items()
}
if (validation_error := validate_response(response_item))
]
)
if not response_error:
yield "response", response_obj
yield "prompt", self.prompt
return
else:
@@ -884,7 +895,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
|
|And this is the error:
|--
|{parsed_error}
|{response_error}
|--
"""
)
@@ -978,17 +989,17 @@ class AITextGeneratorBlock(AIBlockBase):
test_mock={"llm_call": lambda *args, **kwargs: "Response text"},
)
def llm_call(
async def llm_call(
self,
input_data: AIStructuredResponseGeneratorBlock.Input,
credentials: APIKeyCredentials,
) -> str:
) -> dict:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
response = await block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
return response["response"]
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
object_input_data = AIStructuredResponseGeneratorBlock.Input(
@@ -998,7 +1009,8 @@ class AITextGeneratorBlock(AIBlockBase):
},
expected_format={},
)
yield "response", self.llm_call(object_input_data, credentials)
response = await self.llm_call(object_input_data, credentials)
yield "response", response
yield "prompt", self.prompt
@@ -1080,23 +1092,27 @@ class AITextSummarizerBlock(AIBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
for output_name, output_data in self._run(input_data, credentials):
async for output_name, output_data in self._run(input_data, credentials):
yield output_name, output_data
def _run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput:
async 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, credentials)
chunk_summary = await self._summarize_chunk(chunk, input_data, credentials)
summaries.append(chunk_summary)
final_summary = self._combine_summaries(summaries, input_data, credentials)
final_summary = await self._combine_summaries(
summaries, input_data, credentials
)
yield "summary", final_summary
yield "prompt", self.prompt
@@ -1112,22 +1128,22 @@ class AITextSummarizerBlock(AIBlockBase):
return chunks
def llm_call(
async def llm_call(
self,
input_data: AIStructuredResponseGeneratorBlock.Input,
credentials: APIKeyCredentials,
) -> dict:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
response = await block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
return response
def _summarize_chunk(
async 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(
llm_response = await self.llm_call(
AIStructuredResponseGeneratorBlock.Input(
prompt=prompt,
credentials=input_data.credentials,
@@ -1139,7 +1155,7 @@ class AITextSummarizerBlock(AIBlockBase):
return llm_response["summary"]
def _combine_summaries(
async def _combine_summaries(
self, summaries: list[str], input_data: Input, credentials: APIKeyCredentials
) -> str:
combined_text = "\n\n".join(summaries)
@@ -1147,7 +1163,7 @@ class AITextSummarizerBlock(AIBlockBase):
if len(combined_text.split()) <= input_data.max_tokens:
prompt = f"Provide a final summary of the following section summaries in a {input_data.style} form, focus your summary on the topic of `{input_data.focus}` if present:\n\n ```{combined_text}```\n\n Just respond with the final_summary in the format specified."
llm_response = self.llm_call(
llm_response = await self.llm_call(
AIStructuredResponseGeneratorBlock.Input(
prompt=prompt,
credentials=input_data.credentials,
@@ -1162,7 +1178,8 @@ class AITextSummarizerBlock(AIBlockBase):
return llm_response["final_summary"]
else:
# If combined summaries are still too long, recursively summarize
return self._run(
block = AITextSummarizerBlock()
return await block.run_once(
AITextSummarizerBlock.Input(
text=combined_text,
credentials=input_data.credentials,
@@ -1170,10 +1187,9 @@ class AITextSummarizerBlock(AIBlockBase):
max_tokens=input_data.max_tokens,
chunk_overlap=input_data.chunk_overlap,
),
"summary",
credentials=credentials,
).send(None)[
1
] # Get the first yielded value
)
class AIConversationBlock(AIBlockBase):
@@ -1244,20 +1260,20 @@ class AIConversationBlock(AIBlockBase):
},
)
def llm_call(
async def llm_call(
self,
input_data: AIStructuredResponseGeneratorBlock.Input,
credentials: APIKeyCredentials,
) -> str:
) -> dict:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
response = await block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
return response["response"]
return response
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
response = self.llm_call(
response = await self.llm_call(
AIStructuredResponseGeneratorBlock.Input(
prompt=input_data.prompt,
credentials=input_data.credentials,
@@ -1269,7 +1285,6 @@ class AIConversationBlock(AIBlockBase):
),
credentials=credentials,
)
yield "response", response
yield "prompt", self.prompt
@@ -1363,13 +1378,15 @@ class AIListGeneratorBlock(AIBlockBase):
},
)
def llm_call(
async def llm_call(
self,
input_data: AIStructuredResponseGeneratorBlock.Input,
credentials: APIKeyCredentials,
) -> dict[str, str]:
llm_block = AIStructuredResponseGeneratorBlock()
response = llm_block.run_once(input_data, "response", credentials=credentials)
response = await llm_block.run_once(
input_data, "response", credentials=credentials
)
self.merge_llm_stats(llm_block)
return response
@@ -1392,7 +1409,7 @@ class AIListGeneratorBlock(AIBlockBase):
logger.error(f"Failed to convert string to list: {e}")
raise ValueError("Invalid list format. Could not convert to list.")
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
logger.debug(f"Starting AIListGeneratorBlock.run with input data: {input_data}")
@@ -1458,7 +1475,7 @@ class AIListGeneratorBlock(AIBlockBase):
for attempt in range(input_data.max_retries):
try:
logger.debug("Calling LLM")
llm_response = self.llm_call(
llm_response = await self.llm_call(
AIStructuredResponseGeneratorBlock.Input(
sys_prompt=sys_prompt,
prompt=prompt,

View File

@@ -52,7 +52,7 @@ class CalculatorBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
operation = input_data.operation
a = input_data.a
b = input_data.b
@@ -107,7 +107,7 @@ class CountItemsBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
collection = input_data.collection
try:

View File

@@ -39,7 +39,7 @@ class MediaDurationBlock(Block):
output_schema=MediaDurationBlock.Output,
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -47,7 +47,7 @@ class MediaDurationBlock(Block):
**kwargs,
) -> BlockOutput:
# 1) Store the input media locally
local_media_path = store_media_file(
local_media_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.media_in,
return_content=False,
@@ -105,7 +105,7 @@ class LoopVideoBlock(Block):
output_schema=LoopVideoBlock.Output,
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -114,7 +114,7 @@ class LoopVideoBlock(Block):
**kwargs,
) -> BlockOutput:
# 1) Store the input video locally
local_video_path = store_media_file(
local_video_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.video_in,
return_content=False,
@@ -146,7 +146,7 @@ class LoopVideoBlock(Block):
looped_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac")
# Return as data URI
video_out = store_media_file(
video_out = await store_media_file(
graph_exec_id=graph_exec_id,
file=output_filename,
return_content=input_data.output_return_type == "data_uri",
@@ -194,7 +194,7 @@ class AddAudioToVideoBlock(Block):
output_schema=AddAudioToVideoBlock.Output,
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -203,12 +203,12 @@ class AddAudioToVideoBlock(Block):
**kwargs,
) -> BlockOutput:
# 1) Store the inputs locally
local_video_path = store_media_file(
local_video_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.video_in,
return_content=False,
)
local_audio_path = store_media_file(
local_audio_path = await store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.audio_in,
return_content=False,
@@ -236,7 +236,7 @@ class AddAudioToVideoBlock(Block):
final_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac")
# 5) Return either path or data URI
video_out = store_media_file(
video_out = await store_media_file(
graph_exec_id=graph_exec_id,
file=output_filename,
return_content=input_data.output_return_type == "data_uri",

View File

@@ -13,7 +13,7 @@ from backend.data.model import (
SecretField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -130,7 +130,7 @@ class PublishToMediumBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def create_post(
async def create_post(
self,
api_key: SecretStr,
author_id,
@@ -160,18 +160,17 @@ class PublishToMediumBlock(Block):
"notifyFollowers": notify_followers,
}
response = requests.post(
response = await Requests().post(
f"https://api.medium.com/v1/users/{author_id}/posts",
headers=headers,
json=data,
)
return response.json()
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
response = self.create_post(
response = await self.create_post(
credentials.api_key,
input_data.author_id.get_secret_value(),
input_data.title,

View File

@@ -109,7 +109,7 @@ class AddMemoryBlock(Block, Mem0Base):
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -208,7 +208,7 @@ class SearchMemoryBlock(Block, Mem0Base):
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -288,7 +288,7 @@ class GetAllMemoriesBlock(Block, Mem0Base):
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -5,7 +5,7 @@ from backend.blocks.nvidia._auth import (
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.request import requests
from backend.util.request import Requests
from backend.util.type import MediaFileType
@@ -40,7 +40,7 @@ class NvidiaDeepfakeDetectBlock(Block):
output_schema=NvidiaDeepfakeDetectBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: NvidiaCredentials, **kwargs
) -> BlockOutput:
url = "https://ai.api.nvidia.com/v1/cv/hive/deepfake-image-detection"
@@ -59,8 +59,7 @@ class NvidiaDeepfakeDetectBlock(Block):
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
response = await Requests().post(url, headers=headers, json=payload)
data = response.json()
result = data.get("data", [{}])[0]

View File

@@ -56,7 +56,7 @@ class PineconeInitBlock(Block):
output_schema=PineconeInitBlock.Output,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
pc = Pinecone(api_key=credentials.api_key.get_secret_value())
@@ -117,7 +117,7 @@ class PineconeQueryBlock(Block):
output_schema=PineconeQueryBlock.Output,
)
def run(
async def run(
self,
input_data: Input,
*,
@@ -195,7 +195,7 @@ class PineconeInsertBlock(Block):
output_schema=PineconeInsertBlock.Output,
)
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -146,7 +146,7 @@ class GetRedditPostsBlock(Block):
subreddit = client.subreddit(input_data.subreddit)
return subreddit.new(limit=input_data.post_limit or 10)
def run(
async def run(
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput:
current_time = datetime.now(tz=timezone.utc)
@@ -207,7 +207,7 @@ class PostRedditCommentBlock(Block):
raise ValueError("Failed to post comment.")
return new_comment.id
def run(
async def run(
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput:
yield "comment_id", self.reply_post(credentials, input_data.data)

View File

@@ -159,7 +159,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
# If the seed is not provided, generate a random seed
@@ -168,7 +168,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
seed = int.from_bytes(os.urandom(4), "big")
# Run the model using the provided inputs
result = self.run_model(
result = await self.run_model(
api_key=credentials.api_key,
model_name=input_data.replicate_model_name.api_name,
prompt=input_data.prompt,
@@ -183,7 +183,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
)
yield "result", result
def run_model(
async def run_model(
self,
api_key: SecretStr,
model_name,
@@ -201,7 +201,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
client = ReplicateClient(api_token=api_key.get_secret_value())
# Run the model with additional parameters
output: FileOutput | list[FileOutput] = client.run( # type: ignore This is because they changed the return type, and didn't update the type hint! It should be overloaded depending on the value of `use_file_output` to `FileOutput | list[FileOutput]` but it's `Any | Iterator[Any]`
output: FileOutput | list[FileOutput] = await client.async_run( # type: ignore This is because they changed the return type, and didn't update the type hint! It should be overloaded depending on the value of `use_file_output` to `FileOutput | list[FileOutput]` but it's `Any | Iterator[Any]`
f"{model_name}",
input={
"prompt": prompt,

View File

@@ -1,4 +1,4 @@
import time
import asyncio
from datetime import datetime, timedelta, timezone
from typing import Any
@@ -87,7 +87,7 @@ class ReadRSSFeedBlock(Block):
def parse_feed(url: str) -> dict[str, Any]:
return feedparser.parse(url) # type: ignore
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
keep_going = True
start_time = datetime.now(timezone.utc) - timedelta(
minutes=input_data.time_period
@@ -113,4 +113,4 @@ class ReadRSSFeedBlock(Block):
),
)
time.sleep(input_data.polling_rate)
await asyncio.sleep(input_data.polling_rate)

View File

@@ -93,7 +93,7 @@ class DataSamplingBlock(Block):
)
self.accumulated_data = []
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
if input_data.accumulate:
if isinstance(input_data.data, dict):
self.accumulated_data.append(input_data.data)

View File

@@ -105,7 +105,7 @@ class ScreenshotWebPageBlock(Block):
)
@staticmethod
def take_screenshot(
async def take_screenshot(
credentials: APIKeyCredentials,
graph_exec_id: str,
url: str,
@@ -121,11 +121,10 @@ class ScreenshotWebPageBlock(Block):
"""
Takes a screenshot using the ScreenshotOne API
"""
api = Requests(trusted_origins=["https://api.screenshotone.com"])
api = Requests()
# Build API URL with parameters
# Build API parameters
params = {
"access_key": credentials.api_key.get_secret_value(),
"url": url,
"viewport_width": viewport_width,
"viewport_height": viewport_height,
@@ -137,19 +136,28 @@ class ScreenshotWebPageBlock(Block):
"cache": str(cache).lower(),
}
response = api.get("https://api.screenshotone.com/take", params=params)
# Make the API request
# Use header-based authentication instead of query parameter
headers = {
"X-Access-Key": credentials.api_key.get_secret_value(),
}
response = await api.get(
"https://api.screenshotone.com/take", params=params, headers=headers
)
content = response.content
return {
"image": store_media_file(
"image": await store_media_file(
graph_exec_id=graph_exec_id,
file=MediaFileType(
f"data:image/{format.value};base64,{b64encode(response.content).decode('utf-8')}"
f"data:image/{format.value};base64,{b64encode(content).decode('utf-8')}"
),
return_content=True,
)
}
def run(
async def run(
self,
input_data: Input,
*,
@@ -158,7 +166,7 @@ class ScreenshotWebPageBlock(Block):
**kwargs,
) -> BlockOutput:
try:
screenshot_data = self.take_screenshot(
screenshot_data = await self.take_screenshot(
credentials=credentials,
graph_exec_id=graph_exec_id,
url=input_data.url,

View File

@@ -36,10 +36,10 @@ class GetWikipediaSummaryBlock(Block, GetRequest):
test_mock={"get_request": lambda url, json: {"extract": "summary content"}},
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
topic = input_data.topic
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}"
response = self.get_request(url, json=True)
response = await self.get_request(url, json=True)
if "extract" not in response:
raise RuntimeError(f"Unable to parse Wikipedia response: {response}")
yield "summary", response["extract"]
@@ -113,14 +113,14 @@ class GetWeatherInformationBlock(Block, GetRequest):
test_credentials=TEST_CREDENTIALS,
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
units = "metric" if input_data.use_celsius else "imperial"
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)
weather_data = await self.get_request(url, json=True)
if "main" in weather_data and "weather" in weather_data:
yield "temperature", str(weather_data["main"]["temp"])

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from backend.data.block import Block
from backend.util.request import requests
from backend.util.request import Requests
from ._api import Color, CustomerDetails, OrderItem, Profile
@@ -14,20 +14,25 @@ class Slant3DBlockBase(Block):
def _get_headers(self, api_key: str) -> Dict[str, str]:
return {"api-key": api_key, "Content-Type": "application/json"}
def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict:
async def _make_request(
self, method: str, endpoint: str, api_key: str, **kwargs
) -> Dict:
url = f"{self.BASE_URL}/{endpoint}"
response = requests.request(
response = await Requests().request(
method=method, url=url, headers=self._get_headers(api_key), **kwargs
)
resp = response.json()
if not response.ok:
error_msg = response.json().get("error", "Unknown error")
error_msg = resp.get("error", "Unknown error")
raise RuntimeError(f"API request failed: {error_msg}")
return response.json()
return resp
def _check_valid_color(self, profile: Profile, color: Color, api_key: str) -> str:
response = self._make_request(
async def _check_valid_color(
self, profile: Profile, color: Color, api_key: str
) -> str:
response = await self._make_request(
"GET",
"filament",
api_key,
@@ -48,10 +53,12 @@ Valid colors for {profile.value} are:
)
return color_tag
def _convert_to_color(self, profile: Profile, color: Color, api_key: str) -> str:
return self._check_valid_color(profile, color, api_key)
async def _convert_to_color(
self, profile: Profile, color: Color, api_key: str
) -> str:
return await self._check_valid_color(profile, color, api_key)
def _format_order_data(
async def _format_order_data(
self,
customer: CustomerDetails,
order_number: str,
@@ -61,6 +68,7 @@ Valid colors for {profile.value} are:
"""Helper function to format order data for API requests"""
orders = []
for item in items:
color_tag = await self._convert_to_color(item.profile, item.color, api_key)
order_data = {
"email": customer.email,
"phone": customer.phone,
@@ -85,9 +93,7 @@ Valid colors for {profile.value} are:
"order_quantity": item.quantity,
"order_image_url": "",
"order_sku": "NOT_USED",
"order_item_color": self._convert_to_color(
item.profile, item.color, api_key
),
"order_item_color": color_tag,
"profile": item.profile.value,
}
orders.append(order_data)

View File

@@ -72,11 +72,11 @@ class Slant3DFilamentBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
result = await self._make_request(
"GET", "filament", credentials.api_key.get_secret_value()
)
yield "filaments", result["filaments"]

View File

@@ -1,8 +1,6 @@
import uuid
from typing import List
import requests as baserequests
from backend.data.block import BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials, SchemaField
from backend.util import settings
@@ -76,17 +74,17 @@ class Slant3DCreateOrderBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
order_data = self._format_order_data(
order_data = await self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
result = self._make_request(
result = await self._make_request(
"POST", "order", credentials.api_key.get_secret_value(), json=order_data
)
yield "order_id", result["orderId"]
@@ -162,28 +160,24 @@ class Slant3DEstimateOrderBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
order_data = self._format_order_data(
order_data = await self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
try:
result = self._make_request(
"POST",
"order/estimate",
credentials.api_key.get_secret_value(),
json=order_data,
)
yield "total_price", result["totalPrice"]
yield "shipping_cost", result["shippingCost"]
yield "printing_cost", result["printingCost"]
except baserequests.HTTPError as e:
yield "error", str(f"Error estimating order: {e} {e.response.text}")
raise
result = await self._make_request(
"POST",
"order/estimate",
credentials.api_key.get_secret_value(),
json=order_data,
)
yield "total_price", result["totalPrice"]
yield "shipping_cost", result["shippingCost"]
yield "printing_cost", result["printingCost"]
class Slant3DEstimateShippingBlock(Slant3DBlockBase):
@@ -246,17 +240,17 @@ class Slant3DEstimateShippingBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
order_data = self._format_order_data(
order_data = await self._format_order_data(
input_data.customer,
input_data.order_number,
input_data.items,
credentials.api_key.get_secret_value(),
)
result = self._make_request(
result = await self._make_request(
"POST",
"order/estimateShipping",
credentials.api_key.get_secret_value(),
@@ -312,11 +306,11 @@ class Slant3DGetOrdersBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
result = await self._make_request(
"GET", "order", credentials.api_key.get_secret_value()
)
yield "orders", [str(order["orderId"]) for order in result["ordersData"]]
@@ -359,11 +353,11 @@ class Slant3DTrackingBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
result = await self._make_request(
"GET",
f"order/{input_data.order_id}/get-tracking",
credentials.api_key.get_secret_value(),
@@ -403,11 +397,11 @@ class Slant3DCancelOrderBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
result = await self._make_request(
"DELETE",
f"order/{input_data.order_id}",
credentials.api_key.get_secret_value(),

View File

@@ -44,11 +44,11 @@ class Slant3DSlicerBlock(Slant3DBlockBase):
},
)
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = self._make_request(
result = await self._make_request(
"POST",
"slicer",
credentials.api_key.get_secret_value(),

View File

@@ -37,7 +37,7 @@ class Slant3DTriggerBase:
description="Error message if payload processing failed"
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "payload", input_data.payload
yield "order_id", input_data.payload["orderId"]
@@ -117,8 +117,9 @@ class Slant3DOrderWebhookBlock(Slant3DTriggerBase, Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
yield from super().run(input_data, **kwargs)
async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore
async for name, value in super().run(input_data, **kwargs):
yield name, value
# Extract and normalize values from the payload
yield "status", input_data.payload["status"]

View File

@@ -142,6 +142,12 @@ class SmartDecisionMakerBlock(Block):
advanced=False,
)
credentials: llm.AICredentials = llm.AICredentialsField()
multiple_tool_calls: bool = SchemaField(
title="Multiple Tool Calls",
default=False,
description="Whether to allow multiple tool calls in a single response.",
advanced=True,
)
sys_prompt: str = SchemaField(
title="System Prompt",
default="Thinking carefully step by step decide which function to call. "
@@ -150,7 +156,7 @@ class SmartDecisionMakerBlock(Block):
"matching the required jsonschema signature, no missing argument is allowed. "
"If you have already completed the task objective, you can end the task "
"by providing the end result of your work as a finish message. "
"Only provide EXACTLY one function call, multiple tool calls is strictly prohibited.",
"Function parameters that has no default value and not optional typed has to be provided. ",
description="The system prompt to provide additional context to the model.",
)
conversation_history: list[dict] = SchemaField(
@@ -273,29 +279,18 @@ class SmartDecisionMakerBlock(Block):
"name": SmartDecisionMakerBlock.cleanup(block.name),
"description": block.description,
}
sink_block_input_schema = block.input_schema
properties = {}
required = []
for link in links:
sink_block_input_schema = block.input_schema
description = (
sink_block_input_schema.model_fields[link.sink_name].description
if link.sink_name in sink_block_input_schema.model_fields
and sink_block_input_schema.model_fields[link.sink_name].description
else f"The {link.sink_name} of the tool"
sink_name = SmartDecisionMakerBlock.cleanup(link.sink_name)
properties[sink_name] = sink_block_input_schema.get_field_schema(
link.sink_name
)
properties[SmartDecisionMakerBlock.cleanup(link.sink_name)] = {
"type": "string",
"description": description,
}
tool_function["parameters"] = {
"type": "object",
**block.input_schema.jsonschema(),
"properties": properties,
"required": required,
"additionalProperties": False,
"strict": True,
}
return {"type": "function", "function": tool_function}
@@ -335,25 +330,27 @@ class SmartDecisionMakerBlock(Block):
}
properties = {}
required = []
for link in links:
sink_block_input_schema = sink_node.input_default["input_schema"]
sink_block_properties = sink_block_input_schema.get("properties", {}).get(
link.sink_name, {}
)
sink_name = SmartDecisionMakerBlock.cleanup(link.sink_name)
description = (
sink_block_input_schema["properties"][link.sink_name]["description"]
if "description"
in sink_block_input_schema["properties"][link.sink_name]
sink_block_properties["description"]
if "description" in sink_block_properties
else f"The {link.sink_name} of the tool"
)
properties[SmartDecisionMakerBlock.cleanup(link.sink_name)] = {
properties[sink_name] = {
"type": "string",
"description": description,
"default": json.dumps(sink_block_properties.get("default", None)),
}
tool_function["parameters"] = {
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": False,
"strict": True,
}
@@ -417,7 +414,7 @@ class SmartDecisionMakerBlock(Block):
return return_tool_functions
def run(
async def run(
self,
input_data: Input,
*,
@@ -430,6 +427,7 @@ class SmartDecisionMakerBlock(Block):
**kwargs,
) -> BlockOutput:
tool_functions = self._create_function_signature(node_id)
yield "tool_functions", json.dumps(tool_functions)
input_data.conversation_history = input_data.conversation_history or []
prompt = [json.to_dict(p) for p in input_data.conversation_history if p]
@@ -469,6 +467,10 @@ class SmartDecisionMakerBlock(Block):
)
prompt.extend(tool_output)
if input_data.multiple_tool_calls:
input_data.sys_prompt += "\nYou can call a tool (different tools) multiple times in a single response."
else:
input_data.sys_prompt += "\nOnly provide EXACTLY one function call, multiple tool calls is strictly prohibited."
values = input_data.prompt_values
if values:
@@ -487,7 +489,7 @@ class SmartDecisionMakerBlock(Block):
):
prompt.append({"role": "user", "content": prefix + input_data.prompt})
response = llm.llm_call(
response = await llm.llm_call(
credentials=credentials,
llm_model=input_data.model,
prompt=prompt,
@@ -495,7 +497,7 @@ class SmartDecisionMakerBlock(Block):
max_tokens=input_data.max_tokens,
tools=tool_functions,
ollama_host=input_data.ollama_host,
parallel_tool_calls=False,
parallel_tool_calls=True if input_data.multiple_tool_calls else None,
)
if not response.tool_calls:
@@ -506,8 +508,31 @@ class SmartDecisionMakerBlock(Block):
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
for arg_name, arg_value in tool_args.items():
yield f"tools_^_{tool_name}_~_{arg_name}", arg_value
# Find the tool definition to get the expected arguments
tool_def = next(
(
tool
for tool in tool_functions
if tool["function"]["name"] == tool_name
),
None,
)
if (
tool_def
and "function" in tool_def
and "parameters" in tool_def["function"]
):
expected_args = tool_def["function"]["parameters"].get("properties", {})
else:
expected_args = tool_args.keys()
# Yield provided arguments and None for missing ones
for arg_name in expected_args:
if arg_name in tool_args:
yield f"tools_^_{tool_name}_~_{arg_name}", tool_args[arg_name]
else:
yield f"tools_^_{tool_name}_~_{arg_name}", None
response.prompt.append(response.raw_response)
yield "conversations", response.prompt

View File

@@ -27,9 +27,11 @@ class SmartLeadClient:
def _handle_error(self, e: Exception) -> str:
return e.__str__().replace(self.api_key, "API KEY")
def create_campaign(self, request: CreateCampaignRequest) -> CreateCampaignResponse:
async def create_campaign(
self, request: CreateCampaignRequest
) -> CreateCampaignResponse:
try:
response = self.requests.post(
response = await self.requests.post(
self._add_auth_to_url(f"{self.API_URL}/campaigns/create"),
json=request.model_dump(),
)
@@ -40,11 +42,11 @@ class SmartLeadClient:
except Exception as e:
raise ValueError(f"Failed to create campaign: {self._handle_error(e)}")
def add_leads_to_campaign(
async def add_leads_to_campaign(
self, request: AddLeadsRequest
) -> AddLeadsToCampaignResponse:
try:
response = self.requests.post(
response = await self.requests.post(
self._add_auth_to_url(
f"{self.API_URL}/campaigns/{request.campaign_id}/leads"
),
@@ -64,7 +66,7 @@ class SmartLeadClient:
f"Failed to add leads to campaign: {self._handle_error(e)}"
)
def save_campaign_sequences(
async def save_campaign_sequences(
self, campaign_id: int, request: SaveSequencesRequest
) -> SaveSequencesResponse:
"""
@@ -84,13 +86,13 @@ class SmartLeadClient:
- MANUAL_PERCENTAGE: Requires variant_distribution_percentage in seq_variants
"""
try:
response = self.requests.post(
response = await self.requests.post(
self._add_auth_to_url(
f"{self.API_URL}/campaigns/{campaign_id}/sequences"
),
json=request.model_dump(exclude_none=True),
)
return SaveSequencesResponse(**response.json())
return SaveSequencesResponse(**(response.json()))
except Exception as e:
raise ValueError(
f"Failed to save campaign sequences: {e.__str__().replace(self.api_key, 'API KEY')}"

View File

@@ -80,20 +80,20 @@ class CreateCampaignBlock(Block):
)
@staticmethod
def create_campaign(
async def create_campaign(
name: str, credentials: SmartLeadCredentials
) -> CreateCampaignResponse:
client = SmartLeadClient(credentials.api_key.get_secret_value())
return client.create_campaign(CreateCampaignRequest(name=name))
return await client.create_campaign(CreateCampaignRequest(name=name))
def run(
async def run(
self,
input_data: Input,
*,
credentials: SmartLeadCredentials,
**kwargs,
) -> BlockOutput:
response = self.create_campaign(input_data.name, credentials)
response = await self.create_campaign(input_data.name, credentials)
yield "id", response.id
yield "name", response.name
@@ -193,11 +193,11 @@ class AddLeadToCampaignBlock(Block):
)
@staticmethod
def add_leads_to_campaign(
async def add_leads_to_campaign(
campaign_id: int, lead_list: list[LeadInput], credentials: SmartLeadCredentials
) -> AddLeadsToCampaignResponse:
client = SmartLeadClient(credentials.api_key.get_secret_value())
return client.add_leads_to_campaign(
return await client.add_leads_to_campaign(
AddLeadsRequest(
campaign_id=campaign_id,
lead_list=lead_list,
@@ -210,14 +210,14 @@ class AddLeadToCampaignBlock(Block):
),
)
def run(
async def run(
self,
input_data: Input,
*,
credentials: SmartLeadCredentials,
**kwargs,
) -> BlockOutput:
response = self.add_leads_to_campaign(
response = await self.add_leads_to_campaign(
input_data.campaign_id, input_data.lead_list, credentials
)
@@ -297,22 +297,22 @@ class SaveCampaignSequencesBlock(Block):
)
@staticmethod
def save_campaign_sequences(
async def save_campaign_sequences(
campaign_id: int, sequences: list[Sequence], credentials: SmartLeadCredentials
) -> SaveSequencesResponse:
client = SmartLeadClient(credentials.api_key.get_secret_value())
return client.save_campaign_sequences(
return await client.save_campaign_sequences(
campaign_id=campaign_id, request=SaveSequencesRequest(sequences=sequences)
)
def run(
async def run(
self,
input_data: Input,
*,
credentials: SmartLeadCredentials,
**kwargs,
) -> BlockOutput:
response = self.save_campaign_sequences(
response = await self.save_campaign_sequences(
input_data.campaign_id, input_data.sequences, credentials
)

View File

@@ -1,4 +1,4 @@
import time
import asyncio
from typing import Literal
from pydantic import SecretStr
@@ -11,7 +11,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -113,26 +113,26 @@ class CreateTalkingAvatarVideoBlock(Block):
test_credentials=TEST_CREDENTIALS,
)
def create_clip(self, api_key: SecretStr, payload: dict) -> dict:
async 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.get_secret_value()}",
}
response = requests.post(url, json=payload, headers=headers)
response = await Requests().post(url, json=payload, headers=headers)
return response.json()
def get_clip_status(self, api_key: SecretStr, clip_id: str) -> dict:
async 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.get_secret_value()}",
}
response = requests.get(url, headers=headers)
response = await Requests().get(url, headers=headers)
return response.json()
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
# Create the clip
@@ -153,12 +153,12 @@ class CreateTalkingAvatarVideoBlock(Block):
"driver_id": input_data.driver_id,
}
response = self.create_clip(credentials.api_key, payload)
response = await 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(credentials.api_key, clip_id)
status_response = await self.get_clip_status(credentials.api_key, clip_id)
if status_response["status"] == "done":
yield "video_url", status_response["result_url"]
return
@@ -167,6 +167,6 @@ class CreateTalkingAvatarVideoBlock(Block):
f"Clip creation failed: {status_response.get('error', 'Unknown error')}"
)
time.sleep(input_data.polling_interval)
await asyncio.sleep(input_data.polling_interval)
raise TimeoutError("Clip creation timed out")

View File

@@ -43,7 +43,7 @@ class MatchTextPatternBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
output = input_data.data or input_data.text
flags = 0
if not input_data.case_sensitive:
@@ -133,7 +133,7 @@ class ExtractTextInformationBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
flags = 0
if not input_data.case_sensitive:
flags = flags | re.IGNORECASE
@@ -201,7 +201,7 @@ class FillTextTemplateBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", formatter.format_string(input_data.format, input_data.values)
@@ -232,7 +232,7 @@ class CombineTextsBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
combined_text = input_data.delimiter.join(input_data.input)
yield "output", combined_text
@@ -267,7 +267,7 @@ class TextSplitBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
if len(input_data.text) == 0:
yield "texts", []
else:
@@ -301,5 +301,5 @@ class TextReplaceBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text.replace(input_data.old, input_data.new)

View File

@@ -10,7 +10,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from backend.util.request import Requests
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
@@ -71,7 +71,7 @@ class UnrealTextToSpeechBlock(Block):
)
@staticmethod
def call_unreal_speech_api(
async def call_unreal_speech_api(
api_key: SecretStr, text: str, voice_id: str
) -> dict[str, Any]:
url = "https://api.v8.unrealspeech.com/speech"
@@ -88,13 +88,13 @@ class UnrealTextToSpeechBlock(Block):
"TimestampType": "sentence",
}
response = requests.post(url, headers=headers, json=data)
response = await Requests().post(url, headers=headers, json=data)
return response.json()
def run(
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
api_response = self.call_unreal_speech_api(
api_response = await self.call_unreal_speech_api(
credentials.api_key,
input_data.text,
input_data.voice_id,

View File

@@ -1,3 +1,4 @@
import asyncio
import time
from datetime import datetime, timedelta
from typing import Any, Union
@@ -37,7 +38,7 @@ class GetCurrentTimeBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
current_time = time.strftime(input_data.format)
yield "time", current_time
@@ -87,7 +88,7 @@ class GetCurrentDateBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
offset = int(input_data.offset)
except ValueError:
@@ -132,7 +133,7 @@ class GetCurrentDateAndTimeBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
current_date_time = time.strftime(input_data.format)
yield "date_time", current_date_time
@@ -183,7 +184,7 @@ class CountdownTimerBlock(Block):
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
seconds = int(input_data.seconds)
minutes = int(input_data.minutes)
hours = int(input_data.hours)
@@ -192,5 +193,6 @@ class CountdownTimerBlock(Block):
total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400
for _ in range(input_data.repeat):
time.sleep(total_seconds)
if total_seconds > 0:
await asyncio.sleep(total_seconds)
yield "output_message", input_data.input_message

View File

@@ -108,7 +108,7 @@ class TodoistCreateCommentBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -215,7 +215,7 @@ class TodoistGetCommentsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -307,7 +307,7 @@ class TodoistGetCommentBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -371,7 +371,7 @@ class TodoistUpdateCommentBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -429,7 +429,7 @@ class TodoistDeleteCommentBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -80,7 +80,7 @@ class TodoistCreateLabelBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -174,7 +174,7 @@ class TodoistListLabelsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -248,7 +248,7 @@ class TodoistGetLabelBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -321,7 +321,7 @@ class TodoistUpdateLabelBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -389,7 +389,7 @@ class TodoistDeleteLabelBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -444,7 +444,7 @@ class TodoistGetSharedLabelsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -499,7 +499,7 @@ class TodoistRenameSharedLabelsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -551,7 +551,7 @@ class TodoistRemoveSharedLabelsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -95,7 +95,7 @@ class TodoistListProjectsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -185,7 +185,7 @@ class TodoistCreateProjectBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -277,7 +277,7 @@ class TodoistGetProjectBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -375,7 +375,7 @@ class TodoistUpdateProjectBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -438,7 +438,7 @@ class TodoistDeleteProjectBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -548,7 +548,7 @@ class TodoistListCollaboratorsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -96,7 +96,7 @@ class TodoistListSectionsBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -166,7 +166,7 @@ class TodoistListSectionsBlock(Block):
# except Exception as e:
# raise e
# def run(
# async def run(
# self,
# input_data: Input,
# *,
@@ -238,7 +238,7 @@ class TodoistGetSectionBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -295,7 +295,7 @@ class TodoistDeleteSectionBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -130,7 +130,7 @@ class TodoistCreateTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -261,7 +261,7 @@ class TodoistGetTasksBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -345,7 +345,7 @@ class TodoistGetTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -452,7 +452,7 @@ class TodoistUpdateTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -539,7 +539,7 @@ class TodoistCloseTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -592,7 +592,7 @@ class TodoistReopenTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,
@@ -645,7 +645,7 @@ class TodoistDeleteTaskBlock(Block):
except Exception as e:
raise e
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -162,7 +162,7 @@
# except tweepy.TweepyException:
# raise
# def run(
# async def run(
# self,
# input_data: Input,
# *,

View File

@@ -122,7 +122,7 @@
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# async def run(
# self,
# input_data: Input,
# *,
@@ -239,7 +239,7 @@
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# async def run(
# self,
# input_data: Input,
# *,

View File

@@ -68,7 +68,7 @@ class TwitterUnfollowListBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -131,7 +131,7 @@ class TwitterFollowListBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -276,7 +276,7 @@ class TwitterFollowListBlock(Block):
# except tweepy.TweepyException:
# raise
# def run(
# async def run(
# self,
# input_data: Input,
# *,
@@ -438,7 +438,7 @@ class TwitterFollowListBlock(Block):
# except tweepy.TweepyException:
# raise
# def run(
# async def run(
# self,
# input_data: Input,
# *,

View File

@@ -140,7 +140,7 @@ class TwitterGetListBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -312,7 +312,7 @@ class TwitterGetOwnedListsBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -90,7 +90,7 @@ class TwitterRemoveListMemberBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -164,7 +164,7 @@ class TwitterAddListMemberBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -327,7 +327,7 @@ class TwitterGetListMembersBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -493,7 +493,7 @@ class TwitterGetListMembershipsBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -178,7 +178,7 @@ class TwitterGetListTweetsBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -64,7 +64,7 @@ class TwitterDeleteListBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -158,7 +158,7 @@ class TwitterUpdateListBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -263,7 +263,7 @@ class TwitterCreateListBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -76,7 +76,7 @@ class TwitterUnpinListBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -140,7 +140,7 @@ class TwitterPinListBlock(Block):
except Exception:
raise
def run(
async def run(
self,
input_data: Input,
*,
@@ -257,7 +257,7 @@ class TwitterGetPinnedListsBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,

View File

@@ -158,7 +158,7 @@ class TwitterSearchSpacesBlock(Block):
except tweepy.TweepyException:
raise
def run(
async def run(
self,
input_data: Input,
*,

Some files were not shown because too many files have changed in this diff Show More