Compare commits

..

1 Commits

Author SHA1 Message Date
Nicholas Tindle
4b4207760f Update test.py 2025-01-27 14:11:46 +00:00
186 changed files with 3487 additions and 13288 deletions

View File

@@ -1,18 +0,0 @@
version = 1
test_patterns = ["**/*.spec.ts","**/*_test.py","**/*_tests.py","**/test_*.py"]
exclude_patterns = ["classic/**"]
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"

View File

@@ -37,25 +37,6 @@ jobs:
run: |
yarn lint
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "21"
- name: Install dependencies
run: |
yarn install --frozen-lockfile
- name: Run tsc check
run: |
yarn type-check
test:
runs-on: ubuntu-latest
strategy:

View File

@@ -25,7 +25,7 @@ jobs:
close-issue-message: >
This issue was closed automatically because it has been stale for 10 days
with no activity.
days-before-stale: 100
days-before-stale: 50
days-before-close: 10
# Do not touch meta issues:
exempt-issue-labels: meta,fridge,project management

View File

@@ -18,7 +18,7 @@ ERROR_LOG_FILE = "error.log"
SIMPLE_LOG_FORMAT = "%(asctime)s %(levelname)s %(title)s%(message)s"
DEBUG_LOG_FORMAT = (
"%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(title)s%(message)s"
"%(asctime)s %(levelname)s %(filename)s:%(lineno)d" " %(title)s%(message)s"
)

View File

