Merge branch 'dev' into swiftyos/open-2278-implement-agent-preset-functionality

This commit is contained in:
Reinier van der Leer
2025-02-12 00:21:08 +01:00
committed by GitHub
82 changed files with 3255 additions and 2689 deletions

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -6,6 +6,7 @@ version = "2.4.0"
description = "Happy Eyeballs for asyncio"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"},
{file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"},
@@ -17,6 +18,7 @@ version = "3.10.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"},
{file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"},
@@ -129,6 +131,7 @@ version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
@@ -143,6 +146,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -154,6 +158,7 @@ version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
@@ -176,10 +181,12 @@ version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""}
[[package]]
name = "attrs"
@@ -187,6 +194,7 @@ version = "24.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
@@ -206,6 +214,7 @@ version = "5.5.0"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
@@ -217,6 +226,7 @@ version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
@@ -228,6 +238,7 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -327,6 +338,7 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -338,6 +350,7 @@ version = "1.2.14"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
files = [
{file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
{file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
@@ -355,6 +368,7 @@ version = "2.1.0"
description = "A library to handle automated deprecations"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"},
{file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"},
@@ -369,6 +383,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -383,6 +399,7 @@ version = "1.2.2"
description = "Dictionary with auto-expiring values for caching purposes"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "expiringdict-1.2.2-py3-none-any.whl", hash = "sha256:09a5d20bc361163e6432a874edd3179676e935eb81b925eccef48d409a8a45e8"},
{file = "expiringdict-1.2.2.tar.gz", hash = "sha256:300fb92a7e98f15b05cf9a856c1415b3bc4f2e132be07daa326da6414c23ee09"},
@@ -397,6 +414,7 @@ version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
@@ -483,6 +501,7 @@ version = "2.19.2"
description = "Google API client core library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_core-2.19.2-py3-none-any.whl", hash = "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4"},
{file = "google_api_core-2.19.2.tar.gz", hash = "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f"},
@@ -514,6 +533,7 @@ version = "2.34.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65"},
{file = "google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc"},
@@ -537,6 +557,7 @@ version = "1.4.5"
description = "Google Cloud Appengine Logging API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_appengine_logging-1.4.5-py2.py3-none-any.whl", hash = "sha256:344e0244404049b42164e4d6dc718ca2c81b393d066956e7cb85fd9407ed9c48"},
{file = "google_cloud_appengine_logging-1.4.5.tar.gz", hash = "sha256:de7d766e5d67b19fc5833974b505b32d2a5bbdfb283fd941e320e7cfdae4cb83"},
@@ -554,6 +575,7 @@ version = "0.3.0"
description = "Google Cloud Audit Protos"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_audit_log-0.3.0-py2.py3-none-any.whl", hash = "sha256:8340793120a1d5aa143605def8704ecdcead15106f754ef1381ae3bab533722f"},
{file = "google_cloud_audit_log-0.3.0.tar.gz", hash = "sha256:901428b257020d8c1d1133e0fa004164a555e5a395c7ca3cdbb8486513df3a65"},
@@ -569,6 +591,7 @@ version = "2.4.1"
description = "Google Cloud API client core library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"},
{file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"},
@@ -583,13 +606,14 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"]
[[package]]
name = "google-cloud-logging"
version = "3.11.3"
version = "3.11.4"
description = "Stackdriver Logging API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_logging-3.11.3-py2.py3-none-any.whl", hash = "sha256:b8ec23f2998f76a58f8492db26a0f4151dd500425c3f08448586b85972f3c494"},
{file = "google_cloud_logging-3.11.3.tar.gz", hash = "sha256:0a73cd94118875387d4535371d9e9426861edef8e44fba1261e86782d5b8d54f"},
{file = "google_cloud_logging-3.11.4-py2.py3-none-any.whl", hash = "sha256:1d465ac62df29fb94bba4d6b4891035e57d573d84541dd8a40eebbc74422b2f0"},
{file = "google_cloud_logging-3.11.4.tar.gz", hash = "sha256:32305d989323f3c58603044e2ac5d9cf23e9465ede511bbe90b4309270d3195c"},
]
[package.dependencies]
@@ -601,7 +625,8 @@ google-cloud-core = ">=2.0.0,<3.0.0dev"
grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev"
opentelemetry-api = ">=1.9.0"
proto-plus = [
{version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""},
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\" and python_version < \"3.13\""},
{version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@@ -612,6 +637,7 @@ version = "1.65.0"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"},
{file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"},
@@ -630,6 +656,7 @@ version = "2.11.1"
description = "Python Client Library for Supabase Auth"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "gotrue-2.11.1-py3-none-any.whl", hash = "sha256:1b2d915bdc65fd0ad608532759ce9c72fa2e910145c1e6901f2188519e7bcd2d"},
{file = "gotrue-2.11.1.tar.gz", hash = "sha256:5594ceee60bd873e5f4fdd028b08dece3906f6013b6ed08e7786b71c0092fed0"},
@@ -645,6 +672,7 @@ version = "0.13.1"
description = "IAM API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"},
{file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"},
@@ -661,6 +689,7 @@ version = "1.66.1"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "grpcio-1.66.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492"},
{file = "grpcio-1.66.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac"},
@@ -719,6 +748,7 @@ version = "1.66.1"
description = "Status proto mapping for gRPC"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "grpcio_status-1.66.1-py3-none-any.whl", hash = "sha256:cf9ed0b4a83adbe9297211c95cb5488b0cd065707e812145b842c85c4782ff02"},
{file = "grpcio_status-1.66.1.tar.gz", hash = "sha256:b3f7d34ccc46d83fea5261eea3786174459f763c31f6e34f1d24eba6d515d024"},
@@ -735,6 +765,7 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -746,6 +777,7 @@ version = "4.1.0"
description = "HTTP/2 State-Machine based protocol implementation"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
{file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"},
@@ -761,6 +793,7 @@ version = "4.0.0"
description = "Pure-Python HPACK header compression"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
@@ -772,6 +805,7 @@ version = "1.0.5"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
@@ -793,6 +827,7 @@ version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
@@ -819,6 +854,7 @@ version = "6.0.1"
description = "HTTP/2 framing layer for Python"
optional = false
python-versions = ">=3.6.1"
groups = ["main"]
files = [
{file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
@@ -830,6 +866,7 @@ version = "3.8"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
@@ -841,6 +878,7 @@ version = "8.4.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"},
{file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"},
@@ -860,6 +898,7 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -871,6 +910,7 @@ version = "6.1.0"
description = "multidict implementation"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
@@ -975,6 +1015,7 @@ version = "1.27.0"
description = "OpenTelemetry Python API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"},
{file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"},
@@ -990,6 +1031,7 @@ version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
@@ -1001,6 +1043,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -1016,6 +1059,7 @@ version = "0.19.1"
description = "PostgREST client for Python. This library provides an ORM interface to PostgREST."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "postgrest-0.19.1-py3-none-any.whl", hash = "sha256:a8e7be4e1abc69fd8eee5a49d7dc3a76dfbffbd778beed0b2bd7accb3f4f3a2a"},
{file = "postgrest-0.19.1.tar.gz", hash = "sha256:d8fa88953cced4f45efa0f412056c364f64ece8a35b5b35f458a7e58c133fbca"},
@@ -1029,13 +1073,14 @@ strenum = {version = ">=0.4.9,<0.5.0", markers = "python_version < \"3.11\""}
[[package]]
name = "proto-plus"
version = "1.24.0"
description = "Beautiful, Pythonic protocol buffers."
version = "1.26.0"
description = "Beautiful, Pythonic protocol buffers"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"},
{file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"},
{file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"},
{file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"},
]
[package.dependencies]
@@ -1050,6 +1095,7 @@ version = "5.28.0"
description = ""
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "protobuf-5.28.0-cp310-abi3-win32.whl", hash = "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0"},
{file = "protobuf-5.28.0-cp310-abi3-win_amd64.whl", hash = "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6"},
@@ -1070,6 +1116,7 @@ version = "0.6.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
@@ -1081,6 +1128,7 @@ version = "0.4.1"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
@@ -1091,13 +1139,14 @@ pyasn1 = ">=0.4.6,<0.7.0"
[[package]]
name = "pydantic"
version = "2.10.5"
version = "2.10.6"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main"]
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.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
]
[package.dependencies]
@@ -1115,6 +1164,7 @@ version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main"]
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"},
@@ -1227,6 +1277,7 @@ version = "2.7.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
groups = ["main"]
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"},
@@ -1247,6 +1298,7 @@ version = "2.10.1"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
@@ -1264,6 +1316,7 @@ version = "8.3.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
@@ -1282,13 +1335,14 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.25.2"
version = "0.25.3"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
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.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
]
[package.dependencies]
@@ -1304,6 +1358,7 @@ version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
@@ -1321,6 +1376,7 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -1335,6 +1391,7 @@ version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
@@ -1349,6 +1406,7 @@ version = "2.0.2"
description = ""
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "realtime-2.0.2-py3-none-any.whl", hash = "sha256:2634c915bc38807f2013f21e8bcc4d2f79870dfd81460ddb9393883d0489928a"},
{file = "realtime-2.0.2.tar.gz", hash = "sha256:519da9325b3b8102139d51785013d592f6b2403d81fa21d838a0b0234723ed7d"},
@@ -1366,6 +1424,7 @@ version = "5.2.1"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
@@ -1384,6 +1443,7 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -1405,6 +1465,7 @@ version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
groups = ["main"]
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
@@ -1419,6 +1480,7 @@ version = "0.9.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
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"},
@@ -1446,6 +1508,7 @@ version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main"]
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -1457,6 +1520,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -1468,6 +1532,7 @@ version = "0.11.0"
description = "Supabase Storage client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "storage3-0.11.0-py3-none-any.whl", hash = "sha256:de2d8f9c9103ca91a9a9d0d69d80b07a3ab6f647b93e023e6a1a97d3607b9728"},
{file = "storage3-0.11.0.tar.gz", hash = "sha256:243583f2180686c0f0a19e6117d8a9796fd60c0ca72ec567d62b75a5af0d57a1"},
@@ -1483,6 +1548,7 @@ version = "0.4.15"
description = "An Enum that inherits from str."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
{file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
@@ -1495,13 +1561,14 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
[[package]]
name = "supabase"
version = "2.11.0"
version = "2.13.0"
description = "Supabase client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
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.13.0-py3-none-any.whl", hash = "sha256:6cfccc055be21dab311afc5e9d5b37f3a4966f8394703763fbc8f8e86f36eaa6"},
{file = "supabase-2.13.0.tar.gz", hash = "sha256:452574d34bd978c8d11b5f02b0182b48e8854e511c969483c83875ec01495f11"},
]
[package.dependencies]
@@ -1518,6 +1585,7 @@ version = "0.9.2"
description = "Library for Supabase Functions"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "supafunc-0.9.2-py3-none-any.whl", hash = "sha256:be5ee9f53842c4b0ba5f4abfb5bddf9f9e37e69e755ec0526852bb15af9d2ff5"},
{file = "supafunc-0.9.2.tar.gz", hash = "sha256:f5164114a3e65e7e552539f3f1050aa3d4970885abdd7405555c17fd216e2da1"},
@@ -1533,6 +1601,8 @@ version = "2.1.0"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
@@ -1544,6 +1614,7 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -1555,6 +1626,7 @@ version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
@@ -1572,6 +1644,7 @@ version = "12.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
{file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
@@ -1653,6 +1726,7 @@ version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
{file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
@@ -1732,6 +1806,7 @@ version = "1.11.1"
description = "Yet another URL library"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "yarl-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00"},
{file = "yarl-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d"},
@@ -1837,6 +1912,7 @@ version = "3.20.1"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"},
{file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"},
@@ -1851,6 +1927,6 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "62c4ef3f1ae73546a66783a2dc0672e664ce415a7b0514a7b92fc9fe1a23239e"
content-hash = "a4d81b3b55a67036ca7a441793e13e8fbe20af973fcf1623f36cdee7bc82999f"

View File

@@ -9,15 +9,15 @@ packages = [{ include = "autogpt_libs" }]
[tool.poetry.dependencies]
colorama = "^0.4.6"
expiringdict = "^1.2.2"
google-cloud-logging = "^3.11.3"
pydantic = "^2.10.5"
google-cloud-logging = "^3.11.4"
pydantic = "^2.10.6"
pydantic-settings = "^2.7.1"
pyjwt = "^2.10.1"
pytest-asyncio = "^0.25.2"
pytest-asyncio = "^0.25.3"
pytest-mock = "^3.14.0"
python = ">=3.10,<4.0"
python-dotenv = "^1.0.1"
supabase = "^2.11.0"
supabase = "^2.13.0"
[tool.poetry.group.dev.dependencies]
redis = "^5.2.1"

View File

@@ -31,6 +31,12 @@ SUPABASE_URL=http://localhost:8000
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
# RabbitMQ credentials -- Used for communication between services
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_DEFAULT_USER=rabbitmq_user_default
RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
## For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow
## for integrations to work. Defaults to the value of PLATFORM_BASE_URL if not set.
# FRONTEND_BASE_URL=http://localhost:3000

View File

@@ -297,6 +297,7 @@ class AgentOutputBlock(Block):
class Output(BlockSchema):
output: Any = SchemaField(description="The value recorded as output.")
name: Any = SchemaField(description="The name of the value recorded as output.")
def __init__(self):
super().__init__(
@@ -348,6 +349,7 @@ class AgentOutputBlock(Block):
yield "output", f"Error: {e}, {input_data.value}"
else:
yield "output", input_data.value
yield "name", input_data.name
class AddToDictionaryBlock(Block):

View File

@@ -69,6 +69,7 @@ def AICredentialsField() -> AICredentials:
class ModelMetadata(NamedTuple):
provider: str
context_window: int
max_output_tokens: int | None
class LlmModelMeta(EnumMeta):
@@ -92,6 +93,8 @@ 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"
@@ -100,30 +103,31 @@ 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
LLAMA3_8B = "llama3-8b-8192"
LLAMA3_70B = "llama3-70b-8192"
MIXTRAL_8X7B = "mixtral-8x7b-32768"
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_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"
MIXTRAL_8X7B = "mixtral-8x7b-32768"
# Groq preview models
DEEPSEEK_LLAMA_70B = "deepseek-r1-distill-llama-70b"
# 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_8B = "google/gemini-flash-1.5"
GEMINI_FLASH_1_5 = "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"
DEEPSEEK_CHAT = "deepseek/deepseek-chat" # Actually: DeepSeek V3
PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = (
"perplexity/llama-3.1-sonar-large-128k-online"
)
@@ -148,47 +152,74 @@ 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 = {
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),
# 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.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata(
"open_router", 8192
"open_router", 127072, 127072
),
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),
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),
}
for model in LlmModel:
@@ -314,7 +345,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
llm_model: LlmModel,
prompt: list[dict],
json_format: bool,
max_tokens: int | None = None,
max_tokens: int | None,
ollama_host: str = "localhost:11434",
) -> tuple[str, int, int]:
"""
@@ -332,6 +363,7 @@ 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())
@@ -381,7 +413,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
model=llm_model.value,
system=sysprompt,
messages=messages,
max_tokens=max_tokens or 8192,
max_tokens=max_tokens,
)
self.prompt = json.dumps(prompt)

View File

@@ -0,0 +1,174 @@
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

@@ -0,0 +1,37 @@
from gravitasml.parser import Parser
from gravitasml.token import tokenize
from backend.data.block import Block, BlockOutput, BlockSchema
from backend.data.model import SchemaField
class XMLParserBlock(Block):
class Input(BlockSchema):
input_xml: str = SchemaField(description="input xml to be parsed")
class Output(BlockSchema):
parsed_xml: dict = SchemaField(description="output parsed xml to dict")
error: str = SchemaField(description="Error in parsing")
def __init__(self):
super().__init__(
id="286380af-9529-4b55-8be0-1d7c854abdb5",
description="Parses XML using gravitasml to tokenize and coverts it to dict",
input_schema=XMLParserBlock.Input,
output_schema=XMLParserBlock.Output,
test_input={"input_xml": "<tag1><tag2>content</tag2></tag1>"},
test_output=[
("parsed_xml", {"tag1": {"tag2": "content"}}),
],
)
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
tokens = tokenize(input_data.input_xml)
parser = Parser(tokens)
parsed_result = parser.parse()
yield "parsed_xml", parsed_result
except ValueError as val_e:
raise ValueError(f"Validation error for dict:{val_e}") from val_e
except SyntaxError as syn_e:
raise SyntaxError(f"Error in input xml syntax: {syn_e}") from syn_e

View File

@@ -221,7 +221,8 @@ def event():
@test.command()
@click.argument("server_address")
@click.argument("graph_id")
def websocket(server_address: str, graph_id: str):
@click.argument("graph_version")
def websocket(server_address: str, graph_id: str, graph_version: int):
"""
Tests the websocket connection.
"""
@@ -237,7 +238,9 @@ def websocket(server_address: str, graph_id: str):
try:
msg = WsMessage(
method=Methods.SUBSCRIBE,
data=ExecutionSubscription(graph_id=graph_id).model_dump(),
data=ExecutionSubscription(
graph_id=graph_id, graph_version=graph_version
).model_dump(),
).model_dump_json()
await websocket.send(msg)
print(f"Sending: {msg}")

View File

@@ -35,6 +35,8 @@ 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,
@@ -42,20 +44,21 @@ 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_1_405B: 1,
LlmModel.LLAMA3_1_70B: 1,
LlmModel.LLAMA3_3_70B: 1, # $0.59 / $0.79
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_8B: 1,
LlmModel.GEMINI_FLASH_1_5: 1,
LlmModel.GROK_BETA: 5,
LlmModel.MISTRAL_NEMO: 1,
LlmModel.COHERE_COMMAND_R_08_2024: 1,

View File

@@ -10,7 +10,6 @@ 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

@@ -202,15 +202,37 @@ class UserCreditBase(ABC):
transaction_type: CreditTransactionType,
is_active: bool = True,
transaction_key: str | None = None,
ceiling_balance: int | None = None,
metadata: Json = Json({}),
) -> int:
) -> 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 for user {user_id}, balance: {user_balance}, amount: {amount}"
f"Insufficient balance of ${user_balance/100} to run the block that costs ${abs(amount)/100}"
)
# Create the transaction
@@ -225,9 +247,8 @@ class UserCreditBase(ABC):
}
if transaction_key:
transaction_data["transactionKey"] = transaction_key
await CreditTransaction.prisma().create(data=transaction_data)
return user_balance + amount
tx = await CreditTransaction.prisma().create(data=transaction_data)
return user_balance + amount, tx.transactionKey
class UsageTransactionMetadata(BaseModel):
@@ -308,7 +329,7 @@ class UserCredit(UserCreditBase):
if cost == 0:
return 0
balance = await self._add_transaction(
balance, _ = await self._add_transaction(
user_id=entry.user_id,
amount=-cost,
transaction_type=CreditTransactionType.USAGE,
@@ -326,11 +347,17 @@ class UserCredit(UserCreditBase):
)
user_id = entry.user_id
# Auto top-up if balance just went below threshold due to this transaction.
# Auto top-up if balance is below threshold.
auto_top_up = await get_auto_top_up(user_id)
if balance < auto_top_up.threshold <= balance - cost:
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)
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(
@@ -340,9 +367,34 @@ class UserCredit(UserCreditBase):
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")
@@ -379,13 +431,10 @@ class UserCredit(UserCreditBase):
},
)
if payment_intent.status == "succeeded":
await self._add_transaction(
await self._enable_transaction(
transaction_key=transaction_key,
user_id=user_id,
amount=amount,
transaction_type=CreditTransactionType.TOP_UP,
transaction_key=payment_intent.id,
metadata=Json({"payment_intent": payment_intent}),
is_active=True,
)
return
@@ -425,6 +474,7 @@ class UserCredit(UserCreditBase):
+ "/marketplace/credits?topup=success",
cancel_url=settings.config.frontend_base_url
+ "/marketplace/credits?topup=cancel",
allow_promotion_codes=True,
)
await self._add_transaction(
@@ -504,7 +554,11 @@ class UserCredit(UserCreditBase):
)
tx_time = None
for t in transactions:
metadata = UsageTransactionMetadata.model_validate(t.metadata)
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:
@@ -551,12 +605,13 @@ class BetaUserCredit(UserCredit):
return balance
try:
return await self._add_transaction(
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]

View File

@@ -23,6 +23,7 @@ class GraphExecutionEntry(BaseModel):
user_id: str
graph_exec_id: str
graph_id: str
graph_version: int
start_node_execs: list["NodeExecutionEntry"]

View File

@@ -0,0 +1,296 @@
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import Awaitable, Optional
import aio_pika
import pika
import pika.adapters.blocking_connection
from pika.spec import BasicProperties
from pydantic import BaseModel
from backend.util.retry import conn_retry
from backend.util.settings import Settings
logger = logging.getLogger(__name__)
class ExchangeType(str, Enum):
DIRECT = "direct"
FANOUT = "fanout"
TOPIC = "topic"
HEADERS = "headers"
class Exchange(BaseModel):
name: str
type: ExchangeType
durable: bool = True
auto_delete: bool = False
class Queue(BaseModel):
name: str
durable: bool = True
auto_delete: bool = False
# Optional exchange binding configuration
exchange: Optional[Exchange] = None
routing_key: Optional[str] = None
arguments: Optional[dict] = None
class RabbitMQConfig(BaseModel):
"""Configuration for a RabbitMQ service instance"""
vhost: str = "/"
exchanges: list[Exchange]
queues: list[Queue]
class RabbitMQBase(ABC):
"""Base class for RabbitMQ connections with shared configuration"""
def __init__(self, config: RabbitMQConfig):
settings = Settings()
self.host = settings.config.rabbitmq_host
self.port = settings.config.rabbitmq_port
self.username = settings.secrets.rabbitmq_default_user
self.password = settings.secrets.rabbitmq_default_pass
self.config = config
self._connection = None
self._channel = None
@property
def is_connected(self) -> bool:
"""Check if we have a valid connection"""
return bool(self._connection)
@property
def is_ready(self) -> bool:
"""Check if we have a valid channel"""
return bool(self.is_connected and self._channel)
@abstractmethod
def connect(self) -> None | Awaitable[None]:
"""Establish connection to RabbitMQ"""
pass
@abstractmethod
def disconnect(self) -> None | Awaitable[None]:
"""Close connection to RabbitMQ"""
pass
@abstractmethod
def declare_infrastructure(self) -> None | Awaitable[None]:
"""Declare exchanges and queues for this service"""
pass
class SyncRabbitMQ(RabbitMQBase):
"""Synchronous RabbitMQ client"""
@property
def is_connected(self) -> bool:
return bool(self._connection and self._connection.is_open)
@property
def is_ready(self) -> bool:
return bool(self.is_connected and self._channel and self._channel.is_open)
@conn_retry("RabbitMQ", "Acquiring connection")
def connect(self) -> None:
if self.is_connected:
return
credentials = pika.PlainCredentials(self.username, self.password)
parameters = pika.ConnectionParameters(
host=self.host,
port=self.port,
virtual_host=self.config.vhost,
credentials=credentials,
heartbeat=600,
blocked_connection_timeout=300,
)
self._connection = pika.BlockingConnection(parameters)
self._channel = self._connection.channel()
self._channel.basic_qos(prefetch_count=1)
self.declare_infrastructure()
def disconnect(self) -> None:
if self._channel:
if self._channel.is_open:
self._channel.close()
self._channel = None
if self._connection:
if self._connection.is_open:
self._connection.close()
self._connection = None
def declare_infrastructure(self) -> None:
"""Declare exchanges and queues for this service"""
if not self.is_ready:
self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
# Declare exchanges
for exchange in self.config.exchanges:
self._channel.exchange_declare(
exchange=exchange.name,
exchange_type=exchange.type.value,
durable=exchange.durable,
auto_delete=exchange.auto_delete,
)
# Declare queues and bind them to exchanges
for queue in self.config.queues:
self._channel.queue_declare(
queue=queue.name,
durable=queue.durable,
auto_delete=queue.auto_delete,
arguments=queue.arguments,
)
if queue.exchange:
self._channel.queue_bind(
queue=queue.name,
exchange=queue.exchange.name,
routing_key=queue.routing_key or queue.name,
)
def publish_message(
self,
routing_key: str,
message: str,
exchange: Optional[Exchange] = None,
properties: Optional[BasicProperties] = None,
mandatory: bool = True,
) -> None:
if not self.is_ready:
self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
self._channel.basic_publish(
exchange=exchange.name if exchange else "",
routing_key=routing_key,
body=message.encode(),
properties=properties or BasicProperties(delivery_mode=2),
mandatory=mandatory,
)
def get_channel(self) -> pika.adapters.blocking_connection.BlockingChannel:
if not self.is_ready:
self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
return self._channel
class AsyncRabbitMQ(RabbitMQBase):
"""Asynchronous RabbitMQ client"""
@property
def is_connected(self) -> bool:
return bool(self._connection and not self._connection.is_closed)
@property
def is_ready(self) -> bool:
return bool(self.is_connected and self._channel and not self._channel.is_closed)
@conn_retry("AsyncRabbitMQ", "Acquiring async connection")
async def connect(self):
if self.is_connected:
return
self._connection = await aio_pika.connect_robust(
host=self.host,
port=self.port,
login=self.username,
password=self.password,
virtualhost=self.config.vhost.lstrip("/"),
)
self._channel = await self._connection.channel()
await self._channel.set_qos(prefetch_count=1)
await self.declare_infrastructure()
async def disconnect(self):
if self._channel:
await self._channel.close()
self._channel = None
if self._connection:
await self._connection.close()
self._connection = None
async def declare_infrastructure(self):
"""Declare exchanges and queues for this service"""
if not self.is_ready:
await self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
# Declare exchanges
for exchange in self.config.exchanges:
await self._channel.declare_exchange(
name=exchange.name,
type=exchange.type.value,
durable=exchange.durable,
auto_delete=exchange.auto_delete,
)
# Declare queues and bind them to exchanges
for queue in self.config.queues:
queue_obj = await self._channel.declare_queue(
name=queue.name,
durable=queue.durable,
auto_delete=queue.auto_delete,
arguments=queue.arguments,
)
if queue.exchange:
exchange = await self._channel.get_exchange(queue.exchange.name)
await queue_obj.bind(
exchange, routing_key=queue.routing_key or queue.name
)
async def publish_message(
self,
routing_key: str,
message: str,
exchange: Optional[Exchange] = None,
persistent: bool = True,
) -> None:
if not self.is_ready:
await self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
if exchange:
exchange_obj = await self._channel.get_exchange(exchange.name)
else:
exchange_obj = self._channel.default_exchange
await exchange_obj.publish(
aio_pika.Message(
body=message.encode(),
delivery_mode=(
aio_pika.DeliveryMode.PERSISTENT
if persistent
else aio_pika.DeliveryMode.NOT_PERSISTENT
),
),
routing_key=routing_key,
)
async def get_channel(self) -> aio_pika.abc.AbstractChannel:
if not self.is_ready:
await self.connect()
if self._channel is None:
raise RuntimeError("Channel should be established after connect")
return self._channel

View File

@@ -163,6 +163,7 @@ 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)
@@ -192,6 +193,11 @@ def execute_node(
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)
for output_name, output_data in node_block.execute(
input_data, **extra_exec_kwargs
):
@@ -210,16 +216,7 @@ def execute_node(
):
yield execution
# Update execution status and spend credits
res = update_execution(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
)
data.data = input_data
db_client.spend_credits(data, s, t)
update_execution(ExecutionStatus.COMPLETED)
except Exception as e:
error_msg = str(e)
@@ -875,6 +872,7 @@ class ExecutionManager(AppService):
graph_exec = GraphExecutionEntry(
user_id=user_id,
graph_id=graph_id,
graph_version=graph_version or 0,
graph_exec_id=graph_exec_id,
start_node_execs=starting_node_execs,
)

View File

@@ -130,6 +130,13 @@ 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",
@@ -154,8 +161,9 @@ DEFAULT_CREDENTIALS = [
fal_credentials,
exa_credentials,
e2b_credentials,
nvidia_credentials,
mem0_credentials,
nvidia_credentials,
screenshotone_credentials,
]
@@ -219,6 +227,8 @@ 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

View File

@@ -30,6 +30,7 @@ class ProviderName(str, Enum):
REDDIT = "reddit"
REPLICATE = "replicate"
REVID = "revid"
SCREENSHOTONE = "screenshotone"
SLANT3D = "slant3d"
SMTP = "smtp"
TWITTER = "twitter"

View File

@@ -20,24 +20,28 @@ class ConnectionManager:
for subscribers in self.subscriptions.values():
subscribers.discard(websocket)
async def subscribe(self, graph_id: str, websocket: WebSocket):
if graph_id not in self.subscriptions:
self.subscriptions[graph_id] = set()
self.subscriptions[graph_id].add(websocket)
async def subscribe(self, graph_id: str, graph_version: int, websocket: WebSocket):
key = f"{graph_id}_{graph_version}"
if key not in self.subscriptions:
self.subscriptions[key] = set()
self.subscriptions[key].add(websocket)
async def unsubscribe(self, graph_id: str, websocket: WebSocket):
if graph_id in self.subscriptions:
self.subscriptions[graph_id].discard(websocket)
if not self.subscriptions[graph_id]:
del self.subscriptions[graph_id]
async def unsubscribe(
self, graph_id: str, graph_version: int, websocket: WebSocket
):
key = f"{graph_id}_{graph_version}"
if key in self.subscriptions:
self.subscriptions[key].discard(websocket)
if not self.subscriptions[key]:
del self.subscriptions[key]
async def send_execution_result(self, result: execution.ExecutionResult):
graph_id = result.graph_id
if graph_id in self.subscriptions:
key = f"{result.graph_id}_{result.graph_version}"
if key in self.subscriptions:
message = WsMessage(
method=Methods.EXECUTION_EVENT,
channel=graph_id,
channel=key,
data=result.model_dump(),
).model_dump_json()
for connection in self.subscriptions[graph_id]:
for connection in self.subscriptions[key]:
await connection.send_text(message)

View File

@@ -1,16 +1,18 @@
import logging
from collections import defaultdict
from typing import Any, Sequence
from typing import Any, Dict, List, Optional, Sequence
from autogpt_libs.utils.cache import thread_cached
from fastapi import APIRouter, Depends, HTTPException
from prisma.enums import APIKeyPermission
from prisma.enums import AgentExecutionStatus, APIKeyPermission
from typing_extensions import TypedDict
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.data.execution import ExecutionResult
from backend.executor import ExecutionManager
from backend.server.external.middleware import require_permission
from backend.util.service import get_service_client
@@ -28,6 +30,40 @@ logger = logging.getLogger(__name__)
v1_router = APIRouter()
class NodeOutput(TypedDict):
key: str
value: Any
class ExecutionNode(TypedDict):
node_id: str
input: Any
output: Dict[str, Any]
class ExecutionNodeOutput(TypedDict):
node_id: str
outputs: List[NodeOutput]
class GraphExecutionResult(TypedDict):
execution_id: str
status: str
nodes: List[ExecutionNode]
output: Optional[List[Dict[str, str]]]
def get_outputs_with_names(results: List[ExecutionResult]) -> List[Dict[str, str]]:
outputs = []
for result in results:
if "output" in result.output_data:
output_value = result.output_data["output"][0]
name = result.output_data.get("name", [None])[0]
if output_value and name:
outputs.append({name: output_value})
return outputs
@v1_router.get(
path="/blocks",
tags=["blocks"],
@@ -59,17 +95,21 @@ def execute_graph_block(
@v1_router.post(
path="/graphs/{graph_id}/execute",
path="/graphs/{graph_id}/execute/{graph_version}",
tags=["graphs"],
)
def execute_graph(
graph_id: str,
graph_version: int,
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
graph_id,
graph_version=graph_version,
data=node_input,
user_id=api_key.user_id,
)
return {"id": graph_exec.graph_exec_id}
except Exception as e:
@@ -85,27 +125,28 @@ async def get_graph_execution_results(
graph_id: str,
graph_exec_id: str,
api_key: APIKey = Depends(require_permission(APIKeyPermission.READ_GRAPH)),
) -> dict:
) -> GraphExecutionResult:
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)
last_result = results[-1] if results else None
execution_status = (
last_result.status if last_result else AgentExecutionStatus.INCOMPLETE
)
outputs = get_outputs_with_names(results)
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", [])
),
}
return GraphExecutionResult(
execution_id=graph_exec_id,
status=execution_status,
nodes=[
ExecutionNode(
node_id=result.node_id,
input=result.input_data.get("value", result.input_data),
output={k: v for k, v in result.output_data.items()},
)
for result in results
],
}
output=outputs if execution_status == AgentExecutionStatus.COMPLETED else None,
)

View File

@@ -25,12 +25,11 @@ class WsMessage(pydantic.BaseModel):
class ExecutionSubscription(pydantic.BaseModel):
graph_id: str
graph_version: int
class SubscriptionDetails(pydantic.BaseModel):
event_type: str
channel: str
graph_id: str
class ExecuteGraphResponse(pydantic.BaseModel):
graph_exec_id: str
class CreateGraph(pydantic.BaseModel):

View File

@@ -51,6 +51,7 @@ from backend.server.model import (
CreateAPIKeyRequest,
CreateAPIKeyResponse,
CreateGraph,
ExecuteGraphResponse,
RequestTopUp,
SetGraphActiveVersion,
UpdatePermissionsRequest,
@@ -178,6 +179,8 @@ async def configure_user_auto_top_up(
) -> 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")
@@ -240,7 +243,7 @@ async def manage_payment_method(
) -> dict[str, str]:
session = stripe.billing_portal.Session.create(
customer=await get_stripe_customer_id(user_id),
return_url=settings.config.platform_base_url + "/marketplace/credits",
return_url=settings.config.frontend_base_url + "/marketplace/credits",
)
if not session:
raise HTTPException(
@@ -509,7 +512,7 @@ async def set_graph_active_version(
@v1_router.post(
path="/graphs/{graph_id}/execute",
path="/graphs/{graph_id}/execute/{graph_version}",
tags=["graphs"],
dependencies=[Depends(auth_middleware)],
)
@@ -518,13 +521,13 @@ def execute_graph(
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
) -> ExecuteGraphResponse:
try:
logger.info("Node input: %s", node_input)
graph_exec = execution_manager_client().add_execution(
graph_id, node_input, user_id=user_id, graph_version=graph_version
)
return {"id": graph_exec.graph_exec_id}
return ExecuteGraphResponse(graph_exec_id=graph_exec.graph_exec_id)
except Exception as e:
msg = e.__str__().encode().decode("unicode_escape")
raise HTTPException(status_code=400, detail=msg)
@@ -586,6 +589,7 @@ class ScheduleCreationRequest(pydantic.BaseModel):
cron: str
input_data: dict[Any, Any]
graph_id: str
graph_version: int
@v1_router.post(
@@ -597,10 +601,13 @@ async def create_schedule(
user_id: Annotated[str, Depends(get_user_id)],
schedule: ScheduleCreationRequest,
) -> scheduler.JobInfo:
graph = await graph_db.get_graph(schedule.graph_id, user_id=user_id)
graph = await graph_db.get_graph(
schedule.graph_id, schedule.graph_version, user_id=user_id
)
if not graph:
raise HTTPException(
status_code=404, detail=f"Graph #{schedule.graph_id} not found."
status_code=404,
detail=f"Graph #{schedule.graph_id} v.{schedule.graph_version} not found.",
)
return await asyncio.to_thread(

View File

@@ -1,5 +1,4 @@
import logging
import random
from datetime import datetime
from typing import Optional
@@ -17,6 +16,25 @@ from backend.data.graph import GraphModel
logger = logging.getLogger(__name__)
def sanitize_query(query: str | None) -> str | None:
if query is None:
return query
query = query.strip()[:100]
return (
query.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("'", "\\'")
.replace('"', '\\"')
.replace(";", "\\;")
.replace("--", "\\--")
.replace("/*", "\\/*")
.replace("*/", "\\*/")
)
async def get_store_agents(
featured: bool = False,
creator: str | None = None,
@@ -29,29 +47,7 @@ async def get_store_agents(
logger.debug(
f"Getting store agents. featured={featured}, creator={creator}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
)
sanitized_query = None
# Sanitize and validate search query by escaping special characters
if search_query is not None:
sanitized_query = search_query.strip()
if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit
raise backend.server.v2.store.exceptions.DatabaseError(
f"Invalid search query: len({len(sanitized_query)}) query: {search_query}"
)
# Escape special SQL characters
sanitized_query = (
sanitized_query.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("'", "\\'")
.replace('"', '\\"')
.replace(";", "\\;")
.replace("--", "\\--")
.replace("/*", "\\/*")
.replace("*/", "\\*/")
)
sanitized_query = sanitize_query(search_query)
where_clause = {}
if featured:
@@ -93,8 +89,8 @@ async def get_store_agents(
slug=agent.slug,
agent_name=agent.agent_name,
agent_image=agent.agent_image[0] if agent.agent_image else "",
creator=agent.creator_username,
creator_avatar=agent.creator_avatar,
creator=agent.creator_username or "Needs Profile",
creator_avatar=agent.creator_avatar or "",
sub_heading=agent.sub_heading,
description=agent.description,
runs=agent.runs,
@@ -114,7 +110,7 @@ async def get_store_agents(
),
)
except Exception as e:
logger.error(f"Error getting store agents: {str(e)}")
logger.error(f"Error getting store agents: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store agents"
) from e
@@ -156,7 +152,7 @@ async def get_store_agent_details(
except backend.server.v2.store.exceptions.AgentNotFoundError:
raise
except Exception as e:
logger.error(f"Error getting store agent details: {str(e)}")
logger.error(f"Error getting store agent details: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent details"
) from e
@@ -270,7 +266,7 @@ async def get_store_creators(
),
)
except Exception as e:
logger.error(f"Error getting store creators: {str(e)}")
logger.error(f"Error getting store creators: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store creators"
) from e
@@ -307,7 +303,7 @@ async def get_store_creator_details(
except backend.server.v2.store.exceptions.CreatorNotFoundError:
raise
except Exception as e:
logger.error(f"Error getting store creator details: {str(e)}")
logger.error(f"Error getting store creator details: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch creator details"
) from e
@@ -366,7 +362,7 @@ async def get_store_submissions(
)
except Exception as e:
logger.error(f"Error fetching store submissions: {str(e)}")
logger.error(f"Error fetching store submissions: {e}")
# Return empty response rather than exposing internal errors
return backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=[],
@@ -416,7 +412,7 @@ async def delete_store_submission(
return True
except Exception as e:
logger.error(f"Error deleting store submission: {str(e)}")
logger.error(f"Error deleting store submission: {e}")
return False
@@ -539,7 +535,7 @@ async def create_store_submission(
):
raise
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating store submission: {str(e)}")
logger.error(f"Database error creating store submission: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store submission"
) from e
@@ -579,7 +575,7 @@ async def create_store_review(
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating store review: {str(e)}")
logger.error(f"Database error creating store review: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store review"
) from e
@@ -587,7 +583,7 @@ async def create_store_review(
async def get_user_profile(
user_id: str,
) -> backend.server.v2.store.model.ProfileDetails:
) -> backend.server.v2.store.model.ProfileDetails | None:
logger.debug(f"Getting user profile for {user_id}")
try:
@@ -596,25 +592,7 @@ async def get_user_profile(
)
if not profile:
logger.warning(f"Profile not found for user {user_id}")
new_profile = await prisma.models.Profile.prisma().create(
data=prisma.types.ProfileCreateInput(
userId=user_id,
name="No Profile Data",
username=f"{random.choice(['happy', 'clever', 'swift', 'bright', 'wise'])}-{random.choice(['fox', 'wolf', 'bear', 'eagle', 'owl'])}_{random.randint(1000,9999)}".lower(),
description="No Profile Data",
links=[],
avatarUrl="",
)
)
return backend.server.v2.store.model.ProfileDetails(
name=new_profile.name,
username=new_profile.username,
description=new_profile.description,
links=new_profile.links,
avatar_url=new_profile.avatarUrl,
)
return None
return backend.server.v2.store.model.ProfileDetails(
name=profile.name,
username=profile.username,
@@ -623,115 +601,90 @@ async def get_user_profile(
avatar_url=profile.avatarUrl,
)
except Exception as e:
logger.error(f"Error getting user profile: {str(e)}")
return backend.server.v2.store.model.ProfileDetails(
name="No Profile Data",
username="No Profile Data",
description="No Profile Data",
links=[],
avatar_url="",
)
logger.error("Error getting user profile: %s", e)
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to get user profile"
) from e
async def update_or_create_profile(
async def update_profile(
user_id: str, profile: backend.server.v2.store.model.Profile
) -> backend.server.v2.store.model.CreatorDetails:
"""
Update the store profile for a user. Creates a new profile if one doesn't exist.
Only allows updating if the user_id matches the owning user.
If a field is None, it will not overwrite the existing value in the case of an update.
Update the store profile for a user or create a new one if it doesn't exist.
Args:
user_id: ID of the authenticated user
profile: Updated profile details
Returns:
CreatorDetails: The updated profile
CreatorDetails: The updated or created profile details
Raises:
HTTPException: If user is not authorized to update this profile
DatabaseError: If profile cannot be updated due to database issues
DatabaseError: If there's an issue updating or creating the profile
"""
logger.info(f"Updating profile for user {user_id} data: {profile}")
logger.info("Updating profile for user %s with data: %s", user_id, profile)
try:
# Sanitize username to only allow letters and hyphens
# Sanitize username to allow only letters, numbers, and hyphens
username = "".join(
c if c.isalpha() or c == "-" or c.isnumeric() else ""
for c in profile.username
).lower()
# Check if profile exists for the given user_id
existing_profile = await prisma.models.Profile.prisma().find_first(
where={"userId": user_id}
)
# If no profile exists, create a new one
if not existing_profile:
logger.debug(
f"No existing profile found. Creating new profile for user {user_id}"
)
# Create new profile since one doesn't exist
new_profile = await prisma.models.Profile.prisma().create(
data={
"userId": user_id,
"name": profile.name,
"username": username,
"description": profile.description,
"links": profile.links or [],
"avatarUrl": profile.avatar_url,
"isFeatured": False,
}
raise backend.server.v2.store.exceptions.ProfileNotFoundError(
f"Profile not found for user {user_id}. This should not be possible."
)
return backend.server.v2.store.model.CreatorDetails(
name=new_profile.name,
username=new_profile.username,
description=new_profile.description,
links=new_profile.links,
avatar_url=new_profile.avatarUrl or "",
agent_rating=0.0,
agent_runs=0,
top_categories=[],
# Verify that the user is authorized to update this profile
if existing_profile.userId != user_id:
logger.error(
"Unauthorized update attempt for profile %s by user %s",
existing_profile.userId,
user_id,
)
raise backend.server.v2.store.exceptions.DatabaseError(
f"Unauthorized update attempt for profile {existing_profile.id} by user {user_id}"
)
else:
logger.debug(f"Updating existing profile for user {user_id}")
# Update only provided fields for the existing profile
update_data = {}
if profile.name is not None:
update_data["name"] = profile.name
if profile.username is not None:
update_data["username"] = username
if profile.description is not None:
update_data["description"] = profile.description
if profile.links is not None:
update_data["links"] = profile.links
if profile.avatar_url is not None:
update_data["avatarUrl"] = profile.avatar_url
# Update the existing profile
updated_profile = await prisma.models.Profile.prisma().update(
where={"id": existing_profile.id},
data=prisma.types.ProfileUpdateInput(**update_data),
)
if updated_profile is None:
logger.error(f"Failed to update profile for user {user_id}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
)
logger.debug("Updating existing profile for user %s", user_id)
# Prepare update data, only including non-None values
update_data = {}
if profile.name is not None:
update_data["name"] = profile.name
if profile.username is not None:
update_data["username"] = username
if profile.description is not None:
update_data["description"] = profile.description
if profile.links is not None:
update_data["links"] = profile.links
if profile.avatar_url is not None:
update_data["avatarUrl"] = profile.avatar_url
return backend.server.v2.store.model.CreatorDetails(
name=updated_profile.name,
username=updated_profile.username,
description=updated_profile.description,
links=updated_profile.links,
avatar_url=updated_profile.avatarUrl or "",
agent_rating=0.0,
agent_runs=0,
top_categories=[],
# Update the existing profile
updated_profile = await prisma.models.Profile.prisma().update(
where={"id": existing_profile.id},
data=prisma.types.ProfileUpdateInput(**update_data),
)
if updated_profile is None:
logger.error("Failed to update profile for user %s", user_id)
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
)
return backend.server.v2.store.model.CreatorDetails(
name=updated_profile.name,
username=updated_profile.username,
description=updated_profile.description,
links=updated_profile.links,
avatar_url=updated_profile.avatarUrl or "",
agent_rating=0.0,
agent_runs=0,
top_categories=[],
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating profile: {str(e)}")
logger.error("Database error updating profile: %s", e)
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
) from e
@@ -796,7 +749,7 @@ async def get_my_agents(
),
)
except Exception as e:
logger.error(f"Error getting my agents: {str(e)}")
logger.error(f"Error getting my agents: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch my agents"
) from e
@@ -809,7 +762,7 @@ async def get_agent(
try:
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id}
where={"id": store_listing_version_id}, include={"Agent": True}
)
)
@@ -840,7 +793,7 @@ async def get_agent(
return graph
except Exception as e:
logger.error(f"Error getting agent: {str(e)}")
logger.error(f"Error getting agent: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent"
) from e
@@ -905,7 +858,7 @@ async def review_store_submission(
return submission
except Exception as e:
logger.error(f"Could not create store submission review: {str(e)}")
logger.error(f"Could not create store submission review: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store submission review"
) from e

View File

@@ -158,6 +158,26 @@ async def test_create_store_submission(mocker):
agentId="agent-id",
agentVersion=1,
owningUserId="user-id",
StoreListingVersions=[
prisma.models.StoreListingVersion(
id="version-id",
agentId="agent-id",
agentVersion=1,
slug="test-agent",
name="Test Agent",
description="Test description",
createdAt=datetime.now(),
updatedAt=datetime.now(),
subHeading="Test heading",
imageUrls=["image.jpg"],
categories=["test"],
isFeatured=False,
isDeleted=False,
version=1,
isAvailable=True,
isApproved=False,
)
],
)
# Mock prisma calls
@@ -181,6 +201,7 @@ async def test_create_store_submission(mocker):
# Verify results
assert result.name == "Test Agent"
assert result.description == "Test description"
assert result.store_listing_version_id == "version-id"
# Verify mocks called correctly
mock_agent_graph.return_value.find_first.assert_called_once()
@@ -195,6 +216,7 @@ async def test_update_profile(mocker):
id="profile-id",
name="Test Creator",
username="creator",
userId="user-id",
description="Test description",
links=["link1"],
avatarUrl="avatar.jpg",
@@ -221,7 +243,7 @@ async def test_update_profile(mocker):
)
# Call function
result = await db.update_or_create_profile("user-id", profile)
result = await db.update_profile("user-id", profile)
# Verify results
assert result.username == "creator"
@@ -237,7 +259,7 @@ async def test_get_user_profile(mocker):
# Mock data
mock_profile = prisma.models.Profile(
id="profile-id",
name="No Profile Data",
name="Test User",
username="testuser",
description="Test description",
links=["link1", "link2"],
@@ -245,20 +267,22 @@ async def test_get_user_profile(mocker):
isFeatured=False,
createdAt=datetime.now(),
updatedAt=datetime.now(),
userId="user-id",
)
# Mock prisma calls
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
mock_profile_db.return_value.find_unique = mocker.AsyncMock(
mock_profile_db.return_value.find_first = mocker.AsyncMock(
return_value=mock_profile
)
# Call function
result = await db.get_user_profile("user-id")
assert result is not None
# Verify results
assert result.name == "No Profile Data"
assert result.username == "No Profile Data"
assert result.description == "No Profile Data"
assert result.links == []
assert result.avatar_url == ""
assert result.name == "Test User"
assert result.username == "testuser"
assert result.description == "Test description"
assert result.links == ["link1", "link2"]
assert result.avatar_url == "avatar.jpg"

View File

@@ -42,6 +42,11 @@ async def get_profile(
"""
try:
profile = await backend.server.v2.store.db.get_user_profile(user_id)
if profile is None:
return fastapi.responses.JSONResponse(
status_code=404,
content={"detail": "Profile not found"},
)
return profile
except Exception:
logger.exception("Exception occurred whilst getting user profile")
@@ -77,7 +82,7 @@ async def update_or_create_profile(
HTTPException: If there is an error updating the profile
"""
try:
updated_profile = await backend.server.v2.store.db.update_or_create_profile(
updated_profile = await backend.server.v2.store.db.update_profile(
user_id=user_id, profile=profile
)
return updated_profile

View File

@@ -86,13 +86,13 @@ async def handle_subscribe(
)
else:
ex_sub = ExecutionSubscription.model_validate(message.data)
await manager.subscribe(ex_sub.graph_id, websocket)
await manager.subscribe(ex_sub.graph_id, ex_sub.graph_version, websocket)
logger.debug(f"New execution subscription for graph {ex_sub.graph_id}")
await websocket.send_text(
WsMessage(
method=Methods.SUBSCRIBE,
success=True,
channel=ex_sub.graph_id,
channel=f"{ex_sub.graph_id}_{ex_sub.graph_version}",
).model_dump_json()
)
@@ -110,13 +110,13 @@ async def handle_unsubscribe(
)
else:
ex_sub = ExecutionSubscription.model_validate(message.data)
await manager.unsubscribe(ex_sub.graph_id, websocket)
await manager.unsubscribe(ex_sub.graph_id, ex_sub.graph_version, websocket)
logger.debug(f"Removed execution subscription for graph {ex_sub.graph_id}")
await websocket.send_text(
WsMessage(
method=Methods.UNSUBSCRIBE,
success=True,
channel=ex_sub.graph_id,
channel=f"{ex_sub.graph_id}_{ex_sub.graph_version}",
).model_dump_json()
)

View File

@@ -258,7 +258,7 @@ async def block_autogen_agent():
print(response)
result = await wait_execution(
graph_id=test_graph.id,
graph_exec_id=response["id"],
graph_exec_id=response.graph_exec_id,
timeout=1200,
user_id=test_user.id,
)

View File

@@ -160,7 +160,9 @@ async def reddit_marketing_agent():
test_graph.id, input_data, test_user.id
)
print(response)
result = await wait_execution(test_user.id, test_graph.id, response["id"], 120)
result = await wait_execution(
test_user.id, test_graph.id, response.graph_exec_id, 120
)
print(result)

View File

@@ -89,7 +89,9 @@ async def sample_agent():
test_graph.id, input_data, test_user.id
)
print(response)
result = await wait_execution(test_user.id, test_graph.id, response["id"], 10)
result = await wait_execution(
test_user.id, test_graph.id, response.graph_exec_id, 10
)
print(result)

View File

@@ -18,6 +18,7 @@ from typing import (
FrozenSet,
Iterator,
List,
Optional,
Set,
Tuple,
Type,
@@ -33,7 +34,7 @@ from pydantic import BaseModel
from Pyro5 import api as pyro
from Pyro5 import config as pyro_config
from backend.data import db, redis
from backend.data import db, rabbitmq, redis
from backend.util.process import AppProcess
from backend.util.retry import conn_retry
from backend.util.settings import Config, Secrets
@@ -116,6 +117,9 @@ class AppService(AppProcess, ABC):
shared_event_loop: asyncio.AbstractEventLoop
use_db: bool = False
use_redis: bool = False
use_async: bool = False
use_rabbitmq: Optional[rabbitmq.RabbitMQConfig] = None
rabbitmq_service: Optional[rabbitmq.SyncRabbitMQ | rabbitmq.AsyncRabbitMQ] = None
use_supabase: bool = False
def __init__(self):
@@ -130,6 +134,20 @@ class AppService(AppProcess, ABC):
def get_host(cls) -> str:
return os.environ.get(f"{cls.service_name.upper()}_HOST", config.pyro_host)
@property
def rabbit(self) -> rabbitmq.SyncRabbitMQ | rabbitmq.AsyncRabbitMQ:
"""Access the RabbitMQ service. Will raise if not configured."""
if not self.rabbitmq_service:
raise RuntimeError("RabbitMQ not configured for this service")
return self.rabbitmq_service
@property
def rabbit_config(self) -> rabbitmq.RabbitMQConfig:
"""Access the RabbitMQ config. Will raise if not configured."""
if not self.use_rabbitmq:
raise RuntimeError("RabbitMQ not configured for this service")
return self.use_rabbitmq
def run_service(self) -> None:
while True:
time.sleep(10)
@@ -147,6 +165,16 @@ class AppService(AppProcess, ABC):
self.shared_event_loop.run_until_complete(db.connect())
if self.use_redis:
redis.connect()
if self.use_rabbitmq:
logger.info(f"[{self.__class__.__name__}] ⏳ Configuring RabbitMQ...")
if self.use_async:
self.rabbitmq_service = rabbitmq.AsyncRabbitMQ(self.use_rabbitmq)
self.shared_event_loop.run_until_complete(
self.rabbitmq_service.connect()
)
else:
self.rabbitmq_service = rabbitmq.SyncRabbitMQ(self.use_rabbitmq)
self.rabbitmq_service.connect()
if self.use_supabase:
from supabase import create_client
@@ -175,6 +203,8 @@ class AppService(AppProcess, ABC):
if self.use_redis:
logger.info(f"[{self.__class__.__name__}] ⏳ Disconnecting Redis...")
redis.disconnect()
if self.use_rabbitmq:
logger.info(f"[{self.__class__.__name__}] ⏳ Disconnecting RabbitMQ...")
@conn_retry("Pyro", "Starting Pyro Service")
def __start_pyro(self):

View File

@@ -167,6 +167,20 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
description="The pool size for the scheduler database connection pool",
)
rabbitmq_host: str = Field(
default="localhost",
description="The host for the RabbitMQ server",
)
rabbitmq_port: int = Field(
default=5672,
description="The port for the RabbitMQ server",
)
rabbitmq_vhost: str = Field(
default="/",
description="The vhost for the RabbitMQ server",
)
@field_validator("platform_base_url", "frontend_base_url")
@classmethod
def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str:
@@ -258,6 +272,11 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
encryption_key: str = Field(default="", description="Encryption key")
rabbitmq_default_user: str = Field(default="", description="RabbitMQ default user")
rabbitmq_default_pass: str = Field(
default="", description="RabbitMQ default password"
)
# OAuth server credentials for integrations
# --8<-- [start:OAuthServerCredentialsExample]
github_client_id: str = Field(default="", description="GitHub OAuth client ID")
@@ -326,6 +345,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
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
model_config = SettingsConfigDict(

View File

@@ -106,11 +106,11 @@ def execute_block_test(block: Block):
# Populate credentials argument(s)
extra_exec_kwargs: dict = {
"graph_id": uuid.uuid4(),
"node_id": uuid.uuid4(),
"graph_exec_id": uuid.uuid4(),
"node_exec_id": uuid.uuid4(),
"user_id": uuid.uuid4(),
"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()),
}
input_model = cast(type[BlockSchema], block.input_schema)
credentials_input_fields = input_model.get_credentials_fields()

View File

@@ -0,0 +1,109 @@
CREATE OR REPLACE FUNCTION generate_username()
RETURNS TEXT AS $$
DECLARE
-- Random username generation
selected_adjective TEXT;
selected_animal TEXT;
random_int INT;
generated_username TEXT;
BEGIN
FOR i IN 1..10 LOOP
SELECT unnest
INTO selected_adjective
FROM (VALUES ('happy'), ('clever'), ('swift'), ('bright'), ('wise'), ('funny'), ('cool'), ('awesome'), ('amazing'), ('fantastic'), ('wonderful')) AS t(unnest)
ORDER BY random()
LIMIT 1;
SELECT unnest
INTO selected_animal
FROM (VALUES ('fox'), ('wolf'), ('bear'), ('eagle'), ('owl'), ('tiger'), ('lion'), ('elephant'), ('giraffe'), ('zebra')) AS t(unnest)
ORDER BY random()
LIMIT 1;
SELECT floor(random() * (99999 - 10000 + 1) + 10000)::int
INTO random_int;
generated_username := lower(selected_adjective || '-' || selected_animal || '-' || random_int);
-- Check if username is already taken
IF NOT EXISTS (
SELECT 1 FROM platform."Profile" WHERE username = generated_username
) THEN
-- Username is unique, exit the loop
EXIT;
END IF;
-- If we've tried 10 times and still haven't found a unique username
IF i = 10 THEN
RAISE EXCEPTION 'Unable to generate unique username after 10 attempts';
END IF;
END LOOP;
RETURN generated_username;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION add_user_and_profile_to_platform()
RETURNS TRIGGER AS $$
BEGIN
-- Exit early if NEW.id is null to prevent constraint violations
IF NEW.id IS NULL THEN
RAISE EXCEPTION 'Cannot create user/profile: id is null';
END IF;
/*
1) Insert into platform."User"
(If you already have such a row or want different columns, adjust below.)
*/
INSERT INTO platform."User" (id, email, "updatedAt")
VALUES (NEW.id, NEW.email, now());
/*
2) Insert into platform."Profile"
Adjust columns/types depending on how your "Profile" schema is defined:
- "links" might be text[], jsonb, or something else in your table.
- "avatarUrl" and "description" can be defaulted as well.
*/
INSERT INTO platform."Profile"
("id", "userId", name, username, description, links, "avatarUrl", "updatedAt")
VALUES
(
NEW.id,
NEW.id,
COALESCE(split_part(NEW.email, '@', 1), 'user'), -- handle null email
platform.generate_username(),
'I''m new here',
'{}', -- empty array or empty JSON, depending on your column definition
'',
now()
);
RETURN NEW;
EXCEPTION
WHEN OTHERS THEN
-- Log the error details
RAISE NOTICE 'Error in add_user_and_profile_to_platform: %', SQLERRM;
-- Re-raise the error
RAISE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
DO $$
BEGIN
-- Check if the auth schema and users table exist
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'auth'
AND table_name = 'users'
) THEN
-- Drop the trigger if it exists
DROP TRIGGER IF EXISTS user_added_to_platform ON auth.users;
-- Create the trigger
CREATE TRIGGER user_added_to_platform
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION add_user_and_profile_to_platform();
END IF;
END $$;

View File

@@ -0,0 +1,29 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'platform'
AND table_name = 'User'
) AND EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'platform'
AND table_name = 'Profile'
) THEN
INSERT INTO platform."Profile"
("id", "userId", name, username, description, links, "avatarUrl", "updatedAt")
SELECT
u.id,
u.id,
COALESCE(split_part(u.email, '@', 1), 'user'),
platform.generate_username(),
'I''m new here',
'{}',
'',
now()
FROM platform."User" u
LEFT JOIN platform."Profile" p ON u.id = p."userId"
WHERE p.id IS NULL;
END IF;
END $$;

View File

@@ -118,7 +118,6 @@ files = [
[package.dependencies]
aiohappyeyeballs = ">=2.3.0"
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
@@ -209,7 +208,6 @@ files = [
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
@@ -293,14 +291,14 @@ develop = true
[package.dependencies]
colorama = "^0.4.6"
expiringdict = "^1.2.2"
google-cloud-logging = "^3.11.3"
pydantic = "^2.10.5"
google-cloud-logging = "^3.11.4"
pydantic = "^2.10.6"
pydantic-settings = "^2.7.1"
pyjwt = "^2.10.1"
pytest-asyncio = "^0.25.2"
pytest-asyncio = "^0.25.3"
pytest-mock = "^3.14.0"
python-dotenv = "^1.0.1"
supabase = "^2.11.0"
supabase = "^2.13.0"
[package.source]
type = "directory"
@@ -324,7 +322,7 @@ version = "24.10.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
groups = ["main", "dev"]
files = [
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
@@ -356,8 +354,6 @@ mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
@@ -763,14 +759,14 @@ typing-extensions = ">=4.1.0"
[[package]]
name = "e2b-code-interpreter"
version = "1.0.4"
version = "1.0.5"
description = "E2B Code Interpreter - Stateful code execution"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "e2b_code_interpreter-1.0.4-py3-none-any.whl", hash = "sha256:e8cea4946b3457072a524250aee712f7f8d44834b91cd9c13da3bdf96eda1a6e"},
{file = "e2b_code_interpreter-1.0.4.tar.gz", hash = "sha256:fec5651d98ca0d03dd038c5df943a0beaeb59c6d422112356f55f2b662d8dea1"},
{file = "e2b_code_interpreter-1.0.5-py3-none-any.whl", hash = "sha256:4c7814e9eabba58097bf5e4019d327b3a82fab0813eafca4311b29ca6ea0639d"},
{file = "e2b_code_interpreter-1.0.5.tar.gz", hash = "sha256:e7f70b039e6a70f8e592f90f806d696dc1056919414daabeb89e86c9b650a987"},
]
[package.dependencies]
@@ -784,12 +780,11 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
groups = ["main"]
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
markers = {dev = "python_version < \"3.11\""}
[package.extras]
test = ["pytest (>=6)"]
@@ -827,14 +822,14 @@ typing-extensions = "*"
[[package]]
name = "fastapi"
version = "0.115.7"
version = "0.115.8"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e"},
{file = "fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015"},
{file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
{file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
]
[package.dependencies]
@@ -995,14 +990,8 @@ files = [
[package.dependencies]
google-auth = ">=2.14.1,<3.0.dev0"
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
grpcio = [
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
]
grpcio-status = [
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
]
grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = ">=1.22.3,<2.0.0dev"
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
requests = ">=2.18.0,<3.0.0.dev0"
@@ -1147,14 +1136,14 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"]
[[package]]
name = "google-cloud-logging"
version = "3.11.3"
version = "3.11.4"
description = "Stackdriver Logging API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_logging-3.11.3-py2.py3-none-any.whl", hash = "sha256:b8ec23f2998f76a58f8492db26a0f4151dd500425c3f08448586b85972f3c494"},
{file = "google_cloud_logging-3.11.3.tar.gz", hash = "sha256:0a73cd94118875387d4535371d9e9426861edef8e44fba1261e86782d5b8d54f"},
{file = "google_cloud_logging-3.11.4-py2.py3-none-any.whl", hash = "sha256:1d465ac62df29fb94bba4d6b4891035e57d573d84541dd8a40eebbc74422b2f0"},
{file = "google_cloud_logging-3.11.4.tar.gz", hash = "sha256:32305d989323f3c58603044e2ac5d9cf23e9465ede511bbe90b4309270d3195c"},
]
[package.dependencies]
@@ -1165,22 +1154,19 @@ google-cloud-audit-log = ">=0.2.4,<1.0.0dev"
google-cloud-core = ">=2.0.0,<3.0.0dev"
grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev"
opentelemetry-api = ">=1.9.0"
proto-plus = [
{version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""},
{version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""},
]
proto-plus = {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
[[package]]
name = "google-cloud-storage"
version = "2.19.0"
version = "3.0.0"
description = "Google Cloud Storage API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"},
{file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"},
{file = "google_cloud_storage-3.0.0-py2.py3-none-any.whl", hash = "sha256:f85fd059650d2dbb0ac158a9a6b304b66143b35ed2419afec2905ca522eb2c6a"},
{file = "google_cloud_storage-3.0.0.tar.gz", hash = "sha256:2accb3e828e584888beff1165e5f3ac61aa9088965eb0165794a82d8c7f95297"},
]
[package.dependencies]
@@ -1303,6 +1289,23 @@ files = [
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
pydantic = ">=1.10,<3"
[[package]]
name = "gravitasml"
version = "0.1.2"
description = ""
optional = false
python-versions = "<3.13,>=3.11"
groups = ["main"]
files = [
{file = "gravitasml-0.1.2-py3-none-any.whl", hash = "sha256:eafa5a65a6f952f0d23c54762f5fcb654b3f000ede389519a7dc3c3179801121"},
{file = "gravitasml-0.1.2.tar.gz", hash = "sha256:bbb651a9d5be669d47cc5bd7fbf19d55ce277b3dd9966cc37d2ec0ae042aab25"},
]
[package.dependencies]
black = ">=24.10.0,<25.0.0"
pydantic = ">=2.9.2,<3.0.0"
pytest = ">=8.2.1,<9.0.0"
[[package]]
name = "greenlet"
version = "3.1.1"
@@ -1393,14 +1396,14 @@ test = ["objgraph", "psutil"]
[[package]]
name = "groq"
version = "0.15.0"
version = "0.18.0"
description = "The official Python library for the groq API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "groq-0.15.0-py3-none-any.whl", hash = "sha256:c200558b67fee4b4f2bb89cc166337e3419a68c23280065770f8f8b0729c79ef"},
{file = "groq-0.15.0.tar.gz", hash = "sha256:9ad08ba6156c67d0975595a8515b517f22ff63158e063c55192e161ed3648af1"},
{file = "groq-0.18.0-py3-none-any.whl", hash = "sha256:81d5ac00057a45d8ce559d23ab5d3b3893011d1f12c35187ab35a9182d826ea6"},
{file = "groq-0.18.0.tar.gz", hash = "sha256:8e2ccfea406d68b3525af4b7c0e321fcb3d2a73fc60bb70b4156e6cd88c72f03"},
]
[package.dependencies]
@@ -2312,16 +2315,13 @@ files = [
{file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
]
[package.dependencies]
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
groups = ["main", "dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -2439,14 +2439,14 @@ pydantic = ">=2.9.0,<3.0.0"
[[package]]
name = "openai"
version = "1.60.2"
version = "1.61.1"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "openai-1.60.2-py3-none-any.whl", hash = "sha256:993bd11b96900b9098179c728026f016b4982ded7ee30dfcf4555eab1171fff9"},
{file = "openai-1.60.2.tar.gz", hash = "sha256:a8f843e10f2855713007f491d96afb2694b11b5e02cb97c7d01a0be60bc5bb51"},
{file = "openai-1.61.1-py3-none-any.whl", hash = "sha256:72b0826240ce26026ac2cd17951691f046e5be82ad122d20a8e1b30ca18bd11e"},
{file = "openai-1.61.1.tar.gz", hash = "sha256:ce1851507218209961f89f3520e06726c0aa7d0512386f0f977e3ac3e4f2472e"},
]
[package.dependencies]
@@ -2525,12 +2525,29 @@ version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
groups = ["main", "dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "pika"
version = "1.3.2"
description = "Pika Python AMQP Client Library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "pika-1.3.2-py3-none-any.whl", hash = "sha256:0779a7c1fafd805672796085560d290213a465e4f6f76a6fb19e378d8041a14f"},
{file = "pika-1.3.2.tar.gz", hash = "sha256:b2a327ddddf8570b4965b3576ac77091b850262d34ce8c1d8cb4e4146aa4145f"},
]
[package.extras]
gevent = ["gevent"]
tornado = ["tornado"]
twisted = ["twisted"]
[[package]]
name = "pillow"
version = "10.4.0"
@@ -2689,7 +2706,7 @@ version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
groups = ["main", "dev"]
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
@@ -2731,7 +2748,6 @@ files = [
[package.dependencies]
pastel = ">=0.2.1,<0.3.0"
pyyaml = ">=6.0.2,<7.0.0"
tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""}
[package.extras]
poetry-plugin = ["poetry (>=1.0,<3.0)"]
@@ -2772,7 +2788,6 @@ files = [
deprecation = ">=2.1.0,<3.0.0"
httpx = {version = ">=0.26,<0.29", extras = ["http2"]}
pydantic = ">=1.9,<3.0"
strenum = {version = ">=0.4.9,<0.5.0", markers = "python_version < \"3.11\""}
[[package]]
name = "posthog"
@@ -2863,7 +2878,6 @@ jinja2 = ">=2.11.2"
nodeenv = "*"
pydantic = ">=1.10.0,<3"
python-dotenv = ">=0.12.0"
StrEnum = {version = "*", markers = "python_version < \"3.11\""}
tomlkit = "*"
typing-extensions = ">=4.5.0"
@@ -3440,11 +3454,9 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
@@ -3499,7 +3511,6 @@ files = [
]
[package.dependencies]
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
watchdog = ">=2.0.0"
[[package]]
@@ -4246,14 +4257,14 @@ typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""}
[[package]]
name = "supabase"
version = "2.12.0"
version = "2.13.0"
description = "Supabase client for Python."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "supabase-2.12.0-py3-none-any.whl", hash = "sha256:f8896f3314179fdf27f8bb8357947493ec32b98dcdac7114208aaf21cd59ce35"},
{file = "supabase-2.12.0.tar.gz", hash = "sha256:284612c3e94ff4ed2f18c985f5eba4d6e17b5e2bf16a9a24290bb83c9c217078"},
{file = "supabase-2.13.0-py3-none-any.whl", hash = "sha256:6cfccc055be21dab311afc5e9d5b37f3a4966f8394703763fbc8f8e86f36eaa6"},
{file = "supabase-2.13.0.tar.gz", hash = "sha256:452574d34bd978c8d11b5f02b0182b48e8854e511c969483c83875ec01495f11"},
]
[package.dependencies]
@@ -4311,49 +4322,6 @@ files = [
[package.dependencies]
requests = ">=2.32.3,<3.0.0"
[[package]]
name = "tomli"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
name = "tomlkit"
version = "0.13.2"
@@ -4524,7 +4492,6 @@ h11 = ">=0.8"
httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
@@ -5049,5 +5016,5 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "0f7dda46ad3cce4486a3ca4162babff974bd270c100c7b82b1ff8dec563488ad"
python-versions = ">=3.11,<3.13"
content-hash = "cf3242137be7977795e01a61c058ac01202a1b6e0a03172a22894eb652ff2aab"

View File

@@ -8,25 +8,25 @@ packages = [{ include = "backend", format = "sdist" }]
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
python = ">=3.11,<3.13"
aio-pika = "^9.5.4"
anthropic = "^0.45.2"
apscheduler = "^3.11.0"
autogpt-libs = { path = "../autogpt_libs", develop = true }
click = "^8.1.7"
discord-py = "^2.4.0"
e2b-code-interpreter = "^1.0.1"
fastapi = "^0.115.5"
e2b-code-interpreter = "^1.0.5"
fastapi = "^0.115.8"
feedparser = "^6.0.11"
flake8 = "^7.0.0"
google-api-python-client = "^2.160.0"
google-auth-oauthlib = "^1.2.1"
groq = "^0.15.0"
groq = "^0.18.0"
jinja2 = "^3.1.4"
jsonref = "^1.1.0"
jsonschema = "^4.22.0"
ollama = "^0.4.1"
openai = "^1.60.2"
openai = "^1.61.1"
praw = "~7.8.1"
prisma = "^0.15.0"
psutil = "^6.1.0"
@@ -40,7 +40,7 @@ redis = "^5.2.0"
sentry-sdk = "2.20.0"
strenum = "^0.4.9"
stripe = "^11.5.0"
supabase = "2.12.0"
supabase = "2.13.0"
tenacity = "^9.0.0"
tweepy = "^4.14.0"
uvicorn = { extras = ["standard"], version = "^0.34.0" }
@@ -53,11 +53,13 @@ cryptography = "^43.0"
python-multipart = "^0.0.20"
sqlalchemy = "^2.0.36"
psycopg2-binary = "^2.9.10"
google-cloud-storage = "^2.18.2"
google-cloud-storage = "^3.0.0"
launchdarkly-server-sdk = "^9.8.0"
mem0ai = "^0.1.48"
todoist-api-python = "^2.1.7"
moviepy = "^2.1.2"
gravitasml = "0.1.2"
pika = "^1.3.2"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.32.1"

View File

@@ -40,7 +40,7 @@ async def execute_graph(
graph_version=test_graph.version,
node_input=input_data,
)
graph_exec_id = response["id"]
graph_exec_id = response.graph_exec_id
logger.info(f"Created execution with ID: {graph_exec_id}")
# Execution queue should be empty

View File

@@ -34,29 +34,29 @@ def test_disconnect(
connection_manager: ConnectionManager, mock_websocket: AsyncMock
) -> None:
connection_manager.active_connections.add(mock_websocket)
connection_manager.subscriptions["test_graph"] = {mock_websocket}
connection_manager.subscriptions["test_graph_1"] = {mock_websocket}
connection_manager.disconnect(mock_websocket)
assert mock_websocket not in connection_manager.active_connections
assert mock_websocket not in connection_manager.subscriptions["test_graph"]
assert mock_websocket not in connection_manager.subscriptions["test_graph_1"]
@pytest.mark.asyncio
async def test_subscribe(
connection_manager: ConnectionManager, mock_websocket: AsyncMock
) -> None:
await connection_manager.subscribe("test_graph", mock_websocket)
assert mock_websocket in connection_manager.subscriptions["test_graph"]
await connection_manager.subscribe("test_graph", 1, mock_websocket)
assert mock_websocket in connection_manager.subscriptions["test_graph_1"]
@pytest.mark.asyncio
async def test_unsubscribe(
connection_manager: ConnectionManager, mock_websocket: AsyncMock
) -> None:
connection_manager.subscriptions["test_graph"] = {mock_websocket}
connection_manager.subscriptions["test_graph_1"] = {mock_websocket}
await connection_manager.unsubscribe("test_graph", mock_websocket)
await connection_manager.unsubscribe("test_graph", 1, mock_websocket)
assert "test_graph" not in connection_manager.subscriptions
@@ -65,7 +65,7 @@ async def test_unsubscribe(
async def test_send_execution_result(
connection_manager: ConnectionManager, mock_websocket: AsyncMock
) -> None:
connection_manager.subscriptions["test_graph"] = {mock_websocket}
connection_manager.subscriptions["test_graph_1"] = {mock_websocket}
result: ExecutionResult = ExecutionResult(
graph_id="test_graph",
graph_version=1,
@@ -87,7 +87,7 @@ async def test_send_execution_result(
mock_websocket.send_text.assert_called_once_with(
WsMessage(
method=Methods.EXECUTION_EVENT,
channel="test_graph",
channel="test_graph_1",
data=result.model_dump(),
).model_dump_json()
)

View File

@@ -30,7 +30,8 @@ async def test_websocket_router_subscribe(
) -> None:
mock_websocket.receive_text.side_effect = [
WsMessage(
method=Methods.SUBSCRIBE, data={"graph_id": "test_graph"}
method=Methods.SUBSCRIBE,
data={"graph_id": "test_graph", "graph_version": 1},
).model_dump_json(),
WebSocketDisconnect(),
]
@@ -40,7 +41,7 @@ async def test_websocket_router_subscribe(
)
mock_manager.connect.assert_called_once_with(mock_websocket)
mock_manager.subscribe.assert_called_once_with("test_graph", mock_websocket)
mock_manager.subscribe.assert_called_once_with("test_graph", 1, mock_websocket)
mock_websocket.send_text.assert_called_once()
assert '"method":"subscribe"' in mock_websocket.send_text.call_args[0][0]
assert '"success":true' in mock_websocket.send_text.call_args[0][0]
@@ -53,7 +54,8 @@ async def test_websocket_router_unsubscribe(
) -> None:
mock_websocket.receive_text.side_effect = [
WsMessage(
method=Methods.UNSUBSCRIBE, data={"graph_id": "test_graph"}
method=Methods.UNSUBSCRIBE,
data={"graph_id": "test_graph", "graph_version": 1},
).model_dump_json(),
WebSocketDisconnect(),
]
@@ -63,7 +65,7 @@ async def test_websocket_router_unsubscribe(
)
mock_manager.connect.assert_called_once_with(mock_websocket)
mock_manager.unsubscribe.assert_called_once_with("test_graph", mock_websocket)
mock_manager.unsubscribe.assert_called_once_with("test_graph", 1, mock_websocket)
mock_websocket.send_text.assert_called_once()
assert '"method":"unsubscribe"' in mock_websocket.send_text.call_args[0][0]
assert '"success":true' in mock_websocket.send_text.call_args[0][0]
@@ -94,13 +96,15 @@ async def test_websocket_router_invalid_method(
async def test_handle_subscribe_success(
mock_websocket: AsyncMock, mock_manager: AsyncMock
) -> None:
message = WsMessage(method=Methods.SUBSCRIBE, data={"graph_id": "test_graph"})
message = WsMessage(
method=Methods.SUBSCRIBE, data={"graph_id": "test_graph", "graph_version": 1}
)
await handle_subscribe(
cast(WebSocket, mock_websocket), cast(ConnectionManager, mock_manager), message
)
mock_manager.subscribe.assert_called_once_with("test_graph", mock_websocket)
mock_manager.subscribe.assert_called_once_with("test_graph", 1, mock_websocket)
mock_websocket.send_text.assert_called_once()
assert '"method":"subscribe"' in mock_websocket.send_text.call_args[0][0]
assert '"success":true' in mock_websocket.send_text.call_args[0][0]
@@ -126,13 +130,15 @@ async def test_handle_subscribe_missing_data(
async def test_handle_unsubscribe_success(
mock_websocket: AsyncMock, mock_manager: AsyncMock
) -> None:
message = WsMessage(method=Methods.UNSUBSCRIBE, data={"graph_id": "test_graph"})
message = WsMessage(
method=Methods.UNSUBSCRIBE, data={"graph_id": "test_graph", "graph_version": 1}
)
await handle_unsubscribe(
cast(WebSocket, mock_websocket), cast(ConnectionManager, mock_manager), message
)
mock_manager.unsubscribe.assert_called_once_with("test_graph", mock_websocket)
mock_manager.unsubscribe.assert_called_once_with("test_graph", 1, mock_websocket)
mock_websocket.send_text.assert_called_once()
assert '"method":"unsubscribe"' in mock_websocket.send_text.call_args[0][0]
assert '"success":true' in mock_websocket.send_text.call_args[0][0]

View File

@@ -36,6 +36,21 @@ services:
interval: 10s
timeout: 5s
retries: 5
rabbitmq:
image: rabbitmq:management
container_name: rabbitmq
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 30s
timeout: 10s
retries: 5
start_period: 10s
environment:
- RABBITMQ_DEFAULT_USER=rabbitmq_user_default
- RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 # CHANGE THIS TO A RANDOM PASSWORD IN PRODUCTION -- everywhere lol
ports:
- "5672:5672"
- "15672:15672"
rest_server:
build:
@@ -55,6 +70,8 @@ services:
condition: service_healthy
migrate:
condition: service_completed_successfully
rabbitmq:
condition: service_healthy
environment:
- SUPABASE_URL=http://kong:8000
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
@@ -62,6 +79,10 @@ services:
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform
- REDIS_HOST=redis
- REDIS_PORT=6379
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
- RABBITMQ_USER=rabbitmq_user_default
- RABBITMQ_PASSWORD=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
- REDIS_PASSWORD=password
- ENABLE_AUTH=true
- PYRO_HOST=0.0.0.0
@@ -69,7 +90,7 @@ services:
- EXECUTIONMANAGER_HOST=executor
- FRONTEND_BASE_URL=http://localhost:3000
- BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"]
- ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!!
- ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!!
ports:
- "8006:8006"
- "8003:8003" # execution scheduler
@@ -90,6 +111,8 @@ services:
depends_on:
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
db:
condition: service_healthy
migrate:
@@ -102,10 +125,14 @@ services:
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=password
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
- RABBITMQ_USER=rabbitmq_user_default
- RABBITMQ_PASSWORD=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
- ENABLE_AUTH=true
- PYRO_HOST=0.0.0.0
- AGENTSERVER_HOST=rest_server
- ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!!
- ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!!
ports:
- "8002:8000"
networks:
@@ -127,14 +154,20 @@ services:
condition: service_healthy
redis:
condition: service_healthy
# rabbitmq:
# condition: service_healthy
migrate:
condition: service_completed_successfully
condition: service_completed_successfully
environment:
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=password
# - RABBITMQ_HOST=rabbitmq # TODO: Uncomment this when we have a need for it in websocket (like nofifying when stuff went down)
# - RABBITMQ_PORT=5672
# - RABBITMQ_USER=rabbitmq_user_default
# - RABBITMQ_PASSWORD=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7
- ENABLE_AUTH=true
- PYRO_HOST=0.0.0.0
- BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"]

View File

@@ -33,6 +33,12 @@ services:
file: ./docker-compose.platform.yml
service: redis
rabbitmq:
<<: *agpt-services
extends:
file: ./docker-compose.platform.yml
service: rabbitmq
rest_server:
<<: *agpt-services
extends:
@@ -146,3 +152,4 @@ services:
- db
- vector
- redis
- rabbitmq

View File

@@ -45,7 +45,7 @@
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",
"@sentry/nextjs": "^8",
"@stripe/stripe-js": "^5.5.0",
"@stripe/stripe-js": "^5.6.0",
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.48.1",
"@tanstack/react-table": "^8.20.6",
@@ -60,9 +60,9 @@
"dotenv": "^16.4.7",
"elliptic": "6.6.1",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^11.16.0",
"framer-motion": "^12.0.11",
"geist": "^1.3.1",
"launchdarkly-react-client-sdk": "^3.6.0",
"launchdarkly-react-client-sdk": "^3.6.1",
"lucide-react": "^0.474.0",
"moment": "^2.30.1",
"next": "^14.2.21",
@@ -75,7 +75,7 @@
"react-markdown": "^9.0.3",
"react-modal": "^3.16.3",
"react-shepherd": "^6.1.7",
"recharts": "^2.14.1",
"recharts": "^2.15.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.5",
@@ -83,24 +83,24 @@
},
"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",
"@playwright/test": "^1.50.1",
"@storybook/addon-a11y": "^8.5.3",
"@storybook/addon-essentials": "^8.5.3",
"@storybook/addon-interactions": "^8.5.3",
"@storybook/addon-links": "^8.5.3",
"@storybook/addon-onboarding": "^8.5.3",
"@storybook/blocks": "^8.5.3",
"@storybook/nextjs": "^8.5.3",
"@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.13.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-modal": "^3.16.3",
"axe-playwright": "^2.0.3",
"chromatic": "^11.25.1",
"chromatic": "^11.25.2",
"concurrently": "^9.1.2",
"eslint": "^8",
"eslint-config-next": "15.1.6",
@@ -110,7 +110,7 @@
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.5.2",
"storybook": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5"
},

View File

@@ -10,6 +10,7 @@ export default function Home() {
<FlowEditor
className="flow-container"
flowID={query.get("flowID") ?? undefined}
flowVersion={query.get("flowVersion") ?? undefined}
/>
);
}

View File

@@ -66,7 +66,7 @@ export default async function RootLayout({
{
icon: IconType.Edit,
text: "Edit profile",
href: "/marketplace/profile",
href: "/profile",
},
],
},
@@ -75,7 +75,7 @@ export default async function RootLayout({
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/marketplace/dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
@@ -88,7 +88,7 @@ export default async function RootLayout({
{
icon: IconType.Settings,
text: "Settings",
href: "/marketplace/settings",
href: "/profile/settings",
},
],
},

View File

@@ -1,9 +1,6 @@
import * as React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import {
FeaturedSection,
FeaturedAgent,
} from "@/components/agptui/composite/FeaturedSection";
import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
@@ -155,9 +152,7 @@ export default async function Page({}: {}) {
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
<FeaturedSection
featuredAgents={featuredAgents.agents as FeaturedAgent[]}
/>
<FeaturedSection featuredAgents={featuredAgents.agents} />
<Separator />
<AgentsSection
sectionTitle="Top Agents"

View File

@@ -37,8 +37,8 @@ const Monitor = () => {
);
const fetchAgents = useCallback(() => {
api.listLibraryAgents().then((agent) => {
setFlows(agent);
api.listLibraryAgents().then((agents) => {
setFlows(agents);
});
api.getExecutions().then((executions) => {
setExecutions(executions);

View File

@@ -23,6 +23,7 @@ export default function CreditsPage() {
updateAutoTopUpConfig,
transactionHistory,
fetchTransactionHistory,
formatCredits,
} = useCredits();
const router = useRouter();
const searchParams = useSearchParams();
@@ -59,7 +60,8 @@ export default function CreditsPage() {
const submitTopUp = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const amount = parseInt(new FormData(form).get("topUpAmount") as string);
const amount =
parseInt(new FormData(form).get("topUpAmount") as string) * 100;
toastOnFail("request top-up", () => requestTopUp(amount));
};
@@ -67,8 +69,8 @@ export default function CreditsPage() {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const amount = parseInt(formData.get("topUpAmount") as string);
const threshold = parseInt(formData.get("threshold") as string);
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! 🎉" });
@@ -79,7 +81,7 @@ export default function CreditsPage() {
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
Billing
</h1>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
@@ -108,16 +110,16 @@ export default function CreditsPage() {
htmlFor="topUpAmount"
className="mb-1 block text-neutral-700"
>
Top-up Amount (Credits)
Top-up amount (USD), minimum $5:
</label>
<input
type="number"
id="topUpAmount"
name="topUpAmount"
placeholder="Enter top-up amount"
min="500"
step="100"
defaultValue={500}
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
/>
@@ -130,49 +132,83 @@ export default function CreditsPage() {
{/* Auto Top-up Form */}
<form onSubmit={submitAutoTopUpConfig} className="mt-6 space-y-4">
<h3 className="text-lg font-medium">Auto Top-up Configuration</h3>
<div>
<label
htmlFor="autoTopUpAmount"
className="mb-1 block text-neutral-700"
>
Auto Top-up Amount (Credits)
</label>
<input
type="number"
id="autoTopUpAmount"
name="topUpAmount"
defaultValue={autoTopUpConfig?.amount || ""}
placeholder="Enter auto top-up amount"
step="100"
className="w-full rounded-md border border-slate-200 px-4 py-2 dark:border-slate-700 dark:bg-slate-800"
required
/>
</div>
<h3 className="text-lg font-medium">Automatic Refill Settings</h3>
<div>
<label
htmlFor="threshold"
className="mb-1 block text-neutral-700"
>
Threshold (Credits)
When my balance goes below this amount:
</label>
<input
type="number"
id="threshold"
name="threshold"
defaultValue={autoTopUpConfig?.threshold || ""}
placeholder="Enter threshold value"
step="100"
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>
<Button type="submit" className="w-full">
Save
</Button>
<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>
<p className="text-sm">
<b>Note:</b> For your safety, we will top up your balance{" "}
<b>at most once</b> per agent execution to prevent unintended
excessive charges. Therefore, ensure that the automatic top-up
amount is sufficient for your agent&apos;s operation.
</p>
{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>
@@ -232,9 +268,9 @@ export default function CreditsPage() {
transaction.amount > 0 ? "text-green-500" : "text-red-500"
}
>
<b>{transaction.amount}</b>
<b>{formatCredits(transaction.amount)}</b>
</TableCell>
<TableCell>{transaction.balance}</TableCell>
<TableCell>{formatCredits(transaction.balance)}</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -2,7 +2,6 @@
import * as React from "react";
import { AgentTable } from "@/components/agptui/AgentTable";
import { AgentTableRowProps } from "@/components/agptui/AgentTableRow";
import { Button } from "@/components/agptui/Button";
import { Separator } from "@/components/ui/separator";
import { StatusType } from "@/components/agptui/Status";

View File

@@ -2,10 +2,9 @@
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useCallback, useContext, useMemo, useState } from "react";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/components/ui/use-toast";
import { IconKey, IconUser } from "@/components/ui/icons";
import { LogOutIcon, Trash2Icon } from "lucide-react";
import { Trash2Icon } from "lucide-react";
import { providerIcons } from "@/components/integrations/credentials-input";
import { CredentialsProvidersContext } from "@/components/integrations/credentials-provider";
import {

View File

@@ -5,13 +5,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const sidebarLinkGroups = [
{
links: [
{ text: "Creator Dashboard", href: "/marketplace/dashboard" },
{ text: "Agent dashboard", href: "/marketplace/agent-dashboard" },
{ text: "Credits", href: "/marketplace/credits" },
{ text: "Integrations", href: "/marketplace/integrations" },
{ text: "API Keys", href: "/marketplace/api_keys" },
{ text: "Profile", href: "/marketplace/profile" },
{ text: "Settings", href: "/marketplace/settings" },
{ text: "Creator Dashboard", href: "/profile/dashboard" },
{ text: "Agent dashboard", href: "/profile/agent-dashboard" },
{ text: "Billing", href: "/profile/credits" },
{ text: "Integrations", href: "/profile/integrations" },
{ text: "API Keys", href: "/profile/api_keys" },
{ text: "Profile", href: "/profile" },
{ text: "Settings", href: "/profile/settings" },
],
},
];

View File

@@ -1,242 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useCallback, useContext, useMemo, useState } from "react";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/components/ui/use-toast";
import { IconKey, IconUser } from "@/components/ui/icons";
import { LogOutIcon, Trash2Icon } from "lucide-react";
import { providerIcons } from "@/components/integrations/credentials-input";
import { CredentialsProvidersContext } from "@/components/integrations/credentials-provider";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CredentialsProviderName } from "@/lib/autogpt-server-api";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import useSupabase from "@/hooks/useSupabase";
import Spinner from "@/components/Spinner";
export default function PrivatePage() {
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const providers = useContext(CredentialsProvidersContext);
const { toast } = useToast();
const [confirmationDialogState, setConfirmationDialogState] = useState<
| {
open: true;
message: string;
onConfirm: () => void;
onReject: () => void;
}
| { open: false }
>({ open: false });
const removeCredentials = useCallback(
async (
provider: CredentialsProviderName,
id: string,
force: boolean = false,
) => {
if (!providers || !providers[provider]) {
return;
}
let result;
try {
result = await providers[provider].deleteCredentials(id, force);
} catch (error: any) {
toast({
title: "Something went wrong when deleting credentials: " + error,
variant: "destructive",
duration: 2000,
});
setConfirmationDialogState({ open: false });
return;
}
if (result.deleted) {
if (result.revoked) {
toast({
title: "Credentials deleted",
duration: 2000,
});
} else {
toast({
title: "Credentials deleted from AutoGPT",
description: `You may also manually remove the connection to AutoGPT at ${provider}!`,
duration: 3000,
});
}
setConfirmationDialogState({ open: false });
} else if (result.need_confirmation) {
setConfirmationDialogState({
open: true,
message: result.message,
onConfirm: () => removeCredentials(provider, id, true),
onReject: () => setConfirmationDialogState({ open: false }),
});
}
},
[providers, toast],
);
//TODO: remove when the way system credentials are handled is updated
// This contains ids for built-in "Use Credits for X" credentials
const hiddenCredentials = useMemo(
() => [
"744fdc56-071a-4761-b5a5-0af0ce10a2b5", // Ollama
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
"53c25cb8-e3ee-465c-a4d1-e75a4c899c2a", // OpenAI
"24e5d942-d9e3-4798-8151-90143ee55629", // Anthropic
"4ec22295-8f97-4dd1-b42b-2c6957a02545", // Groq
"7f7b0654-c36b-4565-8fa7-9a52575dfae2", // D-ID
"7f26de70-ba0d-494e-ba76-238e65e7b45f", // Jina
"66f20754-1b81-48e4-91d0-f4f0dd82145f", // Unreal Speech
"b5a0e27d-0c98-4df3-a4b9-10193e1f3c40", // Open Router
"6c0f5bd0-9008-4638-9d79-4b40b631803e", // FAL
"96153e04-9c6c-4486-895f-5bb683b1ecec", // Exa
"78d19fd7-4d59-4a16-8277-3ce310acf2b7", // E2B
"96b83908-2789-4dec-9968-18f0ece4ceb3", // Nvidia
"ed55ac19-356e-4243-a6cb-bc599e9b716f", // Mem0
],
[],
);
if (isUserLoading) {
return <Spinner />;
}
if (!user || !supabase) {
router.push("/login");
return null;
}
const allCredentials = providers
? Object.values(providers).flatMap((provider) =>
[
...provider.savedOAuthCredentials,
...provider.savedApiKeys,
...provider.savedUserPasswordCredentials,
]
.filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({
...credentials,
provider: provider.provider,
providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider],
TypeIcon: {
oauth2: IconUser,
api_key: IconKey,
user_password: IconKey,
}[credentials.type],
})),
)
: [];
return (
<div className="mx-auto max-w-3xl md:py-8">
<div className="flex items-center justify-between">
<p>
Hello <span data-testid="profile-email">{user.email}</span>
</p>
<Button onClick={() => supabase.auth.signOut()}>
<LogOutIcon className="mr-1.5 size-4" />
Log out
</Button>
</div>
<Separator className="my-6" />
<h2 className="mb-4 text-lg">Connections & Credentials</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Provider</TableHead>
<TableHead>Name</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allCredentials.map((cred) => (
<TableRow key={cred.id}>
<TableCell>
<div className="flex items-center space-x-1.5">
<cred.ProviderIcon className="h-4 w-4" />
<strong>{cred.providerName}</strong>
</div>
</TableCell>
<TableCell>
<div className="flex h-full items-center space-x-1.5">
<cred.TypeIcon />
<span>{cred.title || cred.username}</span>
</div>
<small className="text-muted-foreground">
{
{
oauth2: "OAuth2 credentials",
api_key: "API key",
user_password: "User password",
}[cred.type]
}{" "}
- <code>{cred.id}</code>
</small>
</TableCell>
<TableCell className="w-0 whitespace-nowrap">
<Button
variant="destructive"
onClick={() => removeCredentials(cred.provider, cred.id)}
>
<Trash2Icon className="mr-1.5 size-4" /> Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<AlertDialog open={confirmationDialogState.open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
{confirmationDialogState.open && confirmationDialogState.message}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={() =>
confirmationDialogState.open &&
confirmationDialogState.onReject()
}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() =>
confirmationDialogState.open &&
confirmationDialogState.onConfirm()
}
>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -36,9 +36,8 @@ export async function signup(values: z.infer<typeof signupFormSchema>) {
if (data.session) {
await supabase.auth.setSession(data.session);
}
console.log("Signed up");
revalidatePath("/", "layout");
redirect("/marketplace/profile");
redirect("/");
},
);
}

View File

@@ -40,7 +40,6 @@ export function CustomEdge({
targetY,
markerEnd,
}: EdgeProps<CustomEdge>) {
const [isHovered, setIsHovered] = useState(false);
const [beads, setBeads] = useState<{
beads: Bead[];
created: number;
@@ -182,13 +181,7 @@ export function CustomEdge({
<BaseEdge
path={svgPath}
markerEnd={markerEnd}
style={{
strokeWidth: (isHovered ? 3 : 2) + (data?.isStatic ? 0.5 : 0),
stroke:
(data?.edgeColor ?? "#555555") +
(selected || isHovered ? "" : "80"),
strokeDasharray: data?.isStatic ? "5 3" : "0",
}}
className={`transition-all duration-200 ${data?.isStatic ? "[stroke-dasharray:5_3]" : "[stroke-dasharray:0]"} [stroke-width:${data?.isStatic ? 2.5 : 2}px] hover:[stroke-width:${data?.isStatic ? 3.5 : 3}px] ${selected ? `[stroke:${data?.edgeColor ?? "#555555"}]` : `[stroke:${data?.edgeColor ?? "#555555"}80] hover:[stroke:${data?.edgeColor ?? "#555555"}]`}`}
/>
<path
d={svgPath}
@@ -196,8 +189,6 @@ export function CustomEdge({
strokeOpacity={0}
strokeWidth={20}
className="react-flow__edge-interaction"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
<EdgeLabelRenderer>
<div
@@ -209,9 +200,7 @@ export function CustomEdge({
className="edge-label-renderer"
>
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`edge-label-button ${isHovered ? "visible" : ""}`}
className="edge-label-button opacity-0 transition-opacity duration-200 hover:opacity-100"
onClick={onEdgeRemoveClick}
>
<X className="size-4" />

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ import {
import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { BlockUIType, Link } from "@/lib/autogpt-server-api";
import { BlockUIType, formatEdgeID } from "@/lib/autogpt-server-api";
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
@@ -70,8 +70,9 @@ export const FlowContext = createContext<FlowContextType | null>(null);
const FlowEditor: React.FC<{
flowID?: string;
flowVersion?: string;
className?: string;
}> = ({ flowID, className }) => {
}> = ({ flowID, flowVersion, className }) => {
const {
addNodes,
addEdges,
@@ -85,6 +86,7 @@ const FlowEditor: React.FC<{
const [visualizeBeads, setVisualizeBeads] = useState<
"no" | "static" | "animate"
>("animate");
const [flowExecutionID, setFlowExecutionID] = useState<string | undefined>();
const {
agentName,
setAgentName,
@@ -107,7 +109,12 @@ const FlowEditor: React.FC<{
setNodes,
edges,
setEdges,
} = useAgentGraph(flowID, visualizeBeads !== "no");
} = useAgentGraph(
flowID,
flowVersion ? parseInt(flowVersion) : undefined,
flowExecutionID,
visualizeBeads !== "no",
);
const router = useRouter();
const pathname = usePathname();
@@ -157,6 +164,7 @@ const FlowEditor: React.FC<{
if (params.get("open_scheduling") === "true") {
setOpenCron(true);
}
setFlowExecutionID(params.get("flowExecutionID") || undefined);
}, [params]);
useEffect(() => {
@@ -267,14 +275,6 @@ const FlowEditor: React.FC<{
[deleteElements, setNodes, nodes, edges, addNodes],
);
const formatEdgeID = useCallback((conn: Link | Connection): string => {
if ("sink_id" in conn) {
return `${conn.source_id}_${conn.source_name}_${conn.sink_id}_${conn.sink_name}`;
} else {
return `${conn.source}_${conn.sourceHandle}_${conn.target}_${conn.targetHandle}`;
}
}, []);
const onConnect: OnConnect = useCallback(
(connection: Connection) => {
// Check if this exact connection already exists

View File

@@ -1,84 +0,0 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import Image from "next/image";
import getServerUser from "@/lib/supabase/getServerUser";
import ProfileDropdown from "./ProfileDropdown";
import { IconCircleUser, IconMenu } from "@/components/ui/icons";
import CreditButton from "@/components/nav/CreditButton";
import { NavBarButtons } from "./nav/NavBarButtons";
export async function NavBar() {
const isAvailable = Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
const { user } = await getServerUser();
return user ? (
<header className="sticky top-0 z-50 mx-4 flex h-16 select-none items-center gap-4 border border-gray-300 bg-background p-3 md:rounded-b-2xl md:px-6 md:shadow">
<div className="flex flex-1 items-center gap-4">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<IconMenu />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="grid gap-6 text-lg font-medium">
<NavBarButtons className="flex flex-row items-center gap-2" />
</nav>
</SheetContent>
</Sheet>
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-8">
<div className="flex h-10 w-20 flex-1 flex-row items-center justify-center gap-2">
<a href="https://agpt.co/">
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={40}
priority
/>
</a>
</div>
<NavBarButtons className="flex flex-row items-center gap-1 border border-white font-semibold hover:border-gray-900" />
</nav>
</div>
<div className="flex flex-1 items-center justify-end gap-4">
{isAvailable && user && <CreditButton />}
{isAvailable && !user && (
<Link
href="/login"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
Log In
<IconCircleUser />
</Link>
)}
{isAvailable && user && <ProfileDropdown />}
</div>
</header>
) : (
<nav className="flex w-full items-center p-2 pt-8">
<div className="flex h-10 w-20 flex-1 flex-row items-center justify-center gap-2">
<a href="https://agpt.co/">
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={40}
priority
/>
</a>
</div>
</nav>
);
}

View File

@@ -1,6 +1,6 @@
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { FC, memo, useCallback } from "react";
import { Handle, Position } from "@xyflow/react";
import SchemaTooltip from "./SchemaTooltip";
@@ -13,6 +13,32 @@ type HandleProps = {
title?: string;
};
// Move the constant out of the component to avoid re-creation on every render.
const TYPE_NAME: Record<string, string> = {
string: "text",
number: "number",
integer: "integer",
boolean: "true/false",
object: "object",
array: "list",
null: "null",
};
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
const Dot: FC<{ isConnected: boolean; type?: string }> = memo(
({ isConnected, type }) => {
const color = isConnected
? getTypeBgColor(type || "any")
: "border-gray-300 dark:border-gray-600";
return (
<div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
/>
);
},
);
Dot.displayName = "Dot";
const NodeHandle: FC<HandleProps> = ({
keyName,
schema,
@@ -21,17 +47,9 @@ const NodeHandle: FC<HandleProps> = ({
side,
title,
}) => {
const typeName: Record<string, string> = {
string: "text",
number: "number",
integer: "integer",
boolean: "true/false",
object: "object",
array: "list",
null: "null",
};
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${side === "left" ? "text-left" : "text-right"}`;
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${
side === "left" ? "text-left" : "text-right"
}`;
const label = (
<div className="flex flex-grow flex-row">
@@ -40,25 +58,27 @@ const NodeHandle: FC<HandleProps> = ({
{isRequired ? "*" : ""}
</span>
<span className={`${typeClass} flex items-end`}>
({typeName[schema.type as keyof typeof typeName] || "any"})
({TYPE_NAME[schema.type as keyof typeof TYPE_NAME] || "any"})
</span>
</div>
);
const Dot = () => {
const color = isConnected
? getTypeBgColor(schema.type || "any")
: "border-gray-300 dark:border-gray-600";
return (
<div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
/>
);
};
// Use a native HTML onContextMenu handler instead of wrapping a large node with a Radix ContextMenu trigger.
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
// Optionally, you can trigger a custom, lightweight context menu here.
},
[],
);
if (side === "left") {
return (
<div key={keyName} className="handle-container">
<div
key={keyName}
className="handle-container"
onContextMenu={handleContextMenu}
>
<Handle
type="target"
data-testid={`input-handle-${keyName}`}
@@ -67,7 +87,7 @@ const NodeHandle: FC<HandleProps> = ({
className="group -ml-[38px]"
>
<div className="pointer-events-none flex items-center">
<Dot />
<Dot isConnected={isConnected} type={schema.type} />
{label}
</div>
</Handle>
@@ -76,7 +96,11 @@ const NodeHandle: FC<HandleProps> = ({
);
} else {
return (
<div key={keyName} className="handle-container justify-end">
<div
key={keyName}
className="handle-container justify-end"
onContextMenu={handleContextMenu}
>
<Handle
type="source"
data-testid={`output-handle-${keyName}`}
@@ -86,7 +110,7 @@ const NodeHandle: FC<HandleProps> = ({
>
<div className="pointer-events-none flex items-center">
{label}
<Dot />
<Dot isConnected={isConnected} type={schema.type} />
</div>
</Handle>
</div>
@@ -94,4 +118,4 @@ const NodeHandle: FC<HandleProps> = ({
}
};
export default NodeHandle;
export default memo(NodeHandle);

View File

@@ -8,28 +8,21 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import useCredits from "@/hooks/useCredits";
interface CreditsCardProps {
credits: number;
}
const CreditsCard = ({ credits }: CreditsCardProps) => {
const [currentCredits, setCurrentCredits] = useState(credits);
const CreditsCard = () => {
const { credits, formatCredits, fetchCredits } = useCredits();
const api = useBackendAPI();
const onRefresh = async () => {
const { credits } = await api.getUserCredit("credits-card");
setCurrentCredits(credits);
fetchCredits();
};
return (
<div className="inline-flex h-[48px] items-center gap-2.5 rounded-2xl bg-neutral-200 p-4 dark:bg-neutral-800">
<div className="flex items-center gap-0.5">
<span className="p-ui-semibold text-base leading-7 text-neutral-900 dark:text-neutral-50">
{currentCredits.toLocaleString()}
</span>
<span className="p-ui pl-1 text-base leading-7 text-neutral-900 dark:text-neutral-50">
credits
Balance: {formatCredits(credits)}
</span>
</div>
<Tooltip key="RefreshCredits" delayDuration={500}>

View File

@@ -0,0 +1,77 @@
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useState } from "react";
import { StoreAgent } from "@/lib/autogpt-server-api";
interface FeaturedStoreCardProps {
agent: StoreAgent;
backgroundColor: string;
}
export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
agent,
backgroundColor,
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
<Card
data-testid="featured-store-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={backgroundColor}
>
<CardHeader>
<CardTitle>{agent.agent_name}</CardTitle>
<CardDescription>{agent.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="relative h-[397px] w-full overflow-hidden rounded-xl">
<div
className={`transition-opacity duration-200 ${isHovered ? "opacity-0" : "opacity-100"}`}
>
<Image
src={agent.agent_image || "/AUTOgpt_Logo_dark.png"}
alt={`${agent.agent_name} preview`}
fill
sizes="100%"
className="rounded-xl object-cover"
/>
</div>
<div
className={`absolute inset-0 overflow-y-auto transition-opacity duration-200 ${
isHovered ? "opacity-100" : "opacity-0"
} rounded-xl dark:bg-neutral-700`}
>
<p className="text-base text-neutral-800 dark:text-neutral-200">
{agent.description}
</p>
</div>
</div>
</CardContent>
<CardFooter className="flex items-center justify-between">
<div className="font-semibold">
{agent.runs?.toLocaleString() ?? "0"} runs
</div>
<div className="flex items-center gap-1.5">
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
<div
className="inline-flex items-center justify-start gap-px"
role="img"
aria-label={`Rating: ${agent.rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(agent.rating)}
</div>
</div>
</CardFooter>
</Card>
);
};

View File

@@ -1,10 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeaturedStoreCard } from "./FeaturedStoreCard";
import { FeaturedAgentCard } from "./FeaturedAgentCard";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/Featured Store Card",
component: FeaturedStoreCard,
component: FeaturedAgentCard,
parameters: {
layout: {
center: true,
@@ -13,123 +13,63 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
agentName: { control: "text" },
subHeading: { control: "text" },
agentImage: { control: "text" },
creatorImage: { control: "text" },
creatorName: { control: "text" },
description: { control: "text" },
runs: { control: "number" },
rating: { control: "number", min: 0, max: 5, step: 0.1 },
onClick: { action: "clicked" },
agent: {
agent_name: { control: "text" },
sub_heading: { control: "text" },
agent_image: { control: "text" },
creator_avatar: { control: "text" },
creator: { control: "text" },
runs: { control: "number" },
rating: { control: "number", min: 0, max: 5, step: 0.1 },
slug: { control: "text" },
},
backgroundColor: {
control: "color",
},
},
} satisfies Meta<typeof FeaturedStoreCard>;
} satisfies Meta<typeof FeaturedAgentCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
agentName: "Personalized Morning Coffee Newsletter example of three lines",
subHeading:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
description:
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "AI Solutions Inc.",
runs: 50000,
rating: 4.7,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const LowRating: Story = {
args: {
agentName: "Data Analyzer Lite",
subHeading: "Basic data analysis tool",
description:
"A lightweight data analysis tool for basic data processing needs.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "DataTech",
runs: 10000,
rating: 2.8,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const HighRuns: Story = {
args: {
agentName: "CodeAssist AI",
subHeading: "Your AI coding companion",
description:
"An intelligent coding assistant that helps developers write better code faster.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "DevTools Co.",
runs: 1000000,
rating: 4.9,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const NoCreatorImage: Story = {
args: {
agentName: "MultiTasker",
subHeading: "All-in-one productivity suite",
description:
"A comprehensive productivity suite that combines task management, note-taking, and project planning into one seamless interface.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "Productivity Plus",
runs: 75000,
rating: 4.5,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const ShortDescription: Story = {
args: {
agentName: "QuickTask",
subHeading: "Fast task automation",
description: "Simple and efficient task automation tool.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "EfficientWorks",
runs: 50000,
rating: 4.2,
onClick: () => console.log("Card clicked"),
agent: {
agent_name:
"Personalized Morning Coffee Newsletter example of three lines",
sub_heading:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
description:
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator: "AI Solutions Inc.",
runs: 50000,
rating: 4.7,
slug: "",
},
backgroundColor: "bg-white",
},
};
export const WithInteraction: Story = {
args: {
agentName: "AI Writing Assistant",
subHeading: "Enhance your writing",
description:
"An AI-powered writing assistant that helps improve your writing style and clarity.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "WordCraft AI",
runs: 200000,
rating: 4.6,
onClick: () => console.log("Card clicked"),
agent: {
slug: "",
agent_name: "AI Writing Assistant",
sub_heading: "Enhance your writing",
description:
"An AI-powered writing assistant that helps improve your writing style and clarity.",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator: "WordCraft AI",
runs: 200000,
rating: 4.6,
},
backgroundColor: "bg-white",
},
play: async ({ canvasElement }) => {

View File

@@ -1,96 +0,0 @@
import * as React from "react";
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
interface FeaturedStoreCardProps {
agentName: string;
subHeading: string;
agentImage: string;
creatorImage?: string;
creatorName: string;
description: string; // Added description prop
runs: number;
rating: number;
onClick: () => void;
backgroundColor: string;
}
export const FeaturedStoreCard: React.FC<FeaturedStoreCardProps> = ({
agentName,
subHeading,
agentImage,
creatorImage,
creatorName,
description,
runs,
rating,
onClick,
backgroundColor,
}) => {
return (
<div
className={`group h-[755px] w-[440px] px-[22px] pb-5 pt-[30px] ${backgroundColor} inline-flex flex-col items-start justify-start gap-7 rounded-[26px] transition-all duration-200 hover:brightness-95 dark:bg-neutral-800`}
onClick={onClick}
data-testid="featured-store-card"
>
<div className="flex h-[188px] flex-col items-start justify-start gap-3 self-stretch">
<h2 className="font-poppins self-stretch text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100">
{agentName}
</h2>
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">
{subHeading}
</div>
</div>
<div className="flex h-[489px] flex-col items-start justify-start gap-[18px] self-stretch">
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">
by {creatorName}
</div>
<div className="relative h-[397px] self-stretch">
<Image
src={agentImage}
alt={`${agentName} preview`}
layout="fill"
objectFit="cover"
className="rounded-xl transition-opacity duration-200 group-hover:opacity-0"
/>
<div className="absolute inset-0 overflow-y-auto rounded-xl bg-white p-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100 dark:bg-neutral-700">
<div className="font-geist text-base font-normal leading-normal text-neutral-800 dark:text-neutral-200">
{description}
</div>
</div>
{creatorImage && (
<div className="absolute left-[8.74px] top-[313px] h-[74px] w-[74px] overflow-hidden rounded-full transition-opacity duration-200 group-hover:opacity-0">
<Image
src={creatorImage}
alt={`${creatorName} image`}
layout="fill"
className="object-cover"
priority
/>
</div>
)}
</div>
<div className="inline-flex items-center justify-between self-stretch">
<div className="font-large-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center justify-start gap-[5px]">
<div className="font-large-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{rating.toFixed(1)}
</div>
<div
className="inline-flex items-center justify-start gap-px"
role="img"
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(rating)}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -33,26 +33,17 @@ interface NavbarProps {
async function getProfileData() {
const api = new BackendAPI();
const [profile, credits] = await Promise.all([
api.getStoreProfile("navbar"),
api.getUserCredit("navbar"),
]);
const profile = await Promise.resolve(api.getStoreProfile("navbar"));
return {
profile,
credits,
};
return profile;
}
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
let credits: { credits: number } = { credits: 0 };
if (isLoggedIn) {
const { profile: t_profile, credits: t_credits } = await getProfileData();
profile = t_profile;
credits = t_credits;
profile = await getProfileData();
}
return (
@@ -70,7 +61,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
<div className="flex items-center gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <CreditsCard credits={credits.credits} />}
{profile && <CreditsCard />}
<ProfilePopoutMenu
menuItemGroups={menuItemGroups}
userName={profile?.username}

View File

@@ -46,7 +46,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
<Link
href="/marketplace/dashboard"
href="/profile/dashboard"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconDashboardLayout className="h-6 w-6" />
@@ -56,17 +56,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</Link>
{stripeAvailable && (
<Link
href="/marketplace/credits"
href="/profile/credits"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconCoin className="h-6 w-6" />
<div className="p-ui-medium text-base font-medium leading-normal">
Credits
Billing
</div>
</Link>
)}
<Link
href="/marketplace/integrations"
href="/profile/integrations"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconIntegrations className="h-6 w-6" />
@@ -75,7 +75,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/api_keys"
href="/profile/api_keys"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<KeyIcon className="h-6 w-6" />
@@ -84,7 +84,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/profile"
href="/profile"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconProfile className="h-6 w-6" />
@@ -93,7 +93,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/settings"
href="/profile/settings"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconSliders className="h-6 w-6" />
@@ -110,7 +110,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
<Link
href="/marketplace/dashboard"
href="/profile/dashboard"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconDashboardLayout className="h-6 w-6" />
@@ -120,17 +120,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</Link>
{stripeAvailable && (
<Link
href="/marketplace/credits"
href="/profile/credits"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconCoin className="h-6 w-6" />
<div className="p-ui-medium text-base font-medium leading-normal">
Credits
Billing
</div>
</Link>
)}
<Link
href="/marketplace/integrations"
href="/profile/integrations"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconIntegrations className="h-6 w-6" />
@@ -139,7 +139,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/api_keys"
href="/profile/api_keys"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<KeyIcon className="h-6 w-6" strokeWidth={1} />
@@ -148,7 +148,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/profile"
href="/profile"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconProfile className="h-6 w-6" />
@@ -157,7 +157,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/marketplace/settings"
href="/profile/settings"
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
>
<IconSliders className="h-6 w-6" />

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeaturedAgent, FeaturedSection } from "./FeaturedSection";
import { userEvent, within, expect } from "@storybook/test";
import { FeaturedSection } from "./FeaturedSection";
import { userEvent, within } from "@storybook/test";
import { StoreAgent } from "@/lib/autogpt-server-api";
const meta = {
title: "AGPT UI/Composite/Featured Agents",
@@ -15,7 +16,6 @@ const meta = {
tags: ["autodocs"],
argTypes: {
featuredAgents: { control: "object" },
// onCardClick: { action: "clicked" },
},
} satisfies Meta<typeof FeaturedSection>;
@@ -93,7 +93,7 @@ const mockFeaturedAgents = [
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "quicktask",
},
] satisfies FeaturedAgent[];
] satisfies StoreAgent[];
export const Default: Story = {
args: {

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { FeaturedStoreCard } from "@/components/agptui/FeaturedStoreCard";
import { FeaturedAgentCard } from "@/components/agptui/FeaturedAgentCard";
import {
Carousel,
CarouselContent,
@@ -11,7 +11,8 @@ import {
CarouselIndicator,
} from "@/components/ui/carousel";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { StoreAgent } from "@/lib/autogpt-server-api";
import Link from "next/link";
const BACKGROUND_COLORS = [
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
@@ -19,33 +20,14 @@ const BACKGROUND_COLORS = [
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
];
export interface FeaturedAgent {
slug: string;
agent_name: string;
agent_image: string;
creator: string;
creator_avatar: string;
sub_heading: string;
description: string;
runs: number;
rating: number;
}
interface FeaturedSectionProps {
featuredAgents: FeaturedAgent[];
featuredAgents: StoreAgent[];
}
export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
featuredAgents,
}) => {
const [currentSlide, setCurrentSlide] = useState(0);
const router = useRouter();
const handleCardClick = (creator: string, slug: string) => {
router.push(
`/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`,
);
};
const handlePrevSlide = useCallback(() => {
setCurrentSlide((prev) =>
@@ -84,17 +66,14 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
key={index}
className="max-w-[460px] flex-[0_0_auto]"
>
<FeaturedStoreCard
agentName={agent.agent_name}
subHeading={agent.sub_heading}
agentImage={agent.agent_image}
creatorName={agent.creator}
description={agent.description}
runs={agent.runs}
rating={agent.rating}
backgroundColor={getBackgroundColor(index)}
onClick={() => handleCardClick(agent.creator, agent.slug)}
/>
<Link
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
>
<FeaturedAgentCard
agent={agent}
backgroundColor={getBackgroundColor(index)}
/>
</Link>
</CarouselItem>
))}
</CarouselContent>

View File

@@ -76,6 +76,7 @@ export const providerIcons: Record<
open_router: fallbackIcon,
pinecone: fallbackIcon,
slant3d: fallbackIcon,
screenshotone: fallbackIcon,
smtp: fallbackIcon,
replicate: fallbackIcon,
reddit: fallbackIcon,

View File

@@ -41,6 +41,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
open_router: "Open Router",
pinecone: "Pinecone",
slant3d: "Slant3D",
screenshotone: "ScreenshotOne",
smtp: "SMTP",
reddit: "Reddit",
replicate: "Replicate",

View File

@@ -65,7 +65,7 @@ export const FlowInfo: React.FC<
setNodes,
edges,
setEdges,
} = useAgentGraph(flow.id, false);
} = useAgentGraph(flow.id, flow.version, undefined, false);
const api = useBackendAPI();
const { toast } = useToast();
@@ -224,7 +224,7 @@ export const FlowInfo: React.FC<
)}
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.id}`}
href={`/build?flowID=${flow.id}&flowVersion=${flow.version}`}
>
<Pencil2Icon className="mr-2" />
Open in Builder

View File

@@ -109,7 +109,7 @@ export const FlowRunInfo: React.FC<
</Button>
<Link
className={buttonVariants({ variant: "default" })}
href={`/build?flowID=${flow.id}`}
href={`/build?flowID=${flow.id}&flowVersion=${execution.graph_version}&flowExecutionID=${execution.execution_id}`}
>
<Pencil2Icon className="mr-2" /> Open in Builder
</Link>

View File

@@ -31,6 +31,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { TextRenderer } from "../ui/render";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
interface SchedulesTableProps {
schedules: Schedule[];
@@ -53,6 +55,8 @@ export const SchedulesTable = ({
const router = useRouter();
const cron_manager = new CronExpressionManager();
const [selectedAgent, setSelectedAgent] = useState<string>("");
const [selectedVersion, setSelectedVersion] = useState<number>(0);
const [maxVersion, setMaxVersion] = useState<number>(0);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<string>("");
@@ -86,13 +90,38 @@ export const SchedulesTable = ({
const handleAgentSelect = (agentId: string) => {
setSelectedAgent(agentId);
const agent = agents.find((a) => a.id === agentId);
setMaxVersion(agent!.version);
setSelectedVersion(agent!.version);
};
const handleVersionSelect = (version: string) => {
setSelectedVersion(parseInt(version));
};
const handleSchedule = async () => {
if (!selectedAgent || !selectedVersion) {
toast({
title: "Invalid Input",
description: "Please select an agent and a version.",
variant: "destructive",
});
return;
}
if (selectedVersion < 1 || selectedVersion > maxVersion) {
toast({
title: "Invalid Version",
description: `Please select a version between 1 and ${maxVersion}.`,
variant: "destructive",
});
return;
}
setIsLoading(true);
try {
await new Promise((resolve) => setTimeout(resolve, 100));
router.push(`/build?flowID=${selectedAgent}&open_scheduling=true`);
router.push(
`/build?flowID=${selectedAgent}&flowVersion=${selectedVersion}&open_scheduling=true`,
);
} catch (error) {
console.error("Navigation error:", error);
}
@@ -117,6 +146,18 @@ export const SchedulesTable = ({
))}
</SelectContent>
</Select>
<Label className="mt-4">
Select version between 1 and {maxVersion}
</Label>
<Input
type="number"
min={1}
max={selectedAgent ? maxVersion : 0}
value={selectedVersion}
onChange={(e) => handleVersionSelect(e.target.value)}
placeholder="Select version"
className="w-full"
/>
<Button
onClick={handleSchedule}
disabled={isLoading || !selectedAgent}
@@ -165,6 +206,7 @@ export const SchedulesTable = ({
>
Graph Name
</TableHead>
<TableHead className="cursor-pointer">Graph Version</TableHead>
<TableHead
onClick={() => onSort("next_run_time")}
className="cursor-pointer"
@@ -198,6 +240,7 @@ export const SchedulesTable = ({
{agents.find((a) => a.id === schedule.graph_id)?.name ||
schedule.graph_id}
</TableCell>
<TableCell>{schedule.graph_version}</TableCell>
<TableCell>
{new Date(schedule.next_run_time).toLocaleString()}
</TableCell>

View File

@@ -1,24 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { IconRefresh } from "@/components/ui/icons";
import useCredits from "@/hooks/useCredits";
export default function CreditButton() {
const { credits, fetchCredits } = useCredits();
return (
credits !== null && (
<Button
onClick={fetchCredits}
variant="outline"
className="flex items-center space-x-2 rounded-xl bg-gray-200"
>
<span className="mr-2 flex items-center text-foreground">
{credits} <span className="ml-2 text-muted-foreground"> credits</span>
</span>
<IconRefresh />
</Button>
)
);
}

View File

@@ -1,35 +0,0 @@
import { ButtonHTMLAttributes } from "react";
import React from "react";
interface MarketPopupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
marketplaceUrl?: string;
}
export default function MarketPopup({
className = "",
marketplaceUrl = (() => {
if (process.env.NEXT_PUBLIC_APP_ENV === "prod") {
return "https://production-marketplace-url.com";
} else if (process.env.NEXT_PUBLIC_APP_ENV === "dev") {
return "https://dev-builder.agpt.co/marketplace";
} else {
return "http://localhost:3000/marketplace";
}
})(),
children,
...props
}: MarketPopupProps) {
const openMarketplacePopup = () => {
window.open(
marketplaceUrl,
"popupWindow",
"width=600,height=400,toolbar=no,menubar=no,scrollbars=no",
);
};
return (
<button onClick={openMarketplacePopup} className={className} {...props}>
{children}
</button>
);
}

View File

@@ -1,83 +0,0 @@
"use client";
import React from "react";
import Link from "next/link";
import { BsBoxes } from "react-icons/bs";
import { LuLaptop, LuShoppingCart } from "react-icons/lu";
import { BehaveAs, cn } from "@/lib/utils";
import { usePathname } from "next/navigation";
import { getBehaveAs } from "@/lib/utils";
import { IconMarketplace } from "@/components/ui/icons";
import MarketPopup from "./MarketPopup";
export function NavBarButtons({ className }: { className?: string }) {
const pathname = usePathname();
const buttons = [
{
href: "/",
text: "Monitor",
icon: <LuLaptop />,
},
{
href: "/build",
text: "Build",
icon: <BsBoxes />,
},
{
href: "/marketplace",
text: "Marketplace",
icon: <IconMarketplace />,
},
];
const isCloud = getBehaveAs() === BehaveAs.CLOUD;
return (
<>
{buttons.map((button) => {
const isActive = button.href === pathname;
return (
<Link
key={button.href}
href={button.href}
data-testid={`${button.text.toLowerCase()}-nav-link`}
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3",
isActive
? "bg-gray-950 text-white"
: "text-muted-foreground hover:text-foreground",
)}
>
{button.icon} {button.text}
</Link>
);
})}
{isCloud ? (
<Link
href="/marketplace"
data-testid="marketplace-nav-link"
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3",
pathname === "/marketplace"
? "bg-gray-950 text-white"
: "text-muted-foreground hover:text-foreground",
)}
>
<LuShoppingCart /> Marketplace
</Link>
) : (
<MarketPopup
data-testid="marketplace-nav-link"
className={cn(
className,
"flex items-center gap-2 rounded-xl p-3 text-muted-foreground hover:text-foreground",
)}
>
<LuShoppingCart /> Marketplace
</MarketPopup>
)}
</>
);
}

View File

@@ -4,8 +4,8 @@ import BackendAPI, {
Block,
BlockIOSubSchema,
BlockUIType,
formatEdgeID,
Graph,
Link,
NodeExecutionResult,
} from "@/lib/autogpt-server-api";
import {
@@ -14,18 +14,22 @@ import {
removeEmptyStringsAndNulls,
setNestedProperty,
} from "@/lib/utils";
import { Connection, MarkerType } from "@xyflow/react";
import { MarkerType } from "@xyflow/react";
import Ajv from "ajv";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
import { InputItem } from "@/components/RunnerUIWrapper";
import { GraphMeta } from "@/lib/autogpt-server-api";
import useCredits from "./useCredits";
import { default as NextLink } from "next/link";
const ajv = new Ajv({ strict: false, allErrors: true });
export default function useAgentGraph(
flowID?: string,
flowVersion?: number,
flowExecutionID?: string,
passDataToBeads?: boolean,
) {
const { toast } = useToast();
@@ -72,31 +76,13 @@ export default function useAgentGraph(
useState(false);
const [nodes, setNodes] = useState<CustomNode[]>([]);
const [edges, setEdges] = useState<CustomEdge[]>([]);
const { credits, fetchCredits } = useCredits();
const api = useMemo(
() => new BackendAPI(process.env.NEXT_PUBLIC_AGPT_SERVER_URL!),
[],
);
// Connect to WebSocket
useEffect(() => {
api
.connectWebSocket()
.then(() => {
console.debug("WebSocket connected");
api.onWebSocketMessage("execution_event", (data) => {
setUpdateQueue((prev) => [...prev, data]);
});
})
.catch((error) => {
console.error("Failed to connect WebSocket:", error);
});
return () => {
api.disconnectWebSocket();
};
}, [api]);
// Load available blocks & flows
useEffect(() => {
api
@@ -108,16 +94,32 @@ export default function useAgentGraph(
.listGraphs()
.then((flows) => setAvailableFlows(flows))
.catch();
api.connectWebSocket().catch((error) => {
console.error("Failed to connect WebSocket:", error);
});
return () => {
api.disconnectWebSocket();
};
}, [api]);
//TODO to utils? repeated in Flow
const formatEdgeID = useCallback((conn: Link | Connection): string => {
if ("sink_id" in conn) {
return `${conn.source_id}_${conn.source_name}_${conn.sink_id}_${conn.sink_name}`;
} else {
return `${conn.source}_${conn.sourceHandle}_${conn.target}_${conn.targetHandle}`;
// Subscribe to execution events
useEffect(() => {
api.onWebSocketMessage("execution_event", (data) => {
if (data.graph_exec_id != flowExecutionID) {
return;
}
setUpdateQueue((prev) => [...prev, data]);
});
if (flowID && flowVersion) {
api.subscribeToExecution(flowID, flowVersion);
console.debug(
`Subscribed to execution events for ${flowID} v.${flowVersion}`,
);
}
}, []);
}, [api, flowID, flowVersion, flowExecutionID]);
const getOutputType = useCallback(
(nodes: CustomNode[], nodeId: string, handleId: string) => {
@@ -141,80 +143,82 @@ export default function useAgentGraph(
setAgentName(graph.name);
setAgentDescription(graph.description);
setNodes(() => {
const newNodes = graph.nodes
.map((node) => {
const block = availableNodes.find(
(block) => block.id === node.block_id,
)!;
if (!block) return null;
const flow =
block.uiType == BlockUIType.AGENT
? availableFlows.find(
(flow) => flow.id === node.input_default.graph_id,
)
: null;
const newNode: CustomNode = {
id: node.id,
type: "custom",
position: {
x: node?.metadata?.position?.x || 0,
y: node?.metadata?.position?.y || 0,
},
data: {
block_id: block.id,
blockType: flow?.name || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: node.input_default,
webhook: node.webhook,
uiType: block.uiType,
connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({
edge_id: formatEdgeID(link),
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
isOutputOpen: false,
},
};
return newNode;
})
.filter((node) => node !== null);
setEdges((_) =>
graph.links.map((link) => ({
id: formatEdgeID(link),
setNodes((prevNodes) => {
const newNodes = graph.nodes.map((node) => {
const block = availableNodes.find(
(block) => block.id === node.block_id,
)!;
const prevNode = prevNodes.find((n) => n.id === node.id);
const flow =
block.uiType == BlockUIType.AGENT
? availableFlows.find(
(flow) => flow.id === node.input_default.graph_id,
)
: null;
const newNode: CustomNode = {
id: node.id,
type: "custom",
position: {
x: node?.metadata?.position?.x || 0,
y: node?.metadata?.position?.y || 0,
},
data: {
edgeColor: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
sourcePos: newNodes.find((node) => node.id === link.source_id)
?.position,
isStatic: link.is_static,
beadUp: 0,
beadDown: 0,
beadData: [],
isOutputOpen: false,
...prevNode?.data,
block_id: block.id,
blockType: flow?.name || block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: node.input_default,
webhook: node.webhook,
uiType: block.uiType,
connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({
edge_id: formatEdgeID(link),
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
backend_id: node.id,
},
markerEnd: {
type: MarkerType.ArrowClosed,
strokeWidth: 2,
color: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
},
source: link.source_id,
target: link.sink_id,
sourceHandle: link.source_name || undefined,
targetHandle: link.sink_name || undefined,
})),
};
return newNode;
});
setEdges(() =>
graph.links.map((link) => {
return {
id: formatEdgeID(link),
type: "custom",
data: {
edgeColor: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
sourcePos: newNodes.find((node) => node.id === link.source_id)
?.position,
isStatic: link.is_static,
beadUp: 0,
beadDown: 0,
beadData: [],
},
markerEnd: {
type: MarkerType.ArrowClosed,
strokeWidth: 2,
color: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
},
source: link.source_id,
target: link.sink_id,
sourceHandle: link.source_name || undefined,
targetHandle: link.sink_name || undefined,
};
}),
);
return newNodes;
});
@@ -245,7 +249,8 @@ export default function useAgentGraph(
) {
continue;
}
edge.data!.beadUp = (edge.data!.beadUp ?? 0) + 1;
const count = executionData.output_data[key].length;
edge.data!.beadUp = (edge.data!.beadUp ?? 0) + count;
// For static edges beadDown is always one less than beadUp
// Because there's no queueing and one bead is always at the connection point
if (edge.data?.isStatic) {
@@ -253,9 +258,8 @@ export default function useAgentGraph(
edge.data!.beadData = edge.data!.beadData!.slice(0, -1);
continue;
}
//todo kcze this assumes output at key is always array with one element
edge.data!.beadData = [
executionData.output_data[key][0],
...executionData.output_data[key].toReversed(),
...edge.data!.beadData!,
];
}
@@ -332,14 +336,15 @@ export default function useAgentGraph(
[passDataToBeads, updateEdgeBeads],
);
// Load graph
useEffect(() => {
if (!flowID || availableNodes.length == 0) return;
api.getGraph(flowID).then((graph) => {
api.getGraph(flowID, flowVersion).then((graph) => {
console.debug("Loading graph");
loadGraph(graph);
});
}, [flowID, availableNodes, api, loadGraph]);
}, [flowID, flowVersion, availableNodes, api, loadGraph]);
// Update nodes with execution data
useEffect(() => {
@@ -540,44 +545,22 @@ export default function useAgentGraph(
});
return;
}
api.subscribeToExecution(savedAgent.id);
setSaveRunRequest({ request: "run", state: "running" });
api
.executeGraph(savedAgent.id)
.executeGraph(savedAgent.id, savedAgent.version)
.then((graphExecution) => {
setSaveRunRequest({
request: "run",
state: "running",
activeExecutionID: graphExecution.id,
activeExecutionID: graphExecution.graph_exec_id,
});
// Track execution until completed
const pendingNodeExecutions: Set<string> = new Set();
const cancelExecListener = api.onWebSocketMessage(
"execution_event",
(nodeResult) => {
// We are racing the server here, since we need the ID to filter events
if (nodeResult.graph_exec_id != graphExecution.id) {
return;
}
if (
!["COMPLETED", "TERMINATED", "FAILED"].includes(
nodeResult.status,
)
) {
pendingNodeExecutions.add(nodeResult.node_exec_id);
} else {
pendingNodeExecutions.delete(nodeResult.node_exec_id);
}
if (pendingNodeExecutions.size == 0) {
// Assuming the first event is always a QUEUED node, and
// following nodes are QUEUED before all preceding nodes are COMPLETED,
// an empty set means the graph has finished running.
cancelExecListener();
setSaveRunRequest({ request: "none", state: "none" });
}
},
);
// Update URL params
const path = new URLSearchParams(searchParams);
path.set("flowID", savedAgent.id);
path.set("flowVersion", savedAgent.version.toString());
path.set("flowExecutionID", graphExecution.graph_exec_id);
router.push(`${pathname}?${path.toString()}`);
})
.catch((error) => {
const errorMessage =
@@ -618,6 +601,72 @@ export default function useAgentGraph(
validateNodes,
]);
useEffect(() => {
if (!flowID || !flowExecutionID) {
return;
}
const fetchExecutions = async () => {
const results = await api.getGraphExecutionInfo(flowID, flowExecutionID);
setUpdateQueue((prev) => [...prev, ...results]);
// Track execution until completed
const pendingNodeExecutions: Set<string> = new Set();
const cancelExecListener = api.onWebSocketMessage(
"execution_event",
(nodeResult) => {
// We are racing the server here, since we need the ID to filter events
if (nodeResult.graph_exec_id != flowExecutionID) {
return;
}
if (
nodeResult.status === "FAILED" &&
nodeResult.output_data?.error?.[0]
.toLowerCase()
.includes("insufficient balance")
) {
// Show no credits toast if user has low credits
toast({
variant: "destructive",
title: "Credits low",
description: (
<div>
Agent execution failed due to insufficient credits.
<br />
Go to the{" "}
<NextLink
className="text-purple-300"
href="/marketplace/credits"
>
Credits
</NextLink>{" "}
page to top up.
</div>
),
duration: 5000,
});
}
if (
!["COMPLETED", "TERMINATED", "FAILED"].includes(nodeResult.status)
) {
pendingNodeExecutions.add(nodeResult.node_exec_id);
} else {
pendingNodeExecutions.delete(nodeResult.node_exec_id);
}
if (pendingNodeExecutions.size == 0) {
// Assuming the first event is always a QUEUED node, and
// following nodes are QUEUED before all preceding nodes are COMPLETED,
// an empty set means the graph has finished running.
cancelExecListener();
setSaveRunRequest({ request: "none", state: "none" });
}
},
);
};
fetchExecutions();
}, [flowID, flowExecutionID]);
// Check if node ids are synced with saved agent
useEffect(() => {
// Check if all node ids are synced with saved agent (frontend and backend)
@@ -795,6 +844,7 @@ export default function useAgentGraph(
if (!savedAgent) {
const path = new URLSearchParams(searchParams);
path.set("flowID", newSavedAgent.id);
path.set("flowVersion", newSavedAgent.version.toString());
router.push(`${pathname}?${path.toString()}`);
return;
}
@@ -913,6 +963,8 @@ export default function useAgentGraph(
if (flowID) {
await api.createSchedule({
graph_id: flowID,
// flowVersion is always defined here because scheduling is opened for a specific version
graph_version: flowVersion!,
cron: cronExpression,
input_data: inputs.reduce(
(acc, input) => ({

View File

@@ -101,28 +101,26 @@ export function useCopyPaste(getNextNodeId: () => string) {
setNodes((nodes) => {
return nodes.map((node) => {
if (oldToNewIdMap[node.id]) {
const nodeConnections = pastedEdges
.filter(
(edge: Edge) =>
edge.source === node.id || edge.target === node.id,
)
.map((edge: Edge) => ({
edge_id: edge.id,
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
targetHandle: edge.targetHandle,
}));
return {
...node,
data: {
...node.data,
connections: nodeConnections,
},
};
}
return node;
const nodeConnections = getEdges()
.filter(
(edge: Edge) =>
edge.source === node.id || edge.target === node.id,
)
.map((edge: Edge) => ({
edge_id: edge.id,
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
targetHandle: edge.targetHandle,
}));
return {
...node,
data: {
...node.data,
connections: nodeConnections,
},
};
});
});
}

View File

@@ -1,13 +1,9 @@
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { TransactionHistory } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useMemo, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { loadStripe, Stripe } from "@stripe/stripe-js";
import { useRouter } from "next/navigation";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
);
export default function useCredits(): {
credits: number | null;
fetchCredits: () => void;
@@ -17,12 +13,14 @@ export default function useCredits(): {
updateAutoTopUpConfig: (amount: number, threshold: number) => Promise<void>;
transactionHistory: TransactionHistory;
fetchTransactionHistory: () => void;
formatCredits: (credit: number | null) => string;
} {
const [credits, setCredits] = useState<number | null>(null);
const [autoTopUpConfig, setAutoTopUpConfig] = useState<{
amount: number;
threshold: number;
} | null>(null);
const [stripe, setStripe] = useState<Stripe | null>(null);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const router = useRouter();
@@ -36,6 +34,20 @@ export default function useCredits(): {
fetchCredits();
}, [fetchCredits]);
useEffect(() => {
const fetchStripe = async () => {
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY?.trim()) {
console.debug("Stripe publishable key is not set.");
return;
}
const stripe = await loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
);
setStripe(stripe);
};
fetchStripe();
}, []);
const fetchAutoTopUpConfig = useCallback(async () => {
const response = await api.getAutoTopUpConfig();
setAutoTopUpConfig(response);
@@ -55,16 +67,18 @@ export default function useCredits(): {
const requestTopUp = useCallback(
async (credit_amount: number) => {
const stripe = await stripePromise;
if (!stripe) {
console.error(
"Trying to top-up failed because Stripe is not loaded." +
"Did you set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY?",
);
return;
}
const response = await api.requestTopUp(credit_amount);
router.push(response.checkout_url);
},
[api, router],
[api, router, stripe],
);
const [transactionHistory, setTransactionHistory] =
@@ -89,6 +103,20 @@ export default function useCredits(): {
useEffect(() => {
fetchTransactionHistory();
// Note: We only need to fetch transaction history once.
// Hence, we should avoid `fetchTransactionHistory` to the dependency array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formatCredits = useCallback((credit: number | null) => {
if (credit === null) {
return "-";
}
const value = Math.abs(credit);
const sign = credit < 0 ? "-" : "";
const precision =
2 - (value % 100 === 0 ? 1 : 0) - (value % 10 === 0 ? 1 : 0);
return `${sign}$${(value / 100).toFixed(precision)}`;
}, []);
return {
@@ -100,5 +128,6 @@ export default function useCredits(): {
updateAutoTopUpConfig,
transactionHistory,
fetchTransactionHistory,
formatCredits,
};
}

View File

@@ -16,7 +16,6 @@ import {
GraphExecution,
Graph,
GraphCreatable,
GraphExecuteResponse,
GraphMeta,
GraphUpdateable,
MyAgentsResponse,
@@ -191,9 +190,10 @@ export default class BackendAPI {
executeGraph(
id: string,
version: number,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
): Promise<{ graph_exec_id: string }> {
return this._request("POST", `/graphs/${id}/execute/${version}`, inputData);
}
async getGraphExecutionInfo(
@@ -816,8 +816,11 @@ export default class BackendAPI {
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
subscribeToExecution(graphId: string, graphVersion: number) {
this.sendWebSocketMessage("subscribe", {
graph_id: graphId,
graph_version: graphVersion,
});
}
}
@@ -828,7 +831,7 @@ type GraphCreateRequestBody = {
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
subscribe: { graph_id: string; graph_version: number };
execution_event: NodeExecutionResult;
heartbeat: "ping" | "pong";
};

View File

@@ -135,6 +135,7 @@ export const PROVIDER_NAMES = {
OPEN_ROUTER: "open_router",
PINECONE: "pinecone",
SLANT3D: "slant3d",
SCREENSHOTONE: "screenshotone",
SMTP: "smtp",
TWITTER: "twitter",
REPLICATE: "replicate",
@@ -256,14 +257,6 @@ export type GraphUpdateable = Omit<
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string };
/* Derived from backend/executor/manager.py:ExecutionManager.add_execution */
export type GraphExecuteResponse = {
/** ID of the initiated run */
id: string;
/** List of node executions */
executions: Array<{ id: string; node_id: string }>;
};
/* Mirror of backend/data/execution.py:ExecutionResult */
export type NodeExecutionResult = {
graph_id: string;
@@ -519,6 +512,7 @@ export type Schedule = {
export type ScheduleCreatable = {
cron: string;
graph_id: string;
graph_version: number;
input_data: { [key: string]: any };
};

View File

@@ -1,4 +1,5 @@
import { Graph, Block, Node, BlockUIType } from "./types";
import { Connection } from "@xyflow/react";
import { Graph, Block, Node, BlockUIType, Link } from "./types";
/** Creates a copy of the graph with all secrets removed */
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
@@ -44,3 +45,11 @@ export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) {
nodes: modifiedNodes,
};
}
export function formatEdgeID(conn: Link | Connection): string {
if ("sink_id" in conn) {
return `${conn.source_id}_${conn.source_name}_${conn.sink_id}_${conn.sink_name}`;
} else {
return `${conn.source}_${conn.sourceHandle}_${conn.target}_${conn.targetHandle}`;
}
}

View File

@@ -28,7 +28,9 @@ test.describe("Profile", () => {
// Verify email matches test worker's email
const displayedHandle = await profilePage.getDisplayedName();
test.expect(displayedHandle).toBe("No Profile Data");
test.expect(displayedHandle).not.toBeNull();
test.expect(displayedHandle).not.toBe("");
test.expect(displayedHandle).toBeDefined();
});
test("profile navigation is accessible from navbar", async ({ page }) => {

File diff suppressed because it is too large Load Diff