@@ -626,17 +626,17 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
[[package]]
name = "gotrue"
version = "2.11.1"
version = "2.10.0"
description = "Python Client Library for Supabase Auth"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "gotrue-2.11.1-py3-none-any.whl", hash = "sha256:1b2d915bdc65fd0ad608532759ce9c72fa2e910145c1e6901f2188519e7bcd2d"},
{file = "gotrue-2.11.1.tar.gz", hash = "sha256:5594ceee60bd873e5f4fdd028b08dece3906f6013b6ed08e7786b71c0092fed0"},
{file = "gotrue-2.10.0-py3-none-any.whl", hash = "sha256:768e58207488e5184ffbdc4351b7280d913daf97962f4e9f2cca05c80004b042"},
{file = "gotrue-2.10.0.tar.gz", hash = "sha256:4edf4c251da3535f2b044e23deba221e848ca1210c17d0c7a9b19f79a1e3f3c0"},
]
[package.dependencies]
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
httpx = {version = ">=0.26,<0.28", extras = ["http2"]}
pydantic = ">=1.10,<3"
[[package]]
@@ -1012,18 +1012,18 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "postgrest"
version = "0.19.1"
version = "0.18.0"
description = "PostgREST client for Python. This library provides an ORM interface to PostgREST."
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "postgrest-0.19.1-py3-none-any.whl", hash = "sha256:a8e7be4e1abc69fd8eee5a49d7dc3a76dfbffbd778beed0b2bd7accb3f4f3a2a"},
{file = "postgrest-0.19.1.tar.gz", hash = "sha256:d8fa88953cced4f45efa0f412056c364f64ece8a35b5b35f458a7e58c133fbca"},
{file = "postgrest-0.18.0-py3-none-any.whl", hash = "sha256:200baad0d23fee986b3a0ffd3e07bfe0cdd40e09760f11e8e13a6c0c2376d5fa"},
{file = "postgrest-0.18.0.tar.gz", hash = "sha256:29c1a94801a17eb9ad590189993fe5a7a6d8c1bfc11a3c9d0ce7ba146454ebb3"},
]
[package.dependencies]
deprecation = ">=2.1.0,<3.0.0"
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
httpx = {version = ">=0.26,<0.28", extras = ["http2"]}
pydantic = ">=1.9,<3.0"
strenum = {version = ">=0.4.9,<0.5.0", markers = "python_version < \"3.11\""}
@@ -1091,18 +1091,18 @@ pyasn1 = ">=0.4.6,<0.7.0"
[[package]]
name = "pydantic"
version = "2.10.5"
version = "2.10.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
{file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.27.2"
pydantic-core = "2.27.1"
typing-extensions = ">=4.12.2"
[package.extras]
@@ -1111,111 +1111,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
version = "2.27.2"
version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
]
[package.dependencies]
@@ -1223,13 +1223,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.7.1"
version = "2.7.0"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"},
{file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"},
{file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"},
{file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"},
]
[package.dependencies]
@@ -1282,13 +1282,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.25.2"
version = "0.25.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
files = [
{file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"},
{file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"},
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
]
[package.dependencies]
@@ -1415,29 +1415,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.9.3"
version = "0.8.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"},
{file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"},
{file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"},
{file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"},
{file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"},
{file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"},
{file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"},
{file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"},
{file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"},
{file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"},
{file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"},
{file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"},
{file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"},
{file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"},
{file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"},
{file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"},
{file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"},
{file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"},
]
[[package]]
@@ -1464,17 +1464,17 @@ files = [
[[package]]
name = "storage3"
version = "0.11.0"
version = "0.9.0"
description = "Supabase Storage client for Python."
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "storage3-0.11.0-py3-none-any.whl", hash = "sha256:de2d8f9c9103ca91a9a9d0d69d80b07a3ab6f647b93e023e6a1a97d3607b9728"},
{file = "storage3-0.11.0.tar.gz", hash = "sha256:243583f2180686c0f0a19e6117d8a9796fd60c0ca72ec567d62b75a5af0d57a1"},
{file = "storage3-0.9.0-py3-none-any.whl", hash = "sha256:8b2fb91f0c61583a2f4eac74a8bae67e00d41ff38095c8a6cd3f2ce5e0ab76e7"},
{file = "storage3-0.9.0.tar.gz", hash = "sha256:e16697f60894c94e1d9df0d2e4af783c1b3f7dd08c9013d61978825c624188c4"},
]
[package.dependencies]
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
httpx = {version = ">=0.26,<0.28", extras = ["http2"]}
python-dateutil = ">=2.8.2,<3.0.0"
[[package]]
@@ -1495,37 +1495,36 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
name = "supabase"
version = "2.11.0"
version = "2.10.0"
description = "Supabase client for Python."
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "supabase-2.11.0-py3-none-any.whl", hash = "sha256:67a0da498895f4cd6554935e2854b4c41f87b297b78fb9c9414902a382041406"},
{file = "supabase-2.11.0.tar.gz", hash = "sha256:2a906f7909fd9a50f944cd9332ce66c684e2d37c0864284d34c5815e6c63cc01"},
{file = "supabase-2.10.0-py3-none-any.whl", hash = "sha256:183fb23c04528593f8f81c24ceb8178f3a56bff40fec7ed873b6c55ebc2e420a"},
{file = "supabase-2.10.0.tar.gz", hash = "sha256:9ac095f8947bf60780e67c0edcbab53e2db3f6f3f022329397b093500bf2607c"},
]
[package.dependencies]
gotrue = ">=2.11.0,<3.0.0"
httpx = ">=0.26,<0.29"
postgrest = ">=0.19,<0.20"
gotrue = ">=2.10.0,<3.0.0"
httpx = ">=0.26,<0.28"
postgrest = ">=0.18,<0.19"
realtime = ">=2.0.0,<3.0.0"
storage3 = ">=0.10,<0.12"
supafunc = ">=0.9,<0.10"
storage3 = ">=0.9.0,<0.10.0"
supafunc = ">=0.7.0,<0.8.0"
[[package]]
name = "supafunc"
version = "0.9.2"
version = "0.7.0"
description = "Library for Supabase Functions"
optional = false
python-versions = "<4.0,>=3.9"
files = [
{file = "supafunc-0.9.2-py3-none-any.whl", hash = "sha256:be5ee9f53842c4b0ba5f4abfb5bddf9f9e37e69e755ec0526852bb15af9d2ff5"},
{file = "supafunc-0.9.2.tar.gz", hash = "sha256:f5164114a3e65e7e552539f3f1050aa3d4970885abdd7405555c17fd216e2da1"},
{file = "supafunc-0.7.0-py3-none-any.whl", hash = "sha256:4160260dc02bdd906be1e2ffd7cb3ae8b74ae437c892bb475352b6a99d9ff8eb"},
{file = "supafunc-0.7.0.tar.gz", hash = "sha256:5b1c415fba1395740b2b4eedd1d786384bd58b98f6333a11ba7889820a48b6a7"},
]
[package.dependencies]
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
strenum = ">=0.4.15,<0.5.0"
httpx = {version = ">=0.26,<0.28", extras = ["http2"]}
[[package]]
name = "tomli"
@@ -1853,4 +1852,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0"
content-hash = "62c4ef3f1ae73546a66783a2dc0672e664ce415a7b0514a7b92fc9fe1a23239e"
content-hash = "bf1b0125759dadb1369fff05ffba64fea3e82b9b7a43d0068e1c80974a4ebc1c"

View File

@@ -10,18 +10,18 @@ packages = [{ include = "autogpt_libs" }]
colorama = "^0.4.6"
expiringdict = "^1.2.2"
google-cloud-logging = "^3.11.3"
pydantic = "^2.10.5"
pydantic-settings = "^2.7.1"
pydantic = "^2.10.3"
pydantic-settings = "^2.7.0"
pyjwt = "^2.10.1"
pytest-asyncio = "^0.25.2"
pytest-asyncio = "^0.25.0"
pytest-mock = "^3.14.0"
python = ">=3.10,<4.0"
python-dotenv = "^1.0.1"
supabase = "^2.11.0"
supabase = "^2.10.0"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.1"
ruff = "^0.9.3"
ruff = "^0.8.6"
[build-system]
requires = ["poetry-core"]

View File

@@ -15,9 +15,6 @@ REDIS_PORT=6379
REDIS_PASSWORD=password
ENABLE_CREDIT=false
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
# What environment things should be logged under: local dev or prod
APP_ENV=local
# What environment to behave as: "local" or "cloud"
@@ -39,7 +36,7 @@ SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
## to use the platform's webhook-related functionality.
## If you are developing locally, you can use something like ngrok to get a publc URL
## and tunnel it to your locally running backend.
PLATFORM_BASE_URL=http://localhost:3000
PLATFORM_BASE_URL=https://your-public-url-here
## == INTEGRATION CREDENTIALS == ##
# Each set of server side credentials is required for the corresponding 3rd party
@@ -75,20 +72,6 @@ GOOGLE_CLIENT_SECRET=
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
# Linear App
# Make a new workspace for your OAuth APP -- trust me
# https://linear.app/settings/api/applications/new
# Callback URL: http://localhost:3000/auth/integrations/oauth_callback
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# To obtain Todoist API credentials:
# 1. Create a Todoist account at todoist.com
# 2. Visit the Developer Console: https://developer.todoist.com/appconsole.html
# 3. Click "Create new app"
# 4. Once created, copy your Client ID and Client Secret below
TODOIST_CLIENT_ID=
TODOIST_CLIENT_SECRET=
## ===== OPTIONAL API KEYS ===== ##
@@ -99,12 +82,10 @@ GROQ_API_KEY=
OPEN_ROUTER_API_KEY=
# Reddit
# Go to https://www.reddit.com/prefs/apps and create a new app
# Choose "script" for the type
# Fill in the redirect uri as <your_frontend_url>/auth/integrations/oauth_callback, e.g. http://localhost:3000/auth/integrations/oauth_callback
REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
REDDIT_USER_AGENT="AutoGPT:1.0 (by /u/autogpt)"
REDDIT_USERNAME=
REDDIT_PASSWORD=
# Discord
DISCORD_BOT_TOKEN=
@@ -149,9 +130,6 @@ EXA_API_KEY=
# E2B
E2B_API_KEY=
# Mem0
MEM0_API_KEY=
# Nvidia
NVIDIA_API_KEY=

View File

@@ -66,17 +66,10 @@ We use the Poetry to manage the dependencies. To set up the project, follow thes
### Starting the server without Docker
To run the server locally, start in the autogpt_platform folder:
```sh
cd ..
```
Run the following command to run database in docker but the application locally:
```sh
docker compose --profile local up deps --build --detach
cd backend
poetry run app
```

View File

@@ -1,52 +1,13 @@
import enum
from typing import Any, List
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
from backend.data.model import SchemaField
from backend.util.file import MediaFile, store_media_file
from backend.util.mock import MockObject
from backend.util.text import TextFormatter
from backend.util.type import convert
formatter = TextFormatter()
class FileStoreBlock(Block):
class Input(BlockSchema):
file_in: MediaFile = SchemaField(
description="The file to store in the temporary directory, it can be a URL, data URI, or local path."
)
class Output(BlockSchema):
file_out: MediaFile = SchemaField(
description="The relative path to the stored file in the temporary directory."
)
def __init__(self):
super().__init__(
id="cbb50872-625b-42f0-8203-a2ae78242d8a",
description="Stores the input file in the temporary directory.",
categories={BlockCategory.BASIC, BlockCategory.MULTIMEDIA},
input_schema=FileStoreBlock.Input,
output_schema=FileStoreBlock.Output,
static_output=True,
)
def run(
self,
input_data: Input,
*,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
file_path = store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.file_in,
return_content=False,
)
yield "file_out", file_path
class StoreValueBlock(Block):
"""
This block allows you to provide a constant value as a block, in a stateless manner.
@@ -508,48 +469,6 @@ class AddToListBlock(Block):
yield "updated_list", updated_list
class FindInListBlock(Block):
class Input(BlockSchema):
list: List[Any] = SchemaField(description="The list to search in.")
value: Any = SchemaField(description="The value to search for.")
class Output(BlockSchema):
index: int = SchemaField(description="The index of the value in the list.")
found: bool = SchemaField(
description="Whether the value was found in the list."
)
not_found_value: Any = SchemaField(
description="The value that was not found in the list."
)
def __init__(self):
super().__init__(
id="5e2c6d0a-1e37-489f-b1d0-8e1812b23333",
description="Finds the index of the value in the list.",
categories={BlockCategory.BASIC},
input_schema=FindInListBlock.Input,
output_schema=FindInListBlock.Output,
test_input=[
{"list": [1, 2, 3, 4, 5], "value": 3},
{"list": [1, 2, 3, 4, 5], "value": 6},
],
test_output=[
("index", 2),
("found", True),
("found", False),
("not_found_value", 6),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
yield "index", input_data.list.index(input_data.value)
yield "found", True
except ValueError:
yield "found", False
yield "not_found_value", input_data.value
class NoteBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(description="The text to display in the sticky note.")
@@ -671,47 +590,3 @@ class CreateListBlock(Block):
yield "list", input_data.values
except Exception as e:
yield "error", f"Failed to create list: {str(e)}"
class TypeOptions(enum.Enum):
STRING = "string"
NUMBER = "number"
BOOLEAN = "boolean"
LIST = "list"
DICTIONARY = "dictionary"
class UniversalTypeConverterBlock(Block):
class Input(BlockSchema):
value: Any = SchemaField(
description="The value to convert to a universal type."
)
type: TypeOptions = SchemaField(description="The type to convert the value to.")
class Output(BlockSchema):
value: Any = SchemaField(description="The converted value.")
def __init__(self):
super().__init__(
id="95d1b990-ce13-4d88-9737-ba5c2070c97b",
description="This block is used to convert a value to a universal type.",
categories={BlockCategory.BASIC},
input_schema=UniversalTypeConverterBlock.Input,
output_schema=UniversalTypeConverterBlock.Output,
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
converted_value = convert(
input_data.value,
{
TypeOptions.STRING: str,
TypeOptions.NUMBER: float,
TypeOptions.BOOLEAN: bool,
TypeOptions.LIST: list,
TypeOptions.DICTIONARY: dict,
}[input_data.type],
)
yield "value", converted_value
except Exception as e:
yield "error", f"Failed to convert value: {str(e)}"

View File

@@ -107,83 +107,3 @@ class ConditionBlock(Block):
yield "yes_output", yes_value
else:
yield "no_output", no_value
class IfInputMatchesBlock(Block):
class Input(BlockSchema):
input: Any = SchemaField(
description="The input to match against",
placeholder="For example: 10 or 'hello' or True",
)
value: Any = SchemaField(
description="The value to output if the input matches",
placeholder="For example: 'Greater' or 20 or False",
)
yes_value: Any = SchemaField(
description="The value to output if the input matches",
placeholder="For example: 'Greater' or 20 or False",
default=None,
)
no_value: Any = SchemaField(
description="The value to output if the input does not match",
placeholder="For example: 'Greater' or 20 or False",
default=None,
)
class Output(BlockSchema):
result: bool = SchemaField(
description="The result of the condition evaluation (True or False)"
)
yes_output: Any = SchemaField(
description="The output value if the condition is true"
)
no_output: Any = SchemaField(
description="The output value if the condition is false"
)
def __init__(self):
super().__init__(
id="6dbbc4b3-ca6c-42b6-b508-da52d23e13f2",
input_schema=IfInputMatchesBlock.Input,
output_schema=IfInputMatchesBlock.Output,
description="Handles conditional logic based on comparison operators",
categories={BlockCategory.LOGIC},
test_input=[
{
"input": 10,
"value": 10,
"yes_value": "Greater",
"no_value": "Not greater",
},
{
"input": 10,
"value": 20,
"yes_value": "Greater",
"no_value": "Not greater",
},
{
"input": 10,
"value": None,
"yes_value": "Yes",
"no_value": "No",
},
],
test_output=[
("result", True),
("yes_output", "Greater"),
("result", False),
("no_output", "Not greater"),
("result", False),
("no_output", "No"),
# ("result", True),
# ("yes_output", "Yes"),
],
)
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
else:
yield "result", False
yield "no_output", input_data.no_value

View File

@@ -188,270 +188,3 @@ class CodeExecutionBlock(Block):
yield "stderr_logs", stderr_logs
except Exception as e:
yield "error", str(e)
class InstantiationBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.E2B], Literal["api_key"]
] = CredentialsField(
description="Enter your api key for the E2B Sandbox. You can get it in here - https://e2b.dev/docs",
)
# Todo : Option to run commond in background
setup_commands: list[str] = SchemaField(
description=(
"Shell commands to set up the sandbox before running the code. "
"You can use `curl` or `git` to install your desired Debian based "
"package manager. `pip` and `npm` are pre-installed.\n\n"
"These commands are executed with `sh`, in the foreground."
),
placeholder="pip install cowsay",
default=[],
advanced=False,
)
setup_code: str = SchemaField(
description="Code to execute in the sandbox",
placeholder="print('Hello, World!')",
default="",
advanced=False,
)
language: ProgrammingLanguage = SchemaField(
description="Programming language to execute",
default=ProgrammingLanguage.PYTHON,
advanced=False,
)
timeout: int = SchemaField(
description="Execution timeout in seconds", default=300
)
template_id: str = SchemaField(
description=(
"You can use an E2B sandbox template by entering its ID here. "
"Check out the E2B docs for more details: "
"[E2B - Sandbox template](https://e2b.dev/docs/sandbox-template)"
),
default="",
advanced=True,
)
class Output(BlockSchema):
sandbox_id: str = SchemaField(description="ID of the sandbox instance")
response: str = SchemaField(description="Response from code execution")
stdout_logs: str = SchemaField(
description="Standard output logs from execution"
)
stderr_logs: str = SchemaField(description="Standard error logs from execution")
error: str = SchemaField(description="Error message if execution failed")
def __init__(self):
super().__init__(
id="ff0861c9-1726-4aec-9e5b-bf53f3622112",
description="Instantiate an isolated sandbox environment with internet access where to execute code in.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=InstantiationBlock.Input,
output_schema=InstantiationBlock.Output,
test_credentials=TEST_CREDENTIALS,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"setup_code": "print('Hello World')",
"language": ProgrammingLanguage.PYTHON.value,
"setup_commands": [],
"timeout": 300,
"template_id": "",
},
test_output=[
("sandbox_id", str),
("response", "Hello World"),
("stdout_logs", "Hello World\n"),
],
test_mock={
"execute_code": lambda setup_code, language, setup_commands, timeout, api_key, template_id: (
"sandbox_id",
"Hello World",
"Hello World\n",
"",
),
},
)
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
sandbox_id, response, stdout_logs, stderr_logs = self.execute_code(
input_data.setup_code,
input_data.language,
input_data.setup_commands,
input_data.timeout,
credentials.api_key.get_secret_value(),
input_data.template_id,
)
if sandbox_id:
yield "sandbox_id", sandbox_id
else:
yield "error", "Sandbox ID not found"
if response:
yield "response", response
if stdout_logs:
yield "stdout_logs", stdout_logs
if stderr_logs:
yield "stderr_logs", stderr_logs
except Exception as e:
yield "error", str(e)
def execute_code(
self,
code: str,
language: ProgrammingLanguage,
setup_commands: list[str],
timeout: int,
api_key: str,
template_id: str,
):
try:
sandbox = None
if template_id:
sandbox = Sandbox(
template=template_id, api_key=api_key, timeout=timeout
)
else:
sandbox = Sandbox(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)
# Executing the code
execution = sandbox.run_code(
code,
language=language.value,
on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error
)
if execution.error:
raise Exception(execution.error)
response = execution.text
stdout_logs = "".join(execution.logs.stdout)
stderr_logs = "".join(execution.logs.stderr)
return sandbox.sandbox_id, response, stdout_logs, stderr_logs
except Exception as e:
raise e
class StepExecutionBlock(Block):
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.E2B], Literal["api_key"]
] = CredentialsField(
description="Enter your api key for the E2B Sandbox. You can get it in here - https://e2b.dev/docs",
)
sandbox_id: str = SchemaField(
description="ID of the sandbox instance to execute the code in",
advanced=False,
)
step_code: str = SchemaField(
description="Code to execute in the sandbox",
placeholder="print('Hello, World!')",
default="",
advanced=False,
)
language: ProgrammingLanguage = SchemaField(
description="Programming language to execute",
default=ProgrammingLanguage.PYTHON,
advanced=False,
)
class Output(BlockSchema):
response: str = SchemaField(description="Response from code execution")
stdout_logs: str = SchemaField(
description="Standard output logs from execution"
)
stderr_logs: str = SchemaField(description="Standard error logs from execution")
error: str = SchemaField(description="Error message if execution failed")
def __init__(self):
super().__init__(
id="82b59b8e-ea10-4d57-9161-8b169b0adba6",
description="Execute code in a previously instantiated sandbox environment.",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=StepExecutionBlock.Input,
output_schema=StepExecutionBlock.Output,
test_credentials=TEST_CREDENTIALS,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"sandbox_id": "sandbox_id",
"step_code": "print('Hello World')",
"language": ProgrammingLanguage.PYTHON.value,
},
test_output=[
("response", "Hello World"),
("stdout_logs", "Hello World\n"),
],
test_mock={
"execute_step_code": lambda sandbox_id, step_code, language, api_key: (
"Hello World",
"Hello World\n",
"",
),
},
)
def execute_step_code(
self,
sandbox_id: str,
code: str,
language: ProgrammingLanguage,
api_key: str,
):
try:
sandbox = Sandbox.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)
if execution.error:
raise Exception(execution.error)
response = execution.text
stdout_logs = "".join(execution.logs.stdout)
stderr_logs = "".join(execution.logs.stderr)
return response, stdout_logs, stderr_logs
except Exception as e:
raise e
def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
response, stdout_logs, stderr_logs = self.execute_step_code(
input_data.sandbox_id,
input_data.step_code,
input_data.language,
credentials.api_key.get_secret_value(),
)
if response:
yield "response", response
if stdout_logs:
yield "stdout_logs", stdout_logs
if stderr_logs:
yield "stderr_logs", stderr_logs
except Exception as e:
yield "error", str(e)

View File

@@ -1,53 +1,22 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Literal
from pydantic import BaseModel, ConfigDict, SecretStr
from pydantic import BaseModel, ConfigDict
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
SchemaField,
UserPasswordCredentials,
)
from backend.integrations.providers import ProviderName
TEST_CREDENTIALS = UserPasswordCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="smtp",
username=SecretStr("mock-smtp-username"),
password=SecretStr("mock-smtp-password"),
title="Mock SMTP credentials",
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
SMTPCredentials = UserPasswordCredentials
SMTPCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.SMTP],
Literal["user_password"],
]
from backend.data.model import BlockSecret, SchemaField, SecretField
def SMTPCredentialsField() -> SMTPCredentialsInput:
return CredentialsField(
description="The SMTP integration requires a username and password.",
)
class SMTPConfig(BaseModel):
class EmailCredentials(BaseModel):
smtp_server: str = SchemaField(
default="smtp.example.com", description="SMTP server address"
default="smtp.gmail.com", description="SMTP server address"
)
smtp_port: int = SchemaField(default=25, description="SMTP port number")
smtp_username: BlockSecret = SecretField(key="smtp_username")
smtp_password: BlockSecret = SecretField(key="smtp_password")
model_config = ConfigDict(title="SMTP Config")
model_config = ConfigDict(title="Email Credentials")
class SendEmailBlock(Block):
@@ -61,11 +30,10 @@ class SendEmailBlock(Block):
body: str = SchemaField(
description="Body of the email", placeholder="Enter the email body"
)
config: SMTPConfig = SchemaField(
description="SMTP Config",
default=SMTPConfig(),
creds: EmailCredentials = SchemaField(
description="SMTP credentials",
default=EmailCredentials(),
)
credentials: SMTPCredentialsInput = SMTPCredentialsField()
class Output(BlockSchema):
status: str = SchemaField(description="Status of the email sending operation")
@@ -75,6 +43,7 @@ class SendEmailBlock(Block):
def __init__(self):
super().__init__(
disabled=True,
id="4335878a-394e-4e67-adf2-919877ff49ae",
description="This block sends an email using the provided SMTP credentials.",
categories={BlockCategory.OUTPUT},
@@ -84,29 +53,25 @@ class SendEmailBlock(Block):
"to_email": "recipient@example.com",
"subject": "Test Email",
"body": "This is a test email.",
"config": {
"creds": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 25,
"smtp_username": "your-email@gmail.com",
"smtp_password": "your-gmail-password",
},
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("status", "Email sent successfully")],
test_mock={"send_email": lambda *args, **kwargs: "Email sent successfully"},
)
@staticmethod
def send_email(
config: SMTPConfig,
to_email: str,
subject: str,
body: str,
credentials: SMTPCredentials,
creds: EmailCredentials, to_email: str, subject: str, body: str
) -> str:
smtp_server = config.smtp_server
smtp_port = config.smtp_port
smtp_username = credentials.username.get_secret_value()
smtp_password = credentials.password.get_secret_value()
smtp_server = creds.smtp_server
smtp_port = creds.smtp_port
smtp_username = creds.smtp_username.get_secret_value()
smtp_password = creds.smtp_password.get_secret_value()
msg = MIMEMultipart()
msg["From"] = smtp_username
@@ -121,13 +86,10 @@ class SendEmailBlock(Block):
return "Email sent successfully"
def run(
self, input_data: Input, *, credentials: SMTPCredentials, **kwargs
) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "status", self.send_email(
config=input_data.config,
to_email=input_data.to_email,
subject=input_data.subject,
body=input_data.body,
credentials=credentials,
input_data.creds,
input_data.to_email,
input_data.subject,
input_data.body,
)

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional
from pydantic import BaseModel

View File

@@ -1,9 +1,6 @@
from urllib.parse import urlparse
from backend.blocks.github._auth import (
GithubCredentials,
GithubFineGrainedAPICredentials,
)
from backend.blocks.github._auth import GithubCredentials
from backend.util.request import Requests
@@ -33,15 +30,12 @@ def _convert_to_api_url(url: str) -> str:
def _get_headers(credentials: GithubCredentials) -> dict[str, str]:
return {
"Authorization": credentials.auth_header(),
"Authorization": credentials.bearer(),
"Accept": "application/vnd.github.v3+json",
}
def get_api(
credentials: GithubCredentials | GithubFineGrainedAPICredentials,
convert_urls: bool = True,
) -> Requests:
def get_api(credentials: GithubCredentials, convert_urls: bool = True) -> Requests:
return Requests(
trusted_origins=["https://api.github.com", "https://github.com"],
extra_url_validator=_convert_to_api_url if convert_urls else None,

View File

@@ -22,11 +22,6 @@ GithubCredentialsInput = CredentialsMetaInput[
Literal["api_key", "oauth2"] if GITHUB_OAUTH_IS_CONFIGURED else Literal["api_key"],
]
GithubFineGrainedAPICredentials = APIKeyCredentials
GithubFineGrainedAPICredentialsInput = CredentialsMetaInput[
Literal[ProviderName.GITHUB], Literal["api_key"]
]
def GithubCredentialsField(scope: str) -> GithubCredentialsInput:
"""
@@ -42,16 +37,6 @@ def GithubCredentialsField(scope: str) -> GithubCredentialsInput:
)
def GithubFineGrainedAPICredentialsField(
scope: str,
) -> GithubFineGrainedAPICredentialsInput:
return CredentialsField(
required_scopes={scope},
description="The GitHub integration can be used with OAuth, "
"or any API key with sufficient permissions for the blocks it is used on.",
)
TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="github",
@@ -65,18 +50,3 @@ TEST_CREDENTIALS_INPUT = {
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.type,
}
TEST_FINE_GRAINED_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="github",
api_key=SecretStr("mock-github-api-key"),
title="Mock GitHub API key",
expires_at=None,
)
TEST_FINE_GRAINED_CREDENTIALS_INPUT = {
"provider": TEST_FINE_GRAINED_CREDENTIALS.provider,
"id": TEST_FINE_GRAINED_CREDENTIALS.id,
"type": TEST_FINE_GRAINED_CREDENTIALS.type,
"title": TEST_FINE_GRAINED_CREDENTIALS.type,
}

View File

@@ -1,356 +0,0 @@
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from ._api import get_api
from ._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
GithubCredentials,
GithubCredentialsField,
GithubCredentialsInput,
)
# queued, in_progress, completed, waiting, requested, pending
class ChecksStatus(Enum):
QUEUED = "queued"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
WAITING = "waiting"
REQUESTED = "requested"
PENDING = "pending"
class ChecksConclusion(Enum):
SUCCESS = "success"
FAILURE = "failure"
NEUTRAL = "neutral"
CANCELLED = "cancelled"
TIMED_OUT = "timed_out"
ACTION_REQUIRED = "action_required"
SKIPPED = "skipped"
class GithubCreateCheckRunBlock(Block):
"""Block for creating a new check run on a GitHub repository."""
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo:status")
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
name: str = SchemaField(
description="The name of the check run (e.g., 'code-coverage')",
)
head_sha: str = SchemaField(
description="The SHA of the commit to check",
)
status: ChecksStatus = SchemaField(
description="Current status of the check run",
default=ChecksStatus.QUEUED,
)
conclusion: Optional[ChecksConclusion] = SchemaField(
description="The final conclusion of the check (required if status is completed)",
default=None,
)
details_url: str = SchemaField(
description="The URL for the full details of the check",
default="",
)
output_title: str = SchemaField(
description="Title of the check run output",
default="",
)
output_summary: str = SchemaField(
description="Summary of the check run output",
default="",
)
output_text: str = SchemaField(
description="Detailed text of the check run output",
default="",
)
class Output(BlockSchema):
class CheckRunResult(BaseModel):
id: int
html_url: str
status: str
check_run: CheckRunResult = SchemaField(
description="Details of the created check run"
)
error: str = SchemaField(
description="Error message if check run creation failed"
)
def __init__(self):
super().__init__(
id="2f45e89a-3b7d-4f22-b89e-6c4f5c7e1234",
description="Creates a new check run for a specific commit in a GitHub repository",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubCreateCheckRunBlock.Input,
output_schema=GithubCreateCheckRunBlock.Output,
test_input={
"repo_url": "https://github.com/owner/repo",
"name": "test-check",
"head_sha": "ce587453ced02b1526dfb4cb910479d431683101",
"status": ChecksStatus.COMPLETED.value,
"conclusion": ChecksConclusion.SUCCESS.value,
"output_title": "Test Results",
"output_summary": "All tests passed",
"credentials": TEST_CREDENTIALS_INPUT,
},
# requires a github app not available to oauth in our current system
disabled=True,
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"check_run",
{
"id": 4,
"html_url": "https://github.com/owner/repo/runs/4",
"status": "completed",
},
),
],
test_mock={
"create_check_run": lambda *args, **kwargs: {
"id": 4,
"html_url": "https://github.com/owner/repo/runs/4",
"status": "completed",
}
},
)
@staticmethod
def create_check_run(
credentials: GithubCredentials,
repo_url: str,
name: str,
head_sha: str,
status: ChecksStatus,
conclusion: Optional[ChecksConclusion] = None,
details_url: Optional[str] = None,
output_title: Optional[str] = None,
output_summary: Optional[str] = None,
output_text: Optional[str] = None,
) -> dict:
api = get_api(credentials)
class CheckRunData(BaseModel):
name: str
head_sha: str
status: str
conclusion: Optional[str] = None
details_url: Optional[str] = None
output: Optional[dict[str, str]] = None
data = CheckRunData(
name=name,
head_sha=head_sha,
status=status.value,
)
if conclusion:
data.conclusion = conclusion.value
if details_url:
data.details_url = details_url
if output_title or output_summary or output_text:
output_data = {
"title": output_title or "",
"summary": output_summary or "",
"text": output_text or "",
}
data.output = output_data
check_runs_url = f"{repo_url}/check-runs"
response = api.post(check_runs_url)
result = response.json()
return {
"id": result["id"],
"html_url": result["html_url"],
"status": result["status"],
}
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
result = self.create_check_run(
credentials=credentials,
repo_url=input_data.repo_url,
name=input_data.name,
head_sha=input_data.head_sha,
status=input_data.status,
conclusion=input_data.conclusion,
details_url=input_data.details_url,
output_title=input_data.output_title,
output_summary=input_data.output_summary,
output_text=input_data.output_text,
)
yield "check_run", result
except Exception as e:
yield "error", str(e)
class GithubUpdateCheckRunBlock(Block):
"""Block for updating an existing check run on a GitHub repository."""
class Input(BlockSchema):
credentials: GithubCredentialsInput = GithubCredentialsField("repo:status")
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
check_run_id: int = SchemaField(
description="The ID of the check run to update",
)
status: ChecksStatus = SchemaField(
description="New status of the check run",
)
conclusion: ChecksConclusion = SchemaField(
description="The final conclusion of the check (required if status is completed)",
)
output_title: Optional[str] = SchemaField(
description="New title of the check run output",
default=None,
)
output_summary: Optional[str] = SchemaField(
description="New summary of the check run output",
default=None,
)
output_text: Optional[str] = SchemaField(
description="New detailed text of the check run output",
default=None,
)
class Output(BlockSchema):
class CheckRunResult(BaseModel):
id: int
html_url: str
status: str
conclusion: Optional[str]
check_run: CheckRunResult = SchemaField(
description="Details of the updated check run"
)
error: str = SchemaField(description="Error message if check run update failed")
def __init__(self):
super().__init__(
id="8a23c567-9d01-4e56-b789-0c12d3e45678", # Generated UUID
description="Updates an existing check run in a GitHub repository",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubUpdateCheckRunBlock.Input,
output_schema=GithubUpdateCheckRunBlock.Output,
# requires a github app not available to oauth in our current system
disabled=True,
test_input={
"repo_url": "https://github.com/owner/repo",
"check_run_id": 4,
"status": ChecksStatus.COMPLETED.value,
"conclusion": ChecksConclusion.SUCCESS.value,
"output_title": "Updated Results",
"output_summary": "All tests passed after retry",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"check_run",
{
"id": 4,
"html_url": "https://github.com/owner/repo/runs/4",
"status": "completed",
"conclusion": "success",
},
),
],
test_mock={
"update_check_run": lambda *args, **kwargs: {
"id": 4,
"html_url": "https://github.com/owner/repo/runs/4",
"status": "completed",
"conclusion": "success",
}
},
)
@staticmethod
def update_check_run(
credentials: GithubCredentials,
repo_url: str,
check_run_id: int,
status: ChecksStatus,
conclusion: Optional[ChecksConclusion] = None,
output_title: Optional[str] = None,
output_summary: Optional[str] = None,
output_text: Optional[str] = None,
) -> dict:
api = get_api(credentials)
class UpdateCheckRunData(BaseModel):
status: str
conclusion: Optional[str] = None
output: Optional[dict[str, str]] = None
data = UpdateCheckRunData(
status=status.value,
)
if conclusion:
data.conclusion = conclusion.value
if output_title or output_summary or output_text:
output_data = {
"title": output_title or "",
"summary": output_summary or "",
"text": output_text or "",
}
data.output = output_data
check_run_url = f"{repo_url}/check-runs/{check_run_id}"
response = api.patch(check_run_url)
result = response.json()
return {
"id": result["id"],
"html_url": result["html_url"],
"status": result["status"],
"conclusion": result.get("conclusion"),
}
def run(
self,
input_data: Input,
*,
credentials: GithubCredentials,
**kwargs,
) -> BlockOutput:
try:
result = self.update_check_run(
credentials=credentials,
repo_url=input_data.repo_url,
check_run_id=input_data.check_run_id,
status=input_data.status,
conclusion=input_data.conclusion,
output_title=input_data.output_title,
output_summary=input_data.output_summary,
output_text=input_data.output_text,
)
yield "check_run", result
except Exception as e:
yield "error", str(e)

View File

@@ -200,7 +200,6 @@ class GithubReadPullRequestBlock(Block):
include_pr_changes: bool = SchemaField(
description="Whether to include the changes made in the pull request",
default=False,
advanced=False,
)
class Output(BlockSchema):

View File

@@ -1,180 +0,0 @@
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from ._api import get_api
from ._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
GithubFineGrainedAPICredentials,
GithubFineGrainedAPICredentialsField,
GithubFineGrainedAPICredentialsInput,
)
class StatusState(Enum):
ERROR = "error"
FAILURE = "failure"
PENDING = "pending"
SUCCESS = "success"
class GithubCreateStatusBlock(Block):
"""Block for creating a commit status on a GitHub repository."""
class Input(BlockSchema):
credentials: GithubFineGrainedAPICredentialsInput = (
GithubFineGrainedAPICredentialsField("repo:status")
)
repo_url: str = SchemaField(
description="URL of the GitHub repository",
placeholder="https://github.com/owner/repo",
)
sha: str = SchemaField(
description="The SHA of the commit to set status for",
)
state: StatusState = SchemaField(
description="The state of the status (error, failure, pending, success)",
)
target_url: Optional[str] = SchemaField(
description="URL with additional details about this status",
default=None,
)
description: Optional[str] = SchemaField(
description="Short description of the status",
default=None,
)
check_name: Optional[str] = SchemaField(
description="Label to differentiate this status from others",
default="AutoGPT Platform Checks",
advanced=False,
)
class Output(BlockSchema):
class StatusResult(BaseModel):
id: int
url: str
state: str
context: str
description: Optional[str]
target_url: Optional[str]
created_at: str
updated_at: str
status: StatusResult = SchemaField(description="Details of the created status")
error: str = SchemaField(description="Error message if status creation failed")
def __init__(self):
super().__init__(
id="3d67f123-a4b5-4c89-9d01-2e34f5c67890", # Generated UUID
description="Creates a new commit status in a GitHub repository",
categories={BlockCategory.DEVELOPER_TOOLS},
input_schema=GithubCreateStatusBlock.Input,
output_schema=GithubCreateStatusBlock.Output,
test_input={
"repo_url": "https://github.com/owner/repo",
"sha": "ce587453ced02b1526dfb4cb910479d431683101",
"state": StatusState.SUCCESS.value,
"target_url": "https://example.com/build/status",
"description": "The build succeeded!",
"check_name": "continuous-integration/jenkins",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"status",
{
"id": 1234567890,
"url": "https://api.github.com/repos/owner/repo/statuses/ce587453ced02b1526dfb4cb910479d431683101",
"state": "success",
"context": "continuous-integration/jenkins",
"description": "The build succeeded!",
"target_url": "https://example.com/build/status",
"created_at": "2024-01-21T10:00:00Z",
"updated_at": "2024-01-21T10:00:00Z",
},
),
],
test_mock={
"create_status": lambda *args, **kwargs: {
"id": 1234567890,
"url": "https://api.github.com/repos/owner/repo/statuses/ce587453ced02b1526dfb4cb910479d431683101",
"state": "success",
"context": "continuous-integration/jenkins",
"description": "The build succeeded!",
"target_url": "https://example.com/build/status",
"created_at": "2024-01-21T10:00:00Z",
"updated_at": "2024-01-21T10:00:00Z",
}
},
)
@staticmethod
def create_status(
credentials: GithubFineGrainedAPICredentials,
repo_url: str,
sha: str,
state: StatusState,
target_url: Optional[str] = None,
description: Optional[str] = None,
context: str = "default",
) -> dict:
api = get_api(credentials)
class StatusData(BaseModel):
state: str
target_url: Optional[str] = None
description: Optional[str] = None
context: str
data = StatusData(
state=state.value,
context=context,
)
if target_url:
data.target_url = target_url
if description:
data.description = description
status_url = f"{repo_url}/statuses/{sha}"
response = api.post(status_url, json=data)
result = response.json()
return {
"id": result["id"],
"url": result["url"],
"state": result["state"],
"context": result["context"],
"description": result.get("description"),
"target_url": result.get("target_url"),
"created_at": result["created_at"],
"updated_at": result["updated_at"],
}
def run(
self,
input_data: Input,
*,
credentials: GithubFineGrainedAPICredentials,
**kwargs,
) -> BlockOutput:
try:
result = self.create_status(
credentials=credentials,
repo_url=input_data.repo_url,
sha=input_data.sha,
state=input_data.state,
target_url=input_data.target_url,
description=input_data.description,
context=input_data.check_name or "AutoGPT Platform Checks",
)
yield "status", result
except Exception as e:
yield "error", str(e)

View File

@@ -151,7 +151,7 @@ class IdeogramModelBlock(Block):
super().__init__(
id="6ab085e2-20b3-4055-bc3e-08036e01eca6",
description="This block runs Ideogram models with both simple and advanced settings.",
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
categories={BlockCategory.AI},
input_schema=IdeogramModelBlock.Input,
output_schema=IdeogramModelBlock.Output,
test_input={

View File

@@ -1,272 +0,0 @@
from __future__ import annotations
import json
from typing import Any, Dict, Optional
from backend.blocks.linear._auth import LinearCredentials
from backend.blocks.linear.models import (
CreateCommentResponse,
CreateIssueResponse,
Issue,
Project,
)
from backend.util.request import Requests
class LinearAPIException(Exception):
def __init__(self, message: str, status_code: int):
super().__init__(message)
self.status_code = status_code
class LinearClient:
"""Client for the Linear API
If you're looking for the schema: https://studio.apollographql.com/public/Linear-API/variant/current/schema
"""
API_URL = "https://api.linear.app/graphql"
def __init__(
self,
credentials: LinearCredentials | None = None,
custom_requests: Optional[Requests] = None,
):
if custom_requests:
self._requests = custom_requests
else:
headers: Dict[str, str] = {
"Content-Type": "application/json",
}
if credentials:
headers["Authorization"] = credentials.auth_header()
self._requests = Requests(
extra_headers=headers,
trusted_origins=["https://api.linear.app"],
raise_for_status=False,
)
def _execute_graphql_request(
self, query: str, variables: dict | None = None
) -> Any:
"""
Executes a GraphQL request against the Linear API and returns the response data.
Args:
query: The GraphQL query string.
variables (optional): Any GraphQL query variables
Returns:
The parsed JSON response data, or raises a LinearAPIException on error.
"""
payload: Dict[str, Any] = {"query": query}
if variables:
payload["variables"] = variables
response = 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
raise LinearAPIException(
f"Linear API request failed ({response.status_code}): {error_message}",
response.status_code,
)
response_data = response.json()
if "errors" in response_data:
error_messages = [
error.get("message", "") for error in response_data["errors"]
]
raise LinearAPIException(
f"Linear API returned errors: {', '.join(error_messages)}",
response.status_code,
)
return response_data["data"]
def query(self, query: str, variables: Optional[dict] = None) -> dict:
"""Executes a GraphQL query.
Args:
query: The GraphQL query string.
variables: Query variables, if any.
Returns:
The response data.
"""
return self._execute_graphql_request(query, variables)
def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict:
"""Executes a GraphQL mutation.
Args:
mutation: The GraphQL mutation string.
variables: Query variables, if any.
Returns:
The response data.
"""
return self._execute_graphql_request(mutation, variables)
def try_create_comment(self, issue_id: str, comment: str) -> CreateCommentResponse:
try:
mutation = """
mutation CommentCreate($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment {
id
body
}
}
}
"""
variables = {
"input": {
"body": comment,
"issueId": issue_id,
}
}
added_comment = 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:
try:
query = """
query GetTeamId($searchTerm: String!) {
teams(filter: {
or: [
{ name: { eqIgnoreCase: $searchTerm } },
{ key: { eqIgnoreCase: $searchTerm } }
]
}) {
nodes {
id
name
key
}
}
}
"""
variables: dict[str, Any] = {
"searchTerm": team_name,
}
team_id = self.query(query, variables)
return team_id["teams"]["nodes"][0]["id"]
except LinearAPIException as e:
raise e
def try_create_issue(
self,
team_id: str,
title: str,
description: str | None = None,
priority: int | None = None,
project_id: str | None = None,
) -> CreateIssueResponse:
try:
mutation = """
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
issue {
title
description
id
identifier
priority
}
}
}
"""
variables: dict[str, Any] = {
"input": {
"teamId": team_id,
"title": title,
}
}
if project_id:
variables["input"]["projectId"] = project_id
if description:
variables["input"]["description"] = description
if priority:
variables["input"]["priority"] = priority
added_issue = self.mutate(mutation, variables)
return CreateIssueResponse(**added_issue["issueCreate"])
except LinearAPIException as e:
raise e
def try_search_projects(self, term: str) -> list[Project]:
try:
query = """
query SearchProjects($term: String!, $includeComments: Boolean!) {
searchProjects(term: $term, includeComments: $includeComments) {
nodes {
id
name
description
priority
progress
content
}
}
}
"""
variables: dict[str, Any] = {
"term": term,
"includeComments": True,
}
projects = 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]:
try:
query = """
query SearchIssues($term: String!, $includeComments: Boolean!) {
searchIssues(term: $term, includeComments: $includeComments) {
nodes {
id
identifier
title
description
priority
}
}
}
"""
variables: dict[str, Any] = {
"term": term,
"includeComments": True,
}
issues = self.query(query, variables)
return [Issue(**issue) for issue in issues["searchIssues"]["nodes"]]
except LinearAPIException as e:
raise e

View File

@@ -1,101 +0,0 @@
from enum import Enum
from typing import Literal
from pydantic import SecretStr
from backend.data.model import (
APIKeyCredentials,
CredentialsField,
CredentialsMetaInput,
OAuth2Credentials,
)
from backend.integrations.providers import ProviderName
from backend.util.settings import Secrets
secrets = Secrets()
LINEAR_OAUTH_IS_CONFIGURED = bool(
secrets.linear_client_id and secrets.linear_client_secret
)
LinearCredentials = OAuth2Credentials | APIKeyCredentials
# LinearCredentialsInput = CredentialsMetaInput[
# Literal[ProviderName.LINEAR],
# Literal["oauth2", "api_key"] if LINEAR_OAUTH_IS_CONFIGURED else Literal["oauth2"],
# ]
LinearCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.LINEAR], Literal["oauth2"]
]
# (required) Comma separated list of scopes:
# read - (Default) Read access for the user's account. This scope will always be present.
# write - Write access for the user's account. If your application only needs to create comments, use a more targeted scope
# issues:create - Allows creating new issues and their attachments
# comments:create - Allows creating new issue comments
# timeSchedule:write - Allows creating and modifying time schedules
# admin - Full access to admin level endpoints. You should never ask for this permission unless it's absolutely needed
class LinearScope(str, Enum):
READ = "read"
WRITE = "write"
ISSUES_CREATE = "issues:create"
COMMENTS_CREATE = "comments:create"
TIME_SCHEDULE_WRITE = "timeSchedule:write"
ADMIN = "admin"
def LinearCredentialsField(scopes: list[LinearScope]) -> LinearCredentialsInput:
"""
Creates a Linear credentials input on a block.
Params:
scope: The authorization scope needed for the block to work. ([list of available scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes))
""" # noqa
return CredentialsField(
required_scopes=set([LinearScope.READ.value]).union(
set([scope.value for scope in scopes])
),
description="The Linear integration can be used with OAuth, "
"or any API key with sufficient permissions for the blocks it is used on.",
)
TEST_CREDENTIALS_OAUTH = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="linear",
title="Mock Linear API key",
username="mock-linear-username",
access_token=SecretStr("mock-linear-access-token"),
access_token_expires_at=None,
refresh_token=SecretStr("mock-linear-refresh-token"),
refresh_token_expires_at=None,
scopes=["mock-linear-scopes"],
)
TEST_CREDENTIALS_API_KEY = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="linear",
title="Mock Linear API key",
api_key=SecretStr("mock-linear-api-key"),
expires_at=None,
)
TEST_CREDENTIALS_INPUT_OAUTH = {
"provider": TEST_CREDENTIALS_OAUTH.provider,
"id": TEST_CREDENTIALS_OAUTH.id,
"type": TEST_CREDENTIALS_OAUTH.type,
"title": TEST_CREDENTIALS_OAUTH.type,
}
TEST_CREDENTIALS_INPUT_API_KEY = {
"provider": TEST_CREDENTIALS_API_KEY.provider,
"id": TEST_CREDENTIALS_API_KEY.id,
"type": TEST_CREDENTIALS_API_KEY.type,
"title": TEST_CREDENTIALS_API_KEY.type,
}

View File

@@ -1,83 +0,0 @@
from backend.blocks.linear._api import LinearAPIException, LinearClient
from backend.blocks.linear._auth import (
LINEAR_OAUTH_IS_CONFIGURED,
TEST_CREDENTIALS_INPUT_OAUTH,
TEST_CREDENTIALS_OAUTH,
LinearCredentials,
LinearCredentialsField,
LinearCredentialsInput,
LinearScope,
)
from backend.blocks.linear.models import CreateCommentResponse
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class LinearCreateCommentBlock(Block):
"""Block for creating comments on Linear issues"""
class Input(BlockSchema):
credentials: LinearCredentialsInput = LinearCredentialsField(
scopes=[LinearScope.COMMENTS_CREATE],
)
issue_id: str = SchemaField(description="ID of the issue to comment on")
comment: str = SchemaField(description="Comment text to add to the issue")
class Output(BlockSchema):
comment_id: str = SchemaField(description="ID of the created comment")
comment_body: str = SchemaField(
description="Text content of the created comment"
)
error: str = SchemaField(description="Error message if comment creation failed")
def __init__(self):
super().__init__(
id="8f7d3a2e-9b5c-4c6a-8f1d-7c8b3e4a5d6c",
description="Creates a new comment on a Linear issue",
input_schema=self.Input,
output_schema=self.Output,
categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING},
test_input={
"issue_id": "TEST-123",
"comment": "Test comment",
"credentials": TEST_CREDENTIALS_INPUT_OAUTH,
},
disabled=not LINEAR_OAUTH_IS_CONFIGURED,
test_credentials=TEST_CREDENTIALS_OAUTH,
test_output=[("comment_id", "abc123"), ("comment_body", "Test comment")],
test_mock={
"create_comment": lambda *args, **kwargs: (
"abc123",
"Test comment",
)
},
)
@staticmethod
def create_comment(
credentials: LinearCredentials, issue_id: str, comment: str
) -> tuple[str, str]:
client = LinearClient(credentials=credentials)
response: CreateCommentResponse = client.try_create_comment(
issue_id=issue_id, comment=comment
)
return response.comment.id, response.comment.body
def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the comment creation"""
try:
comment_id, comment_body = self.create_comment(
credentials=credentials,
issue_id=input_data.issue_id,
comment=input_data.comment,
)
yield "comment_id", comment_id
yield "comment_body", comment_body
except LinearAPIException as e:
yield "error", str(e)
except Exception as e:
yield "error", f"Unexpected error: {str(e)}"

View File

@@ -1,189 +0,0 @@
from backend.blocks.linear._api import LinearAPIException, LinearClient
from backend.blocks.linear._auth import (
LINEAR_OAUTH_IS_CONFIGURED,
TEST_CREDENTIALS_INPUT_OAUTH,
TEST_CREDENTIALS_OAUTH,
LinearCredentials,
LinearCredentialsField,
LinearCredentialsInput,
LinearScope,
)
from backend.blocks.linear.models import CreateIssueResponse, Issue
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class LinearCreateIssueBlock(Block):
"""Block for creating issues on Linear"""
class Input(BlockSchema):
credentials: LinearCredentialsInput = LinearCredentialsField(
scopes=[LinearScope.ISSUES_CREATE],
)
title: str = SchemaField(description="Title of the issue")
description: str | None = SchemaField(description="Description of the issue")
team_name: str = SchemaField(
description="Name of the team to create the issue on"
)
priority: int | None = SchemaField(
description="Priority of the issue",
default=None,
minimum=0,
maximum=4,
)
project_name: str | None = SchemaField(
description="Name of the project to create the issue on",
default=None,
)
class Output(BlockSchema):
issue_id: str = SchemaField(description="ID of the created issue")
issue_title: str = SchemaField(description="Title of the created issue")
error: str = SchemaField(description="Error message if issue creation failed")
def __init__(self):
super().__init__(
id="f9c68f55-dcca-40a8-8771-abf9601680aa",
description="Creates a new issue on Linear",
disabled=not LINEAR_OAUTH_IS_CONFIGURED,
input_schema=self.Input,
output_schema=self.Output,
categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING},
test_input={
"title": "Test issue",
"description": "Test description",
"team_name": "Test team",
"project_name": "Test project",
"credentials": TEST_CREDENTIALS_INPUT_OAUTH,
},
test_credentials=TEST_CREDENTIALS_OAUTH,
test_output=[("issue_id", "abc123"), ("issue_title", "Test issue")],
test_mock={
"create_issue": lambda *args, **kwargs: (
"abc123",
"Test issue",
)
},
)
@staticmethod
def create_issue(
credentials: LinearCredentials,
team_name: str,
title: str,
description: str | None = None,
priority: int | None = None,
project_name: str | None = None,
) -> tuple[str, str]:
client = LinearClient(credentials=credentials)
team_id = 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)
if projects:
project_id = projects[0].id
else:
raise LinearAPIException("Project not found", status_code=404)
response: CreateIssueResponse = client.try_create_issue(
team_id=team_id,
title=title,
description=description,
priority=priority,
project_id=project_id,
)
return response.issue.identifier, response.issue.title
def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the issue creation"""
try:
issue_id, issue_title = self.create_issue(
credentials=credentials,
team_name=input_data.team_name,
title=input_data.title,
description=input_data.description,
priority=input_data.priority,
project_name=input_data.project_name,
)
yield "issue_id", issue_id
yield "issue_title", issue_title
except LinearAPIException as e:
yield "error", str(e)
except Exception as e:
yield "error", f"Unexpected error: {str(e)}"
class LinearSearchIssuesBlock(Block):
"""Block for searching issues on Linear"""
class Input(BlockSchema):
term: str = SchemaField(description="Term to search for issues")
credentials: LinearCredentialsInput = LinearCredentialsField(
scopes=[LinearScope.READ],
)
class Output(BlockSchema):
issues: list[Issue] = SchemaField(description="List of issues")
def __init__(self):
super().__init__(
id="b5a2a0e6-26b4-4c5b-8a42-bc79e9cb65c2",
description="Searches for issues on Linear",
input_schema=self.Input,
output_schema=self.Output,
disabled=not LINEAR_OAUTH_IS_CONFIGURED,
test_input={
"term": "Test issue",
"credentials": TEST_CREDENTIALS_INPUT_OAUTH,
},
test_credentials=TEST_CREDENTIALS_OAUTH,
test_output=[
(
"issues",
[
Issue(
id="abc123",
identifier="abc123",
title="Test issue",
description="Test description",
priority=1,
)
],
)
],
test_mock={
"search_issues": lambda *args, **kwargs: [
Issue(
id="abc123",
identifier="abc123",
title="Test issue",
description="Test description",
priority=1,
)
]
},
)
@staticmethod
def search_issues(
credentials: LinearCredentials,
term: str,
) -> list[Issue]:
client = LinearClient(credentials=credentials)
response: list[Issue] = client.try_search_issues(term=term)
return response
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)
yield "issues", issues
except LinearAPIException as e:
yield "error", str(e)
except Exception as e:
yield "error", f"Unexpected error: {str(e)}"

View File

@@ -1,41 +0,0 @@
from pydantic import BaseModel
class Comment(BaseModel):
id: str
body: str
class CreateCommentInput(BaseModel):
body: str
issueId: str
class CreateCommentResponse(BaseModel):
success: bool
comment: Comment
class CreateCommentResponseWrapper(BaseModel):
commentCreate: CreateCommentResponse
class Issue(BaseModel):
id: str
identifier: str
title: str
description: str | None
priority: int
class CreateIssueResponse(BaseModel):
issue: Issue
class Project(BaseModel):
id: str
name: str
description: str
priority: int
progress: int
content: str

View File

@@ -1,95 +0,0 @@
from backend.blocks.linear._api import LinearAPIException, LinearClient
from backend.blocks.linear._auth import (
LINEAR_OAUTH_IS_CONFIGURED,
TEST_CREDENTIALS_INPUT_OAUTH,
TEST_CREDENTIALS_OAUTH,
LinearCredentials,
LinearCredentialsField,
LinearCredentialsInput,
LinearScope,
)
from backend.blocks.linear.models import Project
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class LinearSearchProjectsBlock(Block):
"""Block for searching projects on Linear"""
class Input(BlockSchema):
credentials: LinearCredentialsInput = LinearCredentialsField(
scopes=[LinearScope.READ],
)
term: str = SchemaField(description="Term to search for projects")
class Output(BlockSchema):
projects: list[Project] = SchemaField(description="List of projects")
error: str = SchemaField(description="Error message if issue creation failed")
def __init__(self):
super().__init__(
id="446a1d35-9d8f-4ac5-83ea-7684ec50e6af",
description="Searches for projects on Linear",
input_schema=self.Input,
output_schema=self.Output,
categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING},
test_input={
"term": "Test project",
"credentials": TEST_CREDENTIALS_INPUT_OAUTH,
},
disabled=not LINEAR_OAUTH_IS_CONFIGURED,
test_credentials=TEST_CREDENTIALS_OAUTH,
test_output=[
(
"projects",
[
Project(
id="abc123",
name="Test project",
description="Test description",
priority=1,
progress=1,
content="Test content",
)
],
)
],
test_mock={
"search_projects": lambda *args, **kwargs: [
Project(
id="abc123",
name="Test project",
description="Test description",
priority=1,
progress=1,
content="Test content",
)
]
},
)
@staticmethod
def search_projects(
credentials: LinearCredentials,
term: str,
) -> list[Project]:
client = LinearClient(credentials=credentials)
response: list[Project] = client.try_search_projects(term=term)
return response
def run(
self, input_data: Input, *, credentials: LinearCredentials, **kwargs
) -> BlockOutput:
"""Execute the project search"""
try:
projects = self.search_projects(
credentials=credentials,
term=input_data.term,
)
yield "projects", projects
except LinearAPIException as e:
yield "error", str(e)
except Exception as e:
yield "error", f"Unexpected error: {str(e)}"

View File

@@ -1,6 +1,5 @@
import ast
import logging
from abc import ABC
from enum import Enum, EnumMeta
from json import JSONDecodeError
from types import MappingProxyType
@@ -69,7 +68,6 @@ def AICredentialsField() -> AICredentials:
class ModelMetadata(NamedTuple):
provider: str
context_window: int
max_output_tokens: int | None
class LlmModelMeta(EnumMeta):
@@ -93,8 +91,6 @@ class LlmModelMeta(EnumMeta):
class LlmModel(str, Enum, metaclass=LlmModelMeta):
# OpenAI models
O3_MINI = "o3-mini"
O1 = "o1"
O1_PREVIEW = "o1-preview"
O1_MINI = "o1-mini"
GPT4O_MINI = "gpt-4o-mini"
@@ -103,31 +99,30 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
GPT3_5_TURBO = "gpt-3.5-turbo"
# Anthropic models
CLAUDE_3_5_SONNET = "claude-3-5-sonnet-latest"
CLAUDE_3_5_HAIKU = "claude-3-5-haiku-latest"
CLAUDE_3_HAIKU = "claude-3-haiku-20240307"
# Groq models
GEMMA2_9B = "gemma2-9b-it"
LLAMA3_3_70B = "llama-3.3-70b-versatile"
LLAMA3_1_8B = "llama-3.1-8b-instant"
LLAMA3_70B = "llama3-70b-8192"
LLAMA3_8B = "llama3-8b-8192"
LLAMA3_70B = "llama3-70b-8192"
MIXTRAL_8X7B = "mixtral-8x7b-32768"
# Groq preview models
DEEPSEEK_LLAMA_70B = "deepseek-r1-distill-llama-70b"
GEMMA_7B = "gemma-7b-it"
GEMMA2_9B = "gemma2-9b-it"
# New Groq models (Preview)
LLAMA3_1_405B = "llama-3.1-405b-reasoning"
LLAMA3_1_70B = "llama-3.1-70b-versatile"
LLAMA3_1_8B = "llama-3.1-8b-instant"
# Ollama models
OLLAMA_LLAMA3_3 = "llama3.3"
OLLAMA_LLAMA3_2 = "llama3.2"
OLLAMA_LLAMA3_8B = "llama3"
OLLAMA_LLAMA3_405B = "llama3.1:405b"
OLLAMA_DOLPHIN = "dolphin-mistral:latest"
# OpenRouter models
GEMINI_FLASH_1_5 = "google/gemini-flash-1.5"
GEMINI_FLASH_1_5_8B = "google/gemini-flash-1.5"
GROK_BETA = "x-ai/grok-beta"
MISTRAL_NEMO = "mistralai/mistral-nemo"
COHERE_COMMAND_R_08_2024 = "cohere/command-r-08-2024"
COHERE_COMMAND_R_PLUS_08_2024 = "cohere/command-r-plus-08-2024"
EVA_QWEN_2_5_32B = "eva-unit-01/eva-qwen-2.5-32b"
DEEPSEEK_CHAT = "deepseek/deepseek-chat" # Actually: DeepSeek V3
DEEPSEEK_CHAT = "deepseek/deepseek-chat"
PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = (
"perplexity/llama-3.1-sonar-large-128k-online"
)
@@ -152,74 +147,47 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
def context_window(self) -> int:
return self.metadata.context_window
@property
def max_output_tokens(self) -> int | None:
return self.metadata.max_output_tokens
MODEL_METADATA = {
# https://platform.openai.com/docs/models
LlmModel.O3_MINI: ModelMetadata("openai", 200000, 100000), # o3-mini-2025-01-31
LlmModel.O1: ModelMetadata("openai", 200000, 100000), # o1-2024-12-17
LlmModel.O1_PREVIEW: ModelMetadata(
"openai", 128000, 32768
), # o1-preview-2024-09-12
LlmModel.O1_MINI: ModelMetadata("openai", 128000, 65536), # o1-mini-2024-09-12
LlmModel.GPT4O_MINI: ModelMetadata(
"openai", 128000, 16384
), # gpt-4o-mini-2024-07-18
LlmModel.GPT4O: ModelMetadata("openai", 128000, 16384), # gpt-4o-2024-08-06
LlmModel.GPT4_TURBO: ModelMetadata(
"openai", 128000, 4096
), # gpt-4-turbo-2024-04-09
LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, 4096), # gpt-3.5-turbo-0125
# https://docs.anthropic.com/en/docs/about-claude/models
LlmModel.CLAUDE_3_5_SONNET: ModelMetadata(
"anthropic", 200000, 8192
), # claude-3-5-sonnet-20241022
LlmModel.CLAUDE_3_5_HAIKU: ModelMetadata(
"anthropic", 200000, 8192
), # claude-3-5-haiku-20241022
LlmModel.CLAUDE_3_HAIKU: ModelMetadata(
"anthropic", 200000, 4096
), # claude-3-haiku-20240307
# https://console.groq.com/docs/models
LlmModel.GEMMA2_9B: ModelMetadata("groq", 8192, None),
LlmModel.LLAMA3_3_70B: ModelMetadata("groq", 128000, 32768),
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 128000, 8192),
LlmModel.LLAMA3_70B: ModelMetadata("groq", 8192, None),
LlmModel.LLAMA3_8B: ModelMetadata("groq", 8192, None),
LlmModel.MIXTRAL_8X7B: ModelMetadata("groq", 32768, None),
LlmModel.DEEPSEEK_LLAMA_70B: ModelMetadata("groq", 128000, None),
# https://ollama.com/library
LlmModel.OLLAMA_LLAMA3_3: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_DOLPHIN: ModelMetadata("ollama", 32768, None),
# https://openrouter.ai/models
LlmModel.GEMINI_FLASH_1_5: ModelMetadata("open_router", 1000000, 8192),
LlmModel.GROK_BETA: ModelMetadata("open_router", 131072, 131072),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 128000, 4096),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 128000, 4096),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 128000, 4096),
LlmModel.EVA_QWEN_2_5_32B: ModelMetadata("open_router", 16384, 4096),
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 64000, 2048),
LlmModel.O1_PREVIEW: ModelMetadata("openai", 32000),
LlmModel.O1_MINI: ModelMetadata("openai", 62000),
LlmModel.GPT4O_MINI: ModelMetadata("openai", 128000),
LlmModel.GPT4O: ModelMetadata("openai", 128000),
LlmModel.GPT4_TURBO: ModelMetadata("openai", 128000),
LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385),
LlmModel.CLAUDE_3_5_SONNET: ModelMetadata("anthropic", 200000),
LlmModel.CLAUDE_3_HAIKU: ModelMetadata("anthropic", 200000),
LlmModel.LLAMA3_8B: ModelMetadata("groq", 8192),
LlmModel.LLAMA3_70B: ModelMetadata("groq", 8192),
LlmModel.MIXTRAL_8X7B: ModelMetadata("groq", 32768),
LlmModel.GEMMA_7B: ModelMetadata("groq", 8192),
LlmModel.GEMMA2_9B: ModelMetadata("groq", 8192),
LlmModel.LLAMA3_1_405B: ModelMetadata("groq", 8192),
# Limited to 16k during preview
LlmModel.LLAMA3_1_70B: ModelMetadata("groq", 131072),
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072),
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_DOLPHIN: ModelMetadata("ollama", 32768),
LlmModel.GEMINI_FLASH_1_5_8B: ModelMetadata("open_router", 8192),
LlmModel.GROK_BETA: ModelMetadata("open_router", 8192),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 4000),
LlmModel.EVA_QWEN_2_5_32B: ModelMetadata("open_router", 4000),
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 8192),
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata(
"open_router", 127072, 127072
"open_router", 8192
),
LlmModel.QWEN_QWQ_32B_PREVIEW: ModelMetadata("open_router", 32768, 32768),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata(
"open_router", 131000, 4096
),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata(
"open_router", 12288, 12288
),
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 300000, 5120),
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 128000, 5120),
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 300000, 5120),
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata("open_router", 65536, 4096),
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4096, 4096),
LlmModel.QWEN_QWQ_32B_PREVIEW: ModelMetadata("open_router", 4000),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata("open_router", 4000),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 4000),
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 4000),
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata("open_router", 4000),
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4000),
}
for model in LlmModel:
@@ -238,17 +206,7 @@ class Message(BlockSchema):
content: str
class AIBlockBase(Block, ABC):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.prompt = ""
def merge_llm_stats(self, block: "AIBlockBase"):
self.merge_stats(block.execution_stats)
self.prompt = block.prompt
class AIStructuredResponseGeneratorBlock(AIBlockBase):
class AIStructuredResponseGeneratorBlock(Block):
class Input(BlockSchema):
prompt: str = SchemaField(
description="The prompt to send to the language model.",
@@ -300,7 +258,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
response: dict[str, Any] = SchemaField(
description="The response object generated by the language model."
)
prompt: str = SchemaField(description="The prompt sent to the language model.")
error: str = SchemaField(description="Error message if the API call failed.")
def __init__(self):
@@ -320,10 +277,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
"prompt": "User prompt",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("response", {"key1": "key1Value", "key2": "key2Value"}),
("prompt", str),
],
test_output=("response", {"key1": "key1Value", "key2": "key2Value"}),
test_mock={
"llm_call": lambda *args, **kwargs: (
json.dumps(
@@ -337,20 +291,19 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
)
},
)
self.prompt = ""
@staticmethod
def llm_call(
self,
credentials: APIKeyCredentials,
llm_model: LlmModel,
prompt: list[dict],
json_format: bool,
max_tokens: int | None,
max_tokens: int | None = None,
ollama_host: str = "localhost:11434",
) -> tuple[str, int, int]:
"""
Args:
credentials: The API key credentials to use.
api_key: API key for the LLM provider.
llm_model: The LLM model to use.
prompt: The prompt to send to the LLM.
json_format: Whether the response should be in JSON format.
@@ -363,7 +316,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
The number of tokens used in the completion.
"""
provider = llm_model.metadata.provider
max_tokens = max_tokens or llm_model.max_output_tokens or 4096
if provider == "openai":
oai_client = openai.OpenAI(api_key=credentials.api_key.get_secret_value())
@@ -385,7 +337,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
response_format=response_format, # type: ignore
max_completion_tokens=max_tokens,
)
self.prompt = json.dumps(prompt)
return (
response.choices[0].message.content or "",
@@ -413,9 +364,8 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
model=llm_model.value,
system=sysprompt,
messages=messages,
max_tokens=max_tokens,
max_tokens=max_tokens or 8192,
)
self.prompt = json.dumps(prompt)
if not resp.content:
raise ValueError("No content returned from Anthropic.")
@@ -442,7 +392,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
response_format=response_format, # type: ignore
max_tokens=max_tokens,
)
self.prompt = json.dumps(prompt)
return (
response.choices[0].message.content or "",
response.usage.prompt_tokens if response.usage else 0,
@@ -457,7 +406,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
prompt=f"{sys_messages}\n\n{usr_messages}",
stream=False,
)
self.prompt = json.dumps(prompt)
return (
response.get("response") or "",
response.get("prompt_eval_count") or 0,
@@ -478,7 +426,6 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
messages=prompt, # type: ignore
max_tokens=max_tokens,
)
self.prompt = json.dumps(prompt)
# If there's no response, raise an error
if not response.choices:
@@ -578,11 +525,9 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
)
for k, v in parsed_dict.items()
}
yield "prompt", self.prompt
return
else:
yield "response", {"response": response_text}
yield "prompt", self.prompt
return
retry_prompt = trim_prompt(
@@ -613,7 +558,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
raise RuntimeError(retry_prompt)
class AITextGeneratorBlock(AIBlockBase):
class AITextGeneratorBlock(Block):
class Input(BlockSchema):
prompt: str = SchemaField(
description="The prompt to send to the language model. You can use any of the {keys} from Prompt Values to fill in the prompt with values from the prompt values dictionary by putting them in curly braces.",
@@ -656,7 +601,6 @@ class AITextGeneratorBlock(AIBlockBase):
response: str = SchemaField(
description="The response generated by the language model."
)
prompt: str = SchemaField(description="The prompt sent to the language model.")
error: str = SchemaField(description="Error message if the API call failed.")
def __init__(self):
@@ -671,10 +615,7 @@ class AITextGeneratorBlock(AIBlockBase):
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("response", "Response text"),
("prompt", str),
],
test_output=("response", "Response text"),
test_mock={"llm_call": lambda *args, **kwargs: "Response text"},
)
@@ -685,7 +626,7 @@ class AITextGeneratorBlock(AIBlockBase):
) -> str:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
self.merge_stats(block.execution_stats)
return response["response"]
def run(
@@ -696,7 +637,6 @@ class AITextGeneratorBlock(AIBlockBase):
expected_format={},
)
yield "response", self.llm_call(object_input_data, credentials)
yield "prompt", self.prompt
class SummaryStyle(Enum):
@@ -706,7 +646,7 @@ class SummaryStyle(Enum):
NUMBERED_LIST = "numbered list"
class AITextSummarizerBlock(AIBlockBase):
class AITextSummarizerBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(
description="The text to summarize.",
@@ -749,7 +689,6 @@ class AITextSummarizerBlock(AIBlockBase):
class Output(BlockSchema):
summary: str = SchemaField(description="The final summary of the text.")
prompt: str = SchemaField(description="The prompt sent to the language model.")
error: str = SchemaField(description="Error message if the API call failed.")
def __init__(self):
@@ -764,10 +703,7 @@ class AITextSummarizerBlock(AIBlockBase):
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("summary", "Final summary of a long text"),
("prompt", str),
],
test_output=("summary", "Final summary of a long text"),
test_mock={
"llm_call": lambda input_data, credentials: (
{"final_summary": "Final summary of a long text"}
@@ -795,7 +731,6 @@ class AITextSummarizerBlock(AIBlockBase):
final_summary = self._combine_summaries(summaries, input_data, credentials)
yield "summary", final_summary
yield "prompt", self.prompt
@staticmethod
def _split_text(text: str, max_tokens: int, overlap: int) -> list[str]:
@@ -816,7 +751,7 @@ class AITextSummarizerBlock(AIBlockBase):
) -> dict:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
self.merge_stats(block.execution_stats)
return response
def _summarize_chunk(
@@ -873,7 +808,7 @@ class AITextSummarizerBlock(AIBlockBase):
] # Get the first yielded value
class AIConversationBlock(AIBlockBase):
class AIConversationBlock(Block):
class Input(BlockSchema):
messages: List[Message] = SchemaField(
description="List of messages in the conversation.", min_length=1
@@ -899,7 +834,6 @@ class AIConversationBlock(AIBlockBase):
response: str = SchemaField(
description="The model's response to the conversation."
)
prompt: str = SchemaField(description="The prompt sent to the language model.")
error: str = SchemaField(description="Error message if the API call failed.")
def __init__(self):
@@ -923,13 +857,10 @@ class AIConversationBlock(AIBlockBase):
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"response",
"The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
),
("prompt", str),
],
test_output=(
"response",
"The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
),
test_mock={
"llm_call": lambda *args, **kwargs: "The 2020 World Series was played at Globe Life Field in Arlington, Texas."
},
@@ -942,7 +873,7 @@ class AIConversationBlock(AIBlockBase):
) -> str:
block = AIStructuredResponseGeneratorBlock()
response = block.run_once(input_data, "response", credentials=credentials)
self.merge_llm_stats(block)
self.merge_stats(block.execution_stats)
return response["response"]
def run(
@@ -956,16 +887,14 @@ class AIConversationBlock(AIBlockBase):
conversation_history=input_data.messages,
max_tokens=input_data.max_tokens,
expected_format={},
ollama_host=input_data.ollama_host,
),
credentials=credentials,
)
yield "response", response
yield "prompt", self.prompt
class AIListGeneratorBlock(AIBlockBase):
class AIListGeneratorBlock(Block):
class Input(BlockSchema):
focus: str | None = SchemaField(
description="The focus of the list to generate.",
@@ -1008,7 +937,6 @@ class AIListGeneratorBlock(AIBlockBase):
list_item: str = SchemaField(
description="Each individual item in the list.",
)
prompt: str = SchemaField(description="The prompt sent to the language model.")
error: str = SchemaField(
description="Error message if the list generation failed."
)
@@ -1040,7 +968,6 @@ class AIListGeneratorBlock(AIBlockBase):
"generated_list",
["Zylora Prime", "Kharon-9", "Vortexia", "Oceara", "Draknos"],
),
("prompt", str),
("list_item", "Zylora Prime"),
("list_item", "Kharon-9"),
("list_item", "Vortexia"),
@@ -1054,14 +981,13 @@ class AIListGeneratorBlock(AIBlockBase):
},
)
@staticmethod
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)
self.merge_llm_stats(llm_block)
return response
@staticmethod
@@ -1175,7 +1101,6 @@ class AIListGeneratorBlock(AIBlockBase):
# If we reach here, we have a valid Python list
logger.debug("Successfully generated a valid Python list")
yield "generated_list", parsed_list
yield "prompt", self.prompt
# Yield each item in the list
for item in parsed_list:

View File

@@ -1,245 +0,0 @@
import os
import tempfile
from typing import Literal, Optional
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.fx.Loop import Loop
from moviepy.video.io.VideoFileClip import VideoFileClip
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.util.file import MediaFile, get_exec_file_path, store_media_file
class MediaDurationBlock(Block):
class Input(BlockSchema):
media_in: MediaFile = SchemaField(
description="Media input (URL, data URI, or local path)."
)
is_video: bool = SchemaField(
description="Whether the media is a video (True) or audio (False).",
default=True,
)
class Output(BlockSchema):
duration: float = SchemaField(
description="Duration of the media file (in seconds)."
)
error: str = SchemaField(
description="Error message if something fails.", default=""
)
def __init__(self):
super().__init__(
id="d8b91fd4-da26-42d4-8ecb-8b196c6d84b6",
description="Block to get the duration of a media file.",
categories={BlockCategory.MULTIMEDIA},
input_schema=MediaDurationBlock.Input,
output_schema=MediaDurationBlock.Output,
)
def run(
self,
input_data: Input,
*,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
# 1) Store the input media locally
local_media_path = store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.media_in,
return_content=False,
)
media_abspath = get_exec_file_path(graph_exec_id, local_media_path)
# 2) Load the clip
if input_data.is_video:
clip = VideoFileClip(media_abspath)
else:
clip = AudioFileClip(media_abspath)
yield "duration", clip.duration
class LoopVideoBlock(Block):
"""
Block for looping (repeating) a video clip until a given duration or number of loops.
"""
class Input(BlockSchema):
video_in: MediaFile = SchemaField(
description="The input video (can be a URL, data URI, or local path)."
)
# Provide EITHER a `duration` or `n_loops` or both. We'll demonstrate `duration`.
duration: Optional[float] = SchemaField(
description="Target duration (in seconds) to loop the video to. If omitted, defaults to no looping.",
default=None,
ge=0.0,
)
n_loops: Optional[int] = SchemaField(
description="Number of times to repeat the video. If omitted, defaults to 1 (no repeat).",
default=None,
ge=1,
)
output_return_type: Literal["file_path", "data_uri"] = SchemaField(
description="How to return the output video. Either a relative path or base64 data URI.",
default="file_path",
)
class Output(BlockSchema):
video_out: str = SchemaField(
description="Looped video returned either as a relative path or a data URI."
)
error: str = SchemaField(
description="Error message if something fails.", default=""
)
def __init__(self):
super().__init__(
id="8bf9eef6-5451-4213-b265-25306446e94b",
description="Block to loop a video to a given duration or number of repeats.",
categories={BlockCategory.MULTIMEDIA},
input_schema=LoopVideoBlock.Input,
output_schema=LoopVideoBlock.Output,
)
def run(
self,
input_data: Input,
*,
node_exec_id: str,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
# 1) Store the input video locally
local_video_path = store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.video_in,
return_content=False,
)
input_abspath = get_exec_file_path(graph_exec_id, local_video_path)
# 2) Load the clip
clip = VideoFileClip(input_abspath)
# 3) Apply the loop effect
looped_clip = clip
if input_data.duration:
# Loop until we reach the specified duration
looped_clip = looped_clip.with_effects([Loop(duration=input_data.duration)])
elif input_data.n_loops:
looped_clip = looped_clip.with_effects([Loop(n=input_data.n_loops)])
else:
raise ValueError("Either 'duration' or 'n_loops' must be provided.")
assert isinstance(looped_clip, VideoFileClip)
# 4) Save the looped output
output_filename = MediaFile(
f"{node_exec_id}_looped_{os.path.basename(local_video_path)}"
)
output_abspath = get_exec_file_path(graph_exec_id, output_filename)
looped_clip = looped_clip.with_audio(clip.audio)
looped_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac")
# Return as data URI
video_out = store_media_file(
graph_exec_id=graph_exec_id,
file=output_filename,
return_content=input_data.output_return_type == "data_uri",
)
yield "video_out", video_out
class AddAudioToVideoBlock(Block):
"""
Block that adds (attaches) an audio track to an existing video.
Optionally scale the volume of the new track.
"""
class Input(BlockSchema):
video_in: MediaFile = SchemaField(
description="Video input (URL, data URI, or local path)."
)
audio_in: MediaFile = SchemaField(
description="Audio input (URL, data URI, or local path)."
)
volume: float = SchemaField(
description="Volume scale for the newly attached audio track (1.0 = original).",
default=1.0,
)
output_return_type: Literal["file_path", "data_uri"] = SchemaField(
description="Return the final output as a relative path or base64 data URI.",
default="file_path",
)
class Output(BlockSchema):
video_out: MediaFile = SchemaField(
description="Final video (with attached audio), as a path or data URI."
)
error: str = SchemaField(
description="Error message if something fails.", default=""
)
def __init__(self):
super().__init__(
id="3503748d-62b6-4425-91d6-725b064af509",
description="Block to attach an audio file to a video file using moviepy.",
categories={BlockCategory.MULTIMEDIA},
input_schema=AddAudioToVideoBlock.Input,
output_schema=AddAudioToVideoBlock.Output,
)
def run(
self,
input_data: Input,
*,
node_exec_id: str,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
# 1) Store the inputs locally
local_video_path = store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.video_in,
return_content=False,
)
local_audio_path = store_media_file(
graph_exec_id=graph_exec_id,
file=input_data.audio_in,
return_content=False,
)
abs_temp_dir = os.path.join(tempfile.gettempdir(), "exec_file", graph_exec_id)
video_abspath = os.path.join(abs_temp_dir, local_video_path)
audio_abspath = os.path.join(abs_temp_dir, local_audio_path)
# 2) Load video + audio with moviepy
video_clip = VideoFileClip(video_abspath)
audio_clip = AudioFileClip(audio_abspath)
# Optionally scale volume
if input_data.volume != 1.0:
audio_clip = audio_clip.with_volume_scaled(input_data.volume)
# 3) Attach the new audio track
final_clip = video_clip.with_audio(audio_clip)
# 4) Write to output file
output_filename = MediaFile(
f"{node_exec_id}_audio_attached_{os.path.basename(local_video_path)}"
)
output_abspath = os.path.join(abs_temp_dir, output_filename)
final_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac")
# 5) Return either path or data URI
video_out = store_media_file(
graph_exec_id=graph_exec_id,
file=output_filename,
return_content=input_data.output_return_type == "data_uri",
)
yield "video_out", video_out

View File

@@ -1,338 +0,0 @@
from typing import Any, Literal, Optional, Union
from mem0 import MemoryClient
from pydantic import BaseModel, SecretStr
from backend.data.block import Block, BlockOutput, BlockSchema
from backend.data.model import (
APIKeyCredentials,
CredentialsField,
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
TEST_CREDENTIALS = APIKeyCredentials(
id="ed55ac19-356e-4243-a6cb-bc599e9b716f",
provider="mem0",
api_key=SecretStr("mock-mem0-api-key"),
title="Mock Mem0 API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
class Mem0Base:
"""Base class with shared utilities for Mem0 blocks"""
@staticmethod
def _get_client(credentials: APIKeyCredentials) -> MemoryClient:
"""Get initialized Mem0 client"""
return MemoryClient(api_key=credentials.api_key.get_secret_value())
Filter = dict[str, list[dict[str, str | dict[str, list[str]]]]]
class Conversation(BaseModel):
discriminator: Literal["conversation"]
messages: list[dict[str, str]]
class Content(BaseModel):
discriminator: Literal["content"]
content: str
class AddMemoryBlock(Block, Mem0Base):
"""Block for adding memories to Mem0
Always limited by user_id and optional graph_id and graph_exec_id"""
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.MEM0], Literal["api_key"]
] = CredentialsField(description="Mem0 API key credentials")
content: Union[Content, Conversation] = SchemaField(
discriminator="discriminator",
description="Content to add - either a string or list of message objects as output from an AI block",
default=Content(discriminator="content", content="I'm a vegetarian"),
)
metadata: dict[str, Any] = SchemaField(
description="Optional metadata for the memory", default={}
)
limit_memory_to_run: bool = SchemaField(
description="Limit the memory to the run", default=False
)
limit_memory_to_agent: bool = SchemaField(
description="Limit the memory to the agent", default=False
)
class Output(BlockSchema):
action: str = SchemaField(description="Action of the operation")
memory: str = SchemaField(description="Memory created")
error: str = SchemaField(description="Error message if operation fails")
def __init__(self):
super().__init__(
id="dce97578-86be-45a4-ae50-f6de33fc935a",
description="Add new memories to Mem0 with user segmentation",
input_schema=AddMemoryBlock.Input,
output_schema=AddMemoryBlock.Output,
test_input=[
{
"content": {
"discriminator": "conversation",
"messages": [{"role": "user", "content": "I'm a vegetarian"}],
},
"metadata": {"food": "vegetarian"},
"credentials": TEST_CREDENTIALS_INPUT,
},
{
"content": {
"discriminator": "content",
"content": "I am a vegetarian",
},
"metadata": {"food": "vegetarian"},
"credentials": TEST_CREDENTIALS_INPUT,
},
],
test_output=[("action", "NO_CHANGE"), ("action", "NO_CHANGE")],
test_credentials=TEST_CREDENTIALS,
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
user_id: str,
graph_id: str,
graph_exec_id: str,
**kwargs
) -> BlockOutput:
try:
client = self._get_client(credentials)
if isinstance(input_data.content, Conversation):
messages = input_data.content.messages
else:
messages = [{"role": "user", "content": input_data.content}]
params = {
"user_id": user_id,
"output_format": "v1.1",
"metadata": input_data.metadata,
}
if input_data.limit_memory_to_run:
params["run_id"] = graph_exec_id
if input_data.limit_memory_to_agent:
params["agent_id"] = graph_id
# Use the client to add memory
result = client.add(
messages,
**params,
)
if len(result.get("results", [])) > 0:
for result in result.get("results", []):
yield "action", result["event"]
yield "memory", result["memory"]
else:
yield "action", "NO_CHANGE"
except Exception as e:
yield "error", str(object=e)
class SearchMemoryBlock(Block, Mem0Base):
"""Block for searching memories in Mem0"""
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.MEM0], Literal["api_key"]
] = CredentialsField(description="Mem0 API key credentials")
query: str = SchemaField(
description="Search query",
advanced=False,
)
trigger: bool = SchemaField(
description="An unused field that is used to (re-)trigger the block when you have no other inputs",
default=False,
advanced=False,
)
categories_filter: list[str] = SchemaField(
description="Categories to filter by",
default=[],
advanced=True,
)
limit_memory_to_run: bool = SchemaField(
description="Limit the memory to the run", default=False
)
limit_memory_to_agent: bool = SchemaField(
description="Limit the memory to the agent", default=True
)
class Output(BlockSchema):
memories: Any = SchemaField(description="List of matching memories")
error: str = SchemaField(description="Error message if operation fails")
def __init__(self):
super().__init__(
id="bd7c84e3-e073-4b75-810c-600886ec8a5b",
description="Search memories in Mem0 by user",
input_schema=SearchMemoryBlock.Input,
output_schema=SearchMemoryBlock.Output,
test_input={
"query": "vegetarian preferences",
"credentials": TEST_CREDENTIALS_INPUT,
"top_k": 10,
"rerank": True,
},
test_output=[
("memories", [{"id": "test-memory", "content": "test content"}])
],
test_credentials=TEST_CREDENTIALS,
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
user_id: str,
graph_id: str,
graph_exec_id: str,
**kwargs
) -> BlockOutput:
try:
client = self._get_client(credentials)
filters: Filter = {
# This works with only one filter, so we can allow others to add on later
"AND": [
{"user_id": user_id},
]
}
if input_data.categories_filter:
filters["AND"].append(
{"categories": {"contains": input_data.categories_filter}}
)
if input_data.limit_memory_to_run:
filters["AND"].append({"run_id": graph_exec_id})
if input_data.limit_memory_to_agent:
filters["AND"].append({"agent_id": graph_id})
result: list[dict[str, Any]] = client.search(
input_data.query, version="v2", filters=filters
)
yield "memories", result
except Exception as e:
yield "error", str(e)
class GetAllMemoriesBlock(Block, Mem0Base):
"""Block for retrieving all memories from Mem0"""
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.MEM0], Literal["api_key"]
] = CredentialsField(description="Mem0 API key credentials")
trigger: bool = SchemaField(
description="An unused field that is used to trigger the block when you have no other inputs",
default=False,
advanced=False,
)
categories: Optional[list[str]] = SchemaField(
description="Filter by categories", default=None
)
limit_memory_to_run: bool = SchemaField(
description="Limit the memory to the run", default=False
)
limit_memory_to_agent: bool = SchemaField(
description="Limit the memory to the agent", default=False
)
class Output(BlockSchema):
memories: Any = SchemaField(description="List of memories")
error: str = SchemaField(description="Error message if operation fails")
def __init__(self):
super().__init__(
id="45aee5bf-4767-45d1-a28b-e01c5aae9fc1",
description="Retrieve all memories from Mem0 with pagination",
input_schema=GetAllMemoriesBlock.Input,
output_schema=GetAllMemoriesBlock.Output,
test_input={
"user_id": "test_user",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_output=[
("memories", [{"id": "test-memory", "content": "test content"}]),
],
test_credentials=TEST_CREDENTIALS,
test_mock={"_get_client": lambda credentials: MockMemoryClient()},
)
def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
user_id: str,
graph_id: str,
graph_exec_id: str,
**kwargs
) -> BlockOutput:
try:
client = self._get_client(credentials)
filters: Filter = {
"AND": [
{"user_id": user_id},
]
}
if input_data.limit_memory_to_run:
filters["AND"].append({"run_id": graph_exec_id})
if input_data.limit_memory_to_agent:
filters["AND"].append({"agent_id": graph_id})
if input_data.categories:
filters["AND"].append(
{"categories": {"contains": input_data.categories}}
)
memories: list[dict[str, Any]] = client.get_all(
filters=filters,
version="v2",
)
yield "memories", memories
except Exception as e:
yield "error", str(e)
# Mock client for testing
class MockMemoryClient:
"""Mock Mem0 client for testing"""
def add(self, *args, **kwargs):
return {"memory_id": "test-memory-id", "status": "success"}
def search(self, *args, **kwargs) -> list[dict[str, str]]:
return [{"id": "test-memory", "content": "test content"}]
def get_all(self, *args, **kwargs) -> list[dict[str, str]]:
return [{"id": "test-memory", "content": "test content"}]

View File

@@ -1,48 +1,22 @@
from datetime import datetime, timezone
from typing import Iterator, Literal
from typing import Iterator
import praw
from pydantic import BaseModel, SecretStr
from pydantic import BaseModel, ConfigDict
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
SchemaField,
UserPasswordCredentials,
)
from backend.integrations.providers import ProviderName
from backend.data.model import BlockSecret, SchemaField, SecretField
from backend.util.mock import MockObject
from backend.util.settings import Settings
RedditCredentials = UserPasswordCredentials
RedditCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.REDDIT],
Literal["user_password"],
]
def RedditCredentialsField() -> RedditCredentialsInput:
"""Creates a Reddit credentials input on a block."""
return CredentialsField(
description="The Reddit integration requires a username and password.",
)
class RedditCredentials(BaseModel):
client_id: BlockSecret = SecretField(key="reddit_client_id")
client_secret: BlockSecret = SecretField(key="reddit_client_secret")
username: BlockSecret = SecretField(key="reddit_username")
password: BlockSecret = SecretField(key="reddit_password")
user_agent: str = "AutoGPT:1.0 (by /u/autogpt)"
TEST_CREDENTIALS = UserPasswordCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="reddit",
username=SecretStr("mock-reddit-username"),
password=SecretStr("mock-reddit-password"),
title="Mock Reddit credentials",
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
model_config = ConfigDict(title="Reddit Credentials")
class RedditPost(BaseModel):
@@ -57,16 +31,13 @@ class RedditComment(BaseModel):
comment: str
settings = Settings()
def get_praw(creds: RedditCredentials) -> praw.Reddit:
client = praw.Reddit(
client_id=settings.secrets.reddit_client_id,
client_secret=settings.secrets.reddit_client_secret,
client_id=creds.client_id.get_secret_value(),
client_secret=creds.client_secret.get_secret_value(),
username=creds.username.get_secret_value(),
password=creds.password.get_secret_value(),
user_agent=settings.config.reddit_user_agent,
user_agent=creds.user_agent,
)
me = client.user.me()
if not me:
@@ -77,11 +48,11 @@ def get_praw(creds: RedditCredentials) -> praw.Reddit:
class GetRedditPostsBlock(Block):
class Input(BlockSchema):
subreddit: str = SchemaField(
description="Subreddit name, excluding the /r/ prefix",
default="writingprompts",
subreddit: str = SchemaField(description="Subreddit name")
creds: RedditCredentials = SchemaField(
description="Reddit credentials",
default=RedditCredentials(),
)
credentials: RedditCredentialsInput = RedditCredentialsField()
last_minutes: int | None = SchemaField(
description="Post time to stop minutes ago while fetching posts",
default=None,
@@ -99,18 +70,20 @@ class GetRedditPostsBlock(Block):
def __init__(self):
super().__init__(
disabled=True,
id="c6731acb-4285-4ee1-bc9b-03d0766c370f",
description="This block fetches Reddit posts from a defined subreddit name.",
categories={BlockCategory.SOCIAL},
disabled=(
not settings.secrets.reddit_client_id
or not settings.secrets.reddit_client_secret
),
input_schema=GetRedditPostsBlock.Input,
output_schema=GetRedditPostsBlock.Output,
test_credentials=TEST_CREDENTIALS,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"creds": {
"client_id": "client_id",
"client_secret": "client_secret",
"username": "username",
"password": "password",
"user_agent": "user_agent",
},
"subreddit": "subreddit",
"last_post": "id3",
"post_limit": 2,
@@ -130,7 +103,7 @@ class GetRedditPostsBlock(Block):
),
],
test_mock={
"get_posts": lambda input_data, credentials: [
"get_posts": lambda _: [
MockObject(id="id1", title="title1", selftext="body1"),
MockObject(id="id2", title="title2", selftext="body2"),
MockObject(id="id3", title="title2", selftext="body2"),
@@ -139,18 +112,14 @@ class GetRedditPostsBlock(Block):
)
@staticmethod
def get_posts(
input_data: Input, *, credentials: RedditCredentials
) -> Iterator[praw.reddit.Submission]:
client = get_praw(credentials)
def get_posts(input_data: Input) -> Iterator[praw.reddit.Submission]:
client = get_praw(input_data.creds)
subreddit = client.subreddit(input_data.subreddit)
return subreddit.new(limit=input_data.post_limit or 10)
def run(
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
current_time = datetime.now(tz=timezone.utc)
for post in self.get_posts(input_data=input_data, credentials=credentials):
for post in self.get_posts(input_data):
if input_data.last_minutes:
post_datetime = datetime.fromtimestamp(
post.created_utc, tz=timezone.utc
@@ -172,7 +141,9 @@ class GetRedditPostsBlock(Block):
class PostRedditCommentBlock(Block):
class Input(BlockSchema):
credentials: RedditCredentialsInput = RedditCredentialsField()
creds: RedditCredentials = SchemaField(
description="Reddit credentials", default=RedditCredentials()
)
data: RedditComment = SchemaField(description="Reddit comment")
class Output(BlockSchema):
@@ -185,15 +156,7 @@ class PostRedditCommentBlock(Block):
categories={BlockCategory.SOCIAL},
input_schema=PostRedditCommentBlock.Input,
output_schema=PostRedditCommentBlock.Output,
disabled=(
not settings.secrets.reddit_client_id
or not settings.secrets.reddit_client_secret
),
test_credentials=TEST_CREDENTIALS,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"data": {"post_id": "id", "comment": "comment"},
},
test_input={"data": {"post_id": "id", "comment": "comment"}},
test_output=[("comment_id", "dummy_comment_id")],
test_mock={"reply_post": lambda creds, comment: "dummy_comment_id"},
)
@@ -207,7 +170,5 @@ class PostRedditCommentBlock(Block):
raise ValueError("Failed to post comment.")
return new_comment.id
def run(
self, input_data: Input, *, credentials: RedditCredentials, **kwargs
) -> BlockOutput:
yield "comment_id", self.reply_post(credentials, input_data.data)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "comment_id", self.reply_post(input_data.creds, input_data.data)

View File

@@ -131,7 +131,7 @@ class ReplicateFluxAdvancedModelBlock(Block):
super().__init__(
id="90f8c45e-e983-4644-aa0b-b4ebe2f531bc",
description="This block runs Flux models on Replicate with advanced settings.",
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
categories={BlockCategory.AI},
input_schema=ReplicateFluxAdvancedModelBlock.Input,
output_schema=ReplicateFluxAdvancedModelBlock.Output,
test_input={

View File

@@ -1,174 +0,0 @@
from base64 import b64encode
from enum import Enum
from typing import Literal
from pydantic import SecretStr
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import (
APIKeyCredentials,
CredentialsField,
CredentialsMetaInput,
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.file import MediaFile, store_media_file
from backend.util.request import Requests
class Format(str, Enum):
PNG = "png"
JPEG = "jpeg"
WEBP = "webp"
class ScreenshotWebPageBlock(Block):
"""Block for taking screenshots using ScreenshotOne API"""
class Input(BlockSchema):
credentials: CredentialsMetaInput[
Literal[ProviderName.SCREENSHOTONE], Literal["api_key"]
] = CredentialsField(description="The ScreenshotOne API key")
url: str = SchemaField(
description="URL of the website to screenshot",
placeholder="https://example.com",
)
viewport_width: int = SchemaField(
description="Width of the viewport in pixels", default=1920
)
viewport_height: int = SchemaField(
description="Height of the viewport in pixels", default=1080
)
full_page: bool = SchemaField(
description="Whether to capture the full page length", default=False
)
format: Format = SchemaField(
description="Output format (png, jpeg, webp)", default=Format.PNG
)
block_ads: bool = SchemaField(description="Whether to block ads", default=True)
block_cookie_banners: bool = SchemaField(
description="Whether to block cookie banners", default=True
)
block_chats: bool = SchemaField(
description="Whether to block chat widgets", default=True
)
cache: bool = SchemaField(
description="Whether to enable caching", default=False
)
class Output(BlockSchema):
image: MediaFile = SchemaField(description="The screenshot image data")
error: str = SchemaField(description="Error message if the screenshot failed")
def __init__(self):
super().__init__(
id="3a7c4b8d-6e2f-4a5d-b9c1-f8d23c5a9b0e", # Generated UUID
description="Takes a screenshot of a specified website using ScreenshotOne API",
categories={BlockCategory.DATA},
input_schema=ScreenshotWebPageBlock.Input,
output_schema=ScreenshotWebPageBlock.Output,
test_input={
"url": "https://example.com",
"viewport_width": 1920,
"viewport_height": 1080,
"full_page": False,
"format": "png",
"block_ads": True,
"block_cookie_banners": True,
"block_chats": True,
"cache": False,
"credentials": {
"provider": "screenshotone",
"type": "api_key",
"id": "test-id",
"title": "Test API Key",
},
},
test_credentials=APIKeyCredentials(
id="test-id",
provider="screenshotone",
api_key=SecretStr("test-key"),
title="Test API Key",
expires_at=None,
),
test_output=[
(
"image",
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAB5JREFUOE9jZPjP8J+BAsA4agDDaBgwjIYBw7AIAwCV5B/xAsMbygAAAABJRU5ErkJggg==",
),
],
test_mock={
"take_screenshot": lambda *args, **kwargs: {
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAB5JREFUOE9jZPjP8J+BAsA4agDDaBgwjIYBw7AIAwCV5B/xAsMbygAAAABJRU5ErkJggg==",
}
},
)
@staticmethod
def take_screenshot(
credentials: APIKeyCredentials,
graph_exec_id: str,
url: str,
viewport_width: int,
viewport_height: int,
full_page: bool,
format: Format,
block_ads: bool,
block_cookie_banners: bool,
block_chats: bool,
cache: bool,
) -> dict:
"""
Takes a screenshot using the ScreenshotOne API
"""
api = Requests(trusted_origins=["https://api.screenshotone.com"])
# Build API URL with parameters
params = {
"access_key": credentials.api_key.get_secret_value(),
"url": url,
"viewport_width": viewport_width,
"viewport_height": viewport_height,
"full_page": str(full_page).lower(),
"format": format.value,
"block_ads": str(block_ads).lower(),
"block_cookie_banners": str(block_cookie_banners).lower(),
"block_chats": str(block_chats).lower(),
"cache": str(cache).lower(),
}
response = api.get("https://api.screenshotone.com/take", params=params)
return {
"image": store_media_file(
graph_exec_id=graph_exec_id,
file=f"data:image/{format.value};base64,{b64encode(response.content).decode('utf-8')}",
return_content=True,
)
}
def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
graph_exec_id: str,
**kwargs,
) -> BlockOutput:
try:
screenshot_data = self.take_screenshot(
credentials=credentials,
graph_exec_id=graph_exec_id,
url=input_data.url,
viewport_width=input_data.viewport_width,
viewport_height=input_data.viewport_height,
full_page=input_data.full_page,
format=input_data.format,
block_ads=input_data.block_ads,
block_cookie_banners=input_data.block_cookie_banners,
block_chats=input_data.block_chats,
cache=input_data.cache,
)
yield "image", screenshot_data["image"]
except Exception as e:
yield "error", str(e)

View File

@@ -78,7 +78,7 @@ class CreateTalkingAvatarVideoBlock(Block):
super().__init__(
id="98c6f503-8c47-4b1c-a96d-351fc7c87dab",
description="This block integrates with D-ID to create video clips and retrieve their URLs.",
categories={BlockCategory.AI, BlockCategory.MULTIMEDIA},
categories={BlockCategory.AI},
input_schema=CreateTalkingAvatarVideoBlock.Input,
output_schema=CreateTalkingAvatarVideoBlock.Output,
test_input={

View File

@@ -212,71 +212,3 @@ class CombineTextsBlock(Block):
def run(self, input_data: Input, **kwargs) -> BlockOutput:
combined_text = input_data.delimiter.join(input_data.input)
yield "output", combined_text
class TextSplitBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(description="The text to split.")
delimiter: str = SchemaField(description="The delimiter to split the text by.")
strip: bool = SchemaField(
description="Whether to strip the text.", default=True
)
class Output(BlockSchema):
texts: list[str] = SchemaField(
description="The text split into a list of strings."
)
def __init__(self):
super().__init__(
id="d5ea33c8-a575-477a-b42f-2fe3be5055ec",
description="This block is used to split a text into a list of strings.",
categories={BlockCategory.TEXT},
input_schema=TextSplitBlock.Input,
output_schema=TextSplitBlock.Output,
test_input=[
{"text": "Hello, World!", "delimiter": ","},
{"text": "Hello, World!", "delimiter": ",", "strip": False},
],
test_output=[
("texts", ["Hello", "World!"]),
("texts", ["Hello", " World!"]),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
if len(input_data.text) == 0:
yield "texts", []
else:
texts = input_data.text.split(input_data.delimiter)
if input_data.strip:
texts = [text.strip() for text in texts]
yield "texts", texts
class TextReplaceBlock(Block):
class Input(BlockSchema):
text: str = SchemaField(description="The text to replace.")
old: str = SchemaField(description="The old text to replace.")
new: str = SchemaField(description="The new text to replace with.")
class Output(BlockSchema):
output: str = SchemaField(description="The text with the replaced text.")
def __init__(self):
super().__init__(
id="7e7c87ab-3469-4bcc-9abe-67705091b713",
description="This block is used to replace a text with a new text.",
categories={BlockCategory.TEXT},
input_schema=TextReplaceBlock.Input,
output_schema=TextReplaceBlock.Output,
test_input=[
{"text": "Hello, World!", "old": "Hello", "new": "Hi"},
],
test_output=[
("output", "Hi, World!"),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text.replace(input_data.old, input_data.new)

View File

@@ -53,7 +53,7 @@ class UnrealTextToSpeechBlock(Block):
super().__init__(
id="4ff1ff6d-cc40-4caa-ae69-011daa20c378",
description="Converts text to speech using the Unreal Speech API",
categories={BlockCategory.AI, BlockCategory.TEXT, BlockCategory.MULTIMEDIA},
categories={BlockCategory.AI, BlockCategory.TEXT},
input_schema=UnrealTextToSpeechBlock.Input,
output_schema=UnrealTextToSpeechBlock.Output,
test_input={

View File

@@ -1,61 +0,0 @@
from typing import Literal
from pydantic import SecretStr
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
OAuth2Credentials,
ProviderName,
)
from backend.integrations.oauth.todoist import TodoistOAuthHandler
from backend.util.settings import Secrets
secrets = Secrets()
TODOIST_OAUTH_IS_CONFIGURED = bool(
secrets.todoist_client_id and secrets.todoist_client_secret
)
TodoistCredentials = OAuth2Credentials
TodoistCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.TODOIST], Literal["oauth2"]
]
def TodoistCredentialsField(scopes: list[str]) -> TodoistCredentialsInput:
"""
Creates a Todoist credentials input on a block.
Params:
scopes: The authorization scopes needed for the block to work.
"""
return CredentialsField(
required_scopes=set(TodoistOAuthHandler.DEFAULT_SCOPES + scopes),
description="The Todoist integration requires OAuth2 authentication.",
)
TEST_CREDENTIALS = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="todoist",
access_token=SecretStr("mock-todoist-access-token"),
refresh_token=None,
access_token_expires_at=None,
scopes=[
"task:add",
"data:read",
"data:read_write",
"data:delete",
"project:delete",
],
title="Mock Todoist OAuth2 Credentials",
username="mock-todoist-username",
refresh_token_expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}

View File

@@ -1,24 +0,0 @@
from enum import Enum
class Colors(Enum):
berry_red = "berry_red"
red = "red"
orange = "orange"
yellow = "yellow"
olive_green = "olive_green"
lime_green = "lime_green"
green = "green"
mint_green = "mint_green"
teal = "teal"
sky_blue = "sky_blue"
light_blue = "light_blue"
blue = "blue"
grape = "grape"
violet = "violet"
lavender = "lavender"
magenta = "magenta"
salmon = "salmon"
charcoal = "charcoal"
grey = "grey"
taupe = "taupe"

View File

@@ -1,439 +0,0 @@
from typing import Literal, Union
from pydantic import BaseModel
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TaskId(BaseModel):
discriminator: Literal["task"]
task_id: str
class ProjectId(BaseModel):
discriminator: Literal["project"]
project_id: str
class TodoistCreateCommentBlock(Block):
"""Creates a new comment on a Todoist task or project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
content: str = SchemaField(description="Comment content")
id_type: Union[TaskId, ProjectId] = SchemaField(
discriminator="discriminator",
description="Specify either task_id or project_id to comment on",
default=TaskId(discriminator="task", task_id=""),
advanced=False,
)
attachment: Optional[dict] = SchemaField(
description="Optional file attachment", default=None
)
class Output(BlockSchema):
id: str = SchemaField(description="ID of created comment")
content: str = SchemaField(description="Comment content")
posted_at: str = SchemaField(description="Comment timestamp")
task_id: Optional[str] = SchemaField(
description="Associated task ID", default=None
)
project_id: Optional[str] = SchemaField(
description="Associated project ID", default=None
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1bba7e54-2310-4a31-8e6f-54d5f9ab7459",
description="Creates a new comment on a Todoist task or project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateCommentBlock.Input,
output_schema=TodoistCreateCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"content": "Test comment",
"id_type": {"discriminator": "task", "task_id": "2995104339"},
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2992679862"),
("content", "Test comment"),
("posted_at", "2016-09-22T07:00:00.000000Z"),
("task_id", "2995104339"),
("project_id", None),
],
test_mock={
"create_comment": lambda content, credentials, task_id=None, project_id=None, attachment=None: {
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
}
},
)
@staticmethod
def create_comment(
credentials: TodoistCredentials,
content: str,
task_id: Optional[str] = None,
project_id: Optional[str] = None,
attachment: Optional[dict] = None,
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comment = api.add_comment(
content=content,
task_id=task_id,
project_id=project_id,
attachment=attachment,
)
return comment.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_id = None
project_id = None
if isinstance(input_data.id_type, TaskId):
task_id = input_data.id_type.task_id
else:
project_id = input_data.id_type.project_id
comment_data = self.create_comment(
credentials,
input_data.content,
task_id=task_id,
project_id=project_id,
attachment=input_data.attachment,
)
if comment_data:
yield "id", comment_data["id"]
yield "content", comment_data["content"]
yield "posted_at", comment_data["posted_at"]
yield "task_id", comment_data["task_id"]
yield "project_id", comment_data["project_id"]
except Exception as e:
yield "error", str(e)
class TodoistGetCommentsBlock(Block):
"""Get all comments for a Todoist task or project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
id_type: Union[TaskId, ProjectId] = SchemaField(
discriminator="discriminator",
description="Specify either task_id or project_id to get comments for",
default=TaskId(discriminator="task", task_id=""),
advanced=False,
)
class Output(BlockSchema):
comments: list = SchemaField(description="List of comments")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9972d8ae-ddf2-11ef-a9b8-32d3674e8b7e",
description="Get all comments for a Todoist task or project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetCommentsBlock.Input,
output_schema=TodoistGetCommentsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"id_type": {"discriminator": "task", "task_id": "2995104339"},
},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"comments",
[
{
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
"attachment": None,
}
],
)
],
test_mock={
"get_comments": lambda credentials, task_id=None, project_id=None: [
{
"id": "2992679862",
"content": "Test comment",
"posted_at": "2016-09-22T07:00:00.000000Z",
"task_id": "2995104339",
"project_id": None,
"attachment": None,
}
]
},
)
@staticmethod
def get_comments(
credentials: TodoistCredentials,
task_id: Optional[str] = None,
project_id: Optional[str] = None,
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comments = api.get_comments(task_id=task_id, project_id=project_id)
return [comment.__dict__ for comment in comments]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_id = None
project_id = None
if isinstance(input_data.id_type, TaskId):
task_id = input_data.id_type.task_id
else:
project_id = input_data.id_type.project_id
comments = self.get_comments(
credentials, task_id=task_id, project_id=project_id
)
yield "comments", comments
except Exception as e:
yield "error", str(e)
class TodoistGetCommentBlock(Block):
"""Get a single comment from Todoist using comment ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to retrieve")
class Output(BlockSchema):
content: str = SchemaField(description="Comment content")
id: str = SchemaField(description="Comment ID")
posted_at: str = SchemaField(description="Comment timestamp")
project_id: Optional[str] = SchemaField(
description="Associated project ID", default=None
)
task_id: Optional[str] = SchemaField(
description="Associated task ID", default=None
)
attachment: Optional[dict] = SchemaField(
description="Optional file attachment", default=None
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a809d264-ddf2-11ef-9764-32d3674e8b7e",
description="Get a single comment from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetCommentBlock.Input,
output_schema=TodoistGetCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("content", "Test comment"),
("id", "2992679862"),
("posted_at", "2016-09-22T07:00:00.000000Z"),
("project_id", None),
("task_id", "2995104339"),
("attachment", None),
],
test_mock={
"get_comment": lambda credentials, comment_id: {
"content": "Test comment",
"id": "2992679862",
"posted_at": "2016-09-22T07:00:00.000000Z",
"project_id": None,
"task_id": "2995104339",
"attachment": None,
}
},
)
@staticmethod
def get_comment(credentials: TodoistCredentials, comment_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
comment = api.get_comment(comment_id=comment_id)
return comment.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
comment_data = self.get_comment(
credentials, comment_id=input_data.comment_id
)
if comment_data:
yield "content", comment_data["content"]
yield "id", comment_data["id"]
yield "posted_at", comment_data["posted_at"]
yield "project_id", comment_data["project_id"]
yield "task_id", comment_data["task_id"]
yield "attachment", comment_data["attachment"]
except Exception as e:
yield "error", str(e)
class TodoistUpdateCommentBlock(Block):
"""Updates a Todoist comment"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to update")
content: str = SchemaField(description="New content for the comment")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="b773c520-ddf2-11ef-9f34-32d3674e8b7e",
description="Updates a Todoist comment",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateCommentBlock.Input,
output_schema=TodoistUpdateCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
"content": "Need one bottle of milk",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_comment": lambda credentials, comment_id, content: True},
)
@staticmethod
def update_comment(credentials: TodoistCredentials, comment_id: str, content: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
api.update_comment(comment_id=comment_id, content=content)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_comment(
credentials,
comment_id=input_data.comment_id,
content=input_data.content,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteCommentBlock(Block):
"""Deletes a Todoist comment"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
comment_id: str = SchemaField(description="Comment ID to delete")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the deletion was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="bda4c020-ddf2-11ef-b114-32d3674e8b7e",
description="Deletes a Todoist comment",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteCommentBlock.Input,
output_schema=TodoistDeleteCommentBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"comment_id": "2992679862",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_comment": lambda credentials, comment_id: True},
)
@staticmethod
def delete_comment(credentials: TodoistCredentials, comment_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_comment(comment_id=comment_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_comment(credentials, comment_id=input_data.comment_id)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -1,557 +0,0 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.blocks.todoist._types import Colors
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistCreateLabelBlock(Block):
"""Creates a new label in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="Name of the label")
order: Optional[int] = SchemaField(description="Label order", default=None)
color: Optional[Colors] = SchemaField(
description="The color of the label icon", default=Colors.charcoal
)
is_favorite: bool = SchemaField(
description="Whether the label is a favorite", default=False
)
class Output(BlockSchema):
id: str = SchemaField(description="ID of the created label")
name: str = SchemaField(description="Name of the label")
color: str = SchemaField(description="Color of the label")
order: int = SchemaField(description="Label order")
is_favorite: bool = SchemaField(description="Favorite status")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7288a968-de14-11ef-8997-32d3674e8b7e",
description="Creates a new label in Todoist, It will not work if same name already exists",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateLabelBlock.Input,
output_schema=TodoistCreateLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"name": "Test Label",
"color": Colors.charcoal.value,
"order": 1,
"is_favorite": False,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2156154810"),
("name", "Test Label"),
("color", "charcoal"),
("order", 1),
("is_favorite", False),
],
test_mock={
"create_label": lambda *args, **kwargs: {
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
},
)
@staticmethod
def create_label(credentials: TodoistCredentials, name: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
label = api.add_label(name=name, **kwargs)
return label.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_args = {
"order": input_data.order,
"color": (
input_data.color.value if input_data.color is not None else None
),
"is_favorite": input_data.is_favorite,
}
label_data = self.create_label(
credentials,
input_data.name,
**{k: v for k, v in label_args.items() if v is not None},
)
if label_data:
yield "id", label_data["id"]
yield "name", label_data["name"]
yield "color", label_data["color"]
yield "order", label_data["order"]
yield "is_favorite", label_data["is_favorite"]
except Exception as e:
yield "error", str(e)
class TodoistListLabelsBlock(Block):
"""Gets all personal labels from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
labels: list = SchemaField(description="List of complete label data")
label_ids: list = SchemaField(description="List of label IDs")
label_names: list = SchemaField(description="List of label names")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="776dd750-de14-11ef-b927-32d3674e8b7e",
description="Gets all personal labels from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListLabelsBlock.Input,
output_schema=TodoistListLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[
(
"labels",
[
{
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
],
),
("label_ids", ["2156154810"]),
("label_names", ["Test Label"]),
],
test_mock={
"get_labels": lambda *args, **kwargs: [
{
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
]
},
)
@staticmethod
def get_labels(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
labels = api.get_labels()
return [label.__dict__ for label in labels]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
labels = self.get_labels(credentials)
yield "labels", labels
yield "label_ids", [label["id"] for label in labels]
yield "label_names", [label["name"] for label in labels]
except Exception as e:
yield "error", str(e)
class TodoistGetLabelBlock(Block):
"""Gets a personal label from Todoist by ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to retrieve")
class Output(BlockSchema):
id: str = SchemaField(description="ID of the label")
name: str = SchemaField(description="Name of the label")
color: str = SchemaField(description="Color of the label")
order: int = SchemaField(description="Label order")
is_favorite: bool = SchemaField(description="Favorite status")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7f236514-de14-11ef-bd7a-32d3674e8b7e",
description="Gets a personal label from Todoist by ID",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetLabelBlock.Input,
output_schema=TodoistGetLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2156154810"),
("name", "Test Label"),
("color", "charcoal"),
("order", 1),
("is_favorite", False),
],
test_mock={
"get_label": lambda *args, **kwargs: {
"id": "2156154810",
"name": "Test Label",
"color": "charcoal",
"order": 1,
"is_favorite": False,
}
},
)
@staticmethod
def get_label(credentials: TodoistCredentials, label_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
label = api.get_label(label_id=label_id)
return label.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_data = self.get_label(credentials, input_data.label_id)
if label_data:
yield "id", label_data["id"]
yield "name", label_data["name"]
yield "color", label_data["color"]
yield "order", label_data["order"]
yield "is_favorite", label_data["is_favorite"]
except Exception as e:
yield "error", str(e)
class TodoistUpdateLabelBlock(Block):
"""Updates a personal label in Todoist using ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to update")
name: Optional[str] = SchemaField(
description="New name of the label", default=None
)
order: Optional[int] = SchemaField(description="Label order", default=None)
color: Optional[Colors] = SchemaField(
description="The color of the label icon", default=None
)
is_favorite: bool = SchemaField(
description="Whether the label is a favorite (true/false)", default=False
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="8755614c-de14-11ef-9b56-32d3674e8b7e",
description="Updates a personal label in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateLabelBlock.Input,
output_schema=TodoistUpdateLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
"name": "Updated Label",
"color": Colors.charcoal.value,
"order": 2,
"is_favorite": True,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_label": lambda *args, **kwargs: True},
)
@staticmethod
def update_label(credentials: TodoistCredentials, label_id: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
api.update_label(label_id=label_id, **kwargs)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
label_args = {}
if input_data.name is not None:
label_args["name"] = input_data.name
if input_data.order is not None:
label_args["order"] = input_data.order
if input_data.color is not None:
label_args["color"] = input_data.color.value
if input_data.is_favorite is not None:
label_args["is_favorite"] = input_data.is_favorite
success = self.update_label(
credentials,
input_data.label_id,
**{k: v for k, v in label_args.items() if v is not None},
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteLabelBlock(Block):
"""Deletes a personal label in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
label_id: str = SchemaField(description="ID of the label to delete")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the deletion was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="901b8f86-de14-11ef-98b8-32d3674e8b7e",
description="Deletes a personal label in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteLabelBlock.Input,
output_schema=TodoistDeleteLabelBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"label_id": "2156154810",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_label": lambda *args, **kwargs: True},
)
@staticmethod
def delete_label(credentials: TodoistCredentials, label_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_label(label_id=label_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_label(credentials, input_data.label_id)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistGetSharedLabelsBlock(Block):
"""Gets all shared labels from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
labels: list = SchemaField(description="List of shared label names")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="55fba510-de15-11ef-aed2-32d3674e8b7e",
description="Gets all shared labels from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetSharedLabelsBlock.Input,
output_schema=TodoistGetSharedLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT},
test_credentials=TEST_CREDENTIALS,
test_output=[("labels", ["Label1", "Label2", "Label3"])],
test_mock={
"get_shared_labels": lambda *args, **kwargs: [
"Label1",
"Label2",
"Label3",
]
},
)
@staticmethod
def get_shared_labels(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
labels = api.get_shared_labels()
return labels
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
labels = self.get_shared_labels(credentials)
yield "labels", labels
except Exception as e:
yield "error", str(e)
class TodoistRenameSharedLabelsBlock(Block):
"""Renames all instances of a shared label"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="The name of the existing label to rename")
new_name: str = SchemaField(description="The new name for the label")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the rename was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9d63ad9a-de14-11ef-ab3f-32d3674e8b7e",
description="Renames all instances of a shared label",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistRenameSharedLabelsBlock.Input,
output_schema=TodoistRenameSharedLabelsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"name": "OldLabel",
"new_name": "NewLabel",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"rename_shared_labels": lambda *args, **kwargs: True},
)
@staticmethod
def rename_shared_labels(credentials: TodoistCredentials, name: str, new_name: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.rename_shared_label(name=name, new_name=new_name)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.rename_shared_labels(
credentials, input_data.name, input_data.new_name
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistRemoveSharedLabelsBlock(Block):
"""Removes all instances of a shared label"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="The name of the label to remove")
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the removal was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a6c5cbde-de14-11ef-8863-32d3674e8b7e",
description="Removes all instances of a shared label",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistRemoveSharedLabelsBlock.Input,
output_schema=TodoistRemoveSharedLabelsBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "LabelToRemove"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"remove_shared_label": lambda *args, **kwargs: True},
)
@staticmethod
def remove_shared_label(credentials: TodoistCredentials, name: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.remove_shared_label(name=name)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_shared_label(credentials, input_data.name)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -1,566 +0,0 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.blocks.todoist._types import Colors
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistListProjectsBlock(Block):
"""Gets all projects for a Todoist user"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
class Output(BlockSchema):
names_list: list[str] = SchemaField(description="List of project names")
ids_list: list[str] = SchemaField(description="List of project IDs")
url_list: list[str] = SchemaField(description="List of project URLs")
complete_data: list[dict] = SchemaField(
description="Complete project data including all fields"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="5f3e1d5b-6bc5-40e3-97ee-1318b3f38813",
description="Gets all projects and their details from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListProjectsBlock.Input,
output_schema=TodoistListProjectsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("names_list", ["Inbox"]),
("ids_list", ["220474322"]),
("url_list", ["https://todoist.com/showProject?id=220474322"]),
(
"complete_data",
[
{
"id": "220474322",
"name": "Inbox",
"url": "https://todoist.com/showProject?id=220474322",
}
],
),
],
test_mock={
"get_project_lists": lambda *args, **kwargs: (
["Inbox"],
["220474322"],
["https://todoist.com/showProject?id=220474322"],
[
{
"id": "220474322",
"name": "Inbox",
"url": "https://todoist.com/showProject?id=220474322",
}
],
None,
)
},
)
@staticmethod
def get_project_lists(credentials: TodoistCredentials):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
projects = api.get_projects()
names = []
ids = []
urls = []
complete_data = []
for project in projects:
names.append(project.name)
ids.append(project.id)
urls.append(project.url)
complete_data.append(project.__dict__)
return names, ids, urls, complete_data, None
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
names, ids, urls, data, error = self.get_project_lists(credentials)
if names:
yield "names_list", names
if ids:
yield "ids_list", ids
if urls:
yield "url_list", urls
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
class TodoistCreateProjectBlock(Block):
"""Creates a new project in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
name: str = SchemaField(description="Name of the project", advanced=False)
parent_id: Optional[str] = SchemaField(
description="Parent project ID", default=None, advanced=True
)
color: Optional[Colors] = SchemaField(
description="Color of the project icon",
default=Colors.charcoal,
advanced=True,
)
is_favorite: bool = SchemaField(
description="Whether the project is a favorite",
default=False,
advanced=True,
)
view_style: Optional[str] = SchemaField(
description="Display style (list or board)", default=None, advanced=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the creation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ade60136-de14-11ef-b5e5-32d3674e8b7e",
description="Creates a new project in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateProjectBlock.Input,
output_schema=TodoistCreateProjectBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "Test Project"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"create_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def create_project(
credentials: TodoistCredentials,
name: str,
parent_id: Optional[str],
color: Optional[Colors],
is_favorite: bool,
view_style: Optional[str],
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
params = {"name": name, "is_favorite": is_favorite}
if parent_id is not None:
params["parent_id"] = parent_id
if color is not None:
params["color"] = color.value
if view_style is not None:
params["view_style"] = view_style
api.add_project(**params)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.create_project(
credentials=credentials,
name=input_data.name,
parent_id=input_data.parent_id,
color=input_data.color,
is_favorite=input_data.is_favorite,
view_style=input_data.view_style,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistGetProjectBlock(Block):
"""Gets details for a specific Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of the project to get details for", advanced=False
)
class Output(BlockSchema):
project_id: str = SchemaField(description="ID of project")
project_name: str = SchemaField(description="Name of project")
project_url: str = SchemaField(description="URL of project")
complete_data: dict = SchemaField(
description="Complete project data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="b435b5ea-de14-11ef-8b51-32d3674e8b7e",
description="Gets details for a specific Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetProjectBlock.Input,
output_schema=TodoistGetProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("project_id", "2203306141"),
("project_name", "Shopping List"),
("project_url", "https://todoist.com/showProject?id=2203306141"),
(
"complete_data",
{
"id": "2203306141",
"name": "Shopping List",
"url": "https://todoist.com/showProject?id=2203306141",
},
),
],
test_mock={
"get_project": lambda *args, **kwargs: (
"2203306141",
"Shopping List",
"https://todoist.com/showProject?id=2203306141",
{
"id": "2203306141",
"name": "Shopping List",
"url": "https://todoist.com/showProject?id=2203306141",
},
)
},
)
@staticmethod
def get_project(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
project = api.get_project(project_id=project_id)
return project.id, project.name, project.url, project.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
project_id, project_name, project_url, data = self.get_project(
credentials=credentials, project_id=input_data.project_id
)
if project_id:
yield "project_id", project_id
if project_name:
yield "project_name", project_name
if project_url:
yield "project_url", project_url
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
class TodoistUpdateProjectBlock(Block):
"""Updates an existing project in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of project to update", advanced=False
)
name: Optional[str] = SchemaField(
description="New name for the project", default=None, advanced=False
)
color: Optional[Colors] = SchemaField(
description="New color for the project icon", default=None, advanced=True
)
is_favorite: Optional[bool] = SchemaField(
description="Whether the project should be a favorite",
default=None,
advanced=True,
)
view_style: Optional[str] = SchemaField(
description="Display style (list or board)", default=None, advanced=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ba41a20a-de14-11ef-91d7-32d3674e8b7e",
description="Updates an existing project in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateProjectBlock.Input,
output_schema=TodoistUpdateProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
"name": "Things To Buy",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def update_project(
credentials: TodoistCredentials,
project_id: str,
name: Optional[str],
color: Optional[Colors],
is_favorite: Optional[bool],
view_style: Optional[str],
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
params = {}
if name is not None:
params["name"] = name
if color is not None:
params["color"] = color.value
if is_favorite is not None:
params["is_favorite"] = is_favorite
if view_style is not None:
params["view_style"] = view_style
api.update_project(project_id=project_id, **params)
return True
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_project(
credentials=credentials,
project_id=input_data.project_id,
name=input_data.name,
color=input_data.color,
is_favorite=input_data.is_favorite,
view_style=input_data.view_style,
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistDeleteProjectBlock(Block):
"""Deletes a project and all of its sections and tasks"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of project to delete", advanced=False
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the deletion was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c2893acc-de14-11ef-a113-32d3674e8b7e",
description="Deletes a Todoist project and all its contents",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteProjectBlock.Input,
output_schema=TodoistDeleteProjectBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_project": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_project(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_project(project_id=project_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_project(
credentials=credentials, project_id=input_data.project_id
)
yield "success", success
except Exception as e:
yield "error", str(e)
class TodoistListCollaboratorsBlock(Block):
"""Gets all collaborators for a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: str = SchemaField(
description="ID of the project to get collaborators for", advanced=False
)
class Output(BlockSchema):
collaborator_ids: list[str] = SchemaField(
description="List of collaborator IDs"
)
collaborator_names: list[str] = SchemaField(
description="List of collaborator names"
)
collaborator_emails: list[str] = SchemaField(
description="List of collaborator email addresses"
)
complete_data: list[dict] = SchemaField(
description="Complete collaborator data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c99c804e-de14-11ef-9f47-32d3674e8b7e",
description="Gets all collaborators for a specific Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListCollaboratorsBlock.Input,
output_schema=TodoistListCollaboratorsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("collaborator_ids", ["2671362", "2671366"]),
("collaborator_names", ["Alice", "Bob"]),
("collaborator_emails", ["alice@example.com", "bob@example.com"]),
(
"complete_data",
[
{
"id": "2671362",
"name": "Alice",
"email": "alice@example.com",
},
{"id": "2671366", "name": "Bob", "email": "bob@example.com"},
],
),
],
test_mock={
"get_collaborators": lambda *args, **kwargs: (
["2671362", "2671366"],
["Alice", "Bob"],
["alice@example.com", "bob@example.com"],
[
{
"id": "2671362",
"name": "Alice",
"email": "alice@example.com",
},
{"id": "2671366", "name": "Bob", "email": "bob@example.com"},
],
)
},
)
@staticmethod
def get_collaborators(credentials: TodoistCredentials, project_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
collaborators = api.get_collaborators(project_id=project_id)
ids = []
names = []
emails = []
complete_data = []
for collaborator in collaborators:
ids.append(collaborator.id)
names.append(collaborator.name)
emails.append(collaborator.email)
complete_data.append(collaborator.__dict__)
return ids, names, emails, complete_data
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, names, emails, data = self.get_collaborators(
credentials=credentials, project_id=input_data.project_id
)
if ids:
yield "collaborator_ids", ids
if names:
yield "collaborator_names", names
if emails:
yield "collaborator_emails", emails
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)

View File

@@ -1,306 +0,0 @@
from todoist_api_python.api import TodoistAPI
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistListSectionsBlock(Block):
"""Gets all sections for a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: Optional[str] = SchemaField(
description="Optional project ID to filter sections"
)
class Output(BlockSchema):
names_list: list[str] = SchemaField(description="List of section names")
ids_list: list[str] = SchemaField(description="List of section IDs")
complete_data: list[dict] = SchemaField(
description="Complete section data including all fields"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d6a116d8-de14-11ef-a94c-32d3674e8b7e",
description="Gets all sections and their details from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistListSectionsBlock.Input,
output_schema=TodoistListSectionsBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("names_list", ["Groceries"]),
("ids_list", ["7025"]),
(
"complete_data",
[
{
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
],
),
],
test_mock={
"get_section_lists": lambda *args, **kwargs: (
["Groceries"],
["7025"],
[
{
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
],
)
},
)
@staticmethod
def get_section_lists(
credentials: TodoistCredentials, project_id: Optional[str] = None
):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
sections = api.get_sections(project_id=project_id)
names = []
ids = []
complete_data = []
for section in sections:
names.append(section.name)
ids.append(section.id)
complete_data.append(section.__dict__)
return names, ids, complete_data
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
names, ids, data = self.get_section_lists(
credentials, input_data.project_id
)
if names:
yield "names_list", names
if ids:
yield "ids_list", ids
if data:
yield "complete_data", data
except Exception as e:
yield "error", str(e)
# Error in official todoist SDK. Will add this block using sync_api
# class TodoistCreateSectionBlock(Block):
# """Creates a new section in a Todoist project"""
# class Input(BlockSchema):
# credentials: TodoistCredentialsInput = TodoistCredentialsField([])
# name: str = SchemaField(description="Section name")
# project_id: str = SchemaField(description="Project ID this section should belong to")
# order: Optional[int] = SchemaField(description="Optional order among other sections", default=None)
# class Output(BlockSchema):
# success: bool = SchemaField(description="Whether section was successfully created")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="e3025cfc-de14-11ef-b9f2-32d3674e8b7e",
# description="Creates a new section in a Todoist project",
# categories={BlockCategory.PRODUCTIVITY},
# input_schema=TodoistCreateSectionBlock.Input,
# output_schema=TodoistCreateSectionBlock.Output,
# test_input={
# "credentials": TEST_CREDENTIALS_INPUT,
# "name": "Groceries",
# "project_id": "2203306141"
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("success", True)
# ],
# test_mock={
# "create_section": lambda *args, **kwargs: (
# {"id": "7025", "project_id": "2203306141", "order": 1, "name": "Groceries"},
# )
# },
# )
# @staticmethod
# def create_section(credentials: TodoistCredentials, name: str, project_id: str, order: Optional[int] = None):
# try:
# api = TodoistAPI(credentials.access_token.get_secret_value())
# section = api.add_section(name=name, project_id=project_id, order=order)
# return section.__dict__
# except Exception as e:
# raise e
# def run(
# self,
# input_data: Input,
# *,
# credentials: TodoistCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# section_data = self.create_section(
# credentials,
# input_data.name,
# input_data.project_id,
# input_data.order
# )
# if section_data:
# yield "success", True
# except Exception as e:
# yield "error", str(e)
class TodoistGetSectionBlock(Block):
"""Gets a single section from Todoist by ID"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
section_id: str = SchemaField(description="ID of section to fetch")
class Output(BlockSchema):
id: str = SchemaField(description="ID of section")
project_id: str = SchemaField(description="Project ID the section belongs to")
order: int = SchemaField(description="Order of the section")
name: str = SchemaField(description="Name of the section")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ea5580e2-de14-11ef-a5d3-32d3674e8b7e",
description="Gets a single section by ID from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetSectionBlock.Input,
output_schema=TodoistGetSectionBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "7025"),
("project_id", "2203306141"),
("order", 1),
("name", "Groceries"),
],
test_mock={
"get_section": lambda *args, **kwargs: {
"id": "7025",
"project_id": "2203306141",
"order": 1,
"name": "Groceries",
}
},
)
@staticmethod
def get_section(credentials: TodoistCredentials, section_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
section = api.get_section(section_id=section_id)
return section.__dict__
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
section_data = self.get_section(credentials, input_data.section_id)
if section_data:
yield "id", section_data["id"]
yield "project_id", section_data["project_id"]
yield "order", section_data["order"]
yield "name", section_data["name"]
except Exception as e:
yield "error", str(e)
class TodoistDeleteSectionBlock(Block):
"""Deletes a section and all its tasks from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
section_id: str = SchemaField(description="ID of section to delete")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether section was successfully deleted"
)
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="f0e52eee-de14-11ef-9b12-32d3674e8b7e",
description="Deletes a section and all its tasks from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteSectionBlock.Input,
output_schema=TodoistDeleteSectionBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"delete_section": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_section(credentials: TodoistCredentials, section_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
success = api.delete_section(section_id=section_id)
return success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_section(credentials, input_data.section_id)
yield "success", success
except Exception as e:
yield "error", str(e)

View File

@@ -1,660 +0,0 @@
from datetime import datetime
from todoist_api_python.api import TodoistAPI
from todoist_api_python.models import Task
from typing_extensions import Optional
from backend.blocks.todoist._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TodoistCredentials,
TodoistCredentialsField,
TodoistCredentialsInput,
)
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TodoistCreateTaskBlock(Block):
"""Creates a new task in a Todoist project"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
content: str = SchemaField(description="Task content", advanced=False)
description: Optional[str] = SchemaField(
description="Task description", default=None, advanced=False
)
project_id: Optional[str] = SchemaField(
description="Project ID this task should belong to",
default=None,
advanced=False,
)
section_id: Optional[str] = SchemaField(
description="Section ID this task should belong to",
default=None,
advanced=False,
)
parent_id: Optional[str] = SchemaField(
description="Parent task ID", default=None, advanced=True
)
order: Optional[int] = SchemaField(
description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]",
default=None,
advanced=True,
)
labels: Optional[list[str]] = SchemaField(
description="Task labels", default=None, advanced=True
)
priority: Optional[int] = SchemaField(
description="Task priority from 1 (normal) to 4 (urgent)",
default=None,
advanced=True,
)
due_date: Optional[datetime] = SchemaField(
description="Due date in YYYY-MM-DD format", advanced=True, default=None
)
deadline_date: Optional[datetime] = SchemaField(
description="Specific date in YYYY-MM-DD format relative to user's timezone",
default=None,
advanced=True,
)
assignee_id: Optional[str] = SchemaField(
description="Responsible user ID", default=None, advanced=True
)
duration_unit: Optional[str] = SchemaField(
description="Task duration unit (minute/day)", default=None, advanced=True
)
duration: Optional[int] = SchemaField(
description="Task duration amount, You need to selecct the duration unit first",
depends_on=["duration_unit"],
default=None,
advanced=True,
)
class Output(BlockSchema):
id: str = SchemaField(description="Task ID")
url: str = SchemaField(description="Task URL")
complete_data: dict = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="fde4f458-de14-11ef-bf0c-32d3674e8b7e",
description="Creates a new task in a Todoist project",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCreateTaskBlock.Input,
output_schema=TodoistCreateTaskBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"content": "Buy groceries",
"project_id": "2203306141",
"priority": 4,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "2995104339"),
("url", "https://todoist.com/showTask?id=2995104339"),
(
"complete_data",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
),
],
test_mock={
"create_task": lambda *args, **kwargs: (
"2995104339",
"https://todoist.com/showTask?id=2995104339",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
)
},
)
@staticmethod
def create_task(credentials: TodoistCredentials, content: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
task = api.add_task(content=content, **kwargs)
task_dict = Task.to_dict(task)
return task.id, task.url, task_dict
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
due_date = (
input_data.due_date.strftime("%Y-%m-%d")
if input_data.due_date
else None
)
deadline_date = (
input_data.deadline_date.strftime("%Y-%m-%d")
if input_data.deadline_date
else None
)
task_args = {
"description": input_data.description,
"project_id": input_data.project_id,
"section_id": input_data.section_id,
"parent_id": input_data.parent_id,
"order": input_data.order,
"labels": input_data.labels,
"priority": input_data.priority,
"due_date": due_date,
"deadline_date": deadline_date,
"assignee_id": input_data.assignee_id,
"duration": input_data.duration,
"duration_unit": input_data.duration_unit,
}
id, url, complete_data = self.create_task(
credentials,
input_data.content,
**{k: v for k, v in task_args.items() if v is not None},
)
yield "id", id
yield "url", url
yield "complete_data", complete_data
except Exception as e:
yield "error", str(e)
class TodoistGetTasksBlock(Block):
"""Get active tasks from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
project_id: Optional[str] = SchemaField(
description="Filter tasks by project ID", default=None, advanced=False
)
section_id: Optional[str] = SchemaField(
description="Filter tasks by section ID", default=None, advanced=True
)
label: Optional[str] = SchemaField(
description="Filter tasks by label name", default=None, advanced=True
)
filter: Optional[str] = SchemaField(
description="Filter by any supported filter, You can see How to use filters or create one of your one here - https://todoist.com/help/articles/introduction-to-filters-V98wIH",
default=None,
advanced=True,
)
lang: Optional[str] = SchemaField(
description="IETF language tag for filter language", default=None
)
ids: Optional[list[str]] = SchemaField(
description="List of task IDs to retrieve", default=None, advanced=False
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="Task IDs")
urls: list[str] = SchemaField(description="Task URLs")
complete_data: list[dict] = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="0b706e86-de15-11ef-a113-32d3674e8b7e",
description="Get active tasks from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetTasksBlock.Input,
output_schema=TodoistGetTasksBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"project_id": "2203306141",
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["2995104339"]),
("urls", ["https://todoist.com/showTask?id=2995104339"]),
(
"complete_data",
[
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
"is_completed": False,
}
],
),
],
test_mock={
"get_tasks": lambda *args, **kwargs: [
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
"is_completed": False,
}
]
},
)
@staticmethod
def get_tasks(credentials: TodoistCredentials, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
tasks = api.get_tasks(**kwargs)
return [Task.to_dict(task) for task in tasks]
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_filters = {
"project_id": input_data.project_id,
"section_id": input_data.section_id,
"label": input_data.label,
"filter": input_data.filter,
"lang": input_data.lang,
"ids": input_data.ids,
}
tasks = self.get_tasks(
credentials, **{k: v for k, v in task_filters.items() if v is not None}
)
yield "ids", [task["id"] for task in tasks]
yield "urls", [task["url"] for task in tasks]
yield "complete_data", tasks
except Exception as e:
yield "error", str(e)
class TodoistGetTaskBlock(Block):
"""Get an active task from Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to retrieve")
class Output(BlockSchema):
project_id: str = SchemaField(description="Project ID containing the task")
url: str = SchemaField(description="Task URL")
complete_data: dict = SchemaField(
description="Complete task data as dictionary"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="16d7dc8c-de15-11ef-8ace-32d3674e8b7e",
description="Get an active task from Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistGetTaskBlock.Input,
output_schema=TodoistGetTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("project_id", "2203306141"),
("url", "https://todoist.com/showTask?id=2995104339"),
(
"complete_data",
{
"id": "2995104339",
"project_id": "2203306141",
"url": "https://todoist.com/showTask?id=2995104339",
},
),
],
test_mock={
"get_task": lambda *args, **kwargs: {
"project_id": "2203306141",
"id": "2995104339",
"url": "https://todoist.com/showTask?id=2995104339",
}
},
)
@staticmethod
def get_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
task = api.get_task(task_id=task_id)
return Task.to_dict(task)
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
task_data = self.get_task(credentials, input_data.task_id)
if task_data:
yield "project_id", task_data["project_id"]
yield "url", task_data["url"]
yield "complete_data", task_data
except Exception as e:
yield "error", str(e)
class TodoistUpdateTaskBlock(Block):
"""Updates an existing task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to update")
content: str = SchemaField(description="Task content", advanced=False)
description: Optional[str] = SchemaField(
description="Task description", default=None, advanced=False
)
project_id: Optional[str] = SchemaField(
description="Project ID this task should belong to",
default=None,
advanced=False,
)
section_id: Optional[str] = SchemaField(
description="Section ID this task should belong to",
default=None,
advanced=False,
)
parent_id: Optional[str] = SchemaField(
description="Parent task ID", default=None, advanced=True
)
order: Optional[int] = SchemaField(
description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]",
default=None,
advanced=True,
)
labels: Optional[list[str]] = SchemaField(
description="Task labels", default=None, advanced=True
)
priority: Optional[int] = SchemaField(
description="Task priority from 1 (normal) to 4 (urgent)",
default=None,
advanced=True,
)
due_date: Optional[datetime] = SchemaField(
description="Due date in YYYY-MM-DD format", advanced=True, default=None
)
deadline_date: Optional[datetime] = SchemaField(
description="Specific date in YYYY-MM-DD format relative to user's timezone",
default=None,
advanced=True,
)
assignee_id: Optional[str] = SchemaField(
description="Responsible user ID", default=None, advanced=True
)
duration_unit: Optional[str] = SchemaField(
description="Task duration unit (minute/day)", default=None, advanced=True
)
duration: Optional[int] = SchemaField(
description="Task duration amount, You need to selecct the duration unit first",
depends_on=["duration_unit"],
default=None,
advanced=True,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="1eee6d32-de15-11ef-a2ff-32d3674e8b7e",
description="Updates an existing task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistUpdateTaskBlock.Input,
output_schema=TodoistUpdateTaskBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"task_id": "2995104339",
"content": "Buy Coffee",
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"update_task": lambda *args, **kwargs: True},
)
@staticmethod
def update_task(credentials: TodoistCredentials, task_id: str, **kwargs):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.update_task(task_id=task_id, **kwargs)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
due_date = (
input_data.due_date.strftime("%Y-%m-%d")
if input_data.due_date
else None
)
deadline_date = (
input_data.deadline_date.strftime("%Y-%m-%d")
if input_data.deadline_date
else None
)
task_updates = {}
if input_data.content is not None:
task_updates["content"] = input_data.content
if input_data.description is not None:
task_updates["description"] = input_data.description
if input_data.project_id is not None:
task_updates["project_id"] = input_data.project_id
if input_data.section_id is not None:
task_updates["section_id"] = input_data.section_id
if input_data.parent_id is not None:
task_updates["parent_id"] = input_data.parent_id
if input_data.order is not None:
task_updates["order"] = input_data.order
if input_data.labels is not None:
task_updates["labels"] = input_data.labels
if input_data.priority is not None:
task_updates["priority"] = input_data.priority
if due_date is not None:
task_updates["due_date"] = due_date
if deadline_date is not None:
task_updates["deadline_date"] = deadline_date
if input_data.assignee_id is not None:
task_updates["assignee_id"] = input_data.assignee_id
if input_data.duration is not None:
task_updates["duration"] = input_data.duration
if input_data.duration_unit is not None:
task_updates["duration_unit"] = input_data.duration_unit
self.update_task(
credentials,
input_data.task_id,
**{k: v for k, v in task_updates.items() if v is not None},
)
yield "success", True
except Exception as e:
yield "error", str(e)
class TodoistCloseTaskBlock(Block):
"""Closes a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to close")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully closed"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="29fac798-de15-11ef-b839-32d3674e8b7e",
description="Closes a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistCloseTaskBlock.Input,
output_schema=TodoistCloseTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={"close_task": lambda *args, **kwargs: True},
)
@staticmethod
def close_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.close_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.close_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)
class TodoistReopenTaskBlock(Block):
"""Reopens a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to reopen")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully reopened"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="2e6bf6f8-de15-11ef-ae7c-32d3674e8b7e",
description="Reopens a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistReopenTaskBlock.Input,
output_schema=TodoistReopenTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"reopen_task": lambda *args, **kwargs: (True)},
)
@staticmethod
def reopen_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.reopen_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.reopen_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)
class TodoistDeleteTaskBlock(Block):
"""Deletes a task in Todoist"""
class Input(BlockSchema):
credentials: TodoistCredentialsInput = TodoistCredentialsField([])
task_id: str = SchemaField(description="Task ID to delete")
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the task was successfully deleted"
)
error: str = SchemaField(description="Error message if request failed")
def __init__(self):
super().__init__(
id="33c29ada-de15-11ef-bcbb-32d3674e8b7e",
description="Deletes a task in Todoist",
categories={BlockCategory.PRODUCTIVITY},
input_schema=TodoistDeleteTaskBlock.Input,
output_schema=TodoistDeleteTaskBlock.Output,
test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"delete_task": lambda *args, **kwargs: (True)},
)
@staticmethod
def delete_task(credentials: TodoistCredentials, task_id: str):
try:
api = TodoistAPI(credentials.access_token.get_secret_value())
is_success = api.delete_task(task_id=task_id)
return is_success
except Exception as e:
raise e
def run(
self,
input_data: Input,
*,
credentials: TodoistCredentials,
**kwargs,
) -> BlockOutput:
try:
is_success = self.delete_task(credentials, input_data.task_id)
yield "success", is_success
except Exception as e:
yield "error", str(e)

View File

@@ -92,8 +92,7 @@ class TwitterPostTweetBlock(Block):
attachment: Union[Media, DeepLink, Poll, Place, Quote] | None = SchemaField(
discriminator="discriminator",
description="Additional tweet data (media, deep link, poll, place or quote)",
advanced=False,
default=Media(discriminator="media"),
advanced=True,
)
exclude_reply_user_ids: Optional[List[str]] = SchemaField(

View File

@@ -23,6 +23,71 @@ from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class TwitterUnblockUserBlock(Block):
"""
Unblock a specific user on Twitter. The request succeeds with no action when the user sends a request to a user they're not blocking or have already unblocked.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unblock",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unblock was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="0f1b6570-a631-11ef-a3ea-230cbe9650dd",
description="This block unblocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnblockUserBlock.Input,
output_schema=TwitterUnblockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"unblock_user": lambda *args, **kwargs: True},
)
@staticmethod
def unblock_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unblock(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unblock_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetBlockedUsersBlock(Block):
"""
Get a list of users who are blocked by the authenticating user
@@ -173,3 +238,68 @@ class TwitterGetBlockedUsersBlock(Block):
yield "next_token", next_token
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterBlockUserBlock(Block):
"""
Block a specific user on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to block",
placeholder="Enter target user ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the block was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="fc258b94-a630-11ef-abc3-df050b75b816",
description="This block blocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterBlockUserBlock.Input,
output_schema=TwitterBlockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={"block_user": lambda *args, **kwargs: True},
)
@staticmethod
def block_user(credentials: TwitterCredentials, target_user_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.block(target_user_id=target_user_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.block_user(credentials, input_data.target_user_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@@ -64,9 +64,6 @@ class BlockCategory(Enum):
SAFETY = (
"Block that provides AI safety mechanisms such as detecting harmful content"
)
PRODUCTIVITY = "Block that helps with productivity"
ISSUE_TRACKING = "Block that helps with issue tracking"
MULTIMEDIA = "Block that interacts with multimedia content"
def dict(self) -> dict[str, str]:
return {"category": self.name, "description": self.value}
@@ -398,7 +395,6 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
}
def execute(self, input_data: BlockInput, **kwargs) -> BlockOutput:
# Merge the input data with the extra execution arguments, preferring the args for security
if error := self.input_schema.validate_data(input_data):
raise ValueError(
f"Unable to execute block with invalid input data: {error}"

View File

@@ -35,8 +35,6 @@ from backend.integrations.credentials_store import (
# =============== Configure the cost for each LLM Model call =============== #
MODEL_COST: dict[LlmModel, int] = {
LlmModel.O3_MINI: 2, # $1.10 / $4.40
LlmModel.O1: 16, # $15 / $60
LlmModel.O1_PREVIEW: 16,
LlmModel.O1_MINI: 4,
LlmModel.GPT4O_MINI: 1,
@@ -44,21 +42,20 @@ MODEL_COST: dict[LlmModel, int] = {
LlmModel.GPT4_TURBO: 10,
LlmModel.GPT3_5_TURBO: 1,
LlmModel.CLAUDE_3_5_SONNET: 4,
LlmModel.CLAUDE_3_5_HAIKU: 1, # $0.80 / $4.00
LlmModel.CLAUDE_3_HAIKU: 1,
LlmModel.LLAMA3_8B: 1,
LlmModel.LLAMA3_70B: 1,
LlmModel.MIXTRAL_8X7B: 1,
LlmModel.GEMMA_7B: 1,
LlmModel.GEMMA2_9B: 1,
LlmModel.LLAMA3_3_70B: 1, # $0.59 / $0.79
LlmModel.LLAMA3_1_405B: 1,
LlmModel.LLAMA3_1_70B: 1,
LlmModel.LLAMA3_1_8B: 1,
LlmModel.OLLAMA_LLAMA3_3: 1,
LlmModel.OLLAMA_LLAMA3_2: 1,
LlmModel.OLLAMA_LLAMA3_8B: 1,
LlmModel.OLLAMA_LLAMA3_405B: 1,
LlmModel.DEEPSEEK_LLAMA_70B: 1, # ? / ?
LlmModel.OLLAMA_DOLPHIN: 1,
LlmModel.GEMINI_FLASH_1_5: 1,
LlmModel.GEMINI_FLASH_1_5_8B: 1,
LlmModel.GROK_BETA: 5,
LlmModel.MISTRAL_NEMO: 1,
LlmModel.COHERE_COMMAND_R_08_2024: 1,

View File

@@ -10,6 +10,7 @@ class BlockCostType(str, Enum):
RUN = "run" # cost X credits per run
BYTE = "byte" # cost X credits per byte
SECOND = "second" # cost X credits per second
DOLLAR = "dollar" # cost X dollars per run
class BlockCost(BaseModel):

View File

@@ -1,65 +1,40 @@
import logging
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime, timezone
import stripe
from prisma import Json
from prisma.enums import CreditTransactionType
from prisma.errors import UniqueViolationError
from prisma.models import CreditTransaction, User
from prisma.types import CreditTransactionCreateInput, CreditTransactionWhereInput
from pydantic import BaseModel
from prisma.models import CreditTransaction
from backend.data import db
from backend.data.block import Block, BlockInput, get_block
from backend.data.block_cost_config import BLOCK_COSTS
from backend.data.cost import BlockCost, BlockCostType
from backend.data.execution import NodeExecutionEntry
from backend.data.model import AutoTopUpConfig, TransactionHistory, UserTransaction
from backend.data.user import get_user_by_id
from backend.util.settings import Settings
from backend.util.settings import Config
settings = Settings()
stripe.api_key = settings.secrets.stripe_api_key
logger = logging.getLogger(__name__)
config = Config()
class UserCreditBase(ABC):
@abstractmethod
async def get_credits(self, user_id: str) -> int:
"""
Get the current credits for the user.
Returns:
int: The current credits for the user.
"""
pass
def __init__(self, num_user_credits_refill: int):
self.num_user_credits_refill = num_user_credits_refill
@abstractmethod
async def get_transaction_history(
self,
user_id: str,
transaction_time: datetime,
transaction_count_limit: int,
) -> TransactionHistory:
async def get_or_refill_credit(self, user_id: str) -> int:
"""
Get the credit transactions for the user.
Args:
user_id (str): The user ID.
transaction_time (datetime): The upper bound of the transaction time.
transaction_count_limit (int): The transaction count limit.
Get the current credit for the user and refill if no transaction has been made in the current cycle.
Returns:
TransactionHistory: The credit transactions for the user.
int: The current credit for the user.
"""
pass
@abstractmethod
async def spend_credits(
self,
entry: NodeExecutionEntry,
user_id: str,
user_credit: int,
block_id: str,
input_data: BlockInput,
data_size: float,
run_time: float,
) -> int:
@@ -67,7 +42,10 @@ class UserCreditBase(ABC):
Spend the credits for the user based on the block usage.
Args:
entry (NodeExecutionEntry): The node execution identifiers & data.
user_id (str): The user ID.
user_credit (int): The current credit for the user.
block_id (str): The block ID.
input_data (BlockInput): The input data for the block.
data_size (float): The size of the data being processed.
run_time (float): The time taken to run the block.
@@ -79,7 +57,7 @@ class UserCreditBase(ABC):
@abstractmethod
async def top_up_credits(self, user_id: str, amount: int):
"""
Top up the credits for the user immediately.
Top up the credits for the user.
Args:
user_id (str): The user ID.
@@ -87,181 +65,51 @@ class UserCreditBase(ABC):
"""
pass
@abstractmethod
async def top_up_intent(self, user_id: str, amount: int) -> str:
"""
Create a payment intent to top up the credits for the user.
Args:
user_id (str): The user ID.
amount (int): The amount of credits to top up.
Returns:
str: The redirect url to the payment page.
"""
pass
@abstractmethod
async def fulfill_checkout(
self, *, session_id: str | None = None, user_id: str | None = None
):
"""
Fulfill the Stripe checkout session.
Args:
session_id (str | None): The checkout session ID. Will try to fulfill most recent if None.
user_id (str | None): The user ID must be provided if session_id is None.
"""
pass
@staticmethod
def time_now() -> datetime:
return datetime.now(timezone.utc)
# ====== Transaction Helper Methods ====== #
# Any modifications to the transaction table should only be done through these methods #
async def _get_credits(self, user_id: str) -> tuple[int, datetime]:
"""
Returns the current balance of the user & the latest balance snapshot time.
"""
top_time = self.time_now()
snapshot = await CreditTransaction.prisma().find_first(
where={
"userId": user_id,
"createdAt": {"lte": top_time},
"isActive": True,
"runningBalance": {"not": None}, # type: ignore
},
order={"createdAt": "desc"},
)
datetime_min = datetime.min.replace(tzinfo=timezone.utc)
snapshot_balance = snapshot.runningBalance or 0 if snapshot else 0
snapshot_time = snapshot.createdAt if snapshot else datetime_min
# Get transactions after the snapshot, this should not exist, but just in case.
transactions = await CreditTransaction.prisma().group_by(
by=["userId"],
sum={"amount": True},
max={"createdAt": True},
where={
"userId": user_id,
"createdAt": {
"gt": snapshot_time,
"lte": top_time,
},
"isActive": True,
},
)
transaction_balance = (
int(transactions[0].get("_sum", {}).get("amount", 0) + snapshot_balance)
if transactions
else snapshot_balance
)
transaction_time = (
datetime.fromisoformat(
str(transactions[0].get("_max", {}).get("createdAt", datetime_min))
)
if transactions
else snapshot_time
)
return transaction_balance, transaction_time
async def _enable_transaction(
self, transaction_key: str, user_id: str, metadata: Json
):
transaction = await CreditTransaction.prisma().find_first_or_raise(
where={"transactionKey": transaction_key, "userId": user_id}
)
if transaction.isActive:
return
async with db.locked_transaction(f"usr_trx_{user_id}"):
user_balance, _ = await self._get_credits(user_id)
await CreditTransaction.prisma().update(
where={
"creditTransactionIdentifier": {
"transactionKey": transaction_key,
"userId": user_id,
}
},
data={
"isActive": True,
"runningBalance": user_balance + transaction.amount,
"createdAt": self.time_now(),
"metadata": metadata,
},
)
async def _add_transaction(
self,
user_id: str,
amount: int,
transaction_type: CreditTransactionType,
is_active: bool = True,
transaction_key: str | None = None,
ceiling_balance: int | None = None,
metadata: Json = Json({}),
) -> tuple[int, str]:
"""
Add a new transaction for the user.
This is the only method that should be used to add a new transaction.
Args:
user_id (str): The user ID.
amount (int): The amount of credits to add.
transaction_type (CreditTransactionType): The type of transaction.
is_active (bool): Whether the transaction is active or needs to be manually activated through _enable_transaction.
transaction_key (str | None): The transaction key. Avoids adding transaction if the key already exists.
ceiling_balance (int | None): The ceiling balance. Avoids adding more credits if the balance is already above the ceiling.
metadata (Json): The metadata of the transaction.
Returns:
tuple[int, str]: The new balance & the transaction key.
"""
async with db.locked_transaction(f"usr_trx_{user_id}"):
# Get latest balance snapshot
user_balance, _ = await self._get_credits(user_id)
if ceiling_balance and user_balance >= ceiling_balance:
raise ValueError(
f"You already have enough balance for user {user_id}, balance: {user_balance}, ceiling: {ceiling_balance}"
)
if amount < 0 and user_balance < abs(amount):
raise ValueError(
f"Insufficient balance of ${user_balance/100} to run the block that costs ${abs(amount)/100}"
)
# Create the transaction
transaction_data: CreditTransactionCreateInput = {
"userId": user_id,
"amount": amount,
"runningBalance": user_balance + amount,
"type": transaction_type,
"metadata": metadata,
"isActive": is_active,
"createdAt": self.time_now(),
}
if transaction_key:
transaction_data["transactionKey"] = transaction_key
tx = await CreditTransaction.prisma().create(data=transaction_data)
return user_balance + amount, tx.transactionKey
class UsageTransactionMetadata(BaseModel):
graph_exec_id: str | None = None
graph_id: str | None = None
node_id: str | None = None
node_exec_id: str | None = None
block_id: str | None = None
block: str | None = None
input: BlockInput | None = None
class UserCredit(UserCreditBase):
async def get_or_refill_credit(self, user_id: str) -> int:
cur_time = self.time_now()
cur_month = cur_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
nxt_month = (
cur_month.replace(month=cur_month.month + 1)
if cur_month.month < 12
else cur_month.replace(year=cur_month.year + 1, month=1)
)
user_credit = await CreditTransaction.prisma().group_by(
by=["userId"],
sum={"amount": True},
where={
"userId": user_id,
"createdAt": {"gte": cur_month, "lt": nxt_month},
"isActive": True,
},
)
if user_credit:
credit_sum = user_credit[0].get("_sum") or {}
return credit_sum.get("amount", 0)
key = f"MONTHLY-CREDIT-TOP-UP-{cur_month}"
try:
await CreditTransaction.prisma().create(
data={
"amount": self.num_user_credits_refill,
"type": CreditTransactionType.TOP_UP,
"userId": user_id,
"transactionKey": key,
"createdAt": self.time_now(),
}
)
except UniqueViolationError:
pass # Already refilled this month
return self.num_user_credits_refill
@staticmethod
def time_now():
return datetime.now(timezone.utc)
def _block_usage_cost(
self,
@@ -300,8 +148,8 @@ class UserCredit(UserCreditBase):
) -> bool:
"""
Filter rules:
- If cost_filter is an object, then check if cost_filter is the subset of input_data
- Otherwise, check if cost_filter is equal to input_data.
- If costFilter is an object, then check if costFilter is the subset of inputValues
- Otherwise, check if costFilter is equal to inputValues.
- Undefined, null, and empty string are considered as equal.
"""
if not isinstance(cost_filter, dict) or not isinstance(input_data, dict):
@@ -315,366 +163,72 @@ class UserCredit(UserCreditBase):
async def spend_credits(
self,
entry: NodeExecutionEntry,
user_id: str,
user_credit: int,
block_id: str,
input_data: BlockInput,
data_size: float,
run_time: float,
validate_balance: bool = True,
) -> int:
block = get_block(entry.block_id)
block = get_block(block_id)
if not block:
raise ValueError(f"Block not found: {entry.block_id}")
raise ValueError(f"Block not found: {block_id}")
cost, matching_filter = self._block_usage_cost(
block=block, input_data=entry.data, data_size=data_size, run_time=run_time
block=block, input_data=input_data, data_size=data_size, run_time=run_time
)
if cost == 0:
if cost <= 0:
return 0
balance, _ = await self._add_transaction(
user_id=entry.user_id,
amount=-cost,
transaction_type=CreditTransactionType.USAGE,
metadata=Json(
UsageTransactionMetadata(
graph_exec_id=entry.graph_exec_id,
graph_id=entry.graph_id,
node_id=entry.node_id,
node_exec_id=entry.node_exec_id,
block_id=entry.block_id,
block=block.name,
input=matching_filter,
).model_dump()
),
if validate_balance and user_credit < cost:
raise ValueError(f"Insufficient credit: {user_credit} < {cost}")
await CreditTransaction.prisma().create(
data={
"userId": user_id,
"amount": -cost,
"type": CreditTransactionType.USAGE,
"blockId": block.id,
"metadata": Json(
{
"block": block.name,
"input": matching_filter,
}
),
"createdAt": self.time_now(),
}
)
user_id = entry.user_id
# Auto top-up if balance is below threshold.
auto_top_up = await get_auto_top_up(user_id)
if auto_top_up.threshold and balance < auto_top_up.threshold:
try:
await self._top_up_credits(
user_id=user_id,
amount=auto_top_up.amount,
# Avoid multiple auto top-ups within the same graph execution.
key=f"AUTO-TOP-UP-{user_id}-{entry.graph_exec_id}",
ceiling_balance=auto_top_up.threshold,
)
except Exception as e:
# Failed top-up is not critical, we can move on.
logger.error(
f"Auto top-up failed for user {user_id}, balance: {balance}, amount: {auto_top_up.amount}, error: {e}"
)
return cost
async def top_up_credits(self, user_id: str, amount: int):
await self._top_up_credits(user_id, amount)
async def _top_up_credits(
self,
user_id: str,
amount: int,
key: str | None = None,
ceiling_balance: int | None = None,
):
if amount < 0:
raise ValueError(f"Top up amount must not be negative: {amount}")
if key is not None and (
await CreditTransaction.prisma().find_first(
where={"transactionKey": key, "userId": user_id}
)
):
raise ValueError(f"Transaction key {key} already exists for user {user_id}")
_, transaction_key = await self._add_transaction(
user_id=user_id,
amount=amount,
transaction_type=CreditTransactionType.TOP_UP,
is_active=False,
transaction_key=key,
ceiling_balance=ceiling_balance,
)
customer_id = await get_stripe_customer_id(user_id)
payment_methods = stripe.PaymentMethod.list(customer=customer_id, type="card")
if not payment_methods:
raise ValueError("No payment method found, please add it on the platform.")
for payment_method in payment_methods:
if amount == 0:
setup_intent = stripe.SetupIntent.create(
customer=customer_id,
usage="off_session",
confirm=True,
payment_method=payment_method.id,
automatic_payment_methods={
"enabled": True,
"allow_redirects": "never",
},
)
if setup_intent.status == "succeeded":
return
else:
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency="usd",
description="AutoGPT Platform Credits",
customer=customer_id,
off_session=True,
confirm=True,
payment_method=payment_method.id,
automatic_payment_methods={
"enabled": True,
"allow_redirects": "never",
},
)
if payment_intent.status == "succeeded":
await self._enable_transaction(
transaction_key=transaction_key,
user_id=user_id,
metadata=Json({"payment_intent": payment_intent}),
)
return
raise ValueError(
f"Out of {len(payment_methods)} payment methods tried, none is supported"
)
async def top_up_intent(self, user_id: str, amount: int) -> str:
if amount < 500 or amount % 100 != 0:
raise ValueError(
f"Top up amount must be at least 500 credits and multiple of 100 but is {amount}"
)
# Create checkout session
# https://docs.stripe.com/checkout/quickstart?client=react
# unit_amount param is always in the smallest currency unit (so cents for usd)
# which is equal to amount of credits
checkout_session = stripe.checkout.Session.create(
customer=await get_stripe_customer_id(user_id),
line_items=[
{
"price_data": {
"currency": "usd",
"product_data": {
"name": "AutoGPT Platform Credits",
},
"unit_amount": amount,
},
"quantity": 1,
}
],
mode="payment",
ui_mode="hosted",
payment_intent_data={"setup_future_usage": "off_session"},
saved_payment_method_options={"payment_method_save": "enabled"},
success_url=settings.config.frontend_base_url
+ "/marketplace/credits?topup=success",
cancel_url=settings.config.frontend_base_url
+ "/marketplace/credits?topup=cancel",
allow_promotion_codes=True,
)
await self._add_transaction(
user_id=user_id,
amount=amount,
transaction_type=CreditTransactionType.TOP_UP,
transaction_key=checkout_session.id,
is_active=False,
metadata=Json({"checkout_session": checkout_session}),
)
return checkout_session.url or ""
# https://docs.stripe.com/checkout/fulfillment
async def fulfill_checkout(
self, *, session_id: str | None = None, user_id: str | None = None
):
if (not session_id and not user_id) or (session_id and user_id):
raise ValueError("Either session_id or user_id must be provided")
# Retrieve CreditTransaction
find_filter: CreditTransactionWhereInput = {
"type": CreditTransactionType.TOP_UP,
"isActive": False,
}
if session_id:
find_filter["transactionKey"] = session_id
if user_id:
find_filter["userId"] = user_id
# Find the most recent inactive top-up transaction
credit_transaction = await CreditTransaction.prisma().find_first(
where=find_filter,
order={"createdAt": "desc"},
)
# This can be called multiple times for one id, so ignore if already fulfilled
if not credit_transaction:
return
# Retrieve the Checkout Session from the API
checkout_session = stripe.checkout.Session.retrieve(
credit_transaction.transactionKey
)
# Check the Checkout Session's payment_status property
# to determine if fulfillment should be performed
if checkout_session.payment_status in ["paid", "no_payment_required"]:
await self._enable_transaction(
transaction_key=credit_transaction.transactionKey,
user_id=credit_transaction.userId,
metadata=Json({"checkout_session": checkout_session}),
)
async def get_credits(self, user_id: str) -> int:
balance, _ = await self._get_credits(user_id)
return balance
async def get_transaction_history(
self,
user_id: str,
transaction_time: datetime,
transaction_count_limit: int,
) -> TransactionHistory:
transactions = await CreditTransaction.prisma().find_many(
where={
await CreditTransaction.prisma().create(
data={
"userId": user_id,
"createdAt": {"lt": transaction_time},
"isActive": True,
},
order={"createdAt": "desc"},
take=transaction_count_limit,
"amount": amount,
"type": CreditTransactionType.TOP_UP,
"createdAt": self.time_now(),
}
)
grouped_transactions: dict[str, UserTransaction] = defaultdict(
lambda: UserTransaction()
)
tx_time = None
for t in transactions:
metadata = (
UsageTransactionMetadata.model_validate(t.metadata)
if t.metadata
else UsageTransactionMetadata()
)
tx_time = t.createdAt.replace(tzinfo=None)
if t.type == CreditTransactionType.USAGE and metadata.graph_exec_id:
gt = grouped_transactions[metadata.graph_exec_id]
gid = metadata.graph_id[:8] if metadata.graph_id else "UNKNOWN"
gt.description = f"Graph #{gid} Execution"
gt.usage_node_count += 1
gt.usage_start_time = min(gt.usage_start_time, tx_time)
gt.usage_execution_id = metadata.graph_exec_id
gt.usage_graph_id = metadata.graph_id
else:
gt = grouped_transactions[t.transactionKey]
gt.description = f"{t.type} Transaction"
gt.amount += t.amount
gt.transaction_type = t.type
if tx_time > gt.transaction_time:
gt.transaction_time = tx_time
gt.balance = t.runningBalance or 0
return TransactionHistory(
transactions=list(grouped_transactions.values()),
next_transaction_time=(
tx_time if len(transactions) == transaction_count_limit else None
),
)
class BetaUserCredit(UserCredit):
"""
This is a temporary class to handle the test user utilizing monthly credit refill.
TODO: Remove this class & its feature toggle.
"""
def __init__(self, num_user_credits_refill: int):
self.num_user_credits_refill = num_user_credits_refill
async def get_credits(self, user_id: str) -> int:
cur_time = self.time_now().date()
balance, snapshot_time = await self._get_credits(user_id)
if (snapshot_time.year, snapshot_time.month) == (cur_time.year, cur_time.month):
return balance
try:
balance, _ = await self._add_transaction(
user_id=user_id,
amount=max(self.num_user_credits_refill - balance, 0),
transaction_type=CreditTransactionType.TOP_UP,
transaction_key=f"MONTHLY-CREDIT-TOP-UP-{cur_time}",
)
return balance
except UniqueViolationError:
# Already refilled this month
return (await self._get_credits(user_id))[0]
class DisabledUserCredit(UserCreditBase):
async def get_credits(self, *args, **kwargs) -> int:
async def get_or_refill_credit(self, *args, **kwargs) -> int:
return 0
async def get_transaction_history(self, *args, **kwargs) -> TransactionHistory:
return TransactionHistory(transactions=[], next_transaction_time=None)
async def spend_credits(self, *args, **kwargs) -> int:
return 0
async def top_up_credits(self, *args, **kwargs):
pass
async def top_up_intent(self, *args, **kwargs) -> str:
return ""
async def fulfill_checkout(self, *args, **kwargs):
pass
def get_user_credit_model() -> UserCreditBase:
if not settings.config.enable_credit:
return DisabledUserCredit()
if settings.config.enable_beta_monthly_credit:
return BetaUserCredit(settings.config.num_user_credits_refill)
return UserCredit()
if config.enable_credit.lower() == "true":
return UserCredit(config.num_user_credits_refill)
else:
return DisabledUserCredit(0)
def get_block_costs() -> dict[str, list[BlockCost]]:
return {block().id: costs for block, costs in BLOCK_COSTS.items()}
async def get_stripe_customer_id(user_id: str) -> str:
user = await get_user_by_id(user_id)
if user.stripeCustomerId:
return user.stripeCustomerId
customer = stripe.Customer.create(name=user.name or "", email=user.email)
await User.prisma().update(
where={"id": user_id}, data={"stripeCustomerId": customer.id}
)
return customer.id
async def set_auto_top_up(user_id: str, config: AutoTopUpConfig):
await User.prisma().update(
where={"id": user_id},
data={"topUpConfig": Json(config.model_dump())},
)
async def get_auto_top_up(user_id: str) -> AutoTopUpConfig:
user = await get_user_by_id(user_id)
if not user.topUpConfig:
return AutoTopUpConfig(threshold=0, amount=0)
return AutoTopUpConfig.model_validate(user.topUpConfig)

View File

@@ -1,6 +1,5 @@
import logging
import os
import zlib
from contextlib import asynccontextmanager
from uuid import uuid4
@@ -55,14 +54,6 @@ async def transaction():
yield tx
@asynccontextmanager
async def locked_transaction(key: str):
lock_key = zlib.crc32(key.encode("utf-8"))
async with transaction() as tx:
await tx.execute_raw(f"SELECT pg_advisory_xact_lock({lock_key})")
yield tx
class BaseDbModel(BaseModel):
id: str = Field(default_factory=lambda: str(uuid4()))

View File

@@ -1,10 +1,9 @@
from collections import defaultdict
from datetime import datetime, timezone
from multiprocessing import Manager
from typing import Any, AsyncGenerator, Generator, Generic, Optional, TypeVar
from typing import Any, AsyncGenerator, Generator, Generic, TypeVar
from prisma.enums import AgentExecutionStatus
from prisma.errors import PrismaError
from prisma.models import (
AgentGraphExecution,
AgentNodeExecution,
@@ -32,7 +31,6 @@ class NodeExecutionEntry(BaseModel):
graph_id: str
node_exec_id: str
node_id: str
block_id: str
data: BlockInput
@@ -326,30 +324,6 @@ async def update_execution_status(
return ExecutionResult.from_db(res)
async def get_execution(
execution_id: str, user_id: str
) -> Optional[AgentNodeExecution]:
"""
Get an execution by ID. Returns None if not found.
Args:
execution_id: The ID of the execution to retrieve
Returns:
The execution if found, None otherwise
"""
try:
execution = await AgentNodeExecution.prisma().find_unique(
where={
"id": execution_id,
"userId": user_id,
}
)
return execution
except PrismaError:
return None
async def get_execution_results(graph_exec_id: str) -> list[ExecutionResult]:
executions = await AgentNodeExecution.prisma().find_many(
where={"agentGraphExecutionId": graph_exec_id},

View File

@@ -6,13 +6,7 @@ from datetime import datetime, timezone
from typing import Any, Literal, Optional, Type
import prisma
from prisma.models import (
AgentGraph,
AgentGraphExecution,
AgentNode,
AgentNodeLink,
StoreListingVersion,
)
from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink
from prisma.types import AgentGraphWhereInput
from pydantic.fields import computed_field
@@ -549,35 +543,21 @@ async def get_graph(
where_clause: AgentGraphWhereInput = {
"id": graph_id,
}
if version is not None:
where_clause["version"] = version
elif not template:
where_clause["isActive"] = True
# TODO: Fix hack workaround to get adding store agents to work
if user_id is not None and not template:
where_clause["userId"] = user_id
graph = await AgentGraph.prisma().find_first(
where=where_clause,
include=AGENT_GRAPH_INCLUDE,
order={"version": "desc"},
)
# For access, the graph must be owned by the user or listed in the store
if graph is None or (
graph.userId != user_id
and not (
await StoreListingVersion.prisma().find_first(
where={
"agentId": graph_id,
"agentVersion": version or graph.version,
"isDeleted": False,
"StoreListing": {"is": {"isApproved": True}},
}
)
)
):
return None
return GraphModel.from_db(graph, for_export)
return GraphModel.from_db(graph, for_export) if graph else None
async def set_graph_active_version(graph_id: str, version: int, user_id: str) -> None:

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import base64
import logging
from datetime import datetime
from typing import (
TYPE_CHECKING,
Annotated,
@@ -18,7 +16,6 @@ from typing import (
)
from uuid import uuid4
from prisma.enums import CreditTransactionType
from pydantic import (
BaseModel,
ConfigDict,
@@ -202,42 +199,27 @@ class OAuth2Credentials(_BaseCredentials):
scopes: list[str]
metadata: dict[str, Any] = Field(default_factory=dict)
def auth_header(self) -> str:
def bearer(self) -> str:
return f"Bearer {self.access_token.get_secret_value()}"
class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key"
api_key: SecretStr
expires_at: Optional[int] = Field(
default=None,
description="Unix timestamp (seconds) indicating when the API key expires (if at all)",
)
expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""
def auth_header(self) -> str:
def bearer(self) -> str:
return f"Bearer {self.api_key.get_secret_value()}"
class UserPasswordCredentials(_BaseCredentials):
type: Literal["user_password"] = "user_password"
username: SecretStr
password: SecretStr
def auth_header(self) -> str:
# Converting the string to bytes using encode()
# Base64 encoding it with base64.b64encode()
# Converting the resulting bytes back to a string with decode()
return f"Basic {base64.b64encode(f'{self.username.get_secret_value()}:{self.password.get_secret_value()}'.encode()).decode()}"
Credentials = Annotated[
OAuth2Credentials | APIKeyCredentials | UserPasswordCredentials,
OAuth2Credentials | APIKeyCredentials,
Field(discriminator="type"),
]
CredentialsType = Literal["api_key", "oauth2", "user_password"]
CredentialsType = Literal["api_key", "oauth2"]
class OAuthState(BaseModel):
@@ -365,27 +347,3 @@ def CredentialsField(
class ContributorDetails(BaseModel):
name: str = Field(title="Name", description="The name of the contributor.")
class AutoTopUpConfig(BaseModel):
amount: int
"""Amount of credits to top up."""
threshold: int
"""Threshold to trigger auto top up."""
class UserTransaction(BaseModel):
transaction_time: datetime = datetime.min
transaction_type: CreditTransactionType = CreditTransactionType.USAGE
amount: int = 0
balance: int = 0
description: str | None = None
usage_graph_id: str | None = None
usage_execution_id: str | None = None
usage_node_count: int = 0
usage_start_time: datetime = datetime.max
class TransactionHistory(BaseModel):
transactions: list[UserTransaction]
next_transaction_time: datetime | None

View File

@@ -34,11 +34,9 @@ async def get_or_create_user(user_data: dict) -> User:
return User.model_validate(user)
async def get_user_by_id(user_id: str) -> User:
async def get_user_by_id(user_id: str) -> Optional[User]:
user = await prisma.user.find_unique(where={"id": user_id})
if not user:
raise ValueError(f"User not found with ID: {user_id}")
return User.model_validate(user)
return User.model_validate(user) if user else None
async def create_default_user() -> Optional[User]:

View File

@@ -4,7 +4,6 @@ from typing import Any, Callable, Concatenate, Coroutine, ParamSpec, TypeVar, ca
from backend.data.credit import get_user_credit_model
from backend.data.execution import (
ExecutionResult,
NodeExecutionEntry,
RedisExecutionEventBus,
create_graph_execution,
get_execution_results,
@@ -79,8 +78,12 @@ class DatabaseManager(AppService):
# Credits
user_credit_model = get_user_credit_model()
get_or_refill_credit = cast(
Callable[[Any, str], int],
exposed_run_and_wait(user_credit_model.get_or_refill_credit),
)
spend_credits = cast(
Callable[[Any, NodeExecutionEntry, float, float], int],
Callable[[Any, str, int, str, dict[str, str], float, float], int],
exposed_run_and_wait(user_credit_model.spend_credits),
)

View File

@@ -8,7 +8,7 @@ import threading
from concurrent.futures import Future, ProcessPoolExecutor
from contextlib import contextmanager
from multiprocessing.pool import AsyncResult, Pool
from typing import TYPE_CHECKING, Any, Generator, Optional, TypeVar, cast
from typing import TYPE_CHECKING, Any, Generator, TypeVar, cast
from redis.lock import Lock as RedisLock
@@ -40,7 +40,6 @@ from backend.data.graph import GraphModel, Link, Node
from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.util import json
from backend.util.decorator import error_logged, time_measured
from backend.util.file import clean_exec_files
from backend.util.logging import configure_logging
from backend.util.process import set_service_name
from backend.util.service import (
@@ -163,7 +162,6 @@ def execute_node(
# AgentExecutorBlock specially separate the node input_data & its input_default.
if isinstance(node_block, AgentExecutorBlock):
input_data = {**node.input_default, "data": input_data}
data.data = input_data
# Execute the node
input_data_str = json.dumps(input_data)
@@ -171,15 +169,7 @@ def execute_node(
log_metadata.info("Executed node with input", input=input_data_str)
update_execution(ExecutionStatus.RUNNING)
# Inject extra execution arguments for the blocks via kwargs
extra_exec_kwargs: dict = {
"graph_id": graph_id,
"node_id": node_id,
"graph_exec_id": graph_exec_id,
"node_exec_id": node_exec_id,
"user_id": user_id,
}
extra_exec_kwargs = {}
# Last-minute fetch credentials + acquire a system-wide read-write lock to prevent
# changes during execution. ⚠️ This means a set of credentials can only be used by
# one (running) block at a time; simultaneous execution of blocks using same
@@ -192,12 +182,12 @@ def execute_node(
extra_exec_kwargs[field_name] = credentials
output_size = 0
try:
# Charge the user for the execution before running the block.
# TODO: We assume the block is executed within 0 seconds.
# This is fine because for now, there is no block that is charged by time.
db_client.spend_credits(data, input_size + output_size, 0)
end_status = ExecutionStatus.COMPLETED
credit = db_client.get_or_refill_credit(user_id)
if credit < 0:
raise ValueError(f"Insufficient credit: {credit}")
try:
for output_name, output_data in node_block.execute(
input_data, **extra_exec_kwargs
):
@@ -216,12 +206,11 @@ def execute_node(
):
yield execution
update_execution(ExecutionStatus.COMPLETED)
except Exception as e:
end_status = ExecutionStatus.FAILED
error_msg = str(e)
log_metadata.exception(f"Node execution failed with error {error_msg}")
db_client.upsert_execution_output(node_exec_id, "error", error_msg)
update_execution(ExecutionStatus.FAILED)
for execution in _enqueue_next_nodes(
db_client=db_client,
@@ -243,6 +232,17 @@ def execute_node(
except Exception as e:
log_metadata.error(f"Failed to release credentials lock: {e}")
# Update execution status and spend credits
res = update_execution(end_status)
if end_status == ExecutionStatus.COMPLETED:
s = input_size + output_size
t = (
(res.end_time - res.start_time).total_seconds()
if res.end_time and res.start_time
else 0
)
db_client.spend_credits(user_id, credit, node_block.id, input_data, s, t)
# Update execution stats
if execution_stats is not None:
execution_stats.update(node_block.execution_stats)
@@ -260,7 +260,7 @@ def _enqueue_next_nodes(
log_metadata: LogMetadata,
) -> list[NodeExecutionEntry]:
def add_enqueued_execution(
node_exec_id: str, node_id: str, block_id: str, data: BlockInput
node_exec_id: str, node_id: str, data: BlockInput
) -> NodeExecutionEntry:
exec_update = db_client.update_execution_status(
node_exec_id, ExecutionStatus.QUEUED, data
@@ -272,7 +272,6 @@ def _enqueue_next_nodes(
graph_id=graph_id,
node_exec_id=node_exec_id,
node_id=node_id,
block_id=block_id,
data=data,
)
@@ -326,12 +325,7 @@ def _enqueue_next_nodes(
# Input is complete, enqueue the execution.
log_metadata.info(f"Enqueued {suffix}")
enqueued_executions.append(
add_enqueued_execution(
node_exec_id=next_node_exec_id,
node_id=next_node_id,
block_id=next_node.block_id,
data=next_node_input,
)
add_enqueued_execution(next_node_exec_id, next_node_id, next_node_input)
)
# Next execution stops here if the link is not static.
@@ -361,12 +355,7 @@ def _enqueue_next_nodes(
continue
log_metadata.info(f"Enqueueing static-link execution {suffix}")
enqueued_executions.append(
add_enqueued_execution(
node_exec_id=iexec.node_exec_id,
node_id=next_node_id,
block_id=next_node.block_id,
data=idata,
)
add_enqueued_execution(iexec.node_exec_id, next_node_id, idata)
)
return enqueued_executions
@@ -559,15 +548,9 @@ class Executor:
q.add(execution)
log_metadata.info(f"Finished node execution {node_exec.node_exec_id}")
except Exception as e:
# Avoid user error being marked as an actual error.
if isinstance(e, ValueError):
log_metadata.info(
f"Failed node execution {node_exec.node_exec_id}: {e}"
)
else:
log_metadata.exception(
f"Failed node execution {node_exec.node_exec_id}: {e}"
)
log_metadata.exception(
f"Failed node execution {node_exec.node_exec_id}: {e}"
)
@classmethod
def on_graph_executor_start(cls):
@@ -665,10 +648,6 @@ class Executor:
try:
queue = ExecutionQueue[NodeExecutionEntry]()
for node_exec in graph_exec.start_node_execs:
exec_update = cls.db_client.update_execution_status(
node_exec.node_exec_id, ExecutionStatus.QUEUED, node_exec.data
)
cls.db_client.send_execution_update(exec_update)
queue.add(node_exec)
running_executions: dict[str, AsyncResult] = {}
@@ -737,7 +716,6 @@ class Executor:
finished = True
cancel.set()
cancel_thread.join()
clean_exec_files(graph_exec.graph_exec_id)
return (
exec_stats,
@@ -802,7 +780,7 @@ class ExecutionManager(AppService):
graph_id: str,
data: BlockInput,
user_id: str,
graph_version: Optional[int] = None,
graph_version: int | None = None,
) -> GraphExecutionEntry:
graph: GraphModel | None = self.db_client.get_graph(
graph_id=graph_id, user_id=user_id, version=graph_version
@@ -825,8 +803,8 @@ class ExecutionManager(AppService):
# Extract request input data, and assign it to the input pin.
if block.block_type == BlockType.INPUT:
name = node.input_default.get("name")
if name in data.get("node_input", {}):
input_data = {"value": data["node_input"][name]}
if name and name in data:
input_data = {"value": data[name]}
# Extract webhook payload, and assign it to the input pin
webhook_payload_key = f"webhook_{node.webhook_id}_payload"
@@ -862,10 +840,13 @@ class ExecutionManager(AppService):
graph_id=node_exec.graph_id,
node_exec_id=node_exec.node_exec_id,
node_id=node_exec.node_id,
block_id=node_exec.block_id,
data=node_exec.input_data,
)
)
exec_update = self.db_client.update_execution_status(
node_exec.node_exec_id, ExecutionStatus.QUEUED, node_exec.input_data
)
self.db_client.send_execution_update(exec_update)
graph_exec = GraphExecutionEntry(
user_id=user_id,

View File

@@ -63,10 +63,7 @@ def execute_graph(**kwargs):
try:
log(f"Executing recurring job for graph #{args.graph_id}")
get_execution_client().add_execution(
graph_id=args.graph_id,
data=args.input_data,
user_id=args.user_id,
graph_version=args.graph_version,
args.graph_id, args.input_data, args.user_id
)
except Exception as e:
logger.exception(f"Error executing graph {args.graph_id}: {e}")

View File

@@ -23,15 +23,6 @@ from backend.util.settings import Settings
settings = Settings()
# This is an overrride since ollama doesn't actually require an API key, but the creddential system enforces one be attached
ollama_credentials = APIKeyCredentials(
id="744fdc56-071a-4761-b5a5-0af0ce10a2b5",
provider="ollama",
api_key=SecretStr("FAKE_API_KEY"),
title="Use Credits for Ollama",
expires_at=None,
)
revid_credentials = APIKeyCredentials(
id="fdb7f412-f519-48d1-9b5f-d2f73d0e01fe",
provider="revid",
@@ -130,24 +121,9 @@ nvidia_credentials = APIKeyCredentials(
title="Use Credits for Nvidia",
expires_at=None,
)
screenshotone_credentials = APIKeyCredentials(
id="3b1bdd16-8818-4bc2-8cbb-b23f9a3439ed",
provider="screenshotone",
api_key=SecretStr(settings.secrets.screenshotone_api_key),
title="Use Credits for ScreenshotOne",
expires_at=None,
)
mem0_credentials = APIKeyCredentials(
id="ed55ac19-356e-4243-a6cb-bc599e9b716f",
provider="mem0",
api_key=SecretStr(settings.secrets.mem0_api_key),
title="Use Credits for Mem0",
expires_at=None,
)
DEFAULT_CREDENTIALS = [
ollama_credentials,
revid_credentials,
ideogram_credentials,
replicate_credentials,
@@ -161,9 +137,7 @@ DEFAULT_CREDENTIALS = [
fal_credentials,
exa_credentials,
e2b_credentials,
mem0_credentials,
nvidia_credentials,
screenshotone_credentials,
]
@@ -195,10 +169,6 @@ class IntegrationCredentialsStore:
def get_all_creds(self, user_id: str) -> list[Credentials]:
users_credentials = self._get_user_integrations(user_id).credentials
all_credentials = users_credentials
# These will always be added
all_credentials.append(ollama_credentials)
# These will only be added if the API key is set
if settings.secrets.revid_api_key:
all_credentials.append(revid_credentials)
if settings.secrets.ideogram_api_key:
@@ -227,10 +197,6 @@ class IntegrationCredentialsStore:
all_credentials.append(e2b_credentials)
if settings.secrets.nvidia_api_key:
all_credentials.append(nvidia_credentials)
if settings.secrets.screenshotone_api_key:
all_credentials.append(screenshotone_credentials)
if settings.secrets.mem0_api_key:
all_credentials.append(mem0_credentials)
return all_credentials
def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None:

View File

@@ -1,10 +1,7 @@
from typing import TYPE_CHECKING
from backend.integrations.oauth.todoist import TodoistOAuthHandler
from .github import GitHubOAuthHandler
from .google import GoogleOAuthHandler
from .linear import LinearOAuthHandler
from .notion import NotionOAuthHandler
from .twitter import TwitterOAuthHandler
@@ -20,8 +17,6 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = {
GoogleOAuthHandler,
NotionOAuthHandler,
TwitterOAuthHandler,
LinearOAuthHandler,
TodoistOAuthHandler,
]
}
# --8<-- [end:HANDLERS_BY_NAMEExample]

View File

@@ -1,165 +0,0 @@
import json
from typing import Optional
from urllib.parse import urlencode
from pydantic import SecretStr
from backend.blocks.linear._api import LinearAPIException
from backend.data.model import APIKeyCredentials, OAuth2Credentials
from backend.integrations.providers import ProviderName
from backend.util.request import requests
from .base import BaseOAuthHandler
class LinearOAuthHandler(BaseOAuthHandler):
"""
OAuth2 handler for Linear.
"""
PROVIDER_NAME = ProviderName.LINEAR
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.auth_base_url = "https://linear.app/oauth/authorize"
self.token_url = "https://api.linear.app/oauth/token" # Correct token URL
self.revoke_url = "https://api.linear.app/oauth/revoke"
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"response_type": "code", # Important: include "response_type"
"scope": ",".join(scopes), # Comma-separated, not space-separated
"state": state,
}
return f"{self.auth_base_url}?{urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri})
def revoke_tokens(self, credentials: OAuth2Credentials) -> bool:
if not credentials.access_token:
raise ValueError("No access token to revoke")
headers = {
"Authorization": f"Bearer {credentials.access_token.get_secret_value()}"
}
response = requests.post(self.revoke_url, headers=headers)
if not response.ok:
try:
error_data = response.json()
error_message = error_data.get("error", "Unknown error")
except json.JSONDecodeError:
error_message = response.text
raise LinearAPIException(
f"Failed to revoke Linear tokens ({response.status_code}): {error_message}",
response.status_code,
)
return True # Linear doesn't return JSON on successful revoke
def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
if not credentials.refresh_token:
raise ValueError(
"No refresh token available."
) # Linear uses non-expiring tokens
return self._request_tokens(
{
"refresh_token": credentials.refresh_token.get_secret_value(),
"grant_type": "refresh_token",
}
)
def _request_tokens(
self,
params: dict[str, str],
current_credentials: Optional[OAuth2Credentials] = None,
) -> OAuth2Credentials:
request_body = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "authorization_code", # Ensure grant_type is correct
**params,
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
} # Correct header for token request
response = requests.post(self.token_url, data=request_body, headers=headers)
if not response.ok:
try:
error_data = response.json()
error_message = error_data.get("error", "Unknown error")
except json.JSONDecodeError:
error_message = response.text
raise LinearAPIException(
f"Failed to fetch Linear tokens ({response.status_code}): {error_message}",
response.status_code,
)
token_data = response.json()
# Note: Linear access tokens do not expire, so we set expires_at to None
new_credentials = OAuth2Credentials(
provider=self.PROVIDER_NAME,
title=current_credentials.title if current_credentials else None,
username=token_data.get("user", {}).get(
"name", "Unknown User"
), # extract name or set appropriate
access_token=token_data["access_token"],
scopes=token_data["scope"].split(
","
), # Linear returns comma-separated scopes
refresh_token=token_data.get(
"refresh_token"
), # Linear uses non-expiring tokens so this might be null
access_token_expires_at=None,
refresh_token_expires_at=None,
)
if current_credentials:
new_credentials.id = current_credentials.id
return new_credentials
def _request_username(self, access_token: str) -> Optional[str]:
# Use the LinearClient to fetch user details using GraphQL
from backend.blocks.linear._api import LinearClient
try:
linear_client = LinearClient(
APIKeyCredentials(
api_key=SecretStr(access_token),
title="temp",
provider=self.PROVIDER_NAME,
expires_at=None,
)
) # Temporary credentials for this request
query = """
query Viewer {
viewer {
name
}
}
"""
response = linear_client.query(query)
return response["viewer"]["name"]
except Exception as e: # Handle any errors
print(f"Error fetching username: {e}")
return None

View File

@@ -1,81 +0,0 @@
import urllib.parse
from typing import ClassVar, Optional
import requests
from backend.data.model import OAuth2Credentials, ProviderName
from backend.integrations.oauth.base import BaseOAuthHandler
class TodoistOAuthHandler(BaseOAuthHandler):
PROVIDER_NAME = ProviderName.TODOIST
DEFAULT_SCOPES: ClassVar[list[str]] = [
"task:add",
"data:read",
"data:read_write",
"data:delete",
"project:delete",
]
AUTHORIZE_URL = "https://todoist.com/oauth/authorize"
TOKEN_URL = "https://todoist.com/oauth/access_token"
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
params = {
"client_id": self.client_id,
"scope": ",".join(self.DEFAULT_SCOPES),
"state": state,
}
return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
"""Exchange authorization code for access tokens"""
data = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"redirect_uri": self.redirect_uri,
}
response = requests.post(self.TOKEN_URL, data=data)
response.raise_for_status()
tokens = response.json()
response = requests.post(
"https://api.todoist.com/sync/v9/sync",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
data={"sync_token": "*", "resource_types": '["user"]'},
)
response.raise_for_status()
user_info = response.json()
user_email = user_info["user"].get("email")
return OAuth2Credentials(
provider=self.PROVIDER_NAME,
title=None,
username=user_email,
access_token=tokens["access_token"],
refresh_token=None,
access_token_expires_at=None,
refresh_token_expires_at=None,
scopes=scopes,
)
def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
# Todoist does not support token refresh
return credentials
def revoke_tokens(self, credentials: OAuth2Credentials) -> bool:
return False

View File

@@ -17,9 +17,7 @@ class ProviderName(str, Enum):
HUBSPOT = "hubspot"
IDEOGRAM = "ideogram"
JINA = "jina"
LINEAR = "linear"
MEDIUM = "medium"
MEM0 = "mem0"
NOTION = "notion"
NVIDIA = "nvidia"
OLLAMA = "ollama"
@@ -27,13 +25,9 @@ class ProviderName(str, Enum):
OPENWEATHERMAP = "openweathermap"
OPEN_ROUTER = "open_router"
PINECONE = "pinecone"
REDDIT = "reddit"
REPLICATE = "replicate"
REVID = "revid"
SCREENSHOTONE = "screenshotone"
SLANT3D = "slant3d"
SMTP = "smtp"
TWITTER = "twitter"
TODOIST = "todoist"
UNREAL_SPEECH = "unreal_speech"
# --8<-- [end:ProviderName]

View File

@@ -168,7 +168,7 @@ class BaseWebhooksManager(ABC, Generic[WT]):
id = str(uuid4())
secret = secrets.token_hex(32)
provider_name: ProviderName = self.PROVIDER_NAME
provider_name = self.PROVIDER_NAME
ingress_url = webhook_ingress_url(provider_name=provider_name, webhook_id=id)
if register:
if not credentials:

View File

@@ -1,7 +1,7 @@
import logging
from backend.data import integrations
from backend.data.model import Credentials
from backend.data.model import APIKeyCredentials, Credentials, OAuth2Credentials
from ._base import WT, BaseWebhooksManager
@@ -25,6 +25,6 @@ class ManualWebhookManagerBase(BaseWebhooksManager[WT]):
async def _deregister_webhook(
self,
webhook: integrations.Webhook,
credentials: Credentials,
credentials: OAuth2Credentials | APIKeyCredentials,
) -> None:
pass

View File

@@ -67,7 +67,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = {
**self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.auth_header(),
"Authorization": credentials.bearer(),
}
repo, github_hook_id = webhook.resource, webhook.provider_webhook_id
@@ -96,7 +96,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = {
**self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.auth_header(),
"Authorization": credentials.bearer(),
}
webhook_data = {
"name": "web",
@@ -142,7 +142,7 @@ class GithubWebhooksManager(BaseWebhooksManager):
headers = {
**self.GITHUB_API_DEFAULT_HEADERS,
"Authorization": credentials.auth_header(),
"Authorization": credentials.bearer(),
}
if webhook_type == self.WebhookType.REPO:

View File

@@ -1,11 +0,0 @@
from fastapi import FastAPI
from .routes.v1 import v1_router
external_app = FastAPI(
title="AutoGPT External API",
description="External API for AutoGPT integrations",
docs_url="/docs",
version="1.0",
)
external_app.include_router(v1_router, prefix="/v1")

View File

@@ -1,37 +0,0 @@
from fastapi import Depends, HTTPException, Request
from fastapi.security import APIKeyHeader
from prisma.enums import APIKeyPermission
from backend.data.api_key import has_permission, validate_api_key
api_key_header = APIKeyHeader(name="X-API-Key")
async def require_api_key(request: Request):
"""Base middleware for API key authentication"""
api_key = await api_key_header(request)
if api_key is None:
raise HTTPException(status_code=401, detail="Missing API key")
api_key_obj = await validate_api_key(api_key)
if not api_key_obj:
raise HTTPException(status_code=401, detail="Invalid API key")
request.state.api_key = api_key_obj
return api_key_obj
def require_permission(permission: APIKeyPermission):
"""Dependency function for checking specific permissions"""
async def check_permission(api_key=Depends(require_api_key)):
if not has_permission(api_key, permission):
raise HTTPException(
status_code=403,
detail=f"API key missing required permission: {permission}",
)
return api_key
return check_permission

View File

@@ -1,111 +0,0 @@
import logging
from collections import defaultdict
from typing import Any, Sequence
from autogpt_libs.utils.cache import thread_cached
from fastapi import APIRouter, Depends, HTTPException
from prisma.enums import APIKeyPermission
import backend.data.block
from backend.data import execution as execution_db
from backend.data import graph as graph_db
from backend.data.api_key import APIKey
from backend.data.block import BlockInput, CompletedBlockOutput
from backend.executor import ExecutionManager
from backend.server.external.middleware import require_permission
from backend.util.service import get_service_client
from backend.util.settings import Settings
@thread_cached
def execution_manager_client() -> ExecutionManager:
return get_service_client(ExecutionManager)
settings = Settings()
logger = logging.getLogger(__name__)
v1_router = APIRouter()
@v1_router.get(
path="/blocks",
tags=["blocks"],
dependencies=[Depends(require_permission(APIKeyPermission.READ_BLOCK))],
)
def get_graph_blocks() -> Sequence[dict[Any, Any]]:
blocks = [block() for block in backend.data.block.get_blocks().values()]
return [b.to_dict() for b in blocks]
@v1_router.post(
path="/blocks/{block_id}/execute",
tags=["blocks"],
dependencies=[Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK))],
)
def execute_graph_block(
block_id: str,
data: BlockInput,
api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK)),
) -> CompletedBlockOutput:
obj = backend.data.block.get_block(block_id)
if not obj:
raise HTTPException(status_code=404, detail=f"Block #{block_id} not found.")
output = defaultdict(list)
for name, data in obj.execute(data):
output[name].append(data)
return output
@v1_router.post(
path="/graphs/{graph_id}/execute",
tags=["graphs"],
)
def execute_graph(
graph_id: str,
node_input: dict[Any, Any],
api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_GRAPH)),
) -> dict[str, Any]:
try:
graph_exec = execution_manager_client().add_execution(
graph_id, node_input, user_id=api_key.user_id
)
return {"id": graph_exec.graph_exec_id}
except Exception as e:
msg = e.__str__().encode().decode("unicode_escape")
raise HTTPException(status_code=400, detail=msg)
@v1_router.get(
path="/graphs/{graph_id}/executions/{graph_exec_id}/results",
tags=["graphs"],
)
async def get_graph_execution_results(
graph_id: str,
graph_exec_id: str,
api_key: APIKey = Depends(require_permission(APIKeyPermission.READ_GRAPH)),
) -> dict:
graph = await graph_db.get_graph(graph_id, user_id=api_key.user_id)
if not graph:
raise HTTPException(status_code=404, detail=f"Graph #{graph_id} not found.")
results = await execution_db.get_execution_results(graph_exec_id)
return {
"execution_id": graph_exec_id,
"nodes": [
{
"node_id": result.node_id,
"input": (
result.input_data.get("value")
if "value" in result.input_data
else result.input_data
),
"output": result.output_data.get(
"response", result.output_data.get("result", [])
),
}
for result in results
],
}

View File

@@ -2,7 +2,7 @@ import logging
from typing import TYPE_CHECKING, Annotated, Literal
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, SecretStr
from backend.data.graph import set_node_webhook
from backend.data.integrations import (
@@ -12,7 +12,12 @@ from backend.data.integrations import (
publish_webhook_event,
wait_for_webhook_event,
)
from backend.data.model import Credentials, CredentialsType, OAuth2Credentials
from backend.data.model import (
APIKeyCredentials,
Credentials,
CredentialsType,
OAuth2Credentials,
)
from backend.executor.manager import ExecutionManager
from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.oauth import HANDLERS_BY_NAME
@@ -105,11 +110,6 @@ def callback(
logger.debug(f"Received credentials with final scopes: {credentials.scopes}")
# Linear returns scopes as a single string with spaces, so we need to split them
# TODO: make a bypass of this part of the OAuth handler
if len(credentials.scopes) == 1 and " " in credentials.scopes[0]:
credentials.scopes = credentials.scopes[0].split(" ")
# Check if the granted scopes are sufficient for the requested scopes
if not set(scopes).issubset(set(credentials.scopes)):
# For now, we'll just log the warning and continue
@@ -199,21 +199,31 @@ def get_credential(
@router.post("/{provider}/credentials", status_code=201)
def create_credentials(
def create_api_key_credentials(
user_id: Annotated[str, Depends(get_user_id)],
provider: Annotated[
ProviderName, Path(title="The provider to create credentials for")
],
credentials: Credentials,
) -> Credentials:
credentials.provider = provider
api_key: Annotated[str, Body(title="The API key to store")],
title: Annotated[str, Body(title="Optional title for the credentials")],
expires_at: Annotated[
int | None, Body(title="Unix timestamp when the key expires")
] = None,
) -> APIKeyCredentials:
new_credentials = APIKeyCredentials(
provider=provider,
api_key=SecretStr(api_key),
title=title,
expires_at=expires_at,
)
try:
creds_manager.create(user_id, credentials)
creds_manager.create(user_id, new_credentials)
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to store credentials: {str(e)}"
)
return credentials
return new_credentials
class CredentialsDeletionResponse(BaseModel):
@@ -310,8 +320,7 @@ async def webhook_ingress_generic(
continue
logger.debug(f"Executing graph #{node.graph_id} node #{node.id}")
executor.add_execution(
graph_id=node.graph_id,
graph_version=node.graph_version,
node.graph_id,
data={f"webhook_{webhook_id}_payload": payload},
user_id=webhook.user_id,
)

View File

@@ -56,7 +56,3 @@ class SetGraphActiveVersion(pydantic.BaseModel):
class UpdatePermissionsRequest(pydantic.BaseModel):
permissions: List[APIKeyPermission]
class RequestTopUp(pydantic.BaseModel):
credit_amount: int

View File

@@ -1,8 +1,7 @@
import contextlib
import logging
from typing import Any, Optional
import typing
import autogpt_libs.auth.models
import fastapi
import fastapi.responses
import starlette.middleware.cors
@@ -18,11 +17,9 @@ import backend.data.graph
import backend.data.user
import backend.server.routers.v1
import backend.server.v2.library.routes
import backend.server.v2.store.model
import backend.server.v2.store.routes
import backend.util.service
import backend.util.settings
from backend.server.external.api import external_app
settings = backend.util.settings.Settings()
logger = logging.getLogger(__name__)
@@ -97,8 +94,6 @@ app.include_router(
backend.server.v2.library.routes.router, tags=["v2"], prefix="/api/library"
)
app.mount("/external-api", external_app)
@app.get(path="/health", tags=["health"], dependencies=[])
async def health():
@@ -122,27 +117,9 @@ class AgentServer(backend.util.service.AppProcess):
@staticmethod
async def test_execute_graph(
graph_id: str,
node_input: dict[str, Any],
user_id: str,
graph_version: Optional[int] = None,
graph_id: str, node_input: dict[typing.Any, typing.Any], user_id: str
):
return backend.server.routers.v1.execute_graph(
user_id=user_id,
graph_id=graph_id,
graph_version=graph_version,
node_input=node_input,
)
@staticmethod
async def test_get_graph(
graph_id: str,
graph_version: int,
user_id: str,
):
return await backend.server.routers.v1.get_graph(
graph_id, user_id, graph_version
)
return backend.server.routers.v1.execute_graph(graph_id, node_input, user_id)
@staticmethod
async def test_create_graph(
@@ -172,18 +149,5 @@ class AgentServer(backend.util.service.AppProcess):
async def test_delete_graph(graph_id: str, user_id: str):
return await backend.server.routers.v1.delete_graph(graph_id, user_id)
@staticmethod
async def test_create_store_listing(
request: backend.server.v2.store.model.StoreSubmissionRequest, user_id: str
):
return await backend.server.v2.store.routes.create_submission(request, user_id)
@staticmethod
async def test_review_store_listing(
request: backend.server.v2.store.model.ReviewSubmissionRequest,
user: autogpt_libs.auth.models.User,
):
return await backend.server.v2.store.routes.review_submission(request, user)
def set_test_dependency_overrides(self, overrides: dict):
app.dependency_overrides.update(overrides)

View File

@@ -1,15 +1,13 @@
import asyncio
import logging
from collections import defaultdict
from datetime import datetime
from typing import TYPE_CHECKING, Annotated, Any, Sequence
import pydantic
import stripe
from autogpt_libs.auth.middleware import auth_middleware
from autogpt_libs.feature_flag.client import feature_flag
from autogpt_libs.utils.cache import thread_cached
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi import APIRouter, Depends, HTTPException
from typing_extensions import Optional, TypedDict
import backend.data.block
@@ -30,15 +28,7 @@ from backend.data.api_key import (
update_api_key_permissions,
)
from backend.data.block import BlockInput, CompletedBlockOutput
from backend.data.credit import (
AutoTopUpConfig,
TransactionHistory,
get_auto_top_up,
get_block_costs,
get_stripe_customer_id,
get_user_credit_model,
set_auto_top_up,
)
from backend.data.credit import get_block_costs, get_user_credit_model
from backend.data.user import get_or_create_user
from backend.executor import ExecutionManager, ExecutionScheduler, scheduler
from backend.integrations.creds_manager import IntegrationCredentialsManager
@@ -50,7 +40,6 @@ from backend.server.model import (
CreateAPIKeyRequest,
CreateAPIKeyResponse,
CreateGraph,
RequestTopUp,
SetGraphActiveVersion,
UpdatePermissionsRequest,
)
@@ -76,6 +65,7 @@ settings = Settings()
logger = logging.getLogger(__name__)
integration_creds_manager = IntegrationCredentialsManager()
_user_credit_model = get_user_credit_model()
# Define the API routes
@@ -144,126 +134,7 @@ async def get_user_credits(
user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, int]:
# Credits can go negative, so ensure it's at least 0 for user to see.
return {"credits": max(await _user_credit_model.get_credits(user_id), 0)}
@v1_router.post(
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)]
)
async def request_top_up(
request: RequestTopUp, user_id: Annotated[str, Depends(get_user_id)]
):
checkout_url = await _user_credit_model.top_up_intent(
user_id, request.credit_amount
)
return {"checkout_url": checkout_url}
@v1_router.patch(
path="/credits", tags=["credits"], dependencies=[Depends(auth_middleware)]
)
async def fulfill_checkout(user_id: Annotated[str, Depends(get_user_id)]):
await _user_credit_model.fulfill_checkout(user_id=user_id)
return Response(status_code=200)
@v1_router.post(
path="/credits/auto-top-up",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
)
async def configure_user_auto_top_up(
request: AutoTopUpConfig, user_id: Annotated[str, Depends(get_user_id)]
) -> str:
if request.threshold < 0:
raise ValueError("Threshold must be greater than 0")
if request.amount < 500 and request.amount != 0:
raise ValueError("Amount must be greater than or equal to 500")
if request.amount < request.threshold:
raise ValueError("Amount must be greater than or equal to threshold")
current_balance = await _user_credit_model.get_credits(user_id)
if current_balance < request.threshold:
await _user_credit_model.top_up_credits(user_id, request.amount)
else:
await _user_credit_model.top_up_credits(user_id, 0)
await set_auto_top_up(
user_id, AutoTopUpConfig(threshold=request.threshold, amount=request.amount)
)
return "Auto top-up settings updated"
@v1_router.get(
path="/credits/auto-top-up",
tags=["credits"],
dependencies=[Depends(auth_middleware)],
)
async def get_user_auto_top_up(
user_id: Annotated[str, Depends(get_user_id)]
) -> AutoTopUpConfig:
return await get_auto_top_up(user_id)
@v1_router.post(path="/credits/stripe_webhook", tags=["credits"])
async def stripe_webhook(request: Request):
# Get the raw request body
payload = await request.body()
# Get the signature header
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.secrets.stripe_webhook_secret
)
except ValueError:
# Invalid payload
raise HTTPException(status_code=400)
except stripe.SignatureVerificationError:
# Invalid signature
raise HTTPException(status_code=400)
if (
event["type"] == "checkout.session.completed"
or event["type"] == "checkout.session.async_payment_succeeded"
):
await _user_credit_model.fulfill_checkout(
session_id=event["data"]["object"]["id"]
)
return Response(status_code=200)
@v1_router.get(path="/credits/manage", dependencies=[Depends(auth_middleware)])
async def manage_payment_method(
user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, str]:
session = stripe.billing_portal.Session.create(
customer=await get_stripe_customer_id(user_id),
return_url=settings.config.frontend_base_url + "/marketplace/credits",
)
if not session:
raise HTTPException(
status_code=400, detail="Failed to create billing portal session"
)
return {"url": session.url}
@v1_router.get(path="/credits/transactions", dependencies=[Depends(auth_middleware)])
async def get_credit_history(
user_id: Annotated[str, Depends(get_user_id)],
transaction_time: datetime | None = None,
transaction_count_limit: int = 100,
) -> TransactionHistory:
if transaction_count_limit < 1 or transaction_count_limit > 1000:
raise ValueError("Transaction count limit must be between 1 and 1000")
return await _user_credit_model.get_transaction_history(
user_id=user_id,
transaction_time=transaction_time or datetime.max,
transaction_count_limit=transaction_count_limit,
)
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}
########################################################
@@ -499,11 +370,10 @@ def execute_graph(
graph_id: str,
node_input: dict[Any, Any],
user_id: Annotated[str, Depends(get_user_id)],
graph_version: Optional[int] = None,
) -> dict[str, Any]: # FIXME: add proper return type
try:
graph_exec = execution_manager_client().add_execution(
graph_id, node_input, user_id=user_id, graph_version=graph_version
graph_id, node_input, user_id=user_id
)
return {"id": graph_exec.graph_exec_id}
except Exception as e:
@@ -675,6 +545,7 @@ def get_execution_schedules(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def create_api_key(
request: CreateAPIKeyRequest, user_id: Annotated[str, Depends(get_user_id)]
) -> CreateAPIKeyResponse:
@@ -698,6 +569,7 @@ async def create_api_key(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def get_api_keys(
user_id: Annotated[str, Depends(get_user_id)]
) -> list[APIKeyWithoutHash]:
@@ -715,6 +587,7 @@ async def get_api_keys(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def get_api_key(
key_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> APIKeyWithoutHash:

View File

@@ -91,7 +91,7 @@ async def add_agent_to_library(
# Create a new graph from the template
graph = await backend.data.graph.get_graph(
agent.id, agent.version, user_id=user_id
agent.id, agent.version, template=True, user_id=user_id
)
if not graph:

View File

@@ -325,10 +325,7 @@ async def get_store_submissions(
where = prisma.types.StoreSubmissionWhereInput(user_id=user_id)
# Query submissions from database
submissions = await prisma.models.StoreSubmission.prisma().find_many(
where=where,
skip=skip,
take=page_size,
order=[{"date_submitted": "desc"}],
where=where, skip=skip, take=page_size, order=[{"date_submitted": "desc"}]
)
# Get total count for pagination
@@ -408,7 +405,9 @@ async def delete_store_submission(
)
# Delete the submission
await prisma.models.StoreListing.prisma().delete(where={"id": submission.id})
await prisma.models.StoreListing.prisma().delete(
where=prisma.types.StoreListingWhereUniqueInput(id=submission.id)
)
logger.debug(
f"Successfully deleted submission {submission_id} for user {user_id}"
@@ -505,15 +504,7 @@ async def create_store_submission(
"subHeading": sub_heading,
}
},
},
include={"StoreListingVersions": True},
)
store_listing_version_id = (
listing.StoreListingVersions[0].id
if listing.StoreListingVersions is not None
and len(listing.StoreListingVersions) > 0
else None
}
)
logger.debug(f"Created store listing for agent {agent_id}")
@@ -530,7 +521,6 @@ async def create_store_submission(
status=prisma.enums.SubmissionStatus.PENDING,
runs=0,
rating=0.0,
store_listing_version_id=store_listing_version_id,
)
except (
@@ -819,17 +809,15 @@ async def get_agent(
detail=f"Store listing version {store_listing_version_id} not found",
)
graph_id = store_listing_version.agentId
graph_version = store_listing_version.agentVersion
graph = await backend.data.graph.get_graph(graph_id, graph_version)
agent = store_listing_version.Agent
graph = await backend.data.graph.get_graph(
agent.id, agent.version, template=True
)
if not graph:
raise fastapi.HTTPException(
status_code=404,
detail=(
f"Agent #{graph_id} not found "
f"for store listing version #{store_listing_version_id}"
),
status_code=404, detail=f"Agent {agent.id} not found"
)
graph.version = 1
@@ -844,68 +832,3 @@ async def get_agent(
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent"
) from e
async def review_store_submission(
store_listing_version_id: str, is_approved: bool, comments: str, reviewer_id: str
) -> prisma.models.StoreListingSubmission:
"""Review a store listing submission."""
try:
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id},
include={"StoreListing": True},
)
)
if not store_listing_version or not store_listing_version.StoreListing:
raise fastapi.HTTPException(
status_code=404,
detail=f"Store listing version {store_listing_version_id} not found",
)
if is_approved:
await prisma.models.StoreListing.prisma().update(
where={"id": store_listing_version.StoreListing.id},
data={"isApproved": True},
)
submission_status = (
prisma.enums.SubmissionStatus.APPROVED
if is_approved
else prisma.enums.SubmissionStatus.REJECTED
)
update_data: prisma.types.StoreListingSubmissionUpdateInput = {
"Status": submission_status,
"reviewComments": comments,
"Reviewer": {"connect": {"id": reviewer_id}},
"StoreListing": {"connect": {"id": store_listing_version.StoreListing.id}},
}
create_data: prisma.types.StoreListingSubmissionCreateInput = {
**update_data,
"StoreListingVersion": {"connect": {"id": store_listing_version_id}},
}
submission = await prisma.models.StoreListingSubmission.prisma().upsert(
where={"storeListingVersionId": store_listing_version_id},
data={
"create": create_data,
"update": update_data,
},
)
if not submission:
raise fastapi.HTTPException( # FIXME: don't return HTTP exceptions here
status_code=404,
detail=f"Store listing submission {store_listing_version_id} not found",
)
return submission
except Exception as e:
logger.error(f"Could not create store submission review: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store submission review"
) from e

View File

@@ -115,7 +115,6 @@ class StoreSubmission(pydantic.BaseModel):
status: prisma.enums.SubmissionStatus
runs: int
rating: float
store_listing_version_id: str | None = None
class StoreSubmissionsResponse(pydantic.BaseModel):
@@ -152,9 +151,3 @@ class StoreReviewCreate(pydantic.BaseModel):
store_listing_version_id: str
score: int
comments: str | None = None
class ReviewSubmissionRequest(pydantic.BaseModel):
store_listing_version_id: str
is_approved: bool
comments: str

View File

@@ -642,30 +642,3 @@ async def download_agent_file(
return fastapi.responses.FileResponse(
tmp_file.name, filename=file_name, media_type="application/json"
)
@router.post(
"/submissions/review/{store_listing_version_id}",
tags=["store", "private"],
)
async def review_submission(
request: backend.server.v2.store.model.ReviewSubmissionRequest,
user: typing.Annotated[
autogpt_libs.auth.models.User,
fastapi.Depends(autogpt_libs.auth.depends.requires_admin_user),
],
):
# Proceed with the review submission logic
try:
submission = await backend.server.v2.store.db.review_store_submission(
store_listing_version_id=request.store_listing_version_id,
is_approved=request.is_approved,
comments=request.comments,
reviewer_id=user.user_id,
)
return submission
except Exception:
raise fastapi.HTTPException(
status_code=500,
detail="An error occurred while creating the store submission review",
)

View File

@@ -8,19 +8,12 @@ from backend.data.user import get_or_create_user
from backend.util.test import SpinTestServer, wait_execution
async def create_test_user(alt_user: bool = False) -> User:
if alt_user:
test_user_data = {
"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1b",
"email": "testuser2@example.com",
"name": "Test User 2",
}
else:
test_user_data = {
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
"email": "testuser@example.com",
"name": "Test User",
}
async def create_test_user() -> User:
test_user_data = {
"sub": "ef3b97d7-1161-4eb4-92b2-10c24fb154c1",
"email": "testuser#example.com",
"name": "Test User",
}
user = await get_or_create_user(test_user_data)
return user

View File

@@ -1,143 +0,0 @@
import base64
import mimetypes
import re
import shutil
import tempfile
import uuid
from pathlib import Path
from urllib.parse import urlparse
# This "requests" presumably has additional checks against internal networks for SSRF.
from backend.util.request import requests
TEMP_DIR = Path(tempfile.gettempdir()).resolve()
def get_exec_file_path(graph_exec_id: str, path: str) -> str:
"""
Utility to build an absolute path in the {temp}/exec_file/{exec_id}/... folder.
"""
return str(TEMP_DIR / "exec_file" / graph_exec_id / path)
def clean_exec_files(graph_exec_id: str, file: str = "") -> None:
"""
Utility to remove the {temp}/exec_file/{exec_id} folder and its contents.
"""
exec_path = Path(get_exec_file_path(graph_exec_id, file))
if exec_path.exists() and exec_path.is_dir():
shutil.rmtree(exec_path)
"""
MediaFile is a string that represents a file. It can be one of the following:
- Data URI: base64 encoded media file. See https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data/
- URL: Media file hosted on the internet, it starts with http:// or https://.
- Local path (anything else): A temporary file path living within graph execution time.
Note: Replace this type alias into a proper class, when more information is needed.
"""
MediaFile = str
def store_media_file(
graph_exec_id: str, file: MediaFile, return_content: bool = False
) -> MediaFile:
"""
Safely handle 'file' (a data URI, a URL, or a local path relative to {temp}/exec_file/{exec_id}),
placing or verifying it under:
{tempdir}/exec_file/{exec_id}/...
If 'return_content=True', return a data URI (data:<mime>;base64,<content>).
Otherwise, returns the file media path relative to the exec_id folder.
For each MediaFile type:
- Data URI:
-> decode and store in a new random file in that folder
- URL:
-> download and store in that folder
- Local path:
-> interpret as relative to that folder; verify it exists
(no copying, as it's presumably already there).
We realpath-check so no symlink or '..' can escape the folder.
:param graph_exec_id: The unique ID of the graph execution.
:param file: Data URI, URL, or local (relative) path.
:param return_content: If True, return a data URI of the file content.
If False, return the *relative* path inside the exec_id folder.
:return: The requested result: data URI or relative path of the media.
"""
# Build base path
base_path = Path(get_exec_file_path(graph_exec_id, ""))
base_path.mkdir(parents=True, exist_ok=True)
# Helper functions
def _extension_from_mime(mime: str) -> str:
ext = mimetypes.guess_extension(mime, strict=False)
return ext if ext else ".bin"
def _file_to_data_uri(path: Path) -> str:
mime_type, _ = mimetypes.guess_type(path)
mime_type = mime_type or "application/octet-stream"
b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
return f"data:{mime_type};base64,{b64}"
def _ensure_inside_base(path_candidate: Path, base: Path) -> Path:
"""
Resolve symlinks via resolve() and ensure the result is still under base.
"""
real_candidate = path_candidate.resolve()
real_base = base.resolve()
if not real_candidate.is_relative_to(real_base):
raise ValueError(
"Local file path is outside the temp_base directory. Access denied."
)
return real_candidate
def _strip_base_prefix(absolute_path: Path, base: Path) -> str:
"""
Strip base prefix and normalize path.
"""
return str(absolute_path.relative_to(base))
# Process file
if file.startswith("data:"):
# Data URI
match = re.match(r"^data:([^;]+);base64,(.*)$", file, re.DOTALL)
if not match:
raise ValueError(
"Invalid data URI format. Expected data:<mime>;base64,<data>"
)
mime_type = match.group(1).strip().lower()
b64_content = match.group(2).strip()
# Generate filename and decode
extension = _extension_from_mime(mime_type)
filename = f"{uuid.uuid4()}{extension}"
target_path = _ensure_inside_base(base_path / filename, base_path)
target_path.write_bytes(base64.b64decode(b64_content))
elif file.startswith(("http://", "https://")):
# URL
parsed_url = urlparse(file)
filename = Path(parsed_url.path).name or f"{uuid.uuid4()}"
target_path = _ensure_inside_base(base_path / filename, base_path)
# Download and save
resp = requests.get(file)
resp.raise_for_status()
target_path.write_bytes(resp.content)
else:
# Local path
target_path = _ensure_inside_base(base_path / file, base_path)
if not target_path.is_file():
raise ValueError(f"Local file does not exist: {target_path}")
# Return result
if return_content:
return MediaFile(_file_to_data_uri(target_path))
else:
return MediaFile(_strip_base_prefix(target_path, base_path))

View File

@@ -62,10 +62,7 @@ def expose(func: C) -> C:
return func(*args, **kwargs)
except Exception as e:
msg = f"Error in {func.__name__}: {e.__str__()}"
if isinstance(e, ValueError):
logger.warning(msg)
else:
logger.exception(msg)
logger.exception(msg)
raise
register_pydantic_serializers(func)

View File

@@ -81,14 +81,10 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
default=True,
description="If authentication is enabled or not",
)
enable_credit: bool = Field(
default=False,
enable_credit: str = Field(
default="false",
description="If user credit system is enabled or not",
)
enable_beta_monthly_credit: bool = Field(
default=True,
description="If beta monthly credits accounting is enabled or not",
)
num_user_credits_refill: int = Field(
default=1500,
description="Number of credits to refill for each user",
@@ -157,11 +153,6 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
description="The name of the Google Cloud Storage bucket for media files",
)
reddit_user_agent: str = Field(
default="AutoGPT:1.0 (by /u/autogpt)",
description="The user agent for the Reddit API",
)
scheduler_db_pool_size: int = Field(
default=3,
description="The pool size for the scheduler database connection pool",
@@ -285,6 +276,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
reddit_client_id: str = Field(default="", description="Reddit client ID")
reddit_client_secret: str = Field(default="", description="Reddit client secret")
reddit_username: str = Field(default="", description="Reddit username")
reddit_password: str = Field(default="", description="Reddit password")
openweathermap_api_key: str = Field(
default="", description="OpenWeatherMap API key"
@@ -315,18 +308,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
exa_api_key: str = Field(default="", description="Exa API key")
e2b_api_key: str = Field(default="", description="E2B API key")
nvidia_api_key: str = Field(default="", description="Nvidia API key")
mem0_api_key: str = Field(default="", description="Mem0 API key")
linear_client_id: str = Field(default="", description="Linear client ID")
linear_client_secret: str = Field(default="", description="Linear client secret")
todoist_client_id: str = Field(default="", description="Todoist client ID")
todoist_client_secret: str = Field(default="", description="Todoist client secret")
stripe_api_key: str = Field(default="", description="Stripe API Key")
stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret")
screenshotone_api_key: str = Field(default="", description="ScreenshotOne API Key")
# Add more secret fields as needed

View File

@@ -1,6 +1,5 @@
import logging
import time
import uuid
from typing import Sequence, cast
from backend.data import db
@@ -105,13 +104,7 @@ def execute_block_test(block: Block):
log.info(f"{prefix} mock {mock_name} not found in block")
# Populate credentials argument(s)
extra_exec_kwargs: dict = {
"graph_id": str(uuid.uuid4()),
"node_id": str(uuid.uuid4()),
"graph_exec_id": str(uuid.uuid4()),
"node_exec_id": str(uuid.uuid4()),
"user_id": str(uuid.uuid4()),
}
extra_exec_kwargs = {}
input_model = cast(type[BlockSchema], block.input_schema)
credentials_input_fields = input_model.get_credentials_fields()
if len(credentials_input_fields) == 1 and isinstance(
@@ -132,9 +125,7 @@ def execute_block_test(block: Block):
for output_name, output_data in block.execute(input_data, **extra_exec_kwargs):
if output_index >= len(block.test_output):
raise ValueError(
f"{prefix} produced output more than expected {output_index} >= {len(block.test_output)}:\nOutput Expected:\t\t{block.test_output}\nFailed Output Produced:\t('{output_name}', {output_data})\nNote that this may not be the one that was unexpected, but it is the first that triggered the extra output warning"
)
raise ValueError(f"{prefix} produced output more than expected")
ex_output_name, ex_output_data = block.test_output[output_index]
def compare(data, expected_data):
@@ -151,9 +142,7 @@ def execute_block_test(block: Block):
log.info(f"{prefix} {mark} comparing `{data}` vs `{expected_data}`")
if not is_matching:
raise ValueError(
f"{prefix}: wrong output {data} vs {expected_data}\n"
f"Output Expected:\t\t{block.test_output}\n"
f"Failed Output Produced:\t('{output_name}', {output_data})"
f"{prefix}: wrong output {data} vs {expected_data}"
)
compare(output_data, ex_output_data)

View File

@@ -1,29 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[agentId]` on the table `StoreListing` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "StoreListing_agentId_idx";
-- DropIndex
DROP INDEX "StoreListing_isApproved_idx";
-- DropIndex
DROP INDEX "StoreListingVersion_agentId_agentVersion_isApproved_idx";
-- CreateIndex
CREATE INDEX "StoreListing_agentId_owningUserId_idx" ON "StoreListing"("agentId", "owningUserId");
-- CreateIndex
CREATE INDEX "StoreListing_isDeleted_isApproved_idx" ON "StoreListing"("isDeleted", "isApproved");
-- CreateIndex
CREATE INDEX "StoreListing_isDeleted_idx" ON "StoreListing"("isDeleted");
-- CreateIndex
CREATE UNIQUE INDEX "StoreListing_agentId_key" ON "StoreListing"("agentId");
-- CreateIndex
CREATE INDEX "StoreListingVersion_agentId_agentVersion_isDeleted_idx" ON "StoreListingVersion"("agentId", "agentVersion", "isDeleted");

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "CreditTransaction" ADD COLUMN "runningBalance" INTEGER;

View File

@@ -1,35 +0,0 @@
/*
Warnings:
- You are about to drop the column `blockId` on the `CreditTransaction` table. All the data in the column will be moved to metadata->block_id.
*/
BEGIN;
-- DropForeignKey blockId
ALTER TABLE "CreditTransaction" DROP CONSTRAINT "CreditTransaction_blockId_fkey";
-- Update migrate blockId into metadata->"block_id"
UPDATE "CreditTransaction"
SET "metadata" = jsonb_set(
COALESCE("metadata"::jsonb, '{}'),
'{block_id}',
to_jsonb("blockId")
)
WHERE "blockId" IS NOT NULL;
-- AlterTable drop blockId
ALTER TABLE "CreditTransaction" DROP COLUMN "blockId";
COMMIT;
/*
These indices dropped below were part of the cleanup during the schema change applied above.
These indexes were not useful and will not impact anything upon their removal.
*/
-- DropIndex
DROP INDEX "StoreListingReview_storeListingVersionId_idx";
-- DropIndex
DROP INDEX "StoreListingSubmission_Status_idx";

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "topUpConfig" JSONB;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[storeListingVersionId]` on the table `StoreListingSubmission` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "StoreListingSubmission_storeListingVersionId_key" ON "StoreListingSubmission"("storeListingVersionId");

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ packages = [{ include = "backend", format = "sdist" }]
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
aio-pika = "^9.5.4"
anthropic = "^0.45.2"
anthropic = "^0.40.0"
apscheduler = "^3.11.0"
autogpt-libs = { path = "../autogpt_libs", develop = true }
click = "^8.1.7"
@@ -19,14 +19,14 @@ e2b-code-interpreter = "^1.0.1"
fastapi = "^0.115.5"
feedparser = "^6.0.11"
flake8 = "^7.0.0"
google-api-python-client = "^2.160.0"
google-api-python-client = "^2.154.0"
google-auth-oauthlib = "^1.2.1"
groq = "^0.15.0"
groq = "^0.13.1"
jinja2 = "^3.1.4"
jsonref = "^1.1.0"
jsonschema = "^4.22.0"
ollama = "^0.4.1"
openai = "^1.60.2"
openai = "^1.57.4"
praw = "~7.8.1"
prisma = "^0.15.0"
psutil = "^6.1.0"
@@ -34,13 +34,12 @@ pydantic = "^2.9.2"
pydantic-settings = "^2.3.4"
pyro5 = "^5.15"
pytest = "^8.2.1"
pytest-asyncio = "^0.25.3"
pytest-asyncio = "^0.25.0"
python-dotenv = "^1.0.1"
redis = "^5.2.0"
sentry-sdk = "2.20.0"
sentry-sdk = "2.19.2"
strenum = "^0.4.9"
stripe = "^11.5.0"
supabase = "2.12.0"
supabase = "2.11.0"
tenacity = "^9.0.0"
tweepy = "^4.14.0"
uvicorn = { extras = ["standard"], version = "^0.34.0" }
@@ -55,22 +54,19 @@ sqlalchemy = "^2.0.36"
psycopg2-binary = "^2.9.10"
google-cloud-storage = "^2.18.2"
launchdarkly-server-sdk = "^9.8.0"
mem0ai = "^0.1.48"
todoist-api-python = "^2.1.7"
moviepy = "^2.1.2"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.32.1"
poethepoet = "^0.31.0"
httpx = "^0.27.0"
pytest-watcher = "^0.4.2"
requests = "^2.32.3"
ruff = "^0.9.2"
pyright = "^1.1.392"
ruff = "^0.8.0"
pyright = "^1.1.389"
isort = "^5.13.2"
black = "^24.10.0"
aiohappyeyeballs = "^2.4.4"
aiohappyeyeballs = "^2.4.3"
pytest-mock = "^3.14.0"
faker = "^33.3.1"
faker = "^33.1.0"
[build-system]
requires = ["poetry-core"]

View File

@@ -21,7 +21,6 @@ model User {
metadata Json @default("{}")
integrations String @default("")
stripeCustomerId String?
topUpConfig Json?
// Relations
AgentGraphs AgentGraph[]
@@ -33,12 +32,12 @@ model User {
AgentPreset AgentPreset[]
UserAgent UserAgent[]
Profile Profile[]
StoreListing StoreListing[]
StoreListingReview StoreListingReview[]
StoreListingSubmission StoreListingSubmission[]
APIKeys APIKey[]
IntegrationWebhooks IntegrationWebhook[]
Profile Profile[]
StoreListing StoreListing[]
StoreListingReview StoreListingReview[]
StoreListingSubmission StoreListingSubmission[]
APIKeys APIKey[]
IntegrationWebhooks IntegrationWebhook[]
@@index([id])
@@index([email])
@@ -65,23 +64,23 @@ model AgentGraph {
AgentNodes AgentNode[]
AgentGraphExecution AgentGraphExecution[]
AgentPreset AgentPreset[]
UserAgent UserAgent[]
StoreListing StoreListing[]
StoreListingVersion StoreListingVersion?
AgentPreset AgentPreset[]
UserAgent UserAgent[]
StoreListing StoreListing[]
StoreListingVersion StoreListingVersion?
@@id(name: "graphVersionId", [id, version])
@@index([userId, isActive])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////// USER SPECIFIC DATA ////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// An AgentPrest is an Agent + User Configuration of that agent.
// For example, if someone has created a weather agent and they want to set it up to
// For example, if someone has created a weather agent and they want to set it up to
// Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to
// monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset
model AgentPreset {
@@ -103,9 +102,9 @@ model AgentPreset {
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
UserAgents UserAgent[]
AgentExecution AgentGraphExecution[]
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
UserAgents UserAgent[]
AgentExecution AgentGraphExecution[]
@@index([userId])
}
@@ -135,11 +134,11 @@ model UserAgent {
@@index([userId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// AGENT DEFINITION AND EXECUTION TABLES ////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// This model describes a single node in the Agent Graph/Flow (Multi Agent System).
model AgentNode {
@@ -208,6 +207,7 @@ model AgentBlock {
// Prisma requires explicit back-references.
ReferencedByAgentNode AgentNode[]
CreditTransaction CreditTransaction[]
}
// This model describes the status of an AgentGraphExecution or AgentNodeExecution.
@@ -235,7 +235,7 @@ model AgentGraphExecution {
AgentNodeExecutions AgentNodeExecution[]
// Link to User model -- Executed by this user
// Link to User model
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -345,11 +345,11 @@ model AnalyticsDetails {
@@index([type])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// METRICS TRACKING TABLES ////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model AnalyticsMetrics {
id String @id @default(uuid())
createdAt DateTime @default(now())
@@ -375,11 +375,11 @@ enum CreditTransactionType {
USAGE
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// ACCOUNTING AND CREDIT SYSTEM TABLES //////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model CreditTransaction {
transactionKey String @default(uuid())
createdAt DateTime @default(now())
@@ -387,11 +387,12 @@ model CreditTransaction {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
blockId String?
block AgentBlock? @relation(fields: [blockId], references: [id])
amount Int
type CreditTransactionType
runningBalance Int?
isActive Boolean @default(true)
metadata Json?
@@ -399,11 +400,11 @@ model CreditTransaction {
@@index([userId, createdAt])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// Store TABLES ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model Profile {
id String @id @default(uuid())
@@ -411,7 +412,7 @@ model Profile {
updatedAt DateTime @default(now()) @updatedAt
// Only 1 of user or group can be set.
// The user this profile belongs to, if any.
// The user this profile belongs to, if any.
userId String?
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -442,8 +443,6 @@ view Creator {
agent_rating Float
agent_runs Int
is_featured Boolean
// Index or unique are not applied to views
}
view StoreAgent {
@@ -466,7 +465,11 @@ view StoreAgent {
rating Float
versions String[]
// Index or unique are not applied to views
@@unique([creator_username, slug])
@@index([creator_username])
@@index([featured])
@@index([categories])
@@index([storeListingVersionId])
}
view StoreSubmission {
@@ -484,7 +487,7 @@ view StoreSubmission {
agent_id String
agent_version Int
// Index or unique are not applied to views
@@index([user_id])
}
model StoreListing {
@@ -507,13 +510,9 @@ model StoreListing {
StoreListingVersions StoreListingVersion[]
StoreListingSubmission StoreListingSubmission[]
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
@@unique([agentId])
@@index([agentId, owningUserId])
@@index([isApproved])
@@index([agentId])
@@index([owningUserId])
// Used in the view query
@@index([isDeleted, isApproved])
@@index([isDeleted])
}
model StoreListingVersion {
@@ -527,7 +526,7 @@ model StoreListingVersion {
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
// The details for this version of the agent, this allows the author to update the details of the agent,
// The detials for this version of the agent, this allows the author to update the details of the agent,
// But still allow using old versions of the agent with there original details.
// TODO: Create a database view that shows only the latest version of each store listing.
slug String
@@ -554,7 +553,7 @@ model StoreListingVersion {
StoreListingReview StoreListingReview[]
@@unique([agentId, agentVersion])
@@index([agentId, agentVersion, isDeleted])
@@index([agentId, agentVersion, isApproved])
}
model StoreListingReview {
@@ -572,6 +571,7 @@ model StoreListingReview {
comments String?
@@unique([storeListingVersionId, reviewByUserId])
@@index([storeListingVersionId])
}
enum SubmissionStatus {
@@ -598,8 +598,8 @@ model StoreListingSubmission {
Status SubmissionStatus @default(PENDING)
reviewComments String?
@@unique([storeListingVersionId])
@@index([storeListingId])
@@index([Status])
}
enum APIKeyPermission {

View File

@@ -1,119 +1,98 @@
from datetime import datetime, timezone
from datetime import datetime
import pytest
from prisma.enums import CreditTransactionType
from prisma.models import CreditTransaction
from backend.blocks.llm import AITextGeneratorBlock
from backend.data.credit import BetaUserCredit
from backend.data.execution import NodeExecutionEntry
from backend.data.credit import UserCredit
from backend.data.user import DEFAULT_USER_ID
from backend.integrations.credentials_store import openai_credentials
from backend.util.test import SpinTestServer
REFILL_VALUE = 1000
user_credit = BetaUserCredit(REFILL_VALUE)
async def disable_test_user_transactions():
await CreditTransaction.prisma().delete_many(where={"userId": DEFAULT_USER_ID})
async def top_up(amount: int):
await user_credit._add_transaction(
DEFAULT_USER_ID,
amount,
CreditTransactionType.TOP_UP,
)
user_credit = UserCredit(REFILL_VALUE)
@pytest.mark.asyncio(scope="session")
async def test_block_credit_usage(server: SpinTestServer):
await disable_test_user_transactions()
await top_up(100)
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)
current_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
spending_amount_1 = await user_credit.spend_credits(
NodeExecutionEntry(
user_id=DEFAULT_USER_ID,
graph_id="test_graph",
node_id="test_node",
graph_exec_id="test_graph_exec",
node_exec_id="test_node_exec",
block_id=AITextGeneratorBlock().id,
data={
"model": "gpt-4-turbo",
"credentials": {
"id": openai_credentials.id,
"provider": openai_credentials.provider,
"type": openai_credentials.type,
},
DEFAULT_USER_ID,
current_credit,
AITextGeneratorBlock().id,
{
"model": "gpt-4-turbo",
"credentials": {
"id": openai_credentials.id,
"provider": openai_credentials.provider,
"type": openai_credentials.type,
},
),
},
0.0,
0.0,
validate_balance=False,
)
assert spending_amount_1 > 0
spending_amount_2 = await user_credit.spend_credits(
NodeExecutionEntry(
user_id=DEFAULT_USER_ID,
graph_id="test_graph",
node_id="test_node",
graph_exec_id="test_graph_exec",
node_exec_id="test_node_exec",
block_id=AITextGeneratorBlock().id,
data={"model": "gpt-4-turbo", "api_key": "owned_api_key"},
),
DEFAULT_USER_ID,
current_credit,
AITextGeneratorBlock().id,
{"model": "gpt-4-turbo", "api_key": "owned_api_key"},
0.0,
0.0,
validate_balance=False,
)
assert spending_amount_2 == 0
new_credit = await user_credit.get_credits(DEFAULT_USER_ID)
new_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
assert new_credit == current_credit - spending_amount_1 - spending_amount_2
@pytest.mark.asyncio(scope="session")
async def test_block_credit_top_up(server: SpinTestServer):
await disable_test_user_transactions()
current_credit = await user_credit.get_credits(DEFAULT_USER_ID)
current_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
await top_up(100)
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
new_credit = await user_credit.get_credits(DEFAULT_USER_ID)
new_credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
assert new_credit == current_credit + 100
@pytest.mark.asyncio(scope="session")
async def test_block_credit_reset(server: SpinTestServer):
await disable_test_user_transactions()
month1 = 1
month2 = 2
month1 = datetime(2022, 1, 15)
month2 = datetime(2022, 2, 15)
# set the calendar to month 2 but use current time from now
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month2, day=1
)
month2credit = await user_credit.get_credits(DEFAULT_USER_ID)
user_credit.time_now = lambda: month2
month2credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
# Month 1 result should only affect month 1
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month1, day=1
)
month1credit = await user_credit.get_credits(DEFAULT_USER_ID)
await top_up(100)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month1credit + 100
user_credit.time_now = lambda: month1
month1credit = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
assert await user_credit.get_or_refill_credit(DEFAULT_USER_ID) == month1credit + 100
# Month 2 balance is unaffected
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(
month=month2, day=1
)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month2credit
user_credit.time_now = lambda: month2
assert await user_credit.get_or_refill_credit(DEFAULT_USER_ID) == month2credit
@pytest.mark.asyncio(scope="session")
async def test_credit_refill(server: SpinTestServer):
await disable_test_user_transactions()
balance = await user_credit.get_credits(DEFAULT_USER_ID)
# Clear all transactions within the month
await CreditTransaction.prisma().update_many(
where={
"userId": DEFAULT_USER_ID,
"createdAt": {
"gte": datetime(2022, 2, 1),
"lt": datetime(2022, 3, 1),
},
},
data={"isActive": False},
)
user_credit.time_now = lambda: datetime(2022, 2, 15)
balance = await user_credit.get_or_refill_credit(DEFAULT_USER_ID)
assert balance == REFILL_VALUE

View File

@@ -1,18 +1,14 @@
from typing import Any
from uuid import UUID
import autogpt_libs.auth.models
import fastapi.exceptions
import pytest
import backend.server.v2.store.model as store
from backend.blocks.basic import AgentInputBlock, AgentOutputBlock, StoreValueBlock
from backend.data.block import BlockSchema
from backend.data.graph import Graph, Link, Node
from backend.data.model import SchemaField
from backend.data.user import DEFAULT_USER_ID
from backend.server.model import CreateGraph
from backend.usecases.sample import create_test_user
from backend.util.test import SpinTestServer
@@ -206,92 +202,3 @@ async def test_clean_graph(server: SpinTestServer):
n for n in created_graph.nodes if n.block_id == AgentInputBlock().id
)
assert input_node.input_default["value"] == ""
@pytest.mark.asyncio(scope="session")
async def test_access_store_listing_graph(server: SpinTestServer):
"""
Test the access of a store listing graph.
"""
graph = Graph(
id="test_clean_graph",
name="Test Clean Graph",
description="Test graph cleaning",
nodes=[
Node(
id="input_node",
block_id=AgentInputBlock().id,
input_default={
"name": "test_input",
"value": "test value",
"description": "Test input description",
},
),
],
links=[],
)
# Create graph and get model
create_graph = CreateGraph(graph=graph)
created_graph = await server.agent_server.test_create_graph(
create_graph, DEFAULT_USER_ID
)
store_submission_request = store.StoreSubmissionRequest(
agent_id=created_graph.id,
agent_version=created_graph.version,
slug="test-slug",
name="Test name",
sub_heading="Test sub heading",
video_url=None,
image_urls=[],
description="Test description",
categories=[],
)
# First we check the graph an not be accessed by a different user
with pytest.raises(fastapi.exceptions.HTTPException) as exc_info:
await server.agent_server.test_get_graph(
created_graph.id,
created_graph.version,
"3e53486c-cf57-477e-ba2a-cb02dc828e1b",
)
assert exc_info.value.status_code == 404
assert "Graph" in str(exc_info.value.detail)
# Now we create a store listing
store_listing = await server.agent_server.test_create_store_listing(
store_submission_request, DEFAULT_USER_ID
)
if isinstance(store_listing, fastapi.responses.JSONResponse):
assert False, "Failed to create store listing"
slv_id = (
store_listing.store_listing_version_id
if store_listing.store_listing_version_id is not None
else None
)
assert slv_id is not None
admin_user = await create_test_user(alt_user=True)
await server.agent_server.test_review_store_listing(
store.ReviewSubmissionRequest(
store_listing_version_id=slv_id,
is_approved=True,
comments="Test comments",
),
autogpt_libs.auth.models.User(
user_id=admin_user.id,
role="admin",
email=admin_user.email,
phone_number="1234567890",
),
)
# Now we check the graph can be accessed by a user that does not own the graph
got_graph = await server.agent_server.test_get_graph(
created_graph.id, created_graph.version, "3e53486c-cf57-477e-ba2a-cb02dc828e1b"
)
assert got_graph is not None

View File

@@ -1,11 +1,8 @@
import logging
import autogpt_libs.auth.models
import fastapi.responses
import pytest
from prisma.models import User
import backend.server.v2.store.model
from backend.blocks.basic import FindInDictionaryBlock, StoreValueBlock
from backend.blocks.maths import CalculatorBlock, Operation
from backend.data import execution, graph
@@ -34,19 +31,16 @@ async def execute_graph(
# --- Test adding new executions --- #
response = await agent_server.test_execute_graph(
user_id=test_user.id,
graph_id=test_graph.id,
graph_version=test_graph.version,
node_input=input_data,
test_graph.id, input_data, test_user.id
)
graph_exec_id = response["id"]
logger.info(f"Created execution with ID: {graph_exec_id}")
# Execution queue should be empty
logger.info("Waiting for execution to complete...")
result = await wait_execution(test_user.id, test_graph.id, graph_exec_id, 30)
result = await wait_execution(test_user.id, test_graph.id, graph_exec_id)
logger.info(f"Execution completed with {len(result)} results")
assert len(result) == num_execs
assert result and len(result) == num_execs
return graph_exec_id
@@ -131,7 +125,7 @@ async def test_agent_execution(server: SpinTestServer):
logger.info("Starting test_agent_execution")
test_user = await create_test_user()
test_graph = await create_graph(server, create_test_graph(), test_user)
data = {"node_input": {"input_1": "Hello", "input_2": "World"}}
data = {"input_1": "Hello", "input_2": "World"}
graph_exec_id = await execute_graph(
server.agent_server,
test_graph,
@@ -293,67 +287,3 @@ async def test_static_input_link_on_graph(server: SpinTestServer):
assert exec_data.status == execution.ExecutionStatus.COMPLETED
assert exec_data.output_data == {"result": [9]}
logger.info("Completed test_static_input_link_on_graph")
@pytest.mark.asyncio(scope="session")
async def test_store_listing_graph(server: SpinTestServer):
logger.info("Starting test_agent_execution")
test_user = await create_test_user()
test_graph = await create_graph(server, create_test_graph(), test_user)
store_submission_request = backend.server.v2.store.model.StoreSubmissionRequest(
agent_id=test_graph.id,
agent_version=test_graph.version,
slug="test-slug",
name="Test name",
sub_heading="Test sub heading",
video_url=None,
image_urls=[],
description="Test description",
categories=[],
)
store_listing = await server.agent_server.test_create_store_listing(
store_submission_request, test_user.id
)
if isinstance(store_listing, fastapi.responses.JSONResponse):
assert False, "Failed to create store listing"
slv_id = (
store_listing.store_listing_version_id
if store_listing.store_listing_version_id is not None
else None
)
assert slv_id is not None
admin_user = await create_test_user(alt_user=True)
await server.agent_server.test_review_store_listing(
backend.server.v2.store.model.ReviewSubmissionRequest(
store_listing_version_id=slv_id,
is_approved=True,
comments="Test comments",
),
autogpt_libs.auth.models.User(
user_id=admin_user.id,
role="admin",
email=admin_user.email,
phone_number="1234567890",
),
)
alt_test_user = admin_user
data = {"node_input": {"input_1": "Hello", "input_2": "World"}}
graph_exec_id = await execute_graph(
server.agent_server,
test_graph,
alt_test_user,
data,
4,
)
await assert_sample_graph_executions(
server.agent_server, test_graph, alt_test_user, graph_exec_id
)
logger.info("Completed test_agent_execution")

View File

@@ -298,6 +298,7 @@ async def main():
data={
"transactionKey": str(faker.uuid4()),
"userId": user.id,
"blockId": block.id,
"amount": random.randint(1, 100),
"type": (
prisma.enums.CreditTransactionType.TOP_UP

View File

@@ -5,7 +5,6 @@ NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
NEXT_PUBLIC_APP_ENV=dev
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
## Locale settings

View File

@@ -23,33 +23,32 @@
"defaults"
],
"dependencies": {
"@faker-js/faker": "^9.4.0",
"@faker-js/faker": "^9.3.0",
"@hookform/resolvers": "^3.10.0",
"@next/third-parties": "^15.1.6",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@next/third-parties": "^15.1.3",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.5",
"@radix-ui/react-context-menu": "^2.2.4",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@sentry/nextjs": "^8",
"@stripe/stripe-js": "^5.5.0",
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.48.1",
"@supabase/supabase-js": "^2.47.8",
"@tanstack/react-table": "^8.20.6",
"@xyflow/react": "^12.4.2",
"@xyflow/react": "^12.3.6",
"ajv": "^8.17.1",
"boring-avatars": "^1.11.2",
"class-variance-authority": "^0.7.1",
@@ -59,58 +58,58 @@
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"elliptic": "6.6.1",
"embla-carousel-react": "^8.5.2",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.16.0",
"geist": "^1.3.1",
"launchdarkly-react-client-sdk": "^3.6.0",
"lucide-react": "^0.474.0",
"lucide-react": "^0.469.0",
"moment": "^2.30.1",
"next": "^14.2.21",
"next": "^14.2.13",
"next-themes": "^0.4.4",
"react": "^18",
"react-day-picker": "^9.5.1",
"react-day-picker": "^9.5.0",
"react-dom": "^18",
"react-hook-form": "^7.54.2",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.3",
"react-modal": "^3.16.3",
"react-shepherd": "^6.1.7",
"react-shepherd": "^6.1.6",
"recharts": "^2.14.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.5",
"uuid": "^11.0.4",
"zod": "^3.23.8"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.4",
"@playwright/test": "^1.50.0",
"@storybook/addon-a11y": "^8.5.2",
"@storybook/addon-essentials": "^8.5.2",
"@storybook/addon-interactions": "^8.5.2",
"@storybook/addon-links": "^8.5.2",
"@storybook/addon-onboarding": "^8.5.2",
"@storybook/blocks": "^8.5.2",
"@storybook/nextjs": "^8.5.2",
"@chromatic-com/storybook": "^3.2.3",
"@playwright/test": "^1.48.2",
"@storybook/addon-a11y": "^8.3.5",
"@storybook/addon-essentials": "^8.4.2",
"@storybook/addon-interactions": "^8.4.2",
"@storybook/addon-links": "^8.4.2",
"@storybook/addon-onboarding": "^8.4.2",
"@storybook/blocks": "^8.4.2",
"@storybook/nextjs": "^8.4.2",
"@storybook/react": "^8.3.5",
"@storybook/test": "^8.3.5",
"@storybook/test-runner": "^0.21.0",
"@types/negotiator": "^0.6.3",
"@types/node": "^22.10.10",
"@types/node": "^22.10.5",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-modal": "^3.16.3",
"axe-playwright": "^2.0.3",
"chromatic": "^11.25.1",
"chromatic": "^11.22.0",
"concurrently": "^9.1.2",
"eslint": "^8",
"eslint-config-next": "15.1.6",
"eslint-config-next": "15.1.3",
"eslint-plugin-storybook": "^0.11.2",
"msw": "^2.7.0",
"msw-storybook-addon": "^2.0.3",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.5.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"storybook": "^8.4.5",
"tailwindcss": "^3.4.17",
"typescript": "^5"
},

View File

@@ -1,18 +1,18 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
// import { getReviewableAgents } from "@/components/admin/marketplace/actions";
// import AdminMarketplaceAgentList from "@/components/admin/marketplace/AdminMarketplaceAgentList";
// import AdminFeaturedAgentsControl from "@/components/admin/marketplace/AdminFeaturedAgentsControl";
import { getReviewableAgents } from "@/components/admin/marketplace/actions";
import AdminMarketplaceAgentList from "@/components/admin/marketplace/AdminMarketplaceAgentList";
import AdminFeaturedAgentsControl from "@/components/admin/marketplace/AdminFeaturedAgentsControl";
import { Separator } from "@/components/ui/separator";
async function AdminMarketplace() {
// const reviewableAgents = await getReviewableAgents();
const reviewableAgents = await getReviewableAgents();
return (
<>
{/* <AdminMarketplaceAgentList agents={reviewableAgents.items} />
<Separator className="my-4" />
<AdminFeaturedAgentsControl className="mt-4" /> */}
<AdminMarketplaceAgentList agents={reviewableAgents.items} />
<Separator className="my-4" />
<AdminFeaturedAgentsControl className="mt-4" />
</>
);
}

View File

@@ -44,12 +44,12 @@ export default async function RootLayout({
// enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
<div className="flex min-h-screen flex-col items-center justify-center">
<Navbar
links={[
{
name: "Marketplace",
href: "/marketplace",
href: "/store",
},
{
name: "Library",
@@ -66,7 +66,7 @@ export default async function RootLayout({
{
icon: IconType.Edit,
text: "Edit profile",
href: "/marketplace/profile",
href: "/store/profile",
},
],
},
@@ -75,7 +75,7 @@ export default async function RootLayout({
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/marketplace/dashboard",
href: "/store/dashboard",
},
{
icon: IconType.UploadCloud,
@@ -88,7 +88,7 @@ export default async function RootLayout({
{
icon: IconType.Settings,
text: "Settings",
href: "/marketplace/settings",
href: "/store/settings",
},
],
},
@@ -102,7 +102,7 @@ export default async function RootLayout({
},
]}
/>
<main className="w-full flex-grow">{children}</main>
<main className="flex-1">{children}</main>
<TallyPopupSimple />
</div>
<Toaster />

View File

@@ -93,7 +93,7 @@ export default function LoginPage() {
}
return (
<AuthCard className="mx-auto">
<AuthCard>
<AuthHeader>Login to your account</AuthHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onLogin)}>
@@ -104,12 +104,7 @@ export default function LoginPage() {
<FormItem className="mb-6">
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="m@example.com"
{...field}
type="email" // Explicitly specify email type
autoComplete="username" // Added for password managers
/>
<Input placeholder="m@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -130,10 +125,7 @@ export default function LoginPage() {
</Link>
</FormLabel>
<FormControl>
<PasswordInput
{...field}
autoComplete="current-password" // Added for password managers
/>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -1,284 +0,0 @@
"use client";
import { useEffect, useCallback } from "react";
import { Button } from "@/components/agptui/Button";
import useCredits from "@/hooks/useCredits";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useSearchParams, useRouter } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function CreditsPage() {
const api = useBackendAPI();
const {
requestTopUp,
autoTopUpConfig,
updateAutoTopUpConfig,
transactionHistory,
fetchTransactionHistory,
formatCredits,
} = useCredits();
const router = useRouter();
const searchParams = useSearchParams();
const topupStatus = searchParams.get("topup") as "success" | "cancel" | null;
const { toast } = useToast();
const toastOnFail = useCallback(
(action: string, fn: () => Promise<any>) => {
fn().catch((e) => {
toast({
title: `Unable to ${action}`,
description: e.message,
variant: "destructive",
duration: 10000,
});
});
},
[toast],
);
useEffect(() => {
if (api && topupStatus === "success") {
toastOnFail("fulfill checkout", () => api.fulfillCheckout());
}
}, [api, topupStatus, toastOnFail]);
const openBillingPortal = async () => {
toastOnFail("open billing portal", async () => {
const portal = await api.getUserPaymentPortalLink();
router.push(portal.url);
});
};
const submitTopUp = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const amount =
parseInt(new FormData(form).get("topUpAmount") as string) * 100;
toastOnFail("request top-up", () => requestTopUp(amount));
};
const submitAutoTopUpConfig = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const amount = parseInt(formData.get("topUpAmount") as string) * 100;
const threshold = parseInt(formData.get("threshold") as string) * 100;
toastOnFail("update auto top-up config", () =>
updateAutoTopUpConfig(amount, threshold).then(() => {
toast({ title: "Auto top-up config updated! 🎉" });
}),
);
};
return (
<div className="w-full min-w-[800px] px-4 sm:px-8">
<h1 className="mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]">
Credits
</h1>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
{/* Top-up Form */}
<div>
<h2 className="text-lg">Top-up Credits</h2>
<p className="mb-6 text-neutral-600 dark:text-neutral-400">
{topupStatus === "success" && (
<span className="text-green-500">
Your payment was successful. Your credits will be updated
shortly. You can click the refresh icon 🔄 in case it is not
updated.
</span>
)}
{topupStatus === "cancel" && (
<span className="text-red-500">
Payment failed. Your payment method has not been charged.
</span>
)}
</p>
<form onSubmit={submitTopUp} className="space-y-4">
<div>
<label
htmlFor="topUpAmount"
className="mb-1 block text-neutral-700"
>
Top-up amount (USD), minimum $5:
</label>
<input
type="number"
id="topUpAmount"
name="topUpAmount"
placeholder="Enter top-up amount"
min="5"
step="1"
defaultValue={5}
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
</div>
<Button type="submit" className="w-full">
Top-up
</Button>
</form>
{/* Auto Top-up Form */}
<form onSubmit={submitAutoTopUpConfig} className="mt-6 space-y-4">
<h3 className="text-lg font-medium">Automatic Refill Settings</h3>
<div>
<label
htmlFor="threshold"
className="mb-1 block text-neutral-700"
>
When my balance goes below this amount:
</label>
<input
type="number"
id="threshold"
name="threshold"
defaultValue={
autoTopUpConfig?.threshold
? autoTopUpConfig.threshold / 100
: ""
}
placeholder="Refill threshold, minimum $5"
min="5"
step="1"
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
</div>
<div>
<label
htmlFor="autoTopUpAmount"
className="mb-1 block text-neutral-700"
>
Automatically refill my balance with this amount:
</label>
<input
type="number"
id="autoTopUpAmount"
name="topUpAmount"
defaultValue={
autoTopUpConfig?.amount ? autoTopUpConfig.amount / 100 : ""
}
placeholder="Refill amount, minimum $5"
min="5"
step="1"
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
</div>
{autoTopUpConfig?.amount ? (
<>
<Button type="submit" className="w-full">
Save Changes
</Button>
<Button
className="w-full"
variant="destructive"
onClick={() =>
updateAutoTopUpConfig(0, 0).then(() => {
toast({ title: "Auto top-up config disabled! 🎉" });
})
}
>
Disable Auto-Refill
</Button>
</>
) : (
<Button type="submit" className="w-full">
Enable Auto-Refill
</Button>
)}
</form>
</div>
<div>
{/* Payment Portal */}
<h2 className="text-lg">Manage Your Payment Methods</h2>
<br />
<p className="text-neutral-600">
You can manage your cards and see your payment history in the
billing portal.
</p>
<br />
<Button
type="submit"
variant="default"
className="w-full"
onClick={() => openBillingPortal()}
>
Open Portal
</Button>
{/* Transaction History */}
<h2 className="mt-6 text-lg">Transaction History</h2>
<br />
<p className="text-neutral-600">
Running balance might not be ordered accurately when concurrent
executions are happening.
</p>
<br />
{transactionHistory.transactions.length === 0 && (
<p className="text-neutral-600">No transactions found.</p>
)}
<Table
className={
transactionHistory.transactions.length === 0 ? "hidden" : ""
}
>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Balance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactionHistory.transactions.map((transaction, i) => (
<TableRow key={i}>
<TableCell>
{new Date(transaction.transaction_time).toLocaleString()}
</TableCell>
<TableCell>{transaction.description}</TableCell>
{/* Make it green if it's positive, red if it's negative */}
<TableCell
className={
transaction.amount > 0 ? "text-green-500" : "text-red-500"
}
>
<b>{formatCredits(transaction.amount)}</b>
</TableCell>
<TableCell>{formatCredits(transaction.balance)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{transactionHistory.next_transaction_time && (
<Button
type="submit"
className="w-full"
onClick={() => fetchTransactionHistory()}
>
Load More
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,179 +1,7 @@
import * as React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import {
FeaturedSection,
FeaturedAgent,
} from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
} from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import {
FeaturedCreators,
FeaturedCreator,
} from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
import {
StoreAgentsResponse,
CreatorsResponse,
} from "@/lib/autogpt-server-api/types";
import BackendAPI from "@/lib/autogpt-server-api";
"use client";
async function getStoreData() {
try {
const api = new BackendAPI();
import { redirect } from "next/navigation";
// Add error handling and default values
let featuredAgents: StoreAgentsResponse = {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
let topAgents: StoreAgentsResponse = {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
let featuredCreators: CreatorsResponse = {
creators: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
try {
[featuredAgents, topAgents, featuredCreators] = await Promise.all([
api.getStoreAgents({ featured: true }),
api.getStoreAgents({ sorted_by: "runs" }),
api.getStoreCreators({ featured: true, sorted_by: "num_agents" }),
]);
} catch (error) {
console.error("Error fetching store data:", error);
}
return {
featuredAgents,
topAgents,
featuredCreators,
};
} catch (error) {
console.error("Error in getStoreData:", error);
return {
featuredAgents: {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
topAgents: {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
featuredCreators: {
creators: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
};
}
}
// FIX: Correct metadata
export const metadata: Metadata = {
title: "Marketplace - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
applicationName: "NextGen AutoGPT Store",
authors: [{ name: "AutoGPT Team" }],
keywords: [
"AI agents",
"automation",
"artificial intelligence",
"AutoGPT",
"marketplace",
],
robots: {
index: true,
follow: true,
},
openGraph: {
title: "Marketplace - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
type: "website",
siteName: "NextGen AutoGPT Store",
images: [
{
url: "/images/store-og.png",
width: 1200,
height: 630,
alt: "NextGen AutoGPT Store",
},
],
},
twitter: {
card: "summary_large_image",
title: "Marketplace - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
images: ["/images/store-twitter.png"],
},
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
};
export default async function Page({}: {}) {
// Get data server-side
const { featuredAgents, topAgents, featuredCreators } = await getStoreData();
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
<FeaturedSection
featuredAgents={featuredAgents.agents as FeaturedAgent[]}
/>
<Separator />
<AgentsSection
sectionTitle="Top Agents"
agents={topAgents.agents as Agent[]}
/>
<Separator />
<FeaturedCreators
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
/>
<Separator />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
</main>
</div>
);
export default function Page() {
redirect("/store");
}

View File

@@ -73,7 +73,7 @@ const Monitor = () => {
return (
<div
className="grid grid-cols-1 gap-4 p-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
data-testid="monitor-page"
>
<AgentFlowList

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