mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'dev' into toran/open-2273-add-image-to-video-block
This commit is contained in:
@@ -1,40 +1,61 @@
|
||||
# Ignore everything by default, selectively add things to context
|
||||
classic/run
|
||||
*
|
||||
|
||||
# AutoGPT
|
||||
# Platform - Libs
|
||||
!autogpt_platform/autogpt_libs/autogpt_libs/
|
||||
!autogpt_platform/autogpt_libs/pyproject.toml
|
||||
!autogpt_platform/autogpt_libs/poetry.lock
|
||||
!autogpt_platform/autogpt_libs/README.md
|
||||
|
||||
# Platform - Backend
|
||||
!autogpt_platform/backend/backend/
|
||||
!autogpt_platform/backend/migrations/
|
||||
!autogpt_platform/backend/schema.prisma
|
||||
!autogpt_platform/backend/pyproject.toml
|
||||
!autogpt_platform/backend/poetry.lock
|
||||
!autogpt_platform/backend/README.md
|
||||
|
||||
# Platform - Market
|
||||
!autogpt_platform/market/market/
|
||||
!autogpt_platform/market/scripts.py
|
||||
!autogpt_platform/market/schema.prisma
|
||||
!autogpt_platform/market/pyproject.toml
|
||||
!autogpt_platform/market/poetry.lock
|
||||
!autogpt_platform/market/README.md
|
||||
|
||||
# Platform - Frontend
|
||||
!autogpt_platform/frontend/src/
|
||||
!autogpt_platform/frontend/public/
|
||||
!autogpt_platform/frontend/package.json
|
||||
!autogpt_platform/frontend/yarn.lock
|
||||
!autogpt_platform/frontend/tsconfig.json
|
||||
!autogpt_platform/frontend/README.md
|
||||
## config
|
||||
!autogpt_platform/frontend/*.config.*
|
||||
!autogpt_platform/frontend/.env.*
|
||||
|
||||
# Classic - AutoGPT
|
||||
!classic/original_autogpt/autogpt/
|
||||
!classic/original_autogpt/pyproject.toml
|
||||
!classic/original_autogpt/poetry.lock
|
||||
!classic/original_autogpt/README.md
|
||||
!classic/original_autogpt/tests/
|
||||
|
||||
# Benchmark
|
||||
# Classic - Benchmark
|
||||
!classic/benchmark/agbenchmark/
|
||||
!classic/benchmark/pyproject.toml
|
||||
!classic/benchmark/poetry.lock
|
||||
!classic/benchmark/README.md
|
||||
|
||||
# Forge
|
||||
# Classic - Forge
|
||||
!classic/forge/
|
||||
!classic/forge/pyproject.toml
|
||||
!classic/forge/poetry.lock
|
||||
!classic/forge/README.md
|
||||
|
||||
# Frontend
|
||||
# Classic - Frontend
|
||||
!classic/frontend/build/web/
|
||||
|
||||
# Platform
|
||||
!autogpt_platform/
|
||||
|
||||
# Explicitly re-ignore some folders
|
||||
.*
|
||||
**/__pycache__
|
||||
|
||||
autogpt_platform/frontend/.next/
|
||||
autogpt_platform/frontend/node_modules
|
||||
autogpt_platform/frontend/.env.example
|
||||
autogpt_platform/frontend/.env.local
|
||||
autogpt_platform/backend/.env
|
||||
autogpt_platform/backend/.venv/
|
||||
|
||||
autogpt_platform/market/.env
|
||||
|
||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
@@ -89,28 +89,6 @@ updates:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# market (Poetry project)
|
||||
- package-ecosystem: "pip"
|
||||
directory: "autogpt_platform/market"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: "dev"
|
||||
commit-message:
|
||||
prefix: "chore(market/deps)"
|
||||
prefix-development: "chore(market/deps-dev)"
|
||||
groups:
|
||||
production-dependencies:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
development-dependencies:
|
||||
dependency-type: "development"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
|
||||
@@ -35,12 +35,6 @@ jobs:
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.BACKEND_DATABASE_URL }}
|
||||
|
||||
- name: Run Market Migrations
|
||||
working-directory: ./autogpt_platform/market
|
||||
run: |
|
||||
python -m prisma migrate deploy
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.MARKET_DATABASE_URL }}
|
||||
|
||||
trigger:
|
||||
needs: migrate
|
||||
|
||||
@@ -37,13 +37,6 @@ jobs:
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.BACKEND_DATABASE_URL }}
|
||||
|
||||
- name: Run Market Migrations
|
||||
working-directory: ./autogpt_platform/market
|
||||
run: |
|
||||
python -m prisma migrate deploy
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.MARKET_DATABASE_URL }}
|
||||
|
||||
trigger:
|
||||
needs: migrate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/platform-backend-ci.yml
vendored
2
.github/workflows/platform-backend-ci.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Check poetry.lock
|
||||
run: |
|
||||
poetry lock --no-update
|
||||
poetry lock
|
||||
|
||||
if ! git diff --quiet poetry.lock; then
|
||||
echo "Error: poetry.lock not up to date."
|
||||
|
||||
5
.github/workflows/platform-frontend-ci.yml
vendored
5
.github/workflows/platform-frontend-ci.yml
vendored
@@ -88,6 +88,11 @@ jobs:
|
||||
run: |
|
||||
yarn test --project=${{ matrix.browser }}
|
||||
|
||||
- name: Print Docker Compose logs in debug mode
|
||||
if: runner.debug
|
||||
run: |
|
||||
docker compose -f ../docker-compose.yml logs
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
|
||||
126
.github/workflows/platform-market-ci.yml
vendored
126
.github/workflows/platform-market-ci.yml
vendored
@@ -1,126 +0,0 @@
|
||||
name: AutoGPT Platform - Backend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev, ci-test*]
|
||||
paths:
|
||||
- ".github/workflows/platform-market-ci.yml"
|
||||
- "autogpt_platform/market/**"
|
||||
pull_request:
|
||||
branches: [master, dev, release-*]
|
||||
paths:
|
||||
- ".github/workflows/platform-market-ci.yml"
|
||||
- "autogpt_platform/market/**"
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ format('backend-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
|
||||
cancel-in-progress: ${{ startsWith(github.event_name, 'pull_request') }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: autogpt_platform/market
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup Supabase
|
||||
uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- id: get_date
|
||||
name: Get date
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Python dependency cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pypoetry
|
||||
key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/market/poetry.lock') }}
|
||||
|
||||
- name: Install Poetry (Unix)
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
if [ "${{ runner.os }}" = "macOS" ]; then
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: poetry install
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: poetry run prisma generate
|
||||
|
||||
- id: supabase
|
||||
name: Start Supabase
|
||||
working-directory: .
|
||||
run: |
|
||||
supabase init
|
||||
supabase start --exclude postgres-meta,realtime,storage-api,imgproxy,inbucket,studio,edge-runtime,logflare,vector,supavisor
|
||||
supabase status -o env | sed 's/="/=/; s/"$//' >> $GITHUB_OUTPUT
|
||||
# outputs:
|
||||
# DB_URL, API_URL, GRAPHQL_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET
|
||||
|
||||
- name: Run Database Migrations
|
||||
run: poetry run prisma migrate dev --name updates
|
||||
env:
|
||||
DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
|
||||
|
||||
- id: lint
|
||||
name: Run Linter
|
||||
run: poetry run lint
|
||||
|
||||
# Tests comment out because they do not work with prisma mock, nor have they been updated since they were created
|
||||
# - name: Run pytest with coverage
|
||||
# run: |
|
||||
# if [[ "${{ runner.debug }}" == "1" ]]; then
|
||||
# poetry run pytest -s -vv -o log_cli=true -o log_cli_level=DEBUG test
|
||||
# else
|
||||
# poetry run pytest -s -vv test
|
||||
# fi
|
||||
# if: success() || (failure() && steps.lint.outcome == 'failure')
|
||||
# env:
|
||||
# LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
|
||||
# DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
|
||||
# SUPABASE_URL: ${{ steps.supabase.outputs.API_URL }}
|
||||
# SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }}
|
||||
# SUPABASE_JWT_SECRET: ${{ steps.supabase.outputs.JWT_SECRET }}
|
||||
# REDIS_HOST: 'localhost'
|
||||
# REDIS_PORT: '6379'
|
||||
# REDIS_PASSWORD: 'testpassword'
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAIN_OUTPUT: True
|
||||
RUN_ENV: local
|
||||
PORT: 8080
|
||||
|
||||
# - name: Upload coverage reports to Codecov
|
||||
# uses: codecov/codecov-action@v4
|
||||
# with:
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# flags: backend,${{ runner.os }}
|
||||
@@ -110,7 +110,7 @@ repos:
|
||||
- id: isort
|
||||
name: Lint (isort) - AutoGPT Platform - Backend
|
||||
alias: isort-platform-backend
|
||||
entry: poetry -C autogpt_platform/backend run isort -p backend
|
||||
entry: poetry -P autogpt_platform/backend run isort -p backend
|
||||
files: ^autogpt_platform/backend/
|
||||
types: [file, python]
|
||||
language: system
|
||||
@@ -118,7 +118,7 @@ repos:
|
||||
- id: isort
|
||||
name: Lint (isort) - Classic - AutoGPT
|
||||
alias: isort-classic-autogpt
|
||||
entry: poetry -C classic/original_autogpt run isort -p autogpt
|
||||
entry: poetry -P classic/original_autogpt run isort -p autogpt
|
||||
files: ^classic/original_autogpt/
|
||||
types: [file, python]
|
||||
language: system
|
||||
@@ -126,7 +126,7 @@ repos:
|
||||
- id: isort
|
||||
name: Lint (isort) - Classic - Forge
|
||||
alias: isort-classic-forge
|
||||
entry: poetry -C classic/forge run isort -p forge
|
||||
entry: poetry -P classic/forge run isort -p forge
|
||||
files: ^classic/forge/
|
||||
types: [file, python]
|
||||
language: system
|
||||
@@ -134,7 +134,7 @@ repos:
|
||||
- id: isort
|
||||
name: Lint (isort) - Classic - Benchmark
|
||||
alias: isort-classic-benchmark
|
||||
entry: poetry -C classic/benchmark run isort -p agbenchmark
|
||||
entry: poetry -P classic/benchmark run isort -p agbenchmark
|
||||
files: ^classic/benchmark/
|
||||
types: [file, python]
|
||||
language: system
|
||||
@@ -178,7 +178,6 @@ repos:
|
||||
name: Typecheck - AutoGPT Platform - Backend
|
||||
alias: pyright-platform-backend
|
||||
entry: poetry -C autogpt_platform/backend run pyright
|
||||
args: [-p, autogpt_platform/backend, autogpt_platform/backend]
|
||||
# include forge source (since it's a path dependency) but exclude *_test.py files:
|
||||
files: ^autogpt_platform/(backend/((backend|test)/|(\w+\.py|poetry\.lock)$)|autogpt_libs/(autogpt_libs/.*(?<!_test)\.py|poetry\.lock)$)
|
||||
types: [file]
|
||||
@@ -189,7 +188,6 @@ repos:
|
||||
name: Typecheck - AutoGPT Platform - Libs
|
||||
alias: pyright-platform-libs
|
||||
entry: poetry -C autogpt_platform/autogpt_libs run pyright
|
||||
args: [-p, autogpt_platform/autogpt_libs, autogpt_platform/autogpt_libs]
|
||||
files: ^autogpt_platform/autogpt_libs/(autogpt_libs/|poetry\.lock$)
|
||||
types: [file]
|
||||
language: system
|
||||
@@ -199,7 +197,6 @@ repos:
|
||||
name: Typecheck - Classic - AutoGPT
|
||||
alias: pyright-classic-autogpt
|
||||
entry: poetry -C classic/original_autogpt run pyright
|
||||
args: [-p, classic/original_autogpt, classic/original_autogpt]
|
||||
# include forge source (since it's a path dependency) but exclude *_test.py files:
|
||||
files: ^(classic/original_autogpt/((autogpt|scripts|tests)/|poetry\.lock$)|classic/forge/(forge/.*(?<!_test)\.py|poetry\.lock)$)
|
||||
types: [file]
|
||||
@@ -210,7 +207,6 @@ repos:
|
||||
name: Typecheck - Classic - Forge
|
||||
alias: pyright-classic-forge
|
||||
entry: poetry -C classic/forge run pyright
|
||||
args: [-p, classic/forge, classic/forge]
|
||||
files: ^classic/forge/(forge/|poetry\.lock$)
|
||||
types: [file]
|
||||
language: system
|
||||
@@ -220,7 +216,6 @@ repos:
|
||||
name: Typecheck - Classic - Benchmark
|
||||
alias: pyright-classic-benchmark
|
||||
entry: poetry -C classic/benchmark run pyright
|
||||
args: [-p, classic/benchmark, classic/benchmark]
|
||||
files: ^classic/benchmark/(agbenchmark/|tests/|poetry\.lock$)
|
||||
types: [file]
|
||||
language: system
|
||||
|
||||
@@ -31,7 +31,8 @@ class RedisKeyedMutex:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
lock.release()
|
||||
if lock.locked():
|
||||
lock.release()
|
||||
|
||||
def acquire(self, key: Any) -> "RedisLock":
|
||||
"""Acquires and returns a lock with the given key"""
|
||||
@@ -45,7 +46,7 @@ class RedisKeyedMutex:
|
||||
return lock
|
||||
|
||||
def release(self, key: Any):
|
||||
if lock := self.locks.get(key):
|
||||
if (lock := self.locks.get(key)) and lock.locked() and lock.owned():
|
||||
lock.release()
|
||||
|
||||
def release_all_locks(self):
|
||||
|
||||
40
autogpt_platform/autogpt_libs/poetry.lock
generated
40
autogpt_platform/autogpt_libs/poetry.lock
generated
@@ -1415,29 +1415,29 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.3"
|
||||
version = "0.8.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
|
||||
{file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
|
||||
{file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
|
||||
{file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
|
||||
{file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
|
||||
{file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
|
||||
{file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
|
||||
{file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
|
||||
{file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"},
|
||||
{file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"},
|
||||
{file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"},
|
||||
{file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"},
|
||||
{file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"},
|
||||
{file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"},
|
||||
{file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"},
|
||||
{file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"},
|
||||
{file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"},
|
||||
{file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"},
|
||||
{file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"},
|
||||
{file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1852,4 +1852,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "13a36d3be675cab4a3eb2e6a62a1b08df779bded4c7b9164d8be300dc08748d0"
|
||||
content-hash = "bf1b0125759dadb1369fff05ffba64fea3e82b9b7a43d0068e1c80974a4ebc1c"
|
||||
|
||||
@@ -21,7 +21,7 @@ supabase = "^2.10.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
redis = "^5.2.1"
|
||||
ruff = "^0.8.3"
|
||||
ruff = "^0.8.6"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -17,12 +17,11 @@ RUN apt-get install -y libz-dev
|
||||
RUN apt-get install -y libssl-dev
|
||||
RUN apt-get install -y postgresql-client
|
||||
|
||||
ENV POETRY_VERSION=1.8.3
|
||||
ENV POETRY_HOME=/opt/poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH=/opt/poetry/bin:$PATH
|
||||
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
@@ -32,25 +31,21 @@ RUN pip3 install poetry
|
||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||
COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.toml /app/autogpt_platform/backend/
|
||||
WORKDIR /app/autogpt_platform/backend
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-ansi
|
||||
RUN poetry install --no-ansi --no-root
|
||||
|
||||
# Generate Prisma client
|
||||
COPY autogpt_platform/backend/schema.prisma ./
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry run prisma generate
|
||||
RUN poetry run prisma generate
|
||||
|
||||
FROM python:3.11.10-slim-bookworm AS server_dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV POETRY_VERSION=1.8.3
|
||||
ENV POETRY_HOME=/opt/poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV POETRY_HOME=/opt/poetry \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH=/opt/poetry/bin:$PATH
|
||||
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
@@ -76,6 +71,7 @@ WORKDIR /app/autogpt_platform/backend
|
||||
FROM server_dependencies AS server
|
||||
|
||||
COPY autogpt_platform/backend /app/autogpt_platform/backend
|
||||
RUN poetry install --no-ansi --only-root
|
||||
|
||||
ENV DATABASE_URL=""
|
||||
ENV PORT=8000
|
||||
|
||||
@@ -76,7 +76,11 @@ class AgentExecutorBlock(Block):
|
||||
)
|
||||
|
||||
if not event.node_id:
|
||||
if event.status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED]:
|
||||
if event.status in [
|
||||
ExecutionStatus.COMPLETED,
|
||||
ExecutionStatus.TERMINATED,
|
||||
ExecutionStatus.FAILED,
|
||||
]:
|
||||
logger.info(f"Execution {log_id} ended with status {event.status}")
|
||||
break
|
||||
else:
|
||||
|
||||
@@ -699,3 +699,420 @@ class GithubDeleteBranchBlock(Block):
|
||||
input_data.branch,
|
||||
)
|
||||
yield "status", status
|
||||
|
||||
|
||||
class GithubCreateFileBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
|
||||
repo_url: str = SchemaField(
|
||||
description="URL of the GitHub repository",
|
||||
placeholder="https://github.com/owner/repo",
|
||||
)
|
||||
file_path: str = SchemaField(
|
||||
description="Path where the file should be created",
|
||||
placeholder="path/to/file.txt",
|
||||
)
|
||||
content: str = SchemaField(
|
||||
description="Content to write to the file",
|
||||
placeholder="File content here",
|
||||
)
|
||||
branch: str = SchemaField(
|
||||
description="Branch where the file should be created",
|
||||
default="main",
|
||||
)
|
||||
commit_message: str = SchemaField(
|
||||
description="Message for the commit",
|
||||
default="Create new file",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
url: str = SchemaField(description="URL of the created file")
|
||||
sha: str = SchemaField(description="SHA of the commit")
|
||||
error: str = SchemaField(
|
||||
description="Error message if the file creation failed"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="8fd132ac-b917-428a-8159-d62893e8a3fe",
|
||||
description="This block creates a new file in a GitHub repository.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubCreateFileBlock.Input,
|
||||
output_schema=GithubCreateFileBlock.Output,
|
||||
test_input={
|
||||
"repo_url": "https://github.com/owner/repo",
|
||||
"file_path": "test/file.txt",
|
||||
"content": "Test content",
|
||||
"branch": "main",
|
||||
"commit_message": "Create test file",
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[
|
||||
("url", "https://github.com/owner/repo/blob/main/test/file.txt"),
|
||||
("sha", "abc123"),
|
||||
],
|
||||
test_mock={
|
||||
"create_file": lambda *args, **kwargs: (
|
||||
"https://github.com/owner/repo/blob/main/test/file.txt",
|
||||
"abc123",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_file(
|
||||
credentials: GithubCredentials,
|
||||
repo_url: str,
|
||||
file_path: str,
|
||||
content: str,
|
||||
branch: str,
|
||||
commit_message: str,
|
||||
) -> tuple[str, str]:
|
||||
api = get_api(credentials)
|
||||
# Convert content to base64
|
||||
content_bytes = content.encode("utf-8")
|
||||
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
|
||||
|
||||
# Create the file using the GitHub API
|
||||
contents_url = f"{repo_url}/contents/{file_path}"
|
||||
data = {
|
||||
"message": commit_message,
|
||||
"content": content_base64,
|
||||
"branch": branch,
|
||||
}
|
||||
response = api.put(contents_url, json=data)
|
||||
result = response.json()
|
||||
|
||||
return result["content"]["html_url"], result["commit"]["sha"]
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_data: Input,
|
||||
*,
|
||||
credentials: GithubCredentials,
|
||||
**kwargs,
|
||||
) -> BlockOutput:
|
||||
try:
|
||||
url, sha = self.create_file(
|
||||
credentials,
|
||||
input_data.repo_url,
|
||||
input_data.file_path,
|
||||
input_data.content,
|
||||
input_data.branch,
|
||||
input_data.commit_message,
|
||||
)
|
||||
yield "url", url
|
||||
yield "sha", sha
|
||||
except Exception as e:
|
||||
yield "error", str(e)
|
||||
|
||||
|
||||
class GithubUpdateFileBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
|
||||
repo_url: str = SchemaField(
|
||||
description="URL of the GitHub repository",
|
||||
placeholder="https://github.com/owner/repo",
|
||||
)
|
||||
file_path: str = SchemaField(
|
||||
description="Path to the file to update",
|
||||
placeholder="path/to/file.txt",
|
||||
)
|
||||
content: str = SchemaField(
|
||||
description="New content for the file",
|
||||
placeholder="Updated content here",
|
||||
)
|
||||
branch: str = SchemaField(
|
||||
description="Branch containing the file",
|
||||
default="main",
|
||||
)
|
||||
commit_message: str = SchemaField(
|
||||
description="Message for the commit",
|
||||
default="Update file",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
url: str = SchemaField(description="URL of the updated file")
|
||||
sha: str = SchemaField(description="SHA of the commit")
|
||||
error: str = SchemaField(description="Error message if the file update failed")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="30be12a4-57cb-4aa4-baf5-fcc68d136076",
|
||||
description="This block updates an existing file in a GitHub repository.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubUpdateFileBlock.Input,
|
||||
output_schema=GithubUpdateFileBlock.Output,
|
||||
test_input={
|
||||
"repo_url": "https://github.com/owner/repo",
|
||||
"file_path": "test/file.txt",
|
||||
"content": "Updated content",
|
||||
"branch": "main",
|
||||
"commit_message": "Update test file",
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[
|
||||
("url", "https://github.com/owner/repo/blob/main/test/file.txt"),
|
||||
("sha", "def456"),
|
||||
],
|
||||
test_mock={
|
||||
"update_file": lambda *args, **kwargs: (
|
||||
"https://github.com/owner/repo/blob/main/test/file.txt",
|
||||
"def456",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_file(
|
||||
credentials: GithubCredentials,
|
||||
repo_url: str,
|
||||
file_path: str,
|
||||
content: str,
|
||||
branch: str,
|
||||
commit_message: str,
|
||||
) -> tuple[str, str]:
|
||||
api = get_api(credentials)
|
||||
|
||||
# First get the current file to get its SHA
|
||||
contents_url = f"{repo_url}/contents/{file_path}"
|
||||
params = {"ref": branch}
|
||||
response = api.get(contents_url, params=params)
|
||||
current_file = response.json()
|
||||
|
||||
# Convert new content to base64
|
||||
content_bytes = content.encode("utf-8")
|
||||
content_base64 = base64.b64encode(content_bytes).decode("utf-8")
|
||||
|
||||
# Update the file
|
||||
data = {
|
||||
"message": commit_message,
|
||||
"content": content_base64,
|
||||
"sha": current_file["sha"],
|
||||
"branch": branch,
|
||||
}
|
||||
response = api.put(contents_url, json=data)
|
||||
result = response.json()
|
||||
|
||||
return result["content"]["html_url"], result["commit"]["sha"]
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_data: Input,
|
||||
*,
|
||||
credentials: GithubCredentials,
|
||||
**kwargs,
|
||||
) -> BlockOutput:
|
||||
try:
|
||||
url, sha = self.update_file(
|
||||
credentials,
|
||||
input_data.repo_url,
|
||||
input_data.file_path,
|
||||
input_data.content,
|
||||
input_data.branch,
|
||||
input_data.commit_message,
|
||||
)
|
||||
yield "url", url
|
||||
yield "sha", sha
|
||||
except Exception as e:
|
||||
yield "error", str(e)
|
||||
|
||||
|
||||
class GithubCreateRepositoryBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
|
||||
name: str = SchemaField(
|
||||
description="Name of the repository to create",
|
||||
placeholder="my-new-repo",
|
||||
)
|
||||
description: str = SchemaField(
|
||||
description="Description of the repository",
|
||||
placeholder="A description of the repository",
|
||||
default="",
|
||||
)
|
||||
private: bool = SchemaField(
|
||||
description="Whether the repository should be private",
|
||||
default=False,
|
||||
)
|
||||
auto_init: bool = SchemaField(
|
||||
description="Whether to initialize the repository with a README",
|
||||
default=True,
|
||||
)
|
||||
gitignore_template: str = SchemaField(
|
||||
description="Git ignore template to use (e.g., Python, Node, Java)",
|
||||
default="",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
url: str = SchemaField(description="URL of the created repository")
|
||||
clone_url: str = SchemaField(description="Git clone URL of the repository")
|
||||
error: str = SchemaField(
|
||||
description="Error message if the repository creation failed"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="029ec3b8-1cfd-46d3-b6aa-28e4a706efd1",
|
||||
description="This block creates a new GitHub repository.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubCreateRepositoryBlock.Input,
|
||||
output_schema=GithubCreateRepositoryBlock.Output,
|
||||
test_input={
|
||||
"name": "test-repo",
|
||||
"description": "A test repository",
|
||||
"private": False,
|
||||
"auto_init": True,
|
||||
"gitignore_template": "Python",
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[
|
||||
("url", "https://github.com/owner/test-repo"),
|
||||
("clone_url", "https://github.com/owner/test-repo.git"),
|
||||
],
|
||||
test_mock={
|
||||
"create_repository": lambda *args, **kwargs: (
|
||||
"https://github.com/owner/test-repo",
|
||||
"https://github.com/owner/test-repo.git",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_repository(
|
||||
credentials: GithubCredentials,
|
||||
name: str,
|
||||
description: str,
|
||||
private: bool,
|
||||
auto_init: bool,
|
||||
gitignore_template: str,
|
||||
) -> tuple[str, str]:
|
||||
api = get_api(credentials, convert_urls=False) # Disable URL conversion
|
||||
data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"private": private,
|
||||
"auto_init": auto_init,
|
||||
}
|
||||
|
||||
if gitignore_template:
|
||||
data["gitignore_template"] = gitignore_template
|
||||
|
||||
# Create repository using the user endpoint
|
||||
response = api.post("https://api.github.com/user/repos", json=data)
|
||||
result = response.json()
|
||||
|
||||
return result["html_url"], result["clone_url"]
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_data: Input,
|
||||
*,
|
||||
credentials: GithubCredentials,
|
||||
**kwargs,
|
||||
) -> BlockOutput:
|
||||
try:
|
||||
url, clone_url = self.create_repository(
|
||||
credentials,
|
||||
input_data.name,
|
||||
input_data.description,
|
||||
input_data.private,
|
||||
input_data.auto_init,
|
||||
input_data.gitignore_template,
|
||||
)
|
||||
yield "url", url
|
||||
yield "clone_url", clone_url
|
||||
except Exception as e:
|
||||
yield "error", str(e)
|
||||
|
||||
|
||||
class GithubListStargazersBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
|
||||
repo_url: str = SchemaField(
|
||||
description="URL of the GitHub repository",
|
||||
placeholder="https://github.com/owner/repo",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
class StargazerItem(TypedDict):
|
||||
username: str
|
||||
url: str
|
||||
|
||||
stargazer: StargazerItem = SchemaField(
|
||||
title="Stargazer",
|
||||
description="Stargazers with their username and profile URL",
|
||||
)
|
||||
error: str = SchemaField(
|
||||
description="Error message if listing stargazers failed"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="a4b9c2d1-e5f6-4g7h-8i9j-0k1l2m3n4o5p", # Generated unique UUID
|
||||
description="This block lists all users who have starred a specified GitHub repository.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubListStargazersBlock.Input,
|
||||
output_schema=GithubListStargazersBlock.Output,
|
||||
test_input={
|
||||
"repo_url": "https://github.com/owner/repo",
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[
|
||||
(
|
||||
"stargazer",
|
||||
{
|
||||
"username": "octocat",
|
||||
"url": "https://github.com/octocat",
|
||||
},
|
||||
)
|
||||
],
|
||||
test_mock={
|
||||
"list_stargazers": lambda *args, **kwargs: [
|
||||
{
|
||||
"username": "octocat",
|
||||
"url": "https://github.com/octocat",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_stargazers(
|
||||
credentials: GithubCredentials, repo_url: str
|
||||
) -> list[Output.StargazerItem]:
|
||||
api = get_api(credentials)
|
||||
# Add /stargazers to the repo URL to get stargazers endpoint
|
||||
stargazers_url = f"{repo_url}/stargazers"
|
||||
# Set accept header to get starred_at timestamp
|
||||
headers = {"Accept": "application/vnd.github.star+json"}
|
||||
response = api.get(stargazers_url, headers=headers)
|
||||
data = response.json()
|
||||
|
||||
stargazers: list[GithubListStargazersBlock.Output.StargazerItem] = [
|
||||
{
|
||||
"username": stargazer["login"],
|
||||
"url": stargazer["html_url"],
|
||||
}
|
||||
for stargazer in data
|
||||
]
|
||||
return stargazers
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_data: Input,
|
||||
*,
|
||||
credentials: GithubCredentials,
|
||||
**kwargs,
|
||||
) -> BlockOutput:
|
||||
try:
|
||||
stargazers = self.list_stargazers(
|
||||
credentials,
|
||||
input_data.repo_url,
|
||||
)
|
||||
yield from (("stargazer", stargazer) for stargazer in stargazers)
|
||||
except Exception as e:
|
||||
yield "error", str(e)
|
||||
|
||||
32
autogpt_platform/backend/backend/blocks/nvidia/_auth.py
Normal file
32
autogpt_platform/backend/backend/blocks/nvidia/_auth.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
|
||||
from backend.integrations.providers import ProviderName
|
||||
|
||||
NvidiaCredentials = APIKeyCredentials
|
||||
NvidiaCredentialsInput = CredentialsMetaInput[
|
||||
Literal[ProviderName.NVIDIA],
|
||||
Literal["api_key"],
|
||||
]
|
||||
|
||||
TEST_CREDENTIALS = APIKeyCredentials(
|
||||
id="01234567-89ab-cdef-0123-456789abcdef",
|
||||
provider="nvidia",
|
||||
api_key=SecretStr("mock-nvidia-api-key"),
|
||||
title="Mock Nvidia API key",
|
||||
expires_at=None,
|
||||
)
|
||||
|
||||
TEST_CREDENTIALS_INPUT = {
|
||||
"provider": TEST_CREDENTIALS.provider,
|
||||
"id": TEST_CREDENTIALS.id,
|
||||
"type": TEST_CREDENTIALS.type,
|
||||
"title": TEST_CREDENTIALS.title,
|
||||
}
|
||||
|
||||
|
||||
def NvidiaCredentialsField() -> NvidiaCredentialsInput:
|
||||
"""Creates an Nvidia credentials input on a block."""
|
||||
return CredentialsField(description="The Nvidia integration requires an API Key.")
|
||||
90
autogpt_platform/backend/backend/blocks/nvidia/deepfake.py
Normal file
90
autogpt_platform/backend/backend/blocks/nvidia/deepfake.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from backend.blocks.nvidia._auth import (
|
||||
NvidiaCredentials,
|
||||
NvidiaCredentialsField,
|
||||
NvidiaCredentialsInput,
|
||||
)
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.request import requests
|
||||
|
||||
|
||||
class NvidiaDeepfakeDetectBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
credentials: NvidiaCredentialsInput = NvidiaCredentialsField()
|
||||
image_base64: str = SchemaField(
|
||||
description="Image to analyze for deepfakes", image_upload=True
|
||||
)
|
||||
return_image: bool = SchemaField(
|
||||
description="Whether to return the processed image with markings",
|
||||
default=False,
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
status: str = SchemaField(
|
||||
description="Detection status (SUCCESS, ERROR, CONTENT_FILTERED)",
|
||||
default="",
|
||||
)
|
||||
image: str = SchemaField(
|
||||
description="Processed image with detection markings (if return_image=True)",
|
||||
default="",
|
||||
image_output=True,
|
||||
)
|
||||
is_deepfake: float = SchemaField(
|
||||
description="Probability that the image is a deepfake (0-1)",
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="8c7d0d67-e79c-44f6-92a1-c2600c8aac7f",
|
||||
description="Detects potential deepfakes in images using Nvidia's AI API",
|
||||
categories={BlockCategory.SAFETY},
|
||||
input_schema=NvidiaDeepfakeDetectBlock.Input,
|
||||
output_schema=NvidiaDeepfakeDetectBlock.Output,
|
||||
)
|
||||
|
||||
def run(
|
||||
self, input_data: Input, *, credentials: NvidiaCredentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
url = "https://ai.api.nvidia.com/v1/cv/hive/deepfake-image-detection"
|
||||
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"content-type": "application/json",
|
||||
"Authorization": f"Bearer {credentials.api_key.get_secret_value()}",
|
||||
}
|
||||
|
||||
image_data = f"data:image/jpeg;base64,{input_data.image_base64}"
|
||||
|
||||
payload = {
|
||||
"input": [image_data],
|
||||
"return_image": input_data.return_image,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
result = data.get("data", [{}])[0]
|
||||
|
||||
# Get deepfake probability from first bounding box if any
|
||||
deepfake_prob = 0.0
|
||||
if result.get("bounding_boxes"):
|
||||
deepfake_prob = result["bounding_boxes"][0].get("is_deepfake", 0.0)
|
||||
|
||||
yield "status", result.get("status", "ERROR")
|
||||
yield "is_deepfake", deepfake_prob
|
||||
|
||||
if input_data.return_image:
|
||||
image_data = result.get("image", "")
|
||||
output_data = f"data:image/jpeg;base64,{image_data}"
|
||||
yield "image", output_data
|
||||
else:
|
||||
yield "image", ""
|
||||
|
||||
except Exception as e:
|
||||
yield "error", str(e)
|
||||
yield "status", "ERROR"
|
||||
yield "is_deepfake", 0.0
|
||||
yield "image", ""
|
||||
@@ -22,10 +22,10 @@ from backend.util import json
|
||||
from backend.util.settings import Config
|
||||
|
||||
from .model import (
|
||||
CREDENTIALS_FIELD_NAME,
|
||||
ContributorDetails,
|
||||
Credentials,
|
||||
CredentialsMetaInput,
|
||||
is_credentials_field_name,
|
||||
)
|
||||
|
||||
app_config = Config()
|
||||
@@ -61,6 +61,9 @@ class BlockCategory(Enum):
|
||||
HARDWARE = "Block that interacts with hardware."
|
||||
AGENT = "Block that interacts with other agents."
|
||||
CRM = "Block that interacts with CRM services."
|
||||
SAFETY = (
|
||||
"Block that provides AI safety mechanisms such as detecting harmful content"
|
||||
)
|
||||
|
||||
def dict(self) -> dict[str, str]:
|
||||
return {"category": self.name, "description": self.value}
|
||||
@@ -97,11 +100,6 @@ class BlockSchema(BaseModel):
|
||||
|
||||
cls.cached_jsonschema = cast(dict[str, Any], ref_to_dict(model))
|
||||
|
||||
# Set default properties values
|
||||
for field in cls.cached_jsonschema.get("properties", {}).values():
|
||||
if isinstance(field, dict) and "advanced" not in field:
|
||||
field["advanced"] = True
|
||||
|
||||
return cls.cached_jsonschema
|
||||
|
||||
@classmethod
|
||||
@@ -143,17 +141,38 @@ class BlockSchema(BaseModel):
|
||||
@classmethod
|
||||
def __pydantic_init_subclass__(cls, **kwargs):
|
||||
"""Validates the schema definition. Rules:
|
||||
- Only one `CredentialsMetaInput` field may be present.
|
||||
- This field MUST be called `credentials`.
|
||||
- A field that is called `credentials` MUST be a `CredentialsMetaInput`.
|
||||
- Fields with annotation `CredentialsMetaInput` MUST be
|
||||
named `credentials` or `*_credentials`
|
||||
- Fields named `credentials` or `*_credentials` MUST be
|
||||
of type `CredentialsMetaInput`
|
||||
"""
|
||||
super().__pydantic_init_subclass__(**kwargs)
|
||||
|
||||
# Reset cached JSON schema to prevent inheriting it from parent class
|
||||
cls.cached_jsonschema = {}
|
||||
|
||||
credentials_fields = [
|
||||
field_name
|
||||
credentials_fields = cls.get_credentials_fields()
|
||||
|
||||
for field_name in cls.get_fields():
|
||||
if is_credentials_field_name(field_name):
|
||||
if field_name not in credentials_fields:
|
||||
raise TypeError(
|
||||
f"Credentials field '{field_name}' on {cls.__qualname__} "
|
||||
f"is not of type {CredentialsMetaInput.__name__}"
|
||||
)
|
||||
|
||||
credentials_fields[field_name].validate_credentials_field_schema(cls)
|
||||
|
||||
elif field_name in credentials_fields:
|
||||
raise KeyError(
|
||||
f"Credentials field '{field_name}' on {cls.__qualname__} "
|
||||
"has invalid name: must be 'credentials' or *_credentials"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_credentials_fields(cls) -> dict[str, type[CredentialsMetaInput]]:
|
||||
return {
|
||||
field_name: info.annotation
|
||||
for field_name, info in cls.model_fields.items()
|
||||
if (
|
||||
inspect.isclass(info.annotation)
|
||||
@@ -162,32 +181,7 @@ class BlockSchema(BaseModel):
|
||||
CredentialsMetaInput,
|
||||
)
|
||||
)
|
||||
]
|
||||
if len(credentials_fields) > 1:
|
||||
raise ValueError(
|
||||
f"{cls.__qualname__} can only have one CredentialsMetaInput field"
|
||||
)
|
||||
elif (
|
||||
len(credentials_fields) == 1
|
||||
and credentials_fields[0] != CREDENTIALS_FIELD_NAME
|
||||
):
|
||||
raise ValueError(
|
||||
f"CredentialsMetaInput field on {cls.__qualname__} "
|
||||
"must be named 'credentials'"
|
||||
)
|
||||
elif (
|
||||
len(credentials_fields) == 0
|
||||
and CREDENTIALS_FIELD_NAME in cls.model_fields.keys()
|
||||
):
|
||||
raise TypeError(
|
||||
f"Field 'credentials' on {cls.__qualname__} "
|
||||
f"must be of type {CredentialsMetaInput.__name__}"
|
||||
)
|
||||
if credentials_field := cls.model_fields.get(CREDENTIALS_FIELD_NAME):
|
||||
credentials_input_type = cast(
|
||||
CredentialsMetaInput, credentials_field.annotation
|
||||
)
|
||||
credentials_input_type.validate_credentials_field_schema(cls)
|
||||
}
|
||||
|
||||
|
||||
BlockSchemaInputType = TypeVar("BlockSchemaInputType", bound=BlockSchema)
|
||||
@@ -260,7 +254,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
||||
test_input: BlockInput | list[BlockInput] | None = None,
|
||||
test_output: BlockData | list[BlockData] | None = None,
|
||||
test_mock: dict[str, Any] | None = None,
|
||||
test_credentials: Optional[Credentials] = None,
|
||||
test_credentials: Optional[Credentials | dict[str, Credentials]] = None,
|
||||
disabled: bool = False,
|
||||
static_output: bool = False,
|
||||
block_type: BlockType = BlockType.STANDARD,
|
||||
@@ -302,10 +296,16 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
||||
if self.webhook_config:
|
||||
if isinstance(self.webhook_config, BlockWebhookConfig):
|
||||
# Enforce presence of credentials field on auto-setup webhook blocks
|
||||
if CREDENTIALS_FIELD_NAME not in self.input_schema.model_fields:
|
||||
if not (cred_fields := self.input_schema.get_credentials_fields()):
|
||||
raise TypeError(
|
||||
"credentials field is required on auto-setup webhook blocks"
|
||||
)
|
||||
# Disallow multiple credentials inputs on webhook blocks
|
||||
elif len(cred_fields) > 1:
|
||||
raise ValueError(
|
||||
"Multiple credentials inputs not supported on webhook blocks"
|
||||
)
|
||||
|
||||
self.block_type = BlockType.WEBHOOK
|
||||
else:
|
||||
self.block_type = BlockType.WEBHOOK_MANUAL
|
||||
|
||||
@@ -270,9 +270,9 @@ async def update_graph_execution_start_time(graph_exec_id: str):
|
||||
|
||||
async def update_graph_execution_stats(
|
||||
graph_exec_id: str,
|
||||
status: ExecutionStatus,
|
||||
stats: dict[str, Any],
|
||||
) -> ExecutionResult:
|
||||
status = ExecutionStatus.FAILED if stats.get("error") else ExecutionStatus.COMPLETED
|
||||
res = await AgentGraphExecution.prisma().update(
|
||||
where={"id": graph_exec_id},
|
||||
data={
|
||||
|
||||
@@ -193,7 +193,8 @@ class Graph(BaseDbModel):
|
||||
"properties": {
|
||||
p.name: {
|
||||
"secret": p.secret,
|
||||
"advanced": p.advanced,
|
||||
# Default value has to be set for advanced fields.
|
||||
"advanced": p.advanced and p.value is not None,
|
||||
"title": p.title or p.name,
|
||||
**({"description": p.description} if p.description else {}),
|
||||
**({"default": p.value} if p.value is not None else {}),
|
||||
@@ -423,6 +424,26 @@ class GraphModel(Graph):
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def clean_graph(self):
|
||||
blocks = [block() for block in get_blocks().values()]
|
||||
|
||||
input_blocks = [
|
||||
node
|
||||
for node in self.nodes
|
||||
if next(
|
||||
(
|
||||
b
|
||||
for b in blocks
|
||||
if b.id == node.block_id and b.block_type == BlockType.INPUT
|
||||
),
|
||||
None,
|
||||
)
|
||||
]
|
||||
|
||||
for node in self.nodes:
|
||||
if any(input_block.id == node.id for input_block in input_blocks):
|
||||
node.input_default["value"] = ""
|
||||
|
||||
|
||||
# --------------------- CRUD functions --------------------- #
|
||||
|
||||
@@ -608,25 +629,20 @@ async def __create_graph(tx, graph: Graph, user_id: str):
|
||||
"isTemplate": graph.is_template,
|
||||
"isActive": graph.is_active,
|
||||
"userId": user_id,
|
||||
"AgentNodes": {
|
||||
"create": [
|
||||
{
|
||||
"id": node.id,
|
||||
"agentBlockId": node.block_id,
|
||||
"constantInput": json.dumps(node.input_default),
|
||||
"metadata": json.dumps(node.metadata),
|
||||
}
|
||||
for node in graph.nodes
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
AgentNode.prisma(tx).create(
|
||||
{
|
||||
"id": node.id,
|
||||
"agentBlockId": node.block_id,
|
||||
"agentGraphId": graph.id,
|
||||
"agentGraphVersion": graph.version,
|
||||
"constantInput": json.dumps(node.input_default),
|
||||
"metadata": json.dumps(node.metadata),
|
||||
}
|
||||
)
|
||||
for node in graph.nodes
|
||||
]
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
AgentNodeLink.prisma(tx).create(
|
||||
|
||||
@@ -35,7 +35,7 @@ class Webhook(BaseDbModel):
|
||||
@computed_field
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return webhook_ingress_url(self.provider.value, self.id)
|
||||
return webhook_ingress_url(self.provider, self.id)
|
||||
|
||||
@staticmethod
|
||||
def from_db(webhook: IntegrationWebhook):
|
||||
|
||||
@@ -134,13 +134,20 @@ def SchemaField(
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
placeholder: Optional[str] = None,
|
||||
advanced: Optional[bool] = False,
|
||||
advanced: Optional[bool] = None,
|
||||
secret: bool = False,
|
||||
exclude: bool = False,
|
||||
hidden: Optional[bool] = None,
|
||||
depends_on: list[str] | None = None,
|
||||
image_upload: Optional[bool] = None,
|
||||
image_output: Optional[bool] = None,
|
||||
**kwargs,
|
||||
) -> T:
|
||||
if default is PydanticUndefined and default_factory is None:
|
||||
advanced = False
|
||||
elif advanced is None:
|
||||
advanced = True
|
||||
|
||||
json_extra = {
|
||||
k: v
|
||||
for k, v in {
|
||||
@@ -149,6 +156,8 @@ def SchemaField(
|
||||
"advanced": advanced,
|
||||
"hidden": hidden,
|
||||
"depends_on": depends_on,
|
||||
"image_upload": image_upload,
|
||||
"image_output": image_output,
|
||||
}.items()
|
||||
if v is not None
|
||||
}
|
||||
@@ -240,7 +249,8 @@ CP = TypeVar("CP", bound=ProviderName)
|
||||
CT = TypeVar("CT", bound=CredentialsType)
|
||||
|
||||
|
||||
CREDENTIALS_FIELD_NAME = "credentials"
|
||||
def is_credentials_field_name(field_name: str) -> bool:
|
||||
return field_name == "credentials" or field_name.endswith("_credentials")
|
||||
|
||||
|
||||
class CredentialsMetaInput(BaseModel, Generic[CP, CT]):
|
||||
@@ -249,21 +259,21 @@ class CredentialsMetaInput(BaseModel, Generic[CP, CT]):
|
||||
provider: CP
|
||||
type: CT
|
||||
|
||||
@staticmethod
|
||||
def _add_json_schema_extra(schema, cls: CredentialsMetaInput):
|
||||
schema["credentials_provider"] = get_args(
|
||||
cls.model_fields["provider"].annotation
|
||||
)
|
||||
schema["credentials_types"] = get_args(cls.model_fields["type"].annotation)
|
||||
@classmethod
|
||||
def allowed_providers(cls) -> tuple[ProviderName, ...]:
|
||||
return get_args(cls.model_fields["provider"].annotation)
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra=_add_json_schema_extra, # type: ignore
|
||||
)
|
||||
@classmethod
|
||||
def allowed_cred_types(cls) -> tuple[CredentialsType, ...]:
|
||||
return get_args(cls.model_fields["type"].annotation)
|
||||
|
||||
@classmethod
|
||||
def validate_credentials_field_schema(cls, model: type["BlockSchema"]):
|
||||
"""Validates the schema of a `credentials` field"""
|
||||
field_schema = model.jsonschema()["properties"][CREDENTIALS_FIELD_NAME]
|
||||
"""Validates the schema of a credentials input field"""
|
||||
field_name = next(
|
||||
name for name, type in model.get_credentials_fields().items() if type is cls
|
||||
)
|
||||
field_schema = model.jsonschema()["properties"][field_name]
|
||||
try:
|
||||
schema_extra = _CredentialsFieldSchemaExtra[CP, CT].model_validate(
|
||||
field_schema
|
||||
@@ -277,11 +287,20 @@ class CredentialsMetaInput(BaseModel, Generic[CP, CT]):
|
||||
f"{field_schema}"
|
||||
) from e
|
||||
|
||||
if (
|
||||
len(schema_extra.credentials_provider) > 1
|
||||
and not schema_extra.discriminator
|
||||
):
|
||||
raise TypeError("Multi-provider CredentialsField requires discriminator!")
|
||||
if len(cls.allowed_providers()) > 1 and not schema_extra.discriminator:
|
||||
raise TypeError(
|
||||
f"Multi-provider CredentialsField '{field_name}' "
|
||||
"requires discriminator!"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_json_schema_extra(schema, cls: CredentialsMetaInput):
|
||||
schema["credentials_provider"] = cls.allowed_providers()
|
||||
schema["credentials_types"] = cls.allowed_cred_types()
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra=_add_json_schema_extra, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
class _CredentialsFieldSchemaExtra(BaseModel, Generic[CP, CT]):
|
||||
|
||||
@@ -10,7 +10,6 @@ from contextlib import contextmanager
|
||||
from multiprocessing.pool import AsyncResult, Pool
|
||||
from typing import TYPE_CHECKING, Any, Generator, TypeVar, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
from redis.lock import Lock as RedisLock
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -20,7 +19,14 @@ from autogpt_libs.utils.cache import thread_cached
|
||||
|
||||
from backend.blocks.agent import AgentExecutorBlock
|
||||
from backend.data import redis
|
||||
from backend.data.block import Block, BlockData, BlockInput, BlockType, get_block
|
||||
from backend.data.block import (
|
||||
Block,
|
||||
BlockData,
|
||||
BlockInput,
|
||||
BlockSchema,
|
||||
BlockType,
|
||||
get_block,
|
||||
)
|
||||
from backend.data.execution import (
|
||||
ExecutionQueue,
|
||||
ExecutionResult,
|
||||
@@ -31,7 +37,6 @@ from backend.data.execution import (
|
||||
parse_execution_output,
|
||||
)
|
||||
from backend.data.graph import GraphModel, Link, Node
|
||||
from backend.data.model import CREDENTIALS_FIELD_NAME, CredentialsMetaInput
|
||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||
from backend.util import json
|
||||
from backend.util.decorator import error_logged, time_measured
|
||||
@@ -170,10 +175,11 @@ def execute_node(
|
||||
# one (running) block at a time; simultaneous execution of blocks using same
|
||||
# credentials is not supported.
|
||||
creds_lock = None
|
||||
if CREDENTIALS_FIELD_NAME in input_data:
|
||||
credentials_meta = CredentialsMetaInput(**input_data[CREDENTIALS_FIELD_NAME])
|
||||
input_model = cast(type[BlockSchema], node_block.input_schema)
|
||||
for field_name, input_type in input_model.get_credentials_fields().items():
|
||||
credentials_meta = input_type(**input_data[field_name])
|
||||
credentials, creds_lock = creds_manager.acquire(user_id, credentials_meta.id)
|
||||
extra_exec_kwargs["credentials"] = credentials
|
||||
extra_exec_kwargs[field_name] = credentials
|
||||
|
||||
output_size = 0
|
||||
end_status = ExecutionStatus.COMPLETED
|
||||
@@ -591,7 +597,7 @@ class Executor:
|
||||
node_eid="*",
|
||||
block_name="-",
|
||||
)
|
||||
timing_info, (exec_stats, error) = cls._on_graph_execution(
|
||||
timing_info, (exec_stats, status, error) = cls._on_graph_execution(
|
||||
graph_exec, cancel, log_metadata
|
||||
)
|
||||
exec_stats["walltime"] = timing_info.wall_time
|
||||
@@ -599,6 +605,7 @@ class Executor:
|
||||
exec_stats["error"] = str(error) if error else None
|
||||
result = cls.db_client.update_graph_execution_stats(
|
||||
graph_exec_id=graph_exec.graph_exec_id,
|
||||
status=status,
|
||||
stats=exec_stats,
|
||||
)
|
||||
cls.db_client.send_execution_update(result)
|
||||
@@ -610,11 +617,12 @@ class Executor:
|
||||
graph_exec: GraphExecutionEntry,
|
||||
cancel: threading.Event,
|
||||
log_metadata: LogMetadata,
|
||||
) -> tuple[dict[str, Any], Exception | None]:
|
||||
) -> tuple[dict[str, Any], ExecutionStatus, Exception | None]:
|
||||
"""
|
||||
Returns:
|
||||
The execution statistics of the graph execution.
|
||||
The error that occurred during the execution.
|
||||
dict: The execution statistics of the graph execution.
|
||||
ExecutionStatus: The final status of the graph execution.
|
||||
Exception | None: The error that occurred during the execution, if any.
|
||||
"""
|
||||
log_metadata.info(f"Start graph execution {graph_exec.graph_exec_id}")
|
||||
exec_stats = {
|
||||
@@ -659,8 +667,7 @@ class Executor:
|
||||
|
||||
while not queue.empty():
|
||||
if cancel.is_set():
|
||||
error = RuntimeError("Execution is cancelled")
|
||||
return exec_stats, error
|
||||
return exec_stats, ExecutionStatus.TERMINATED, error
|
||||
|
||||
exec_data = queue.get()
|
||||
|
||||
@@ -690,8 +697,7 @@ class Executor:
|
||||
)
|
||||
for node_id, execution in list(running_executions.items()):
|
||||
if cancel.is_set():
|
||||
error = RuntimeError("Execution is cancelled")
|
||||
return exec_stats, error
|
||||
return exec_stats, ExecutionStatus.TERMINATED, error
|
||||
|
||||
if not queue.empty():
|
||||
break # yield to parent loop to execute new queue items
|
||||
@@ -710,7 +716,12 @@ class Executor:
|
||||
finished = True
|
||||
cancel.set()
|
||||
cancel_thread.join()
|
||||
return exec_stats, error
|
||||
|
||||
return (
|
||||
exec_stats,
|
||||
ExecutionStatus.FAILED if error else ExecutionStatus.COMPLETED,
|
||||
error,
|
||||
)
|
||||
|
||||
|
||||
class ExecutionManager(AppService):
|
||||
@@ -876,11 +887,8 @@ class ExecutionManager(AppService):
|
||||
ExecutionStatus.COMPLETED,
|
||||
ExecutionStatus.FAILED,
|
||||
):
|
||||
self.db_client.upsert_execution_output(
|
||||
node_exec.node_exec_id, "error", "TERMINATED"
|
||||
)
|
||||
exec_update = self.db_client.update_execution_status(
|
||||
node_exec.node_exec_id, ExecutionStatus.FAILED
|
||||
node_exec.node_exec_id, ExecutionStatus.TERMINATED
|
||||
)
|
||||
self.db_client.send_execution_update(exec_update)
|
||||
|
||||
@@ -893,41 +901,39 @@ class ExecutionManager(AppService):
|
||||
raise ValueError(f"Unknown block {node.block_id} for node #{node.id}")
|
||||
|
||||
# Find any fields of type CredentialsMetaInput
|
||||
model_fields = cast(type[BaseModel], block.input_schema).model_fields
|
||||
if CREDENTIALS_FIELD_NAME not in model_fields:
|
||||
credentials_fields = cast(
|
||||
type[BlockSchema], block.input_schema
|
||||
).get_credentials_fields()
|
||||
if not credentials_fields:
|
||||
continue
|
||||
|
||||
field = model_fields[CREDENTIALS_FIELD_NAME]
|
||||
|
||||
# The BlockSchema class enforces that a `credentials` field is always a
|
||||
# `CredentialsMetaInput`, so we can safely assume this here.
|
||||
credentials_meta_type = cast(CredentialsMetaInput, field.annotation)
|
||||
credentials_meta = credentials_meta_type.model_validate(
|
||||
node.input_default[CREDENTIALS_FIELD_NAME]
|
||||
)
|
||||
# Fetch the corresponding Credentials and perform sanity checks
|
||||
credentials = self.credentials_store.get_creds_by_id(
|
||||
user_id, credentials_meta.id
|
||||
)
|
||||
if not credentials:
|
||||
raise ValueError(
|
||||
f"Unknown credentials #{credentials_meta.id} "
|
||||
f"for node #{node.id}"
|
||||
for field_name, credentials_meta_type in credentials_fields.items():
|
||||
credentials_meta = credentials_meta_type.model_validate(
|
||||
node.input_default[field_name]
|
||||
)
|
||||
if (
|
||||
credentials.provider != credentials_meta.provider
|
||||
or credentials.type != credentials_meta.type
|
||||
):
|
||||
logger.warning(
|
||||
f"Invalid credentials #{credentials.id} for node #{node.id}: "
|
||||
"type/provider mismatch: "
|
||||
f"{credentials_meta.type}<>{credentials.type};"
|
||||
f"{credentials_meta.provider}<>{credentials.provider}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Invalid credentials #{credentials.id} for node #{node.id}: "
|
||||
"type/provider mismatch"
|
||||
# Fetch the corresponding Credentials and perform sanity checks
|
||||
credentials = self.credentials_store.get_creds_by_id(
|
||||
user_id, credentials_meta.id
|
||||
)
|
||||
if not credentials:
|
||||
raise ValueError(
|
||||
f"Unknown credentials #{credentials_meta.id} "
|
||||
f"for node #{node.id} input '{field_name}'"
|
||||
)
|
||||
if (
|
||||
credentials.provider != credentials_meta.provider
|
||||
or credentials.type != credentials_meta.type
|
||||
):
|
||||
logger.warning(
|
||||
f"Invalid credentials #{credentials.id} for node #{node.id}: "
|
||||
"type/provider mismatch: "
|
||||
f"{credentials_meta.type}<>{credentials.type};"
|
||||
f"{credentials_meta.provider}<>{credentials.provider}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Invalid credentials #{credentials.id} for node #{node.id}: "
|
||||
"type/provider mismatch"
|
||||
)
|
||||
|
||||
|
||||
# ------- UTILITIES ------- #
|
||||
@@ -947,7 +953,8 @@ def synchronized(key: str, timeout: int = 60):
|
||||
lock.acquire()
|
||||
yield
|
||||
finally:
|
||||
lock.release()
|
||||
if lock.locked():
|
||||
lock.release()
|
||||
|
||||
|
||||
def llprint(message: str):
|
||||
|
||||
@@ -99,6 +99,10 @@ class ExecutionScheduler(AppService):
|
||||
def get_port(cls) -> int:
|
||||
return config.execution_scheduler_port
|
||||
|
||||
@classmethod
|
||||
def db_pool_size(cls) -> int:
|
||||
return config.scheduler_db_pool_size
|
||||
|
||||
@property
|
||||
@thread_cached
|
||||
def execution_client(self) -> ExecutionManager:
|
||||
@@ -110,7 +114,11 @@ class ExecutionScheduler(AppService):
|
||||
self.scheduler = BlockingScheduler(
|
||||
jobstores={
|
||||
"default": SQLAlchemyJobStore(
|
||||
engine=create_engine(db_url),
|
||||
engine=create_engine(
|
||||
url=db_url,
|
||||
pool_size=self.db_pool_size(),
|
||||
max_overflow=0,
|
||||
),
|
||||
metadata=MetaData(schema=db_schema),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class IntegrationCredentialsManager:
|
||||
|
||||
fresh_credentials = oauth_handler.refresh_tokens(credentials)
|
||||
self.store.update_creds(user_id, fresh_credentials)
|
||||
if _lock:
|
||||
if _lock and _lock.locked():
|
||||
_lock.release()
|
||||
|
||||
credentials = fresh_credentials
|
||||
@@ -144,7 +144,8 @@ class IntegrationCredentialsManager:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
lock.release()
|
||||
if lock.locked():
|
||||
lock.release()
|
||||
|
||||
def release_all_locks(self):
|
||||
"""Call this on process termination to ensure all locks are released"""
|
||||
|
||||
@@ -19,6 +19,7 @@ class ProviderName(str, Enum):
|
||||
JINA = "jina"
|
||||
MEDIUM = "medium"
|
||||
NOTION = "notion"
|
||||
NVIDIA = "nvidia"
|
||||
OLLAMA = "ollama"
|
||||
OPENAI = "openai"
|
||||
OPENWEATHERMAP = "openweathermap"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable, Optional, cast
|
||||
|
||||
from backend.data.block import BlockWebhookConfig, get_block
|
||||
from backend.data.block import BlockSchema, BlockWebhookConfig, get_block
|
||||
from backend.data.graph import set_node_webhook
|
||||
from backend.data.model import CREDENTIALS_FIELD_NAME
|
||||
from backend.integrations.webhooks import WEBHOOK_MANAGERS_BY_NAME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -30,14 +29,28 @@ async def on_graph_activate(
|
||||
# Compare nodes in new_graph_version with previous_graph_version
|
||||
updated_nodes = []
|
||||
for new_node in graph.nodes:
|
||||
block = get_block(new_node.block_id)
|
||||
if not block:
|
||||
raise ValueError(
|
||||
f"Node #{new_node.id} is instance of unknown block #{new_node.block_id}"
|
||||
)
|
||||
block_input_schema = cast(BlockSchema, block.input_schema)
|
||||
|
||||
node_credentials = None
|
||||
if creds_meta := new_node.input_default.get(CREDENTIALS_FIELD_NAME):
|
||||
node_credentials = get_credentials(creds_meta["id"])
|
||||
if not node_credentials:
|
||||
raise ValueError(
|
||||
f"Node #{new_node.id} updated with non-existent "
|
||||
f"credentials #{node_credentials}"
|
||||
if (
|
||||
# Webhook-triggered blocks are only allowed to have 1 credentials input
|
||||
(
|
||||
creds_field_name := next(
|
||||
iter(block_input_schema.get_credentials_fields()), None
|
||||
)
|
||||
)
|
||||
and (creds_meta := new_node.input_default.get(creds_field_name))
|
||||
and not (node_credentials := get_credentials(creds_meta["id"]))
|
||||
):
|
||||
raise ValueError(
|
||||
f"Node #{new_node.id} input '{creds_field_name}' updated with "
|
||||
f"non-existent credentials #{creds_meta['id']}"
|
||||
)
|
||||
|
||||
updated_node = await on_node_activate(
|
||||
graph.user_id, new_node, credentials=node_credentials
|
||||
@@ -62,14 +75,28 @@ async def on_graph_deactivate(
|
||||
"""
|
||||
updated_nodes = []
|
||||
for node in graph.nodes:
|
||||
block = get_block(node.block_id)
|
||||
if not block:
|
||||
raise ValueError(
|
||||
f"Node #{node.id} is instance of unknown block #{node.block_id}"
|
||||
)
|
||||
block_input_schema = cast(BlockSchema, block.input_schema)
|
||||
|
||||
node_credentials = None
|
||||
if creds_meta := node.input_default.get(CREDENTIALS_FIELD_NAME):
|
||||
node_credentials = get_credentials(creds_meta["id"])
|
||||
if not node_credentials:
|
||||
logger.error(
|
||||
f"Node #{node.id} referenced non-existent "
|
||||
f"credentials #{creds_meta['id']}"
|
||||
if (
|
||||
# Webhook-triggered blocks are only allowed to have 1 credentials input
|
||||
(
|
||||
creds_field_name := next(
|
||||
iter(block_input_schema.get_credentials_fields()), None
|
||||
)
|
||||
)
|
||||
and (creds_meta := node.input_default.get(creds_field_name))
|
||||
and not (node_credentials := get_credentials(creds_meta["id"]))
|
||||
):
|
||||
logger.error(
|
||||
f"Node #{node.id} input '{creds_field_name}' referenced non-existent "
|
||||
f"credentials #{creds_meta['id']}"
|
||||
)
|
||||
|
||||
updated_node = await on_node_deactivate(node, credentials=node_credentials)
|
||||
updated_nodes.append(updated_node)
|
||||
@@ -119,14 +146,17 @@ async def on_node_activate(
|
||||
else:
|
||||
resource = "" # not relevant for manual webhooks
|
||||
|
||||
needs_credentials = CREDENTIALS_FIELD_NAME in block.input_schema.model_fields
|
||||
block_input_schema = cast(BlockSchema, block.input_schema)
|
||||
credentials_field_name = next(iter(block_input_schema.get_credentials_fields()), "")
|
||||
credentials_meta = (
|
||||
node.input_default.get(CREDENTIALS_FIELD_NAME) if needs_credentials else None
|
||||
node.input_default.get(credentials_field_name)
|
||||
if credentials_field_name
|
||||
else None
|
||||
)
|
||||
event_filter_input_name = block.webhook_config.event_filter_input
|
||||
has_everything_for_webhook = (
|
||||
resource is not None
|
||||
and (credentials_meta or not needs_credentials)
|
||||
and (credentials_meta or not credentials_field_name)
|
||||
and (
|
||||
not event_filter_input_name
|
||||
or (
|
||||
@@ -230,7 +260,7 @@ async def on_node_deactivate(
|
||||
)
|
||||
await webhooks_manager.prune_webhook_if_dangling(webhook.id, credentials)
|
||||
if (
|
||||
CREDENTIALS_FIELD_NAME in block.input_schema.model_fields
|
||||
cast(BlockSchema, block.input_schema).get_credentials_fields()
|
||||
and not credentials
|
||||
):
|
||||
logger.warning(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from backend.integrations.providers import ProviderName
|
||||
from backend.util.settings import Config
|
||||
|
||||
app_config = Config()
|
||||
|
||||
|
||||
# TODO: add test to assert this matches the actual API route
|
||||
def webhook_ingress_url(provider_name: str, webhook_id: str) -> str:
|
||||
def webhook_ingress_url(provider_name: ProviderName, webhook_id: str) -> str:
|
||||
return (
|
||||
f"{app_config.platform_base_url}/api/integrations/{provider_name}"
|
||||
f"{app_config.platform_base_url}/api/integrations/{provider_name.value}"
|
||||
f"/webhooks/{webhook_id}/ingress"
|
||||
)
|
||||
|
||||
@@ -541,7 +541,7 @@ def get_execution_schedules(
|
||||
|
||||
@v1_router.post(
|
||||
"/api-keys",
|
||||
response_model=list[CreateAPIKeyResponse] | dict[str, str],
|
||||
response_model=CreateAPIKeyResponse,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -583,7 +583,7 @@ async def get_api_keys(
|
||||
|
||||
@v1_router.get(
|
||||
"/api-keys/{key_id}",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -604,7 +604,7 @@ async def get_api_key(
|
||||
|
||||
@v1_router.delete(
|
||||
"/api-keys/{key_id}",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -626,7 +626,7 @@ async def delete_api_key(
|
||||
|
||||
@v1_router.post(
|
||||
"/api-keys/{key_id}/suspend",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
@@ -648,7 +648,7 @@ async def suspend_key(
|
||||
|
||||
@v1_router.put(
|
||||
"/api-keys/{key_id}/permissions",
|
||||
response_model=list[APIKeyWithoutHash] | dict[str, str],
|
||||
response_model=APIKeyWithoutHash,
|
||||
tags=["api-keys"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import pytest
|
||||
import pytest_mock
|
||||
|
||||
import backend.server.v2.library.db
|
||||
@@ -80,6 +81,7 @@ def test_get_library_agents_error(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call.assert_called_once_with("test-user-id")
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Mocker Not implemented")
|
||||
def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library")
|
||||
mock_db_call.return_value = None
|
||||
@@ -91,6 +93,7 @@ def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Mocker Not implemented")
|
||||
def test_add_agent_to_library_error(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library")
|
||||
mock_db_call.side_effect = Exception("Test error")
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import fastapi
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
|
||||
import backend.data.graph
|
||||
import backend.server.v2.store.exceptions
|
||||
import backend.server.v2.store.model
|
||||
from backend.data.graph import GraphModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,7 +35,7 @@ async def get_store_agents(
|
||||
sanitized_query = search_query.strip()
|
||||
if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Invalid search query"
|
||||
f"Invalid search query: len({len(sanitized_query)}) query: {search_query}"
|
||||
)
|
||||
|
||||
# Escape special SQL characters
|
||||
@@ -449,6 +453,11 @@ async def create_store_submission(
|
||||
)
|
||||
|
||||
try:
|
||||
# Sanitize slug to only allow letters and hyphens
|
||||
slug = "".join(
|
||||
c if c.isalpha() or c == "-" or c.isnumeric() else "" for c in slug
|
||||
).lower()
|
||||
|
||||
# First verify the agent belongs to this user
|
||||
agent = await prisma.models.AgentGraph.prisma().find_first(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
@@ -636,7 +645,12 @@ async def update_or_create_profile(
|
||||
logger.info(f"Updating profile for user {user_id} data: {profile}")
|
||||
|
||||
try:
|
||||
# Check if profile exists for user
|
||||
# Sanitize username to only allow letters and hyphens
|
||||
username = "".join(
|
||||
c if c.isalpha() or c == "-" or c.isnumeric() else ""
|
||||
for c in profile.username
|
||||
).lower()
|
||||
|
||||
existing_profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": user_id}
|
||||
)
|
||||
@@ -651,7 +665,7 @@ async def update_or_create_profile(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"name": profile.name,
|
||||
"username": profile.username.lower(),
|
||||
"username": username,
|
||||
"description": profile.description,
|
||||
"links": profile.links or [],
|
||||
"avatarUrl": profile.avatar_url,
|
||||
@@ -676,7 +690,7 @@ async def update_or_create_profile(
|
||||
if profile.name is not None:
|
||||
update_data["name"] = profile.name
|
||||
if profile.username is not None:
|
||||
update_data["username"] = profile.username.lower()
|
||||
update_data["username"] = username
|
||||
if profile.description is not None:
|
||||
update_data["description"] = profile.description
|
||||
if profile.links is not None:
|
||||
@@ -776,3 +790,45 @@ async def get_my_agents(
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch my agents"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_agent(
|
||||
store_listing_version_id: str, version_id: Optional[int]
|
||||
) -> GraphModel:
|
||||
"""Get agent using the version ID and store listing version ID."""
|
||||
try:
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
)
|
||||
)
|
||||
|
||||
if not store_listing_version or not store_listing_version.Agent:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Store listing version {store_listing_version_id} not found",
|
||||
)
|
||||
|
||||
agent = store_listing_version.Agent
|
||||
|
||||
graph = await backend.data.graph.get_graph(
|
||||
agent.id, agent.version, template=True
|
||||
)
|
||||
|
||||
if not graph:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail=f"Agent {agent.id} not found"
|
||||
)
|
||||
|
||||
graph.version = 1
|
||||
graph.is_template = False
|
||||
graph.is_active = True
|
||||
delattr(graph, "user_id")
|
||||
|
||||
return graph
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch agent"
|
||||
) from e
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import tempfile
|
||||
import typing
|
||||
import urllib.parse
|
||||
|
||||
@@ -6,7 +8,9 @@ import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
import backend.data.block
|
||||
import backend.data.graph
|
||||
import backend.server.v2.store.db
|
||||
import backend.server.v2.store.image_gen
|
||||
@@ -23,12 +27,16 @@ router = fastapi.APIRouter()
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/profile", tags=["store", "private"])
|
||||
@router.get(
|
||||
"/profile",
|
||||
tags=["store", "private"],
|
||||
response_model=backend.server.v2.store.model.ProfileDetails,
|
||||
)
|
||||
async def get_profile(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.ProfileDetails:
|
||||
):
|
||||
"""
|
||||
Get the profile details for the authenticated user.
|
||||
"""
|
||||
@@ -37,20 +45,24 @@ async def get_profile(
|
||||
return profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting user profile")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the user profile"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profile",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.CreatorDetails,
|
||||
)
|
||||
async def update_or_create_profile(
|
||||
profile: backend.server.v2.store.model.Profile,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.CreatorDetails:
|
||||
):
|
||||
"""
|
||||
Update the store profile for the authenticated user.
|
||||
|
||||
@@ -71,7 +83,10 @@ async def update_or_create_profile(
|
||||
return updated_profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst updating profile")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while updating the user profile"},
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
@@ -79,7 +94,11 @@ async def update_or_create_profile(
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/agents", tags=["store", "public"])
|
||||
@router.get(
|
||||
"/agents",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentsResponse,
|
||||
)
|
||||
async def get_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
@@ -88,7 +107,7 @@ async def get_agents(
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreAgentsResponse:
|
||||
):
|
||||
"""
|
||||
Get a paginated list of agents from the store with optional filtering and sorting.
|
||||
|
||||
@@ -138,13 +157,18 @@ async def get_agents(
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occured whilst getting store agents")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the store agents"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{username}/{agent_name}", tags=["store", "public"])
|
||||
async def get_agent(
|
||||
username: str, agent_name: str
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
@router.get(
|
||||
"/agents/{username}/{agent_name}",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentDetails,
|
||||
)
|
||||
async def get_agent(username: str, agent_name: str):
|
||||
"""
|
||||
This is only used on the AgentDetails Page
|
||||
|
||||
@@ -153,20 +177,26 @@ async def get_agent(
|
||||
try:
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
# URL decode the agent name since it comes from the URL path
|
||||
agent_name = urllib.parse.unquote(agent_name)
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
agent = await backend.server.v2.store.db.get_store_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
)
|
||||
return agent
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store agent details")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the store agent details"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/agents/{username}/{agent_name}/review",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreReview,
|
||||
)
|
||||
async def create_review(
|
||||
username: str,
|
||||
@@ -175,7 +205,7 @@ async def create_review(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreReview:
|
||||
):
|
||||
"""
|
||||
Create a review for a store agent.
|
||||
|
||||
@@ -202,7 +232,10 @@ async def create_review(
|
||||
return created_review
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store review")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while creating the store review"},
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
@@ -210,14 +243,18 @@ async def create_review(
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/creators", tags=["store", "public"])
|
||||
@router.get(
|
||||
"/creators",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.CreatorsResponse,
|
||||
)
|
||||
async def get_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.CreatorsResponse:
|
||||
):
|
||||
"""
|
||||
This is needed for:
|
||||
- Home Page Featured Creators
|
||||
@@ -251,11 +288,20 @@ async def get_creators(
|
||||
return creators
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store creators")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the store creators"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/creator/{username}", tags=["store", "public"])
|
||||
async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDetails:
|
||||
@router.get(
|
||||
"/creator/{username}",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.CreatorDetails,
|
||||
)
|
||||
async def get_creator(
|
||||
username: str,
|
||||
):
|
||||
"""
|
||||
Get the details of a creator
|
||||
- Creator Details Page
|
||||
@@ -268,7 +314,12 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet
|
||||
return creator
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting creator details")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the creator details"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
############################################
|
||||
@@ -278,31 +329,36 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet
|
||||
"/myagents",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.MyAgentsResponse,
|
||||
)
|
||||
async def get_my_agents(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.MyAgentsResponse:
|
||||
):
|
||||
try:
|
||||
agents = await backend.server.v2.store.db.get_my_agents(user_id)
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting my agents")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the my agents"},
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/submissions/{submission_id}",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=bool,
|
||||
)
|
||||
async def delete_submission(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
submission_id: str,
|
||||
) -> bool:
|
||||
):
|
||||
"""
|
||||
Delete a store listing submission.
|
||||
|
||||
@@ -321,13 +377,17 @@ async def delete_submission(
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst deleting store submission")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while deleting the store submission"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreSubmissionsResponse,
|
||||
)
|
||||
async def get_submissions(
|
||||
user_id: typing.Annotated[
|
||||
@@ -335,7 +395,7 @@ async def get_submissions(
|
||||
],
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
|
||||
):
|
||||
"""
|
||||
Get a paginated list of store submissions for the authenticated user.
|
||||
|
||||
@@ -368,20 +428,26 @@ async def get_submissions(
|
||||
return listings
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store submissions")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the store submissions"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreSubmission,
|
||||
)
|
||||
async def create_submission(
|
||||
submission_request: backend.server.v2.store.model.StoreSubmissionRequest,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
):
|
||||
"""
|
||||
Create a new store listing submission.
|
||||
|
||||
@@ -411,7 +477,10 @@ async def create_submission(
|
||||
return submission
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store submission")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while creating the store submission"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -424,7 +493,7 @@ async def upload_submission_media(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> str:
|
||||
):
|
||||
"""
|
||||
Upload media (images/videos) for a store listing submission.
|
||||
|
||||
@@ -443,10 +512,11 @@ async def upload_submission_media(
|
||||
user_id=user_id, file=file
|
||||
)
|
||||
return media_url
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst uploading submission media")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"Failed to upload media file: {str(e)}"
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while uploading the media file"},
|
||||
)
|
||||
|
||||
|
||||
@@ -503,8 +573,72 @@ async def generate_image(
|
||||
)
|
||||
|
||||
return fastapi.responses.JSONResponse(content={"image_url": image_url})
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst generating submission image")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"Failed to generate image: {str(e)}"
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while generating the image"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/download/agents/{store_listing_version_id}",
|
||||
tags=["store", "public"],
|
||||
)
|
||||
async def download_agent_file(
|
||||
store_listing_version_id: str = fastapi.Path(
|
||||
..., description="The ID of the agent to download"
|
||||
),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
) -> fastapi.responses.FileResponse:
|
||||
"""
|
||||
Download the agent file by streaming its content.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to download.
|
||||
version (Optional[int]): Specific version of the agent to download.
|
||||
|
||||
Returns:
|
||||
StreamingResponse: A streaming response containing the agent's graph data.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
|
||||
graph_data = await backend.server.v2.store.db.get_agent(
|
||||
store_listing_version_id=store_listing_version_id, version_id=version
|
||||
)
|
||||
|
||||
graph_data.clean_graph()
|
||||
graph_date_dict = jsonable_encoder(graph_data)
|
||||
|
||||
def remove_credentials(obj):
|
||||
if obj and isinstance(obj, dict):
|
||||
if "credentials" in obj:
|
||||
del obj["credentials"]
|
||||
if "creds" in obj:
|
||||
del obj["creds"]
|
||||
|
||||
for value in obj.values():
|
||||
remove_credentials(value)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
remove_credentials(item)
|
||||
return obj
|
||||
|
||||
graph_date_dict = remove_credentials(graph_date_dict)
|
||||
|
||||
file_name = f"agent_{store_listing_version_id}_v{version or 'latest'}.json"
|
||||
|
||||
# Sending graph as a stream (similar to marketplace v1)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False
|
||||
) as tmp_file:
|
||||
tmp_file.write(json.dumps(graph_date_dict))
|
||||
tmp_file.flush()
|
||||
|
||||
return fastapi.responses.FileResponse(
|
||||
tmp_file.name, filename=file_name, media_type="application/json"
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ from backend.util.settings import Config
|
||||
# List of IP networks to block
|
||||
BLOCKED_IP_NETWORKS = [
|
||||
# --8<-- [start:BLOCKED_IP_NETWORKS]
|
||||
# IPv4 Ranges
|
||||
ipaddress.ip_network("0.0.0.0/8"), # "This" Network
|
||||
ipaddress.ip_network("10.0.0.0/8"), # Private-Use
|
||||
ipaddress.ip_network("127.0.0.0/8"), # Loopback
|
||||
@@ -20,6 +21,11 @@ BLOCKED_IP_NETWORKS = [
|
||||
ipaddress.ip_network("192.168.0.0/16"), # Private-Use
|
||||
ipaddress.ip_network("224.0.0.0/4"), # Multicast
|
||||
ipaddress.ip_network("240.0.0.0/4"), # Reserved for Future Use
|
||||
# IPv6 Ranges
|
||||
ipaddress.ip_network("::1/128"), # Loopback
|
||||
ipaddress.ip_network("fc00::/7"), # Unique local addresses (ULA)
|
||||
ipaddress.ip_network("fe80::/10"), # Link-local
|
||||
ipaddress.ip_network("ff00::/8"), # Multicast
|
||||
# --8<-- [end:BLOCKED_IP_NETWORKS]
|
||||
]
|
||||
|
||||
@@ -27,18 +33,6 @@ ALLOWED_SCHEMES = ["http", "https"]
|
||||
HOSTNAME_REGEX = re.compile(r"^[A-Za-z0-9.-]+$") # Basic DNS-safe hostname pattern
|
||||
|
||||
|
||||
def _canonicalize_url(url: str) -> str:
|
||||
# Strip spaces and trailing slashes
|
||||
url = url.strip().strip("/")
|
||||
# Ensure the URL starts with http:// or https://
|
||||
if not url.startswith(("http://", "https://")):
|
||||
url = "http://" + url
|
||||
|
||||
# Replace backslashes with forward slashes to avoid parsing ambiguities
|
||||
url = url.replace("\\", "/")
|
||||
return url
|
||||
|
||||
|
||||
def _is_ip_blocked(ip: str) -> bool:
|
||||
"""
|
||||
Checks if the IP address is in a blocked network.
|
||||
@@ -49,11 +43,16 @@ def _is_ip_blocked(ip: str) -> bool:
|
||||
|
||||
def validate_url(url: str, trusted_origins: list[str]) -> str:
|
||||
"""
|
||||
Validates the URL to prevent SSRF attacks by ensuring it does not point to a private
|
||||
or untrusted IP address, unless whitelisted.
|
||||
Validates the URL to prevent SSRF attacks by ensuring it does not point
|
||||
to a private, link-local, or otherwise blocked IP address — unless
|
||||
the hostname is explicitly trusted.
|
||||
"""
|
||||
url = _canonicalize_url(url)
|
||||
# Canonicalize URL
|
||||
url = url.strip("/ ").replace("\\", "/")
|
||||
parsed = urlparse(url)
|
||||
if not parsed.scheme:
|
||||
url = f"http://{url}"
|
||||
parsed = urlparse(url)
|
||||
|
||||
# Check scheme
|
||||
if parsed.scheme not in ALLOWED_SCHEMES:
|
||||
@@ -61,7 +60,7 @@ def validate_url(url: str, trusted_origins: list[str]) -> str:
|
||||
f"Scheme '{parsed.scheme}' is not allowed. Only HTTP/HTTPS are supported."
|
||||
)
|
||||
|
||||
# Validate and IDNA encode the hostname
|
||||
# Validate and IDNA encode hostname
|
||||
if not parsed.hostname:
|
||||
raise ValueError("Invalid URL: No hostname found.")
|
||||
|
||||
@@ -75,11 +74,11 @@ def validate_url(url: str, trusted_origins: list[str]) -> str:
|
||||
if not HOSTNAME_REGEX.match(ascii_hostname):
|
||||
raise ValueError("Hostname contains invalid characters.")
|
||||
|
||||
# Rebuild the URL with the normalized, IDNA-encoded hostname
|
||||
# Rebuild URL with IDNA-encoded hostname
|
||||
parsed = parsed._replace(netloc=ascii_hostname)
|
||||
url = str(urlunparse(parsed))
|
||||
|
||||
# Check if hostname is a trusted origin (exact match)
|
||||
# If hostname is trusted, skip IP-based checks
|
||||
if ascii_hostname in trusted_origins:
|
||||
return url
|
||||
|
||||
@@ -92,11 +91,12 @@ def validate_url(url: str, trusted_origins: list[str]) -> str:
|
||||
if not ip_addresses:
|
||||
raise ValueError(f"No IP addresses found for {ascii_hostname}")
|
||||
|
||||
# Check if any resolved IP address falls into blocked ranges
|
||||
for ip in ip_addresses:
|
||||
if _is_ip_blocked(ip):
|
||||
# Block any IP address that belongs to a blocked range
|
||||
for ip_str in ip_addresses:
|
||||
if _is_ip_blocked(ip_str):
|
||||
raise ValueError(
|
||||
f"Access to private IP address {ip} for hostname {ascii_hostname} is not allowed."
|
||||
f"Access to blocked or private IP address {ip_str} "
|
||||
f"for hostname {ascii_hostname} is not allowed."
|
||||
)
|
||||
|
||||
return url
|
||||
@@ -104,7 +104,9 @@ def validate_url(url: str, trusted_origins: list[str]) -> str:
|
||||
|
||||
class Requests:
|
||||
"""
|
||||
A wrapper around the requests library that validates URLs before making requests.
|
||||
A wrapper around the requests library that validates URLs before
|
||||
making requests, preventing SSRF by blocking private networks and
|
||||
other disallowed address spaces.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -128,13 +130,16 @@ class Requests:
|
||||
def request(
|
||||
self, method, url, headers=None, allow_redirects=False, *args, **kwargs
|
||||
) -> req.Response:
|
||||
# Merge any extra headers
|
||||
if self.extra_headers is not None:
|
||||
headers = {**(headers or {}), **self.extra_headers}
|
||||
|
||||
# Validate the URL (with optional extra validator)
|
||||
url = validate_url(url, self.trusted_origins)
|
||||
if self.extra_url_validator is not None:
|
||||
url = self.extra_url_validator(url)
|
||||
|
||||
# Perform the request
|
||||
response = req.request(
|
||||
method,
|
||||
url,
|
||||
|
||||
@@ -153,6 +153,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
description="The name of the Google Cloud Storage bucket for media files",
|
||||
)
|
||||
|
||||
scheduler_db_pool_size: int = Field(
|
||||
default=3,
|
||||
description="The pool size for the scheduler database connection pool",
|
||||
)
|
||||
|
||||
@field_validator("platform_base_url", "frontend_base_url")
|
||||
@classmethod
|
||||
def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Sequence
|
||||
from typing import Sequence, cast
|
||||
|
||||
from backend.data import db
|
||||
from backend.data.block import Block, initialize_blocks
|
||||
from backend.data.block import Block, BlockSchema, initialize_blocks
|
||||
from backend.data.execution import ExecutionResult, ExecutionStatus
|
||||
from backend.data.model import CREDENTIALS_FIELD_NAME
|
||||
from backend.data.model import _BaseCredentials
|
||||
from backend.data.user import create_default_user
|
||||
from backend.executor import DatabaseManager, ExecutionManager, ExecutionScheduler
|
||||
from backend.server.rest_api import AgentServer
|
||||
@@ -65,6 +65,9 @@ async def wait_execution(
|
||||
if status == ExecutionStatus.FAILED:
|
||||
log.info("Execution failed")
|
||||
raise Exception("Execution failed")
|
||||
if status == ExecutionStatus.TERMINATED:
|
||||
log.info("Execution terminated")
|
||||
raise Exception("Execution terminated")
|
||||
return status == ExecutionStatus.COMPLETED
|
||||
|
||||
# Wait for the executions to complete
|
||||
@@ -100,14 +103,22 @@ def execute_block_test(block: Block):
|
||||
else:
|
||||
log.info(f"{prefix} mock {mock_name} not found in block")
|
||||
|
||||
# Populate credentials argument(s)
|
||||
extra_exec_kwargs = {}
|
||||
|
||||
if CREDENTIALS_FIELD_NAME in block.input_schema.model_fields:
|
||||
if not block.test_credentials:
|
||||
raise ValueError(
|
||||
f"{prefix} requires credentials but has no test_credentials"
|
||||
)
|
||||
extra_exec_kwargs[CREDENTIALS_FIELD_NAME] = block.test_credentials
|
||||
input_model = cast(type[BlockSchema], block.input_schema)
|
||||
credentials_input_fields = input_model.get_credentials_fields()
|
||||
if len(credentials_input_fields) == 1 and isinstance(
|
||||
block.test_credentials, _BaseCredentials
|
||||
):
|
||||
field_name = next(iter(credentials_input_fields))
|
||||
extra_exec_kwargs[field_name] = block.test_credentials
|
||||
elif credentials_input_fields and block.test_credentials:
|
||||
if not isinstance(block.test_credentials, dict):
|
||||
raise TypeError(f"Block {block.name} has no usable test credentials")
|
||||
else:
|
||||
for field_name in credentials_input_fields:
|
||||
if field_name in block.test_credentials:
|
||||
extra_exec_kwargs[field_name] = block.test_credentials[field_name]
|
||||
|
||||
for input_data in block.test_input:
|
||||
log.info(f"{prefix} in: {input_data}")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW IF EXISTS "StoreAgent";
|
||||
|
||||
CREATE VIEW "StoreAgent" AS
|
||||
WITH ReviewStats AS (
|
||||
SELECT sl."id" AS "storeListingId",
|
||||
COUNT(sr.id) AS review_count,
|
||||
AVG(CAST(sr.score AS DECIMAL)) AS avg_rating
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl."id"
|
||||
JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
GROUP BY sl."id"
|
||||
),
|
||||
AgentRuns AS (
|
||||
SELECT "agentGraphId", COUNT(*) AS run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "agentGraphId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
slv.id AS "storeListingVersionId",
|
||||
slv."createdAt" AS updated_at,
|
||||
slv.slug,
|
||||
slv.name AS agent_name,
|
||||
slv."videoUrl" AS agent_video,
|
||||
COALESCE(slv."imageUrls", ARRAY[]::TEXT[]) AS agent_image,
|
||||
slv."isFeatured" AS featured,
|
||||
p.username AS creator_username,
|
||||
p."avatarUrl" AS creator_avatar,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.categories,
|
||||
COALESCE(ar.run_count, 0) AS runs,
|
||||
CAST(COALESCE(rs.avg_rating, 0.0) AS DOUBLE PRECISION) AS rating,
|
||||
ARRAY_AGG(DISTINCT CAST(slv.version AS TEXT)) AS versions
|
||||
FROM "StoreListing" sl
|
||||
JOIN "AgentGraph" a ON sl."agentId" = a.id AND sl."agentVersion" = a."version"
|
||||
LEFT JOIN "Profile" p ON sl."owningUserId" = p."userId"
|
||||
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN ReviewStats rs ON sl.id = rs."storeListingId"
|
||||
LEFT JOIN AgentRuns ar ON a.id = ar."agentGraphId"
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
AND sl."isApproved" = TRUE
|
||||
GROUP BY sl.id, slv.id, slv.slug, slv."createdAt", slv.name, slv."videoUrl", slv."imageUrls", slv."isFeatured",
|
||||
p.username, p."avatarUrl", slv."subHeading", slv.description, slv.categories,
|
||||
ar.run_count, rs.avg_rating;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add "TERMINATED" to execution status enum type
|
||||
ALTER TYPE "AgentExecutionStatus" ADD VALUE 'TERMINATED';
|
||||
205
autogpt_platform/backend/poetry.lock
generated
205
autogpt_platform/backend/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ version = "0.3.4"
|
||||
description = "A platform for building AI-powered agentic workflows"
|
||||
authors = ["AutoGPT <info@agpt.co>"]
|
||||
readme = "README.md"
|
||||
packages = [{ include = "backend" }]
|
||||
packages = [{ include = "backend", format = "sdist" }]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
@@ -14,7 +14,6 @@ anthropic = "^0.40.0"
|
||||
apscheduler = "^3.11.0"
|
||||
autogpt-libs = { path = "../autogpt_libs", develop = true }
|
||||
click = "^8.1.7"
|
||||
croniter = "^5.0.1"
|
||||
discord-py = "^2.4.0"
|
||||
e2b-code-interpreter = "^1.0.1"
|
||||
fastapi = "^0.115.5"
|
||||
|
||||
@@ -216,6 +216,7 @@ enum AgentExecutionStatus {
|
||||
QUEUED
|
||||
RUNNING
|
||||
COMPLETED
|
||||
TERMINATED
|
||||
FAILED
|
||||
}
|
||||
|
||||
@@ -638,4 +639,4 @@ enum APIKeyStatus {
|
||||
ACTIVE
|
||||
REVOKED
|
||||
SUSPENDED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,12 @@ async def test_get_input_schema(server: SpinTestServer):
|
||||
Node(
|
||||
id="node_0_a",
|
||||
block_id=input_block,
|
||||
input_default={"name": "in_key_a", "title": "Key A", "value": "A"},
|
||||
input_default={
|
||||
"name": "in_key_a",
|
||||
"title": "Key A",
|
||||
"value": "A",
|
||||
"advanced": True,
|
||||
},
|
||||
metadata={"id": "node_0_a"},
|
||||
),
|
||||
Node(
|
||||
@@ -138,8 +143,8 @@ async def test_get_input_schema(server: SpinTestServer):
|
||||
)
|
||||
|
||||
class ExpectedInputSchema(BlockSchema):
|
||||
in_key_a: Any = SchemaField(title="Key A", default="A", advanced=False)
|
||||
in_key_b: Any = SchemaField(title="in_key_b", advanced=True)
|
||||
in_key_a: Any = SchemaField(title="Key A", default="A", advanced=True)
|
||||
in_key_b: Any = SchemaField(title="in_key_b", advanced=False)
|
||||
|
||||
class ExpectedOutputSchema(BlockSchema):
|
||||
out_key: Any = SchemaField(
|
||||
@@ -155,3 +160,45 @@ async def test_get_input_schema(server: SpinTestServer):
|
||||
output_schema = created_graph.output_schema
|
||||
output_schema["title"] = "ExpectedOutputSchema"
|
||||
assert output_schema == ExpectedOutputSchema.jsonschema()
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="session")
|
||||
async def test_clean_graph(server: SpinTestServer):
|
||||
"""
|
||||
Test the clean_graph function that:
|
||||
1. Clears input block values
|
||||
2. Removes credentials from nodes
|
||||
"""
|
||||
# Create a graph with input blocks and credentials
|
||||
graph = Graph(
|
||||
id="test_clean_graph",
|
||||
name="Test Clean Graph",
|
||||
description="Test graph cleaning",
|
||||
nodes=[
|
||||
Node(
|
||||
id="input_node",
|
||||
block_id=AgentInputBlock().id,
|
||||
input_default={
|
||||
"name": "test_input",
|
||||
"value": "test value",
|
||||
"description": "Test input description",
|
||||
},
|
||||
),
|
||||
],
|
||||
links=[],
|
||||
)
|
||||
|
||||
# Create graph and get model
|
||||
create_graph = CreateGraph(graph=graph)
|
||||
created_graph = await server.agent_server.test_create_graph(
|
||||
create_graph, DEFAULT_USER_ID
|
||||
)
|
||||
|
||||
# Clean the graph
|
||||
created_graph.clean_graph()
|
||||
|
||||
# # Verify input block value is cleared
|
||||
input_node = next(
|
||||
n for n in created_graph.nodes if n.block_id == AgentInputBlock().id
|
||||
)
|
||||
assert input_node.input_default["value"] == ""
|
||||
|
||||
@@ -144,51 +144,6 @@ services:
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
market:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: autogpt_platform/market/Dockerfile
|
||||
develop:
|
||||
watch:
|
||||
- path: ./
|
||||
target: autogpt_platform/market/
|
||||
action: rebuild
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
market-migrations:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
- SUPABASE_URL=http://kong:8000
|
||||
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
- SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
|
||||
- BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
ports:
|
||||
- "8015:8015"
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
market-migrations:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: autogpt_platform/market/Dockerfile
|
||||
command: ["sh", "-c", "poetry run prisma migrate deploy"]
|
||||
develop:
|
||||
watch:
|
||||
- path: ./
|
||||
target: autogpt_platform/market/
|
||||
action: rebuild
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SUPABASE_URL=http://kong:8000
|
||||
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
- SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
|
||||
networks:
|
||||
- app-network
|
||||
# frontend:
|
||||
# build:
|
||||
# context: ../
|
||||
|
||||
@@ -51,18 +51,6 @@ services:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: websocket_server
|
||||
|
||||
market:
|
||||
<<: *agpt-services
|
||||
extends:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: market
|
||||
|
||||
market-migrations:
|
||||
<<: *agpt-services
|
||||
extends:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: market-migrations
|
||||
|
||||
# frontend:
|
||||
# <<: *agpt-services
|
||||
# extends:
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@next/third-parties": "^15.0.4",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@next/third-parties": "^15.1.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
@@ -59,29 +59,29 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"elliptic": "6.6.1",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"framer-motion": "^11.16.0",
|
||||
"geist": "^1.3.1",
|
||||
"launchdarkly-react-client-sdk": "^3.6.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.13",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18",
|
||||
"react-day-picker": "^9.4.4",
|
||||
"react-day-picker": "^9.5.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-shepherd": "^6.1.6",
|
||||
"recharts": "^2.14.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.0.3",
|
||||
"uuid": "^11.0.4",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@chromatic-com/storybook": "^3.2.3",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@storybook/addon-a11y": "^8.3.5",
|
||||
"@storybook/addon-essentials": "^8.4.2",
|
||||
@@ -92,25 +92,25 @@
|
||||
"@storybook/nextjs": "^8.4.2",
|
||||
"@storybook/react": "^8.3.5",
|
||||
"@storybook/test": "^8.3.5",
|
||||
"@storybook/test-runner": "^0.20.1",
|
||||
"@storybook/test-runner": "^0.21.0",
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"axe-playwright": "^2.0.3",
|
||||
"chromatic": "^11.12.5",
|
||||
"concurrently": "^9.0.1",
|
||||
"chromatic": "^11.22.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.1.0",
|
||||
"eslint-plugin-storybook": "^0.11.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"eslint-plugin-storybook": "^0.11.2",
|
||||
"msw": "^2.7.0",
|
||||
"msw-storybook-addon": "^2.0.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"storybook": "^8.4.5",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Navbar } from "@/components/agptui/Navbar";
|
||||
@@ -10,8 +10,16 @@ import TallyPopupSimple from "@/components/TallyPopup";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { IconType } from "@/components/ui/icons";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
// Fonts
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "NextGen AutoGPT",
|
||||
@@ -24,7 +32,10 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html
|
||||
lang="en"
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} ${poppins.variable} ${inter.variable}`}
|
||||
>
|
||||
<body className={cn("antialiased transition-colors", inter.className)}>
|
||||
<Providers
|
||||
attribute="class"
|
||||
|
||||
@@ -5,11 +5,7 @@ import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
});
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
|
||||
export async function logout() {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
@@ -25,7 +21,7 @@ export async function logout() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging out", error);
|
||||
console.error("Error logging out", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
@@ -47,18 +43,13 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signInWithPassword(values);
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging in", error);
|
||||
if (error.status == 400) {
|
||||
// Hence User is not present
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
console.error("Error logging in", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
@@ -68,38 +59,34 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function signup(values: z.infer<typeof loginFormSchema>) {
|
||||
"use server";
|
||||
export async function providerLogin(provider: LoginProvider) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
"providerLogin",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
const api = new BackendAPI();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
const { error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo:
|
||||
process.env.AUTH_CALLBACK_URL ??
|
||||
`http://localhost:3000/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log("Error signing up", error);
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
console.error("Error logging in", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Signed up");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/store/profile");
|
||||
await api.createUser();
|
||||
console.log("Logged in");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
import { login, signup } from "./actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { login, providerLogin } from "./actions";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -14,40 +12,69 @@ import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
agreeToTerms: z.boolean().refine((value) => value === true, {
|
||||
message: "You must agree to the Terms of Use and Privacy Policy",
|
||||
}),
|
||||
});
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
AuthBottomText,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import { loginFormSchema } from "@/types/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const api = useBackendAPI();
|
||||
|
||||
const form = useForm<z.infer<typeof loginFormSchema>>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: uncomment when we enable social login
|
||||
// const onProviderLogin = useCallback(async (
|
||||
// provider: LoginProvider,
|
||||
// ) => {
|
||||
// setIsLoading(true);
|
||||
// const error = await providerLogin(provider);
|
||||
// setIsLoading(false);
|
||||
// if (error) {
|
||||
// setFeedback(error);
|
||||
// return;
|
||||
// }
|
||||
// setFeedback(null);
|
||||
// }, [supabase]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
async (data: z.infer<typeof loginFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!(await form.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await login(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
if (user) {
|
||||
console.debug("User exists, redirecting to /");
|
||||
router.push("/");
|
||||
@@ -65,179 +92,60 @@ export default function LoginPage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSignInWithProvider(
|
||||
provider: "google" | "github" | "discord",
|
||||
) {
|
||||
const { data, error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo:
|
||||
process.env.AUTH_CALLBACK_URL ??
|
||||
`http://localhost:3000/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (!error) {
|
||||
setFeedback(null);
|
||||
return;
|
||||
}
|
||||
setFeedback(error.message);
|
||||
}
|
||||
|
||||
const onLogin = async (data: z.infer<typeof loginFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
const error = await login(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
|
||||
<h1 className="text-lg font-medium">Log in to your Account </h1>
|
||||
{/* <div className="mb-6 space-y-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("google")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
<AuthCard>
|
||||
<AuthHeader>Login to your account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="flex w-full items-center justify-between">
|
||||
<span>Password</span>
|
||||
<Link
|
||||
href="/reset_password"
|
||||
className="text-sm font-normal leading-normal text-black underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onLogin(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
<FaGoogle className="mr-2 h-4 w-4" />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("github")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" />
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("discord")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaDiscord className="mr-2 h-4 w-4" />
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div> */}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
I agree to the{" "}
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-6 mt-8 flex w-full space-x-4">
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await login(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Log in"}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await signup(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Sign up"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-sm text-red-500">{feedback}</p>
|
||||
</Form>
|
||||
<Link href="/reset_password" className="text-sm">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
Login
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
<AuthBottomText
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/signup"
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
7
autogpt_platform/frontend/src/app/marketplace/page.tsx
Normal file
7
autogpt_platform/frontend/src/app/marketplace/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/store");
|
||||
}
|
||||
60
autogpt_platform/frontend/src/app/reset_password/actions.ts
Normal file
60
autogpt_platform/frontend/src/app/reset_password/actions.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
"use server";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { redirect } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function sendResetEmail(email: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"sendResetEmail",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
const headersList = headers();
|
||||
const host = headersList.get("host");
|
||||
const protocol =
|
||||
process.env.NODE_ENV === "development" ? "http" : "https";
|
||||
const origin = `${protocol}://${host}`;
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${origin}/reset_password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending reset email", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
console.log("Reset email sent");
|
||||
redirect("/reset_password");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function changePassword(password: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"changePassword",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
console.error("Error changing password", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
await supabase.auth.signOut();
|
||||
redirect("/login");
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -10,54 +17,87 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { sendEmailFormSchema, changePasswordFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
import { z } from "zod";
|
||||
|
||||
const emailFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
});
|
||||
|
||||
const resetPasswordFormSchema = z
|
||||
.object({
|
||||
password: z.string().min(6).max(64),
|
||||
confirmPassword: z.string().min(6).max(64),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
import { changePassword, sendResetEmail } from "./actions";
|
||||
import Spinner from "@/components/Spinner";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const emailForm = useForm<z.infer<typeof emailFormSchema>>({
|
||||
resolver: zodResolver(emailFormSchema),
|
||||
const sendEmailForm = useForm<z.infer<typeof sendEmailFormSchema>>({
|
||||
resolver: zodResolver(sendEmailFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
|
||||
const resetPasswordForm = useForm<z.infer<typeof resetPasswordFormSchema>>({
|
||||
resolver: zodResolver(resetPasswordFormSchema),
|
||||
const changePasswordForm = useForm<z.infer<typeof changePasswordFormSchema>>({
|
||||
resolver: zodResolver(changePasswordFormSchema),
|
||||
defaultValues: {
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSendEmail = useCallback(
|
||||
async (data: z.infer<typeof sendEmailFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await sendEmailForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await sendResetEmail(data.email);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
setDisabled(true);
|
||||
setFeedback(
|
||||
"Password reset email sent if user exists. Please check your email.",
|
||||
);
|
||||
setIsError(false);
|
||||
},
|
||||
[sendEmailForm],
|
||||
);
|
||||
|
||||
const onChangePassword = useCallback(
|
||||
async (data: z.infer<typeof changePasswordFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await changePasswordForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await changePassword(data.password);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
setFeedback("Password changed successfully. Redirecting to login.");
|
||||
setIsError(false);
|
||||
},
|
||||
[changePasswordForm],
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
@@ -68,147 +108,79 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function onSendEmail(d: z.infer<typeof emailFormSchema>) {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await emailForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase!.auth.resetPasswordForEmail(
|
||||
d.email,
|
||||
{
|
||||
redirectTo: `${window.location.origin}/reset_password`,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setFeedback(error.message);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setFeedback("Password reset email sent. Please check your email.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function onResetPassword(d: z.infer<typeof resetPasswordFormSchema>) {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await resetPasswordForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase!.auth.updateUser({
|
||||
password: d.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setFeedback(error.message);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase!.auth.signOut();
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<h1 className="text-center text-3xl font-bold">Reset Password</h1>
|
||||
{user ? (
|
||||
<form
|
||||
onSubmit={resetPasswordForm.handleSubmit(onResetPassword)}
|
||||
className="mt-6 space-y-6"
|
||||
>
|
||||
<Form {...resetPasswordForm}>
|
||||
<FormField
|
||||
control={resetPasswordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetPasswordForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => onResetPassword(resetPasswordForm.getValues())}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="mr-2 animate-spin" /> : null}
|
||||
Reset Password
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={emailForm.handleSubmit(onSendEmail)}
|
||||
className="mt-6 space-y-6"
|
||||
>
|
||||
<Form {...emailForm}>
|
||||
<FormField
|
||||
control={emailForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => onSendEmail(emailForm.getValues())}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="mr-2 animate-spin" /> : null}
|
||||
Send Reset Email
|
||||
</Button>
|
||||
{feedback ? (
|
||||
<div className="text-center text-sm text-red-500">
|
||||
{feedback}
|
||||
</div>
|
||||
) : null}
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AuthCard>
|
||||
<AuthHeader>Reset Password</AuthHeader>
|
||||
{user ? (
|
||||
<form onSubmit={changePasswordForm.handleSubmit(onChangePassword)}>
|
||||
<Form {...changePasswordForm}>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onChangePassword(changePasswordForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Update password
|
||||
</AuthButton>
|
||||
<AuthFeedback message={feedback} isError={isError} />
|
||||
</Form>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={sendEmailForm.handleSubmit(onSendEmail)}>
|
||||
<Form {...sendEmailForm}>
|
||||
<FormField
|
||||
control={sendEmailForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onSendEmail(sendEmailForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
>
|
||||
Send reset email
|
||||
</AuthButton>
|
||||
<AuthFeedback message={feedback} isError={isError} />
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
44
autogpt_platform/frontend/src/app/signup/actions.ts
Normal file
44
autogpt_platform/frontend/src/app/signup/actions.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
|
||||
export async function signup(values: z.infer<typeof signupFormSchema>) {
|
||||
"use server";
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing up", error);
|
||||
// FIXME: supabase doesn't return the correct error message for this case
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Signed up");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/store/profile");
|
||||
},
|
||||
);
|
||||
}
|
||||
219
autogpt_platform/frontend/src/app/signup/page.tsx
Normal file
219
autogpt_platform/frontend/src/app/signup/page.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
import { signup } from "./actions";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
AuthBottomText,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
|
||||
export default function SignupPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showWaitlistPrompt, setShowWaitlistPrompt] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof signupFormSchema>>({
|
||||
resolver: zodResolver(signupFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSignup = useCallback(
|
||||
async (data: z.infer<typeof signupFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!(await form.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await signup(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setShowWaitlistPrompt(true);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
if (user) {
|
||||
console.debug("User exists, redirecting to /");
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return (
|
||||
<div>
|
||||
User accounts are disabled because Supabase client is unavailable
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onSignup(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Sign up
|
||||
</AuthButton>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-6 flex flex-row items-start -space-y-1 space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="">
|
||||
<FormLabel>
|
||||
<span className="mr-1 text-sm font-normal leading-normal text-slate-950">
|
||||
I agree to the
|
||||
</span>
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
and
|
||||
</span>
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
{showWaitlistPrompt && (
|
||||
<div>
|
||||
<span className="mr-1 text-sm font-normal leading-normal text-red-500">
|
||||
The provided email may not be allowed to sign up.
|
||||
</span>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- AutoGPT Platform is currently in closed beta. You can join
|
||||
</span>
|
||||
<Link
|
||||
href="https://agpt.co/waitlist"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
the waitlist here.
|
||||
</Link>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- Make sure you use the same email address you used to sign up for
|
||||
the waitlist.
|
||||
</span>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- You can self host the platform, visit our
|
||||
</span>
|
||||
<Link
|
||||
href="https://agpt.co/waitlist"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
GitHub repository.
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<AuthBottomText
|
||||
text="Already a member?"
|
||||
linkText="Log in"
|
||||
href="/login"
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { APIKeysSection } from "@/components/agptui/composite/APIKeySection";
|
||||
|
||||
const ApiKeysPage = () => {
|
||||
return (
|
||||
<div className="w-full pr-4 pt-24 md:pt-0">
|
||||
<APIKeysSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeysPage;
|
||||
@@ -8,6 +8,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
{ text: "Creator Dashboard", href: "/store/dashboard" },
|
||||
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
|
||||
{ text: "Integrations", href: "/store/integrations" },
|
||||
{ text: "API Keys", href: "/store/api_keys" },
|
||||
{ text: "Profile", href: "/store/profile" },
|
||||
{ text: "Settings", href: "/store/settings" },
|
||||
],
|
||||
@@ -17,7 +18,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="pl-4">{children}</div>
|
||||
<div className="flex-1 pl-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ export default async function Page({
|
||||
const agent = await api.getStoreAgent(creator_lower, params.slug);
|
||||
const otherAgents = await api.getStoreAgents({ creator: creator_lower });
|
||||
const similarAgents = await api.getStoreAgents({
|
||||
search_query: agent.categories[0],
|
||||
// We are using slug as we know its has been sanitized and is not null
|
||||
search_query: agent.slug.replace(/-/g, " "),
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
|
||||
@@ -245,6 +245,7 @@ export function CustomNode({
|
||||
].includes(nodeType) &&
|
||||
// No input connection handles for credentials
|
||||
propKey !== "credentials" &&
|
||||
!propKey.endsWith("_credentials") &&
|
||||
// For OUTPUT blocks, only show the 'value' (hides 'name') input connection handle
|
||||
!(nodeType == BlockUIType.OUTPUT && propKey == "name");
|
||||
const isConnected = isInputHandleConnected(propKey);
|
||||
@@ -261,7 +262,8 @@ export function CustomNode({
|
||||
side="left"
|
||||
/>
|
||||
) : (
|
||||
propKey != "credentials" && (
|
||||
propKey !== "credentials" &&
|
||||
!propKey.endsWith("_credentials") && (
|
||||
<div className="flex gap-1">
|
||||
<span className="text-m green mb-0 text-gray-900 dark:text-gray-100">
|
||||
{propSchema.title || beautifyString(propKey)}
|
||||
@@ -726,13 +728,10 @@ export function CustomNode({
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="ml-5 mt-6 rounded-b-xl">
|
||||
<div className="mx-5 my-6 rounded-b-xl">
|
||||
{/* Input Handles */}
|
||||
{data.uiType !== BlockUIType.NOTE ? (
|
||||
<div
|
||||
className="flex w-fit items-start justify-between"
|
||||
data-id="input-handles"
|
||||
>
|
||||
<div data-id="input-handles">
|
||||
<div>
|
||||
{data.uiType === BlockUIType.WEBHOOK_MANUAL &&
|
||||
(data.webhook ? (
|
||||
@@ -781,7 +780,6 @@ export function CustomNode({
|
||||
<Switch
|
||||
onCheckedChange={toggleAdvancedSettings}
|
||||
checked={isAdvancedOpen}
|
||||
className="mr-5"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -790,7 +788,7 @@ export function CustomNode({
|
||||
{data.uiType !== BlockUIType.NOTE && (
|
||||
<>
|
||||
<LineSeparator />
|
||||
<div className="flex items-start justify-end rounded-b-xl pb-2 pr-2 pt-6">
|
||||
<div className="flex items-start justify-end rounded-b-xl pt-6">
|
||||
<div className="flex-none">
|
||||
{data.outputSchema &&
|
||||
generateOutputHandles(data.outputSchema, data.uiType)}
|
||||
@@ -850,8 +848,10 @@ export function CustomNode({
|
||||
data.status === "COMPLETED",
|
||||
"border-yellow-600 bg-yellow-600 text-white":
|
||||
data.status === "RUNNING",
|
||||
"border-red-600 bg-red-600 text-white":
|
||||
data.status === "FAILED",
|
||||
"border-red-600 bg-red-600 text-white": [
|
||||
"FAILED",
|
||||
"TERMINATED",
|
||||
].includes(data.status || ""),
|
||||
"border-blue-600 bg-blue-600 text-white":
|
||||
data.status === "QUEUED",
|
||||
"border-gray-600 bg-gray-600 font-black":
|
||||
|
||||
@@ -82,7 +82,7 @@ const NodeHandle: FC<HandleProps> = ({
|
||||
data-testid={`output-handle-${keyName}`}
|
||||
position={Position.Right}
|
||||
id={keyName}
|
||||
className="group -mr-[26px]"
|
||||
className="group -mr-[38px]"
|
||||
>
|
||||
<div className="pointer-events-none flex items-center">
|
||||
{label}
|
||||
|
||||
@@ -61,7 +61,7 @@ const TallyPopupSimple = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={resetTutorial}
|
||||
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
|
||||
className="mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left font-inter text-lg font-medium leading-6"
|
||||
>
|
||||
Tutorial
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,10 @@ import { Separator } from "@/components/ui/separator";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { DownloadIcon, LoaderIcon } from "lucide-react";
|
||||
interface AgentInfoProps {
|
||||
name: string;
|
||||
creator: string;
|
||||
@@ -32,8 +36,11 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
storeListingVersionId,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const api = React.useMemo(() => new BackendAPI(), []);
|
||||
const { user } = useSupabase();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [downloading, setDownloading] = React.useState(false);
|
||||
|
||||
const handleAddToLibrary = async () => {
|
||||
try {
|
||||
@@ -45,6 +52,46 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadToLibrary = async () => {
|
||||
const downloadAgent = async (): Promise<void> => {
|
||||
setDownloading(true);
|
||||
try {
|
||||
const file = await api.downloadStoreAgent(storeListingVersionId);
|
||||
|
||||
// Similar to Marketplace v1
|
||||
const jsonData = JSON.stringify(file, null, 2);
|
||||
// Create a Blob from the file content
|
||||
const blob = new Blob([jsonData], { type: "application/json" });
|
||||
|
||||
// Create a temporary URL for the Blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `agent_${storeListingVersionId}.json`; // Set the filename
|
||||
|
||||
// Append the anchor to the body, click it, and remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the temporary URL
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: "Your agent has been successfully downloaded.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error downloading agent:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
await downloadAgent();
|
||||
setDownloading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
@@ -72,15 +119,36 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
|
||||
{/* Run Agent Button */}
|
||||
<div className="mb-4 w-full lg:mb-[60px]">
|
||||
<button
|
||||
onClick={handleAddToLibrary}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4"
|
||||
>
|
||||
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
|
||||
Add To Library
|
||||
</span>
|
||||
</button>
|
||||
{user ? (
|
||||
<button
|
||||
onClick={handleAddToLibrary}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4"
|
||||
>
|
||||
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
|
||||
Add To Library
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownloadToLibrary}
|
||||
className={`inline-flex w-full items-center justify-center gap-2 rounded-[38px] px-4 py-3 transition-colors sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4 ${
|
||||
downloading
|
||||
? "bg-neutral-400"
|
||||
: "bg-violet-600 hover:bg-violet-700"
|
||||
}`}
|
||||
disabled={downloading}
|
||||
>
|
||||
{downloading ? (
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
) : (
|
||||
<DownloadIcon className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
)}
|
||||
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
|
||||
{downloading ? "Downloading..." : "Download Agent as File"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rating and Runs */}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "./Button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Menu } from "lucide-react";
|
||||
import { KeyIcon, Menu } from "lucide-react";
|
||||
import {
|
||||
IconDashboardLayout,
|
||||
IconIntegrations,
|
||||
@@ -58,6 +58,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
Integrations
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/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" />
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
API Keys
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/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"
|
||||
@@ -102,6 +111,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
Integrations
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/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} />
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
API Keys
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/store/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"
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { APIKey, APIKeyPermission } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
import { LuCopy } from "react-icons/lu";
|
||||
import { Loader2, MoreVertical } from "lucide-react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function APIKeysSection() {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [newKeyDescription, setNewKeyDescription] = useState("");
|
||||
const [newApiKey, setNewApiKey] = useState("");
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||
APIKeyPermission[]
|
||||
>([]);
|
||||
const { toast } = useToast();
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
loadAPIKeys();
|
||||
}, []);
|
||||
|
||||
const loadAPIKeys = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const keys = await api.listAPIKeys();
|
||||
setApiKeys(keys.filter((key) => key.status === "ACTIVE"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
try {
|
||||
const response = await api.createAPIKey(
|
||||
newKeyName,
|
||||
selectedPermissions,
|
||||
newKeyDescription,
|
||||
);
|
||||
|
||||
setNewApiKey(response.plain_text_key);
|
||||
setIsCreateOpen(false);
|
||||
setIsKeyDialogOpen(true);
|
||||
loadAPIKeys();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create AutoGPT Platform API key",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyKey = () => {
|
||||
navigator.clipboard.writeText(newApiKey);
|
||||
toast({
|
||||
title: "Copied",
|
||||
description: "AutoGPT Platform API key copied to clipboard",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRevokeKey = async (keyId: string) => {
|
||||
try {
|
||||
await api.revokeAPIKey(keyId);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "AutoGPT Platform API key revoked successfully",
|
||||
});
|
||||
loadAPIKeys();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to revoke AutoGPT Platform API key",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AutoGPT Platform API Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your AutoGPT Platform API keys for programmatic access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Create Key</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New API Key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new AutoGPT Platform API key
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="My AutoGPT Platform API Key"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={newKeyDescription}
|
||||
onChange={(e) => setNewKeyDescription(e.target.value)}
|
||||
placeholder="Used for..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Permissions</Label>
|
||||
{Object.values(APIKeyPermission).map((permission) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={permission}
|
||||
>
|
||||
<Checkbox
|
||||
id={permission}
|
||||
checked={selectedPermissions.includes(permission)}
|
||||
onCheckedChange={(checked) => {
|
||||
setSelectedPermissions(
|
||||
checked
|
||||
? [...selectedPermissions, permission]
|
||||
: selectedPermissions.filter(
|
||||
(p) => p !== permission,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={permission}>{permission}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCreateOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateKey}>Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isKeyDialogOpen} onOpenChange={setIsKeyDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>AutoGPT Platform API Key Created</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please copy your AutoGPT API key now. You won't be able
|
||||
to see it again!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="flex-1 rounded-md bg-secondary p-2 text-sm">
|
||||
{newApiKey}
|
||||
</code>
|
||||
<Button size="icon" variant="outline" onClick={handleCopyKey}>
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setIsKeyDialogOpen(false)}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center p-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>API Key</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Last Used</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell>{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="rounded-md border p-1 px-2 text-xs">
|
||||
{`${key.prefix}******************${key.postfix}`}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
key.status === "ACTIVE" ? "default" : "destructive"
|
||||
}
|
||||
className={
|
||||
key.status === "ACTIVE"
|
||||
? "border-green-600 bg-green-100 text-green-800"
|
||||
: "border-red-600 bg-red-100 text-red-800"
|
||||
}
|
||||
>
|
||||
{key.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(key.created_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{key.last_used_at
|
||||
? new Date(key.last_used_at).toLocaleDateString()
|
||||
: "Never"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => handleRevokeKey(key.id)}
|
||||
>
|
||||
Revoke
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -34,8 +34,8 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
// Take only the first 9 agents
|
||||
const displayedAgents = allAgents.slice(0, 9);
|
||||
// TODO: Update this when we have pagination
|
||||
const displayedAgents = allAgents;
|
||||
|
||||
const handleCardClick = (creator: string, slug: string) => {
|
||||
router.push(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
text: string;
|
||||
linkText?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function AuthBottomText({
|
||||
className = "",
|
||||
text,
|
||||
linkText,
|
||||
href = "",
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"mt-8 inline-flex w-full items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium leading-normal text-slate-950">
|
||||
{text}
|
||||
</span>
|
||||
{linkText && (
|
||||
<Link
|
||||
href={href}
|
||||
className="ml-1 text-sm font-medium leading-normal text-slate-950 underline"
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
autogpt_platform/frontend/src/components/auth/AuthButton.tsx
Normal file
36
autogpt_platform/frontend/src/components/auth/AuthButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
onClick: () => void;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
|
||||
export default function AuthButton({
|
||||
children,
|
||||
onClick,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
}: Props) {
|
||||
return (
|
||||
<Button
|
||||
className="mt-2 w-full self-stretch rounded-md bg-slate-900 px-4 py-2"
|
||||
type={type}
|
||||
disabled={isLoading || disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FaSpinner className="animate-spin" />
|
||||
) : (
|
||||
<div className="text-sm font-medium leading-normal text-slate-50">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
15
autogpt_platform/frontend/src/components/auth/AuthCard.tsx
Normal file
15
autogpt_platform/frontend/src/components/auth/AuthCard.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthCard({ children }: Props) {
|
||||
return (
|
||||
<div className="flex h-[80vh] w-[32rem] items-center justify-center">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
interface Props {
|
||||
message?: string | null;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export default function AuthFeedback({ message = "", isError = false }: Props) {
|
||||
return (
|
||||
<div className="mt-4 text-center text-sm font-medium leading-normal">
|
||||
{isError ? (
|
||||
<div className="text-red-500">{message}</div>
|
||||
) : (
|
||||
<div className="text-slate-950">{message}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
autogpt_platform/frontend/src/components/auth/AuthHeader.tsx
Normal file
13
autogpt_platform/frontend/src/components/auth/AuthHeader.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthHeader({ children }: Props) {
|
||||
return (
|
||||
<div className="mb-8 text-2xl font-semibold leading-normal text-slate-950">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn("hide-password-toggle pr-10", className)}
|
||||
ref={ref}
|
||||
title="password"
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
@@ -23,8 +24,11 @@ const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
onMouseDown={() => setShowPassword(true)}
|
||||
onMouseUp={() => setShowPassword(false)}
|
||||
onMouseLeave={() => setShowPassword(false)}
|
||||
disabled={disabled}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword && !disabled ? (
|
||||
<EyeIcon className="h-4 w-4" aria-hidden="true" />
|
||||
15
autogpt_platform/frontend/src/components/auth/index.ts
Normal file
15
autogpt_platform/frontend/src/components/auth/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import AuthBottomText from "./AuthBottomText";
|
||||
import AuthButton from "./AuthButton";
|
||||
import AuthCard from "./AuthCard";
|
||||
import AuthFeedback from "./AuthFeedback";
|
||||
import AuthHeader from "./AuthHeader";
|
||||
import { PasswordInput } from "./PasswordInput";
|
||||
|
||||
export {
|
||||
AuthBottomText,
|
||||
AuthButton,
|
||||
AuthCard,
|
||||
AuthFeedback,
|
||||
AuthHeader,
|
||||
PasswordInput,
|
||||
};
|
||||
@@ -4,58 +4,22 @@
|
||||
transition: border-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.custom-node [data-id="input-handles"] {
|
||||
padding: 0 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.custom-node [data-id="input-handles"],
|
||||
.custom-node [data-id="input-handles"] > div > div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.handle-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-bottom: 0px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
min-height: 44px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.custom-node input:not([type="checkbox"]),
|
||||
.custom-node textarea,
|
||||
.custom-node select {
|
||||
width: calc(100% - 2.5rem);
|
||||
max-width: 400px;
|
||||
margin: 0.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.custom-node [data-id^="date-picker"] {
|
||||
margin: 0.5rem 1.25rem;
|
||||
width: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.custom-node [data-list-container] {
|
||||
margin: 0.5rem 1.25rem;
|
||||
width: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.custom-node [data-add-item] {
|
||||
margin: 0.5rem 1.25rem;
|
||||
width: calc(100% - 2.5rem);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.array-item-container {
|
||||
.custom-node select,
|
||||
.custom-node [data-id^="date-picker"],
|
||||
.custom-node [data-list-container],
|
||||
.custom-node [data-add-item],
|
||||
.custom-node [data-content-settings]. .array-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.5rem 1.25rem;
|
||||
width: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.custom-node [data-content-settings] {
|
||||
margin: 0.5rem 1.25rem;
|
||||
width: calc(100% - 2.5rem);
|
||||
min-width: calc(100% - 2.5rem);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.custom-node .custom-switch {
|
||||
@@ -68,7 +32,6 @@
|
||||
.error-message {
|
||||
color: #d9534f;
|
||||
font-size: 13px;
|
||||
margin: 0.25rem 1.25rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -53,6 +53,7 @@ export const providerIcons: Record<
|
||||
google: FaGoogle,
|
||||
groq: fallbackIcon,
|
||||
notion: NotionLogoIcon,
|
||||
nvidia: fallbackIcon,
|
||||
discord: FaDiscord,
|
||||
d_id: fallbackIcon,
|
||||
google_maps: FaGoogle,
|
||||
@@ -87,12 +88,13 @@ export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & (
|
||||
);
|
||||
|
||||
export const CredentialsInput: FC<{
|
||||
selfKey: string;
|
||||
className?: string;
|
||||
selectedCredentials?: CredentialsMetaInput;
|
||||
onSelectCredentials: (newValue?: CredentialsMetaInput) => void;
|
||||
}> = ({ className, selectedCredentials, onSelectCredentials }) => {
|
||||
}> = ({ selfKey, className, selectedCredentials, onSelectCredentials }) => {
|
||||
const api = useBackendAPI();
|
||||
const credentials = useCredentials();
|
||||
const credentials = useCredentials(selfKey);
|
||||
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
||||
useState(false);
|
||||
const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false);
|
||||
@@ -209,6 +211,7 @@ export const CredentialsInput: FC<{
|
||||
<>
|
||||
{supportsApiKey && (
|
||||
<APIKeyCredentialsModal
|
||||
credentialsFieldName={selfKey}
|
||||
open={isAPICredentialsModalOpen}
|
||||
onClose={() => setAPICredentialsModalOpen(false)}
|
||||
onCredentialsCreate={(credsMeta) => {
|
||||
@@ -242,7 +245,9 @@ export const CredentialsInput: FC<{
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 flex gap-1">
|
||||
<span className="text-m green text-gray-900">Credentials</span>
|
||||
<span className="text-m green text-gray-900">
|
||||
{providerName} Credentials
|
||||
</span>
|
||||
<SchemaTooltip description={schema.description} />
|
||||
</div>
|
||||
<div className={cn("flex flex-row space-x-2", className)}>
|
||||
@@ -310,7 +315,12 @@ export const CredentialsInput: FC<{
|
||||
// Saved credentials exist
|
||||
return (
|
||||
<>
|
||||
<span className="text-m green mb-0 text-gray-900">Credentials</span>
|
||||
<div className="flex gap-1">
|
||||
<span className="text-m green mb-0 text-gray-900">
|
||||
{providerName} Credentials
|
||||
</span>
|
||||
<SchemaTooltip description={schema.description} />
|
||||
</div>
|
||||
<Select value={selectedCredentials?.id} onValueChange={handleValueChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={schema.placeholder} />
|
||||
@@ -353,11 +363,12 @@ export const CredentialsInput: FC<{
|
||||
};
|
||||
|
||||
export const APIKeyCredentialsModal: FC<{
|
||||
credentialsFieldName: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCredentialsCreate: (creds: CredentialsMetaInput) => void;
|
||||
}> = ({ open, onClose, onCredentialsCreate }) => {
|
||||
const credentials = useCredentials();
|
||||
}> = ({ credentialsFieldName, open, onClose, onCredentialsCreate }) => {
|
||||
const credentials = useCredentials(credentialsFieldName);
|
||||
|
||||
const formSchema = z.object({
|
||||
apiKey: z.string().min(1, "API Key is required"),
|
||||
|
||||
@@ -28,6 +28,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
||||
jina: "Jina",
|
||||
medium: "Medium",
|
||||
notion: "Notion",
|
||||
nvidia: "Nvidia",
|
||||
ollama: "Ollama",
|
||||
openai: "OpenAI",
|
||||
openweathermap: "OpenWeatherMap",
|
||||
|
||||
@@ -101,6 +101,92 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
|
||||
|
||||
export default NodeObjectInputTree;
|
||||
|
||||
const NodeImageInput: FC<{
|
||||
selfKey: string;
|
||||
schema: BlockIOStringSubSchema;
|
||||
value?: string;
|
||||
error?: string;
|
||||
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
|
||||
className?: string;
|
||||
displayName: string;
|
||||
}> = ({
|
||||
selfKey,
|
||||
schema,
|
||||
value = "",
|
||||
error,
|
||||
handleInputChange,
|
||||
className,
|
||||
displayName,
|
||||
}) => {
|
||||
const handleFileChange = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.error("Please upload an image file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = (e.target?.result as string).split(",")[1];
|
||||
handleInputChange(selfKey, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
[selfKey, handleInputChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
<div className="nodrag flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
document.getElementById(`${selfKey}-upload`)?.click()
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{value ? "Change Image" : `Upload ${displayName}`}
|
||||
</Button>
|
||||
{value && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleInputChange(selfKey, "")}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
id={`${selfKey}-upload`}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{value && (
|
||||
<div className="relative mt-2 rounded-md border border-gray-300 p-2 dark:border-gray-600">
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${value}`}
|
||||
alt="Preview"
|
||||
className="max-h-32 w-full rounded-md object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeDateTimeInput: FC<{
|
||||
selfKey: string;
|
||||
schema: BlockIOStringSubSchema;
|
||||
@@ -201,7 +287,7 @@ export const NodeGenericInputField: FC<{
|
||||
className,
|
||||
displayName,
|
||||
}) => {
|
||||
className = cn(className, "my-2");
|
||||
className = cn(className);
|
||||
displayName ||= propSchema.title || beautifyString(propKey);
|
||||
|
||||
if ("allOf" in propSchema) {
|
||||
@@ -418,6 +504,19 @@ export const NodeGenericInputField: FC<{
|
||||
|
||||
switch (propSchema.type) {
|
||||
case "string":
|
||||
if ("image_upload" in propSchema && propSchema.image_upload === true) {
|
||||
return (
|
||||
<NodeImageInput
|
||||
selfKey={propKey}
|
||||
schema={propSchema}
|
||||
value={currentValue}
|
||||
error={errors[propKey]}
|
||||
className={className}
|
||||
displayName={displayName}
|
||||
handleInputChange={handleInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ("format" in propSchema && propSchema.format === "date-time") {
|
||||
return (
|
||||
<NodeDateTimeInput
|
||||
@@ -653,6 +752,7 @@ const NodeCredentialsInput: FC<{
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<CredentialsInput
|
||||
selfKey={selfKey}
|
||||
onSelectCredentials={(credsMeta) =>
|
||||
handleInputChange(selfKey, credsMeta)
|
||||
}
|
||||
@@ -876,18 +976,19 @@ const NodeArrayInput: FC<{
|
||||
(c) => c.targetHandle === entryKey && c.target === nodeId,
|
||||
);
|
||||
return (
|
||||
<div key={entryKey} className="self-start">
|
||||
<div key={entryKey}>
|
||||
<NodeHandle
|
||||
keyName={entryKey}
|
||||
schema={schema.items!}
|
||||
isConnected={isConnected}
|
||||
isRequired={false}
|
||||
side="left"
|
||||
/>
|
||||
<div className="mb-2 flex space-x-2">
|
||||
<NodeHandle
|
||||
keyName={entryKey}
|
||||
schema={schema.items!}
|
||||
isConnected={isConnected}
|
||||
isRequired={false}
|
||||
side="left"
|
||||
/>
|
||||
{!isConnected &&
|
||||
(schema.items ? (
|
||||
<NodeGenericInputField
|
||||
className="w-full"
|
||||
nodeId={nodeId}
|
||||
propKey={entryKey}
|
||||
propSchema={schema.items}
|
||||
|
||||
@@ -17,6 +17,9 @@ const isValidVideoUrl = (url: string): boolean => {
|
||||
};
|
||||
|
||||
const isValidImageUrl = (url: string): boolean => {
|
||||
if (url.startsWith("data:image/")) {
|
||||
return true;
|
||||
}
|
||||
const imageExtensions = /\.(jpeg|jpg|gif|png|svg|webp)$/i;
|
||||
const cleanedUrl = url.split("?")[0];
|
||||
return imageExtensions.test(cleanedUrl);
|
||||
@@ -50,19 +53,21 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ImageRenderer: React.FC<{ imageUrl: string }> = ({ imageUrl }) => (
|
||||
<div className="w-full p-2">
|
||||
<picture>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
className="h-auto max-w-full"
|
||||
width="100%"
|
||||
height="auto"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
const ImageRenderer: React.FC<{ imageUrl: string }> = ({ imageUrl }) => {
|
||||
return (
|
||||
<div className="w-full p-2">
|
||||
<picture>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
className="h-auto max-w-full"
|
||||
width="100%"
|
||||
height="auto"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AudioRenderer: React.FC<{ audioUrl: string }> = ({ audioUrl }) => (
|
||||
<div className="w-full p-2">
|
||||
@@ -92,6 +97,9 @@ export const ContentRenderer: React.FC<{
|
||||
truncateLongData?: boolean;
|
||||
}> = ({ value, truncateLongData }) => {
|
||||
if (typeof value === "string") {
|
||||
if (value.startsWith("data:image/")) {
|
||||
return <ImageRenderer imageUrl={value} />;
|
||||
}
|
||||
if (isValidVideoUrl(value)) {
|
||||
return <VideoRenderer videoUrl={value} />;
|
||||
} else if (isValidImageUrl(value)) {
|
||||
|
||||
@@ -558,8 +558,9 @@ export default function useAgentGraph(
|
||||
return;
|
||||
}
|
||||
if (
|
||||
nodeResult.status != "COMPLETED" &&
|
||||
nodeResult.status != "FAILED"
|
||||
!["COMPLETED", "TERMINATED", "FAILED"].includes(
|
||||
nodeResult.status,
|
||||
)
|
||||
) {
|
||||
pendingNodeExecutions.add(nodeResult.node_exec_id);
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,9 @@ export type CredentialsData =
|
||||
isLoading: false;
|
||||
});
|
||||
|
||||
export default function useCredentials(): CredentialsData | null {
|
||||
export default function useCredentials(
|
||||
inputFieldName: string,
|
||||
): CredentialsData | null {
|
||||
const nodeId = useNodeId();
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
@@ -35,8 +37,9 @@ export default function useCredentials(): CredentialsData | null {
|
||||
}
|
||||
|
||||
const data = useNodesData<Node<CustomNodeData>>(nodeId)!.data;
|
||||
const credentialsSchema = data.inputSchema.properties
|
||||
.credentials as BlockIOCredentialsSubSchema;
|
||||
const credentialsSchema = data.inputSchema.properties[
|
||||
inputFieldName
|
||||
] as BlockIOCredentialsSubSchema;
|
||||
|
||||
const discriminatorValue: CredentialsProviderName | null =
|
||||
(credentialsSchema.discriminator &&
|
||||
|
||||
@@ -28,7 +28,11 @@ export default function useSupabase() {
|
||||
const response = await supabase.auth.getUser();
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error fetching user", response.error);
|
||||
// Display error only if it's not about missing auth session (user is not logged in)
|
||||
if (response.error.message !== "Auth session missing!") {
|
||||
console.error("Error fetching user", response.error);
|
||||
}
|
||||
setUser(null);
|
||||
} else {
|
||||
setUser(response.data.user);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
StoreReview,
|
||||
ScheduleCreatable,
|
||||
Schedule,
|
||||
APIKeyPermission,
|
||||
CreateAPIKeyResponse,
|
||||
APIKey,
|
||||
} from "./types";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import getServerSupabase from "../supabase/getServerSupabase";
|
||||
@@ -221,6 +224,36 @@ export default class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
// API Key related requests
|
||||
async createAPIKey(
|
||||
name: string,
|
||||
permissions: APIKeyPermission[],
|
||||
description?: string,
|
||||
): Promise<CreateAPIKeyResponse> {
|
||||
return this._request("POST", "/api-keys", {
|
||||
name,
|
||||
permissions,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
async listAPIKeys(): Promise<APIKey[]> {
|
||||
return this._get("/api-keys");
|
||||
}
|
||||
|
||||
async revokeAPIKey(keyId: string): Promise<APIKey> {
|
||||
return this._request("DELETE", `/api-keys/${keyId}`);
|
||||
}
|
||||
|
||||
async updateAPIKeyPermissions(
|
||||
keyId: string,
|
||||
permissions: APIKeyPermission[],
|
||||
): Promise<APIKey> {
|
||||
return this._request("PUT", `/api-keys/${keyId}/permissions`, {
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists.
|
||||
* @throws `Error` if the webhook does not exist.
|
||||
@@ -311,7 +344,7 @@ export default class BackendAPI {
|
||||
"/store/submissions/generate_image?agent_id=" + agent_id,
|
||||
);
|
||||
}
|
||||
c;
|
||||
|
||||
deleteStoreSubmission(submission_id: string): Promise<boolean> {
|
||||
return this._request("DELETE", `/store/submissions/${submission_id}`);
|
||||
}
|
||||
@@ -348,6 +381,17 @@ export default class BackendAPI {
|
||||
return this._get("/store/myagents", params);
|
||||
}
|
||||
|
||||
downloadStoreAgent(
|
||||
storeListingVersionId: string,
|
||||
version?: number,
|
||||
): Promise<BlobPart> {
|
||||
const url = version
|
||||
? `/store/download/agents/${storeListingVersionId}?version=${version}`
|
||||
: `/store/download/agents/${storeListingVersionId}`;
|
||||
|
||||
return this._get(url);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////// V2 LIBRARY API //////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
@@ -113,6 +113,7 @@ export const PROVIDER_NAMES = {
|
||||
JINA: "jina",
|
||||
MEDIUM: "medium",
|
||||
NOTION: "notion",
|
||||
NVIDIA: "nvidia",
|
||||
OLLAMA: "ollama",
|
||||
OPENAI: "openai",
|
||||
OPENWEATHERMAP: "openweathermap",
|
||||
@@ -196,7 +197,7 @@ export type GraphExecution = {
|
||||
ended_at: number;
|
||||
duration: number;
|
||||
total_run_time: number;
|
||||
status: "INCOMPLETE" | "QUEUED" | "RUNNING" | "COMPLETED" | "FAILED";
|
||||
status: "QUEUED" | "RUNNING" | "COMPLETED" | "TERMINATED" | "FAILED";
|
||||
graph_id: string;
|
||||
graph_version: number;
|
||||
};
|
||||
@@ -246,7 +247,13 @@ export type NodeExecutionResult = {
|
||||
node_exec_id: string;
|
||||
node_id: string;
|
||||
block_id: string;
|
||||
status: "INCOMPLETE" | "QUEUED" | "RUNNING" | "COMPLETED" | "FAILED";
|
||||
status:
|
||||
| "INCOMPLETE"
|
||||
| "QUEUED"
|
||||
| "RUNNING"
|
||||
| "COMPLETED"
|
||||
| "TERMINATED"
|
||||
| "FAILED";
|
||||
input_data: { [key: string]: any };
|
||||
output_data: { [key: string]: Array<any> };
|
||||
add_time: Date;
|
||||
@@ -506,3 +513,36 @@ export type StoreReviewCreate = {
|
||||
score: number;
|
||||
comments?: string;
|
||||
};
|
||||
|
||||
// API Key Types
|
||||
|
||||
export enum APIKeyPermission {
|
||||
EXECUTE_GRAPH = "EXECUTE_GRAPH",
|
||||
READ_GRAPH = "READ_GRAPH",
|
||||
EXECUTE_BLOCK = "EXECUTE_BLOCK",
|
||||
READ_BLOCK = "READ_BLOCK",
|
||||
}
|
||||
|
||||
export enum APIKeyStatus {
|
||||
ACTIVE = "ACTIVE",
|
||||
REVOKED = "REVOKED",
|
||||
SUSPENDED = "SUSPENDED",
|
||||
}
|
||||
|
||||
export interface APIKey {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
postfix: string;
|
||||
status: APIKeyStatus;
|
||||
permissions: APIKeyPermission[];
|
||||
created_at: string;
|
||||
last_used_at?: string;
|
||||
revoked_at?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateAPIKeyResponse {
|
||||
api_key: APIKey;
|
||||
plain_text_key: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Graph, Block, Node } from "./types";
|
||||
import { Graph, Block, Node, BlockUIType } from "./types";
|
||||
|
||||
/** Creates a copy of the graph with all secrets removed */
|
||||
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
|
||||
graph = removeAgentInputBlockValues(graph, block_defs);
|
||||
return {
|
||||
...graph,
|
||||
nodes: graph.nodes.map((node) => {
|
||||
@@ -18,3 +19,28 @@ export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) {
|
||||
const inputBlocks = graph.nodes.filter(
|
||||
(node) =>
|
||||
blocks.find((b) => b.id === node.block_id)?.uiType === BlockUIType.INPUT,
|
||||
);
|
||||
|
||||
const modifiedNodes = graph.nodes.map((node) => {
|
||||
if (inputBlocks.find((inputNode) => inputNode.id === node.id)) {
|
||||
return {
|
||||
...node,
|
||||
input_default: {
|
||||
...node.input_default,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
return {
|
||||
...graph,
|
||||
nodes: modifiedNodes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,33 +7,28 @@ export class LoginPage {
|
||||
console.log("Attempting login with:", { email, password }); // Debug log
|
||||
|
||||
// Fill email
|
||||
const emailInput = this.page.getByPlaceholder("user@email.com");
|
||||
const emailInput = this.page.getByPlaceholder("m@example.com");
|
||||
await emailInput.waitFor({ state: "visible" });
|
||||
await emailInput.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput = this.page.getByPlaceholder("password");
|
||||
const passwordInput = this.page.getByTitle("Password");
|
||||
await passwordInput.waitFor({ state: "visible" });
|
||||
await passwordInput.fill(password);
|
||||
|
||||
// Check terms
|
||||
const termsCheckbox = this.page.getByLabel("I agree to the Terms of Use");
|
||||
await termsCheckbox.waitFor({ state: "visible" });
|
||||
await termsCheckbox.click();
|
||||
|
||||
// TODO: This is a workaround to wait for the page to load after filling the email and password
|
||||
const emailInput2 = this.page.getByPlaceholder("user@email.com");
|
||||
const emailInput2 = this.page.getByPlaceholder("m@example.com");
|
||||
await emailInput2.waitFor({ state: "visible" });
|
||||
await emailInput2.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput2 = this.page.getByPlaceholder("password");
|
||||
const passwordInput2 = this.page.getByTitle("Password");
|
||||
await passwordInput2.waitFor({ state: "visible" });
|
||||
await passwordInput2.fill(password);
|
||||
|
||||
// Wait for the button to be ready
|
||||
const loginButton = this.page.getByRole("button", {
|
||||
name: "Log in",
|
||||
name: "Login",
|
||||
exact: true,
|
||||
});
|
||||
await loginButton.waitFor({ state: "visible" });
|
||||
|
||||
63
autogpt_platform/frontend/src/types/auth.ts
Normal file
63
autogpt_platform/frontend/src/types/auth.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type LoginProvider = "google" | "github" | "discord";
|
||||
|
||||
export const loginFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
});
|
||||
|
||||
export const signupFormSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
agreeToTerms: z.boolean().refine((value) => value === true, {
|
||||
message: "You must agree to the Terms of Use and Privacy Policy",
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
export const sendEmailFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
});
|
||||
|
||||
export const changePasswordFormSchema = z
|
||||
.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
@@ -18,6 +18,8 @@ const config = {
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
// Include the custom font family
|
||||
neue: ['"PP Neue Montreal TT"', "sans-serif"],
|
||||
poppin: ["var(--font-poppins)"],
|
||||
inter: ["var(--font-inter)"],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
|
||||
@@ -1018,10 +1018,10 @@
|
||||
"@types/tough-cookie" "^4.0.5"
|
||||
tough-cookie "^4.1.4"
|
||||
|
||||
"@chromatic-com/storybook@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-3.2.2.tgz#08754443de55618f802f88450c35266fd6d25db5"
|
||||
integrity sha512-xmXt/GW0hAPbzNTrxYuVo43Adrtjue4DeVrsoIIEeJdGaPNNeNf+DHMlJKOBdlHmCnFUoe9R/0mLM9zUp5bKWw==
|
||||
"@chromatic-com/storybook@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-3.2.3.tgz#0f4d167ac80fcb38293a92c230c43446049b6758"
|
||||
integrity sha512-3+hfANx79kIjP1qrOSLxpoAXOiYUA0S7A0WI0A24kASrv7USFNNW8etR5TjUilMb0LmqKUn3wDwUK2h6aceQ9g==
|
||||
dependencies:
|
||||
chromatic "^11.15.0"
|
||||
filesize "^10.0.12"
|
||||
@@ -1237,10 +1237,10 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@hookform/resolvers@^3.9.1":
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.9.1.tgz#a23883c40bfd449cb6c6ab5a0fa0729184c950ff"
|
||||
integrity sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==
|
||||
"@hookform/resolvers@^3.10.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.10.0.tgz#7bfd18113daca4e57e27e1205b7d5a2d371aa59a"
|
||||
integrity sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
@@ -1698,10 +1698,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.20.tgz#0be2cc955f4eb837516e7d7382284cd5bc1d5a02"
|
||||
integrity sha512-JfDpuOCB0UBKlEgEy/H6qcBSzHimn/YWjUHzKl1jMeUO+QVRdzmTTl8gFJaNO87c8DXmVKhFCtwxQ9acqB3+Pw==
|
||||
|
||||
"@next/eslint-plugin-next@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.0.tgz#421b47ad0772e11b2d67416054675cd32f95b8b7"
|
||||
integrity sha512-+jPT0h+nelBT6HC9ZCHGc7DgGVy04cv4shYdAe6tKlEbjQUtwU3LzQhzbDHQyY2m6g39m6B0kOFVuLGBrxxbGg==
|
||||
"@next/eslint-plugin-next@15.1.3":
|
||||
version "15.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.3.tgz#32777736af151577df52d83f25c0c22bc9f3cb5e"
|
||||
integrity sha512-oeP1vnc5Cq9UoOb8SYHAEPbCXMzOgG70l+Zfd+Ie00R25FOm+CCVNrcIubJvB1tvBgakXE37MmqSycksXVPRqg==
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
@@ -1750,10 +1750,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.20.tgz#689bc7beb8005b73c95d926e7edfb7f73efc78f2"
|
||||
integrity sha512-AFmqeLW6LtxeFTuoB+MXFeM5fm5052i3MU6xD0WzJDOwku6SkZaxb1bxjBaRC8uNqTRTSPl0yMFtjNowIVI67w==
|
||||
|
||||
"@next/third-parties@^15.0.4":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/third-parties/-/third-parties-15.1.0.tgz#ab898927a006fe41ef90888220b51e22e11e110c"
|
||||
integrity sha512-eiv8vTo5HJOE/LabnIjRNVpN0hvjXfqPrE7D/XecmWvHBs9KrIISxlb1NZizDMcvjGtnHkdupWsquM9ur25rYw==
|
||||
"@next/third-parties@^15.1.3":
|
||||
version "15.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@next/third-parties/-/third-parties-15.1.3.tgz#89c5c85b68d98a8787f0fa43a8d55ade7d6d5cf2"
|
||||
integrity sha512-nz2mthh08xMRgNKRA+Z7lM1BqHqukGcFyu5z0nXFo3/KXsBgaPJkfnkfebw/YTqkxryV+aEttf/iAWDB6dUO6A==
|
||||
dependencies:
|
||||
third-party-capital "1.0.20"
|
||||
|
||||
@@ -1801,10 +1801,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
|
||||
integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
|
||||
|
||||
"@opentelemetry/api-logs@0.52.1":
|
||||
version "0.52.1"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc"
|
||||
integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==
|
||||
"@opentelemetry/api-logs@0.53.0":
|
||||
version "0.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be"
|
||||
integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.0.0"
|
||||
|
||||
@@ -2052,13 +2052,13 @@
|
||||
semver "^7.5.2"
|
||||
shimmer "^1.2.1"
|
||||
|
||||
"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0":
|
||||
version "0.52.1"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48"
|
||||
integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==
|
||||
"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0":
|
||||
version "0.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d"
|
||||
integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==
|
||||
dependencies:
|
||||
"@opentelemetry/api-logs" "0.52.1"
|
||||
"@types/shimmer" "^1.0.2"
|
||||
"@opentelemetry/api-logs" "0.53.0"
|
||||
"@types/shimmer" "^1.2.0"
|
||||
import-in-the-middle "^1.8.1"
|
||||
require-in-the-middle "^7.1.1"
|
||||
semver "^7.5.2"
|
||||
@@ -2128,13 +2128,13 @@
|
||||
schema-utils "^4.2.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@prisma/instrumentation@5.19.1":
|
||||
version "5.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.19.1.tgz#146319cf85f22b7a43296f0f40cfeac55516e66e"
|
||||
integrity sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w==
|
||||
"@prisma/instrumentation@5.22.0":
|
||||
version "5.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.22.0.tgz#c39941046e9886e17bdb47dbac45946c24d579aa"
|
||||
integrity sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.8"
|
||||
"@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0"
|
||||
"@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0"
|
||||
"@opentelemetry/sdk-trace-base" "^1.22"
|
||||
|
||||
"@radix-ui/number@1.1.0":
|
||||
@@ -2630,51 +2630,51 @@
|
||||
resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972"
|
||||
integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==
|
||||
|
||||
"@sentry-internal/browser-utils@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.45.1.tgz#1ca97f1dfad8a7f5543074b4abd11dc6bc6a1c7b"
|
||||
integrity sha512-sZwtP3zAzDsjUS7WkMW5VGbvSl7hGKTMc8gAJbpEsrybMxllIP13zzMRwpeFF11RnnvbrZ/FtAeX58Mvj0jahA==
|
||||
"@sentry-internal/browser-utils@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz#320713e29566929894de42d54152064ec19cc9b3"
|
||||
integrity sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==
|
||||
dependencies:
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry-internal/feedback@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.45.1.tgz#8e8f3bd25408bce6c65d6a9176df065d1ba5b568"
|
||||
integrity sha512-zCKptzki4SLnG+s8je8dgnppOKFjiiO4GVBc4fh7uL8zjNPBnxW8wK4SrPfAEKVYaHUzkKc5vixwUqcpmfLLGw==
|
||||
"@sentry-internal/feedback@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.48.0.tgz#92d2301b0e7379716efae6c05bc4a4740687921a"
|
||||
integrity sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==
|
||||
dependencies:
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry-internal/replay-canvas@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.45.1.tgz#748272da93a23a5323f9ef381ef3c7189a177447"
|
||||
integrity sha512-qiPg6XwOwkiMMe/8Qf3EhXCqkSlSnWLlorYngIbdkV2klbWjd7vKnqkFJF4PnaS0g7kkZr7nh+MdzpyLyuj2Mw==
|
||||
"@sentry-internal/replay-canvas@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz#f88282b0594751407ca3016d0a63b133c2e37ac3"
|
||||
integrity sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==
|
||||
dependencies:
|
||||
"@sentry-internal/replay" "8.45.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry-internal/replay" "8.48.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry-internal/replay@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.45.1.tgz#f05518adbe17566b1df6d2a2016d81f5f6c1c9a6"
|
||||
integrity sha512-cOA9CodNSR9+hmICDaGIDUvWiwxQxeMHk/esbjB8uAW8HG4CYTG3CTYTZmlmou7DuysfMd4JNuFmDFBj+YU5/A==
|
||||
"@sentry-internal/replay@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.48.0.tgz#2cc802178f6b0185581b61058f2541b9f3384a8b"
|
||||
integrity sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils" "8.45.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry-internal/browser-utils" "8.48.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry/babel-plugin-component-annotate@2.22.7":
|
||||
version "2.22.7"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd"
|
||||
integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ==
|
||||
|
||||
"@sentry/browser@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.45.1.tgz#a7b9af5deefb57778fbb526cb8e43fa2f6ae3d97"
|
||||
integrity sha512-/KvYhQSRg8m9kotG8h9FrfXCWRlebrvdfXKjj1oE9SyZ2LmR8Ze9AcEw1qzsBsa1F1D/a5FQbUJahSoLBkaQPA==
|
||||
"@sentry/browser@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.48.0.tgz#bdd7793ddd3ae7a65d595066bde93fbb63ce8b9d"
|
||||
integrity sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils" "8.45.1"
|
||||
"@sentry-internal/feedback" "8.45.1"
|
||||
"@sentry-internal/replay" "8.45.1"
|
||||
"@sentry-internal/replay-canvas" "8.45.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry-internal/browser-utils" "8.48.0"
|
||||
"@sentry-internal/feedback" "8.48.0"
|
||||
"@sentry-internal/replay" "8.48.0"
|
||||
"@sentry-internal/replay-canvas" "8.48.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry/bundler-plugin-core@2.22.7":
|
||||
version "2.22.7"
|
||||
@@ -2744,35 +2744,35 @@
|
||||
"@sentry/cli-win32-i686" "2.39.1"
|
||||
"@sentry/cli-win32-x64" "2.39.1"
|
||||
|
||||
"@sentry/core@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.45.1.tgz#da3b13a8fd2276e8a1d4f5a38f9b8a0ed6647b49"
|
||||
integrity sha512-1fGmkr0paZshh38mD29c4CfkRkgFoYDaAGyDLoGYfTbEph/lU8RHB2HWzN93McqNdMEhl1DRRyqIasUZoPlqSA==
|
||||
"@sentry/core@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.48.0.tgz#3bb8d06305f0ec7c873453844687deafdeab168b"
|
||||
integrity sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==
|
||||
|
||||
"@sentry/nextjs@^8":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.45.1.tgz#4bad8fe78709c8efe63f34d5b4ed86bfa086efd6"
|
||||
integrity sha512-EcUuQHGAk8cheuPfjRCXacjoD5ClDsB9qssYBVIvJozNEW5C+A0eJwPj/Qd1C05tdcJ6MMdMRv0NrTHdCN1v1A==
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.48.0.tgz#4882fe8a92833e333c13d276d62648295769106a"
|
||||
integrity sha512-eKbhUW+9KCyK2xIO09iUI3KszfCxtmKgamSYED+N5bb1DzySjDur6BabHFBgA7BcQmYKpTSj/lVxznFNw3H1uQ==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@opentelemetry/semantic-conventions" "^1.28.0"
|
||||
"@rollup/plugin-commonjs" "28.0.1"
|
||||
"@sentry-internal/browser-utils" "8.45.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/node" "8.45.1"
|
||||
"@sentry/opentelemetry" "8.45.1"
|
||||
"@sentry/react" "8.45.1"
|
||||
"@sentry/vercel-edge" "8.45.1"
|
||||
"@sentry-internal/browser-utils" "8.48.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
"@sentry/node" "8.48.0"
|
||||
"@sentry/opentelemetry" "8.48.0"
|
||||
"@sentry/react" "8.48.0"
|
||||
"@sentry/vercel-edge" "8.48.0"
|
||||
"@sentry/webpack-plugin" "2.22.7"
|
||||
chalk "3.0.0"
|
||||
resolve "1.22.8"
|
||||
rollup "3.29.5"
|
||||
stacktrace-parser "^0.1.10"
|
||||
|
||||
"@sentry/node@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.45.1.tgz#24ebe7cb6a1ddc3d95602945a9574978797ac6f9"
|
||||
integrity sha512-xvlXifM/FSOQdLAqQBuo04SiOh7RP8rRRr+c5G/YbBtgJA867Pve0X8JZK2BJpDZ3OrGvzXV1Ubnt9ao4rBfYA==
|
||||
"@sentry/node@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.48.0.tgz#d4d1374431028af7663a06bf7268bf40a9bf1fa0"
|
||||
integrity sha512-pnprAuUOc8cxnJdZA09hutHXNsbQZoDgzf3zPyXMNx0ewB/RviFMOgfe7ViX1mIB/oVrcFenXBgO5uvTd7JwPg==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@opentelemetry/context-async-hooks" "^1.29.0"
|
||||
@@ -2805,34 +2805,34 @@
|
||||
"@opentelemetry/resources" "^1.29.0"
|
||||
"@opentelemetry/sdk-trace-base" "^1.29.0"
|
||||
"@opentelemetry/semantic-conventions" "^1.28.0"
|
||||
"@prisma/instrumentation" "5.19.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/opentelemetry" "8.45.1"
|
||||
"@prisma/instrumentation" "5.22.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
"@sentry/opentelemetry" "8.48.0"
|
||||
import-in-the-middle "^1.11.2"
|
||||
|
||||
"@sentry/opentelemetry@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.45.1.tgz#4c8e686818fb6af45ed486c341902affd9f110cf"
|
||||
integrity sha512-khnR5TS21ksITTXmXnpniRN7brlZS5RNNQuMZ9n3MYi/L1/s9LT73skNh1gder28OV6ZxGUgrTZ+1dtKqn9tig==
|
||||
"@sentry/opentelemetry@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.48.0.tgz#718e7942724d64ffe8e901941b0e4050fa07780b"
|
||||
integrity sha512-1JLXgmIvD3T7xn9ypwWW0V3GirNy4BN2fOUbZau/nUX/Jj5DttSoPn7x7xTaPSpfaA24PiP93zXmJEfZvCk00Q==
|
||||
dependencies:
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry/react@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.45.1.tgz#75afd1508cfdda9dbe7109ffbbb58767464bf56a"
|
||||
integrity sha512-ooMR2Lt4YSc5CMJklBKiL/mb+uidcZMpflxUvVUbtWMif+PqTUkfPRyICv6vs7muxK9i84Rr4iCkyZ4ns4H27A==
|
||||
"@sentry/react@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.48.0.tgz#0550a9a4d123d20c680d94bdaa8a8dbeb3b2661e"
|
||||
integrity sha512-J8XAUOJYbsjXnowTEXE+zWJWLWUzQGP8kMb+smoGdRzFJwwXKrbE709Kr/Boz6rK48EbbRT4UUINoTbHgL3RHQ==
|
||||
dependencies:
|
||||
"@sentry/browser" "8.45.1"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/browser" "8.48.0"
|
||||
"@sentry/core" "8.48.0"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
"@sentry/vercel-edge@8.45.1":
|
||||
version "8.45.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.45.1.tgz#d32b6302d5f062c2a641cbcf2be8614e99a9b30c"
|
||||
integrity sha512-taCKf2ESNzpYRwuaikS17YA2upqzzPbemT+If+kOafgSUrLEq2YRYRvgoruyFLmjOGO9+634+HmGo7Nyn5CGjQ==
|
||||
"@sentry/vercel-edge@8.48.0":
|
||||
version "8.48.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.48.0.tgz#e2d429dc3f24b47f4bdae1e34856d3b258365b03"
|
||||
integrity sha512-5bxMCTkadnvJvCC363ZXEdAHaWS/RAAvsI+8RAFObJO0tUemjKrgbHM/1YcvLRZSuBs6BSn9RjDipzzlFgtBWw==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.9.0"
|
||||
"@sentry/core" "8.45.1"
|
||||
"@sentry/core" "8.48.0"
|
||||
|
||||
"@sentry/webpack-plugin@2.22.7":
|
||||
version "2.22.7"
|
||||
@@ -3213,10 +3213,10 @@
|
||||
"@storybook/react-dom-shim" "8.4.7"
|
||||
"@storybook/theming" "8.4.7"
|
||||
|
||||
"@storybook/test-runner@^0.20.1":
|
||||
version "0.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/test-runner/-/test-runner-0.20.1.tgz#e2efa6266d512312a6b810db376da2919008cccd"
|
||||
integrity sha512-3WU/th/uncIR6vpQDK9hKjiZjmczsluoLbgkRV7ufxY9IgHCGcbIjvT5EPS+XZIaOrNGjaPsyB5cE1okKn9iSA==
|
||||
"@storybook/test-runner@^0.21.0":
|
||||
version "0.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/test-runner/-/test-runner-0.21.0.tgz#31e7a6878e15a3f4d5555c57a135dd4d13fce9c4"
|
||||
integrity sha512-aG2QvKXSIjMN1CA9PJK/esnidZWgt1gAkfo9Kqf8+NqBSsmP/2GyL5vxu1lkRFO/4qCv5JenNZ5Uj6ie4b2oag==
|
||||
dependencies:
|
||||
"@babel/core" "^7.22.5"
|
||||
"@babel/generator" "^7.22.5"
|
||||
@@ -3257,17 +3257,17 @@
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.4.7.tgz#c308f6a883999bd35e87826738ab8a76515932b5"
|
||||
integrity sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==
|
||||
|
||||
"@supabase/auth-js@2.67.1":
|
||||
version "2.67.1"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.1.tgz#b72217136df61d645dcfb7b12c7db8cbb7875a4c"
|
||||
integrity sha512-1SRZG9VkLFz4rtiyEc1l49tMq9jTYu4wJt3pMQEWi7yshZFIBdVH1o5sshk1plQd5LY6GcrPIpCydM2gGDxchA==
|
||||
"@supabase/auth-js@2.67.3":
|
||||
version "2.67.3"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.3.tgz#a1f5eb22440b0cdbf87fe2ecae662a8dd8bb2028"
|
||||
integrity sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/functions-js@2.4.3":
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.3.tgz#ac1c696d3a1ebe00f60d5cea69b208078678ef8b"
|
||||
integrity sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==
|
||||
"@supabase/functions-js@2.4.4":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.4.tgz#45fcd94d546bdfa66d01f93a796ca0304ec154b8"
|
||||
integrity sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
@@ -3311,12 +3311,12 @@
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/supabase-js@^2.47.8":
|
||||
version "2.47.8"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.8.tgz#6471a356b694e14170a00e6582bdbd0126944ec6"
|
||||
integrity sha512-2GjK8/PrGJYDVBcjqGyM2irBLMQXvvkJLbS8VFPlym2uuNz+pPMnwLbNf5njkknUTy3PamjgIRoADpuPPPA6oA==
|
||||
version "2.47.10"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.10.tgz#310ce81dc734116f9445dbce7f9341ae1c24d834"
|
||||
integrity sha512-vJfPF820Ho5WILYHfKiBykDQ1SB9odTHrRZ0JxHfuLMC8GRvv21YLkUZQK7/rSVCkLvD6/ZwMWaOAfdUd//guw==
|
||||
dependencies:
|
||||
"@supabase/auth-js" "2.67.1"
|
||||
"@supabase/functions-js" "2.4.3"
|
||||
"@supabase/auth-js" "2.67.3"
|
||||
"@supabase/functions-js" "2.4.4"
|
||||
"@supabase/node-fetch" "2.6.15"
|
||||
"@supabase/postgrest-js" "1.17.7"
|
||||
"@supabase/realtime-js" "2.11.2"
|
||||
@@ -3714,10 +3714,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.3.tgz#29e8fce64e35f57f6fe9c624f8e4ed304357745a"
|
||||
integrity sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==
|
||||
|
||||
"@types/node@*", "@types/node@^22.0.0", "@types/node@^22.9.0":
|
||||
version "22.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9"
|
||||
integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==
|
||||
"@types/node@*", "@types/node@^22.0.0", "@types/node@^22.10.5":
|
||||
version "22.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b"
|
||||
integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
@@ -3798,7 +3798,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
|
||||
|
||||
"@types/shimmer@^1.0.2", "@types/shimmer@^1.2.0":
|
||||
"@types/shimmer@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded"
|
||||
integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==
|
||||
@@ -4990,10 +4990,10 @@ chokidar@^3.5.3, chokidar@^3.6.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chromatic@^11.12.5, chromatic@^11.15.0:
|
||||
version "11.20.2"
|
||||
resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.20.2.tgz#10b309179cdc0b9195a5b68970366f9ebe67dfd1"
|
||||
integrity sha512-c+M3HVl5Y60c7ipGTZTyeWzWubRW70YsJ7PPDpO1D735ib8+Lu3yGF90j61pvgkXGngpkTPHZyBw83lcu2JMxA==
|
||||
chromatic@^11.15.0, chromatic@^11.22.0:
|
||||
version "11.22.0"
|
||||
resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.22.0.tgz#9c2b05a0c5a94c5c0cc2b6be6969c112ac4543d0"
|
||||
integrity sha512-u1kAPR9lj9aFzsCp0iWPXBbsKgcxFU7iJO6mFbgNHGVg+YPBqiJMuvgB8EQHdNbHjk5amFnGnIz/Ww8fK3t9Hw==
|
||||
|
||||
chrome-trace-event@^1.0.2:
|
||||
version "1.0.4"
|
||||
@@ -5192,10 +5192,10 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
concurrently@^9.0.1:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.0.tgz#8da6d609f4321752912dab9be8710232ac496aa0"
|
||||
integrity sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==
|
||||
concurrently@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
|
||||
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
lodash "^4.17.21"
|
||||
@@ -5540,6 +5540,11 @@ data-view-byte-offset@^1.0.0:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
date-fns-jalali@^4.1.0-0:
|
||||
version "4.1.0-0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz#9c7fb286004fab267a300d3e9f1ada9f10b4b6b0"
|
||||
integrity sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==
|
||||
|
||||
date-fns@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
|
||||
@@ -6120,12 +6125,12 @@ escape-string-regexp@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
eslint-config-next@15.1.0:
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.0.tgz#25a9a076b059905fd0cf3f6f832771724dfcbbdf"
|
||||
integrity sha512-gADO+nKVseGso3DtOrYX9H7TxB/MuX7AUYhMlvQMqLYvUWu4HrOQuU7cC1HW74tHIqkAvXdwgAz3TCbczzSEXw==
|
||||
eslint-config-next@15.1.3:
|
||||
version "15.1.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.3.tgz#7656b47591745bcdbd60d396282924d89f82eea6"
|
||||
integrity sha512-wGYlNuWnh4ujuKtZvH+7B2Z2vy9nONZE6ztd+DKF7hAsIabkrxmD4TzYHzASHENo42lmz2tnT2B+zN2sOHvpJg==
|
||||
dependencies:
|
||||
"@next/eslint-plugin-next" "15.1.0"
|
||||
"@next/eslint-plugin-next" "15.1.3"
|
||||
"@rushstack/eslint-patch" "^1.10.3"
|
||||
"@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
"@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -6241,10 +6246,10 @@ eslint-plugin-react@^7.37.0:
|
||||
string.prototype.matchall "^4.0.11"
|
||||
string.prototype.repeat "^1.0.0"
|
||||
|
||||
eslint-plugin-storybook@^0.11.0:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.1.tgz#4ef4f3550855fdc4a902296dfc278340ec287506"
|
||||
integrity sha512-yGKpAYkBm/Q2hZg476vRUAvd9lAccjjSvzU5nYy3BSQbKTPy7uopx7JEpwk2vSuw4weTMZzWF64z9/gp/K5RCg==
|
||||
eslint-plugin-storybook@^0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.2.tgz#a46f8fa2b87d15f66251e832a10d5481fc73a028"
|
||||
integrity sha512-0Z4DUklJrC+GHjCRXa7PYfPzWC15DaVnwaOYenpgXiCEijXPZkLKCms+rHhtoRcWccP7Z8DpOOaP1gc3P9oOwg==
|
||||
dependencies:
|
||||
"@storybook/csf" "^0.1.11"
|
||||
"@typescript-eslint/utils" "^8.8.1"
|
||||
@@ -6695,13 +6700,13 @@ forwarded-parse@2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325"
|
||||
integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==
|
||||
|
||||
framer-motion@^11.15.0:
|
||||
version "11.15.0"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.15.0.tgz#93e5d1839d500ba9cab1d617959a36142a61212b"
|
||||
integrity sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==
|
||||
framer-motion@^11.16.0:
|
||||
version "11.16.0"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.16.0.tgz#6592689bc8d6422207a55e48297eae99e9aa2eb2"
|
||||
integrity sha512-oL2AWqLQuw0+CNEUa0sz3mWC/n3i147CckvpQn8bLRs30b+HxTxlRi0YR2FpHHhAbWV7DKjNdHU42KHLfBWh/g==
|
||||
dependencies:
|
||||
motion-dom "^11.14.3"
|
||||
motion-utils "^11.14.3"
|
||||
motion-dom "^11.16.0"
|
||||
motion-utils "^11.16.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
fromentries@^1.2.0:
|
||||
@@ -8398,10 +8403,10 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lucide-react@^0.468.0:
|
||||
version "0.468.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.468.0.tgz#830c1bfd905575ddd23b832baa420c87db166910"
|
||||
integrity sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==
|
||||
lucide-react@^0.469.0:
|
||||
version "0.469.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.469.0.tgz#f16936ca6521482fef754a7eabb310e6c68e1482"
|
||||
integrity sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
@@ -8884,15 +8889,17 @@ moment@^2.30.1:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
|
||||
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
|
||||
|
||||
motion-dom@^11.14.3:
|
||||
version "11.14.3"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.14.3.tgz#725c72c0f1d0b632e42fdd8d13b69ecf9fe202c0"
|
||||
integrity sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==
|
||||
motion-dom@^11.16.0:
|
||||
version "11.16.0"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.16.0.tgz#1766e4f9ada72c30ba8f3e698774372fcbcc95b3"
|
||||
integrity sha512-4bmEwajSdrljzDAYpu6ceEdtI4J5PH25fmN8YSx7Qxk6OMrC10CXM0D5y+VO/pFZjhmCvm2bGf7Rus482kwhzA==
|
||||
dependencies:
|
||||
motion-utils "^11.16.0"
|
||||
|
||||
motion-utils@^11.14.3:
|
||||
version "11.14.3"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.14.3.tgz#cd4a413463739498411f82abb67b3dd58768b0f8"
|
||||
integrity sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==
|
||||
motion-utils@^11.16.0:
|
||||
version "11.16.0"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.16.0.tgz#e75865442278be49e411ca9105c9139edc572811"
|
||||
integrity sha512-ngdWPjg31rD4WGXFi0eZ00DQQqKKu04QExyv/ymlC+3k+WIgYVFbt6gS5JsFPbJODTF/r8XiE/X+SsoT9c0ocw==
|
||||
|
||||
ms@^2.1.1, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
@@ -9895,13 +9902,14 @@ react-confetti@^6.1.0:
|
||||
dependencies:
|
||||
tween-functions "^1.2.0"
|
||||
|
||||
react-day-picker@^9.4.4:
|
||||
version "9.4.4"
|
||||
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-9.4.4.tgz#1c514c86489ede20046eec957c68a7b31542bd49"
|
||||
integrity sha512-1s+jA/bFYtoxhhr8M0kkFHLiMTSII6qU8UfDFprRAUStTVHljLTjg4oarvAngPlQ1cQAC+LUb0k/qMc+jjhmxw==
|
||||
react-day-picker@^9.5.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-9.5.0.tgz#2ae36e85d6506026d72e350f49b5607d011cfd6f"
|
||||
integrity sha512-WmJnPFVLnKh5Qscm7wavMNg86rqPverSWjx+zgK8/ZmGRSQ8c8OoqW10RI+AzAfT2atIxImpCUU2R9Z7Xb2SUA==
|
||||
dependencies:
|
||||
"@date-fns/tz" "^1.2.0"
|
||||
date-fns "^4.1.0"
|
||||
date-fns-jalali "^4.1.0-0"
|
||||
|
||||
react-docgen-typescript@^2.2.2:
|
||||
version "2.2.2"
|
||||
@@ -9932,10 +9940,10 @@ react-docgen@^7.0.0:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-hook-form@^7.54.0:
|
||||
version "7.54.1"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.1.tgz#e99c2a55a5e4859fb47a8f55adf66b34d6ac331d"
|
||||
integrity sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg==
|
||||
react-hook-form@^7.54.2:
|
||||
version "7.54.2"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211"
|
||||
integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==
|
||||
|
||||
react-icons@^5.4.0:
|
||||
version "5.4.0"
|
||||
@@ -9962,10 +9970,10 @@ react-lifecycles-compat@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-markdown@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1"
|
||||
integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==
|
||||
react-markdown@^9.0.3:
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.3.tgz#c12bf60dad05e9bf650b86bcc612d80636e8456e"
|
||||
integrity sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
devlop "^1.0.0"
|
||||
@@ -9978,10 +9986,10 @@ react-markdown@^9.0.1:
|
||||
unist-util-visit "^5.0.0"
|
||||
vfile "^6.0.0"
|
||||
|
||||
react-modal@^3.16.1:
|
||||
version "3.16.1"
|
||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b"
|
||||
integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==
|
||||
react-modal@^3.16.3:
|
||||
version "3.16.3"
|
||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.3.tgz#c412d41915782e3c261253435d01468e2439b11b"
|
||||
integrity sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==
|
||||
dependencies:
|
||||
exenv "^1.2.0"
|
||||
prop-types "^15.7.2"
|
||||
@@ -11035,20 +11043,20 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
tailwind-merge@^2.5.5:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.5.tgz#98167859b856a2a6b8d2baf038ee171b9d814e39"
|
||||
integrity sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==
|
||||
tailwind-merge@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||
|
||||
tailwindcss-animate@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
|
||||
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
|
||||
|
||||
tailwindcss@^3.4.15:
|
||||
version "3.4.16"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.16.tgz#35a7c3030844d6000fc271878db4096b6a8d2ec9"
|
||||
integrity sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==
|
||||
tailwindcss@^3.4.17:
|
||||
version "3.4.17"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
||||
integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
|
||||
dependencies:
|
||||
"@alloc/quick-lru" "^5.2.0"
|
||||
arg "^5.0.2"
|
||||
@@ -11553,10 +11561,10 @@ utila@~0.4:
|
||||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||
integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==
|
||||
|
||||
uuid@^11.0.3:
|
||||
version "11.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d"
|
||||
integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==
|
||||
uuid@^11.0.4:
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.4.tgz#37943977894ef806d2919a7ca3f89d6e23c60bac"
|
||||
integrity sha512-IzL6VtTTYcAhA/oghbFJ1Dkmqev+FpQWnCBaKq/gUluLxliWvO8DPFWfIviRmYbtaavtSQe4WBL++rFjdcGWEg==
|
||||
|
||||
uuid@^8.0.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
DB_USER=postgres
|
||||
DB_PASS=your-super-secret-and-long-postgres-password
|
||||
DB_NAME=postgres
|
||||
DB_PORT=5432
|
||||
DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=market"
|
||||
SENTRY_DSN=https://11d0640fef35640e0eb9f022eb7d7626@o4505260022104064.ingest.us.sentry.io/4507890252447744
|
||||
|
||||
ENABLE_AUTH=true
|
||||
SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
|
||||
APP_ENV=local
|
||||
6
autogpt_platform/market/.gitignore
vendored
6
autogpt_platform/market/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
database.db
|
||||
database.db-journal
|
||||
build/
|
||||
config.json
|
||||
secrets/*
|
||||
!secrets/.gitkeep
|
||||
@@ -1,72 +0,0 @@
|
||||
FROM python:3.11.10-slim-bookworm AS builder
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
|
||||
|
||||
RUN apt-get update --allow-releaseinfo-change --fix-missing
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get install -y build-essential
|
||||
RUN apt-get install -y libpq5
|
||||
RUN apt-get install -y libz-dev
|
||||
RUN apt-get install -y libssl-dev
|
||||
|
||||
ENV POETRY_VERSION=1.8.3 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
RUN pip3 install poetry
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||
COPY autogpt_platform/market/poetry.lock autogpt_platform/market/pyproject.toml /app/autogpt_platform/market/
|
||||
WORKDIR /app/autogpt_platform/market
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-ansi
|
||||
|
||||
# Generate Prisma client
|
||||
COPY autogpt_platform/market /app/autogpt_platform/market
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry run prisma generate
|
||||
|
||||
FROM python:3.11.10-slim-bookworm AS server_dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
# Copy only necessary files from builder
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
# Copy Prisma binaries
|
||||
COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
RUN mkdir -p /app/autogpt_platform/autogpt_libs
|
||||
RUN mkdir -p /app/autogpt_platform/market
|
||||
|
||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||
|
||||
COPY autogpt_platform/market /app/autogpt_platform/market
|
||||
|
||||
WORKDIR /app/autogpt_platform/market
|
||||
|
||||
FROM server_dependencies AS server
|
||||
|
||||
ENV DATABASE_URL=""
|
||||
ENV PORT=8015
|
||||
|
||||
CMD ["poetry", "run", "app"]
|
||||
@@ -1,37 +0,0 @@
|
||||
# AutoGPT Agent Marketplace
|
||||
|
||||
## Overview
|
||||
|
||||
AutoGPT Agent Marketplace is an open-source platform for autonomous AI agents. This project aims to create a user-friendly, accessible marketplace where users can discover, utilize, and contribute to a diverse ecosystem of AI solutions.
|
||||
|
||||
## Vision
|
||||
|
||||
Our vision is to empower users with customizable and free AI agents, fostering an open-source community that drives innovation in AI automation across various industries.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Agent Discovery and Search
|
||||
- Agent Listings with Detailed Information
|
||||
- User Profiles
|
||||
- Data Protection and Compliance
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with the AutoGPT Agent Marketplace, follow these steps:
|
||||
|
||||
- Copy `.env.example` to `.env` and fill in the required environment variables
|
||||
- Run `poetry run setup`
|
||||
- Run `poetry run populate`
|
||||
- Run `poetry run app`
|
||||
|
||||
## Poetry Run Commands
|
||||
|
||||
This section outlines the available command line scripts for this project, configured using Poetry. You can execute these scripts directly using Poetry. Each command performs a specific operation as described below:
|
||||
|
||||
- `poetry run format`: Runs the formatting script to ensure code consistency.
|
||||
- `poetry run lint`: Executes the linting script to identify and fix potential code issues.
|
||||
- `poetry run app`: Starts the main application.
|
||||
- `poetry run setup`: Runs the setup script to configure the database.
|
||||
- `poetry run populate`: Populates the database with initial data using the specified script.
|
||||
|
||||
To run any of these commands, ensure Poetry is installed on your system and execute the commands from the project's root directory.
|
||||
@@ -1,16 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector:latest
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASS}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
PGUSER: ${DB_USER}
|
||||
healthcheck:
|
||||
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "${DB_PORT}:5432"
|
||||
@@ -1,97 +0,0 @@
|
||||
import contextlib
|
||||
import logging.config
|
||||
import os
|
||||
|
||||
import dotenv
|
||||
import fastapi
|
||||
import fastapi.middleware.cors
|
||||
import fastapi.middleware.gzip
|
||||
import prisma
|
||||
import prometheus_fastapi_instrumentator
|
||||
import sentry_sdk
|
||||
import sentry_sdk.integrations.asyncio
|
||||
import sentry_sdk.integrations.fastapi
|
||||
import sentry_sdk.integrations.starlette
|
||||
|
||||
import market.config
|
||||
import market.routes.admin
|
||||
import market.routes.agents
|
||||
import market.routes.analytics
|
||||
import market.routes.search
|
||||
import market.routes.submissions
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
logging.config.dictConfig(market.config.LogConfig().model_dump())
|
||||
|
||||
if os.environ.get("SENTRY_DSN"):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
traces_sample_rate=1.0,
|
||||
profiles_sample_rate=1.0,
|
||||
enable_tracing=True,
|
||||
environment=os.environ.get("RUN_ENV", default="CLOUD").lower(),
|
||||
integrations=[
|
||||
sentry_sdk.integrations.starlette.StarletteIntegration(
|
||||
transaction_style="url"
|
||||
),
|
||||
sentry_sdk.integrations.fastapi.FastApiIntegration(transaction_style="url"),
|
||||
sentry_sdk.integrations.asyncio.AsyncioIntegration(),
|
||||
],
|
||||
)
|
||||
|
||||
db_client = prisma.Prisma(auto_register=True)
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: fastapi.FastAPI):
|
||||
await db_client.connect()
|
||||
yield
|
||||
await db_client.disconnect()
|
||||
|
||||
|
||||
docs_url = "/docs"
|
||||
app = fastapi.FastAPI(
|
||||
title="Marketplace API",
|
||||
description="AutoGPT Marketplace API is a service that allows users to share AI agents.",
|
||||
summary="Maketplace API",
|
||||
version="0.1",
|
||||
lifespan=lifespan,
|
||||
root_path="/api/v1/market",
|
||||
docs_url=docs_url,
|
||||
)
|
||||
|
||||
app.add_middleware(fastapi.middleware.gzip.GZipMiddleware, minimum_size=1000)
|
||||
app.add_middleware(
|
||||
middleware_class=fastapi.middleware.cors.CORSMiddleware,
|
||||
allow_origins=os.environ.get(
|
||||
"BACKEND_CORS_ALLOW_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"
|
||||
).split(","),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(market.routes.agents.router, tags=["agents"])
|
||||
app.include_router(market.routes.search.router, tags=["search"])
|
||||
app.include_router(market.routes.submissions.router, tags=["submissions"])
|
||||
app.include_router(market.routes.admin.router, prefix="/admin", tags=["admin"])
|
||||
app.include_router(
|
||||
market.routes.analytics.router, prefix="/analytics", tags=["analytics"]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return fastapi.responses.HTMLResponse(
|
||||
content="<h1>Marketplace API</h1>", status_code=200
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def default():
|
||||
return fastapi.responses.HTMLResponse(
|
||||
content="<h1>Marketplace API</h1>", status_code=200
|
||||
)
|
||||
|
||||
|
||||
prometheus_fastapi_instrumentator.Instrumentator().instrument(app).expose(app)
|
||||
@@ -1,30 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LogConfig(BaseModel):
|
||||
"""Logging configuration to be set for the server"""
|
||||
|
||||
LOGGER_NAME: str = "marketplace"
|
||||
LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
|
||||
LOG_LEVEL: str = "DEBUG"
|
||||
|
||||
# Logging config
|
||||
version: int = 1
|
||||
disable_existing_loggers: bool = False
|
||||
formatters: dict = {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
}
|
||||
handlers: dict = {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
}
|
||||
loggers: dict = {
|
||||
LOGGER_NAME: {"handlers": ["default"], "level": LOG_LEVEL},
|
||||
}
|
||||
@@ -1,725 +0,0 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import fuzzywuzzy.fuzz
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
import pydantic
|
||||
|
||||
import market.model
|
||||
import market.utils.extension_types
|
||||
|
||||
|
||||
class AgentQueryError(Exception):
|
||||
"""Custom exception for agent query errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TopAgentsDBResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response containing a list of top agents.
|
||||
|
||||
Attributes:
|
||||
analytics (list[AgentResponse]): The list of top agents.
|
||||
total_count (int): The total count of agents.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of agents per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
analytics: list[prisma.models.AnalyticsTracker]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class FeaturedAgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response containing a list of featured agents.
|
||||
|
||||
Attributes:
|
||||
featured_agents (list[FeaturedAgent]): The list of featured agents.
|
||||
total_count (int): The total count of featured agents.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of agents per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
featured_agents: list[prisma.models.FeaturedAgent]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
async def delete_agent(agent_id: str) -> prisma.models.Agents | None:
|
||||
"""
|
||||
Delete an agent from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to delete.
|
||||
|
||||
Returns:
|
||||
prisma.models.Agents | None: The deleted agent if found, None otherwise.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error deleting the agent from the database.
|
||||
"""
|
||||
try:
|
||||
deleted_agent = await prisma.models.Agents.prisma().delete(
|
||||
where={"id": agent_id}
|
||||
)
|
||||
return deleted_agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def create_agent_entry(
|
||||
name: str,
|
||||
description: str,
|
||||
author: str,
|
||||
keywords: typing.List[str],
|
||||
categories: typing.List[str],
|
||||
graph: prisma.Json,
|
||||
submission_state: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.PENDING,
|
||||
):
|
||||
"""
|
||||
Create a new agent entry in the database.
|
||||
|
||||
Args:
|
||||
name (str): The name of the agent.
|
||||
description (str): The description of the agent.
|
||||
author (str): The author of the agent.
|
||||
keywords (List[str]): The keywords associated with the agent.
|
||||
categories (List[str]): The categories associated with the agent.
|
||||
graph (dict): The graph data of the agent.
|
||||
|
||||
Returns:
|
||||
dict: The newly created agent entry.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error creating the agent entry.
|
||||
"""
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().create(
|
||||
data={
|
||||
"name": name,
|
||||
"description": description,
|
||||
"author": author,
|
||||
"keywords": keywords,
|
||||
"categories": categories,
|
||||
"graph": graph,
|
||||
"AnalyticsTracker": {"create": {"downloads": 0, "views": 0}},
|
||||
"submissionStatus": submission_state,
|
||||
}
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def update_agent_entry(
|
||||
agent_id: str,
|
||||
version: int,
|
||||
submission_state: prisma.enums.SubmissionStatus,
|
||||
comments: str | None = None,
|
||||
) -> prisma.models.Agents | None:
|
||||
"""
|
||||
Update an existing agent entry in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int): The version of the agent.
|
||||
submission_state (prisma.enums.SubmissionStatus): The submission state of the agent.
|
||||
"""
|
||||
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().update(
|
||||
where={"id": agent_id},
|
||||
data={
|
||||
"version": version,
|
||||
"submissionStatus": submission_state,
|
||||
"submissionReviewDate": datetime.datetime.now(datetime.timezone.utc),
|
||||
"submissionReviewComments": comments,
|
||||
},
|
||||
)
|
||||
|
||||
return agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Agent Update Failed Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agents(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
name: str | None = None,
|
||||
keyword: str | None = None,
|
||||
category: str | None = None,
|
||||
description: str | None = None,
|
||||
description_threshold: int = 60,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
sort_by: str = "createdAt",
|
||||
sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
|
||||
):
|
||||
"""
|
||||
Retrieve a list of agents from the database based on the provided filters and pagination parameters.
|
||||
|
||||
Args:
|
||||
page (int, optional): The page number to retrieve. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
name (str, optional): Filter agents by name. Defaults to None.
|
||||
keyword (str, optional): Filter agents by keyword. Defaults to None.
|
||||
category (str, optional): Filter agents by category. Defaults to None.
|
||||
description (str, optional): Filter agents by description. Defaults to None.
|
||||
description_threshold (int, optional): The minimum fuzzy search threshold for the description. Defaults to 60.
|
||||
sort_by (str, optional): The field to sort the agents by. Defaults to "createdAt".
|
||||
sort_order (str, optional): The sort order ("asc" or "desc"). Defaults to "desc".
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Define the base query
|
||||
query = {}
|
||||
|
||||
# Add optional filters
|
||||
if name:
|
||||
query["name"] = {"contains": name, "mode": "insensitive"}
|
||||
if keyword:
|
||||
query["keywords"] = {"has": keyword}
|
||||
if category:
|
||||
query["categories"] = {"has": category}
|
||||
|
||||
query["submissionStatus"] = submission_status
|
||||
|
||||
# Define sorting
|
||||
order = {sort_by: sort_order}
|
||||
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
agents = await prisma.models.Agents.prisma().find_many(
|
||||
where=query, # type: ignore
|
||||
order=order, # type: ignore
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
# Apply fuzzy search on description if provided
|
||||
if description:
|
||||
try:
|
||||
filtered_agents = []
|
||||
for agent in agents:
|
||||
if (
|
||||
agent.description
|
||||
and fuzzywuzzy.fuzz.partial_ratio(
|
||||
description.lower(), agent.description.lower()
|
||||
)
|
||||
>= description_threshold
|
||||
):
|
||||
filtered_agents.append(agent)
|
||||
agents = filtered_agents
|
||||
except AttributeError as e:
|
||||
raise AgentQueryError(f"Error during fuzzy search: {str(e)}")
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(agents)
|
||||
|
||||
return {
|
||||
"agents": agents,
|
||||
"total_count": total_count,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total_count + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}")
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agent_details(agent_id: str, version: int | None = None):
|
||||
"""
|
||||
Retrieve agent details from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The agent details.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If the agent is not found or if there is an error querying the database.
|
||||
"""
|
||||
try:
|
||||
query = {"id": agent_id}
|
||||
if version is not None:
|
||||
query["version"] = version # type: ignore
|
||||
|
||||
agent = await prisma.models.Agents.prisma().find_first(where=query) # type: ignore
|
||||
|
||||
if not agent:
|
||||
raise AgentQueryError("Agent not found")
|
||||
|
||||
return agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def search_db(
|
||||
query: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
categories: typing.List[str] | None = None,
|
||||
description_threshold: int = 60,
|
||||
sort_by: str = "rank",
|
||||
sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
|
||||
"""Perform a search for agents based on the provided query string.
|
||||
|
||||
Args:
|
||||
query (str): the search string
|
||||
page (int, optional): page for searching. Defaults to 1.
|
||||
page_size (int, optional): the number of results to return. Defaults to 10.
|
||||
categories (List[str] | None, optional): list of category filters. Defaults to None.
|
||||
description_threshold (int, optional): number of characters to return. Defaults to 60.
|
||||
sort_by (str, optional): sort by option. Defaults to "rank".
|
||||
sort_order ("asc" | "desc", optional): the sort order. Defaults to "desc".
|
||||
|
||||
Raises:
|
||||
AgentQueryError: Raises an error if the query fails.
|
||||
AgentQueryError: Raises if an unexpected error occurs.
|
||||
|
||||
Returns:
|
||||
List[AgentsWithRank]: List of agents matching the search criteria.
|
||||
"""
|
||||
try:
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
category_filter = "1=1"
|
||||
if categories:
|
||||
category_conditions = [f"'{cat}' = ANY(categories)" for cat in categories]
|
||||
category_filter = "AND (" + " OR ".join(category_conditions) + ")"
|
||||
|
||||
# Construct the ORDER BY clause based on the sort_by parameter
|
||||
if sort_by in ["createdAt", "updatedAt"]:
|
||||
order_by_clause = f'"{sort_by}" {sort_order.upper()}, rank DESC'
|
||||
elif sort_by == "name":
|
||||
order_by_clause = f"name {sort_order.upper()}, rank DESC"
|
||||
else:
|
||||
order_by_clause = 'rank DESC, "createdAt" DESC'
|
||||
|
||||
submission_status_filter = f""""submissionStatus" = '{submission_status}'"""
|
||||
|
||||
sql_query = f"""
|
||||
WITH query AS (
|
||||
SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
|
||||
FROM unnest(to_tsvector('{query}'))
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
version,
|
||||
name,
|
||||
LEFT(description, {description_threshold}) AS description,
|
||||
author,
|
||||
keywords,
|
||||
categories,
|
||||
graph,
|
||||
"submissionStatus",
|
||||
"submissionDate",
|
||||
CASE
|
||||
WHEN query.q::text = '' THEN 1.0
|
||||
ELSE COALESCE(ts_rank(CAST(search AS tsvector), query.q), 0.0)
|
||||
END AS rank
|
||||
FROM market."Agents", query
|
||||
WHERE
|
||||
(query.q::text = '' OR search @@ query.q)
|
||||
AND {category_filter}
|
||||
AND {submission_status_filter}
|
||||
ORDER BY {order_by_clause}
|
||||
LIMIT {page_size}
|
||||
OFFSET {offset};
|
||||
"""
|
||||
|
||||
results = await prisma.client.get_client().query_raw(
|
||||
query=sql_query,
|
||||
model=market.utils.extension_types.AgentsWithRank,
|
||||
)
|
||||
|
||||
class CountResponse(pydantic.BaseModel):
|
||||
count: int
|
||||
|
||||
count_query = f"""
|
||||
WITH query AS (
|
||||
SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
|
||||
FROM unnest(to_tsvector('{query}'))
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
FROM market."Agents", query
|
||||
WHERE (search @@ query.q OR query.q = '') AND {category_filter} AND {submission_status_filter};
|
||||
"""
|
||||
|
||||
total_count = await prisma.client.get_client().query_first(
|
||||
query=count_query,
|
||||
model=CountResponse,
|
||||
)
|
||||
total_count = total_count.count if total_count else 0
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=results,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_top_agents_by_downloads(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> market.model.ListResponse[prisma.models.AnalyticsTracker]:
|
||||
"""Retrieve the top agents by download count.
|
||||
|
||||
Args:
|
||||
page (int, optional): The page number. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
# Agents with no downloads will not be included in the results... is this the desired behavior?
|
||||
analytics = await prisma.models.AnalyticsTracker.prisma().find_many(
|
||||
include={"agent": True},
|
||||
order={"downloads": "desc"},
|
||||
where={"agent": {"is": {"submissionStatus": submission_status}}},
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
try:
|
||||
total_count = await prisma.models.AnalyticsTracker.prisma().count(
|
||||
where={"agent": {"is": {"submissionStatus": submission_status}}},
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=analytics,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e from e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
|
||||
|
||||
|
||||
async def set_agent_featured(
|
||||
agent_id: str, is_active: bool = True, featured_categories: list[str] = ["featured"]
|
||||
) -> prisma.models.FeaturedAgent:
|
||||
"""Set an agent as featured in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str, optional): The category to set the agent as featured. Defaults to "featured".
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error setting the agent as featured.
|
||||
"""
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().find_unique(where={"id": agent_id})
|
||||
if not agent:
|
||||
raise AgentQueryError(f"Agent with ID {agent_id} not found.")
|
||||
|
||||
featured = await prisma.models.FeaturedAgent.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {
|
||||
"featuredCategories": featured_categories,
|
||||
"isActive": is_active,
|
||||
},
|
||||
"create": {
|
||||
"featuredCategories": featured_categories,
|
||||
"isActive": is_active,
|
||||
"agent": {"connect": {"id": agent_id}},
|
||||
},
|
||||
},
|
||||
)
|
||||
return featured
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_featured_agents(
|
||||
category: str = "featured",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> FeaturedAgentResponse:
|
||||
"""Retrieve a list of featured agents from the database based on the provided category.
|
||||
|
||||
Args:
|
||||
category (str, optional): The category of featured agents to retrieve. Defaults to "featured".
|
||||
page (int, optional): The page number to retrieve. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of featured agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
featured_agents = await prisma.models.FeaturedAgent.prisma().find_many(
|
||||
where={
|
||||
"featuredCategories": {"has": category},
|
||||
"isActive": True,
|
||||
"agent": {"is": {"submissionStatus": submission_status}},
|
||||
},
|
||||
include={"agent": {"include": {"AnalyticsTracker": True}}},
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(featured_agents)
|
||||
|
||||
return FeaturedAgentResponse(
|
||||
featured_agents=featured_agents,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e from e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
|
||||
|
||||
|
||||
async def remove_featured_category(
|
||||
agent_id: str, category: str
|
||||
) -> prisma.models.FeaturedAgent | None:
|
||||
"""Adds a featured category to an agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str): The category to add to the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The updated list of featured agents.
|
||||
"""
|
||||
try:
|
||||
# get the existing categories
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
|
||||
where={"agentId": agent_id},
|
||||
include={"agent": True},
|
||||
)
|
||||
|
||||
if not featured_agent:
|
||||
raise AgentQueryError(f"Agent with ID {agent_id} not found.")
|
||||
|
||||
# remove the category from the list
|
||||
featured_agent.featuredCategories.remove(category)
|
||||
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().update(
|
||||
where={"agentId": agent_id},
|
||||
data={"featuredCategories": featured_agent.featuredCategories},
|
||||
)
|
||||
|
||||
return featured_agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def add_featured_category(
|
||||
agent_id: str, category: str
|
||||
) -> prisma.models.FeaturedAgent | None:
|
||||
"""Removes a featured category from an agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str): The category to remove from the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The updated list of featured agents.
|
||||
"""
|
||||
try:
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().update(
|
||||
where={"agentId": agent_id},
|
||||
data={"featuredCategories": {"push": [category]}},
|
||||
)
|
||||
|
||||
return featured_agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agent_featured(agent_id: str) -> prisma.models.FeaturedAgent | None:
|
||||
"""Retrieve an agent's featured categories from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The list of featured agents.
|
||||
"""
|
||||
try:
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
|
||||
where={"agentId": agent_id},
|
||||
)
|
||||
return featured_agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_not_featured_agents(
|
||||
page: int = 1, page_size: int = 10
|
||||
) -> typing.List[prisma.models.Agents]:
|
||||
"""
|
||||
Retrieve a list of not featured agents from the database.
|
||||
"""
|
||||
try:
|
||||
agents = await prisma.client.get_client().query_raw(
|
||||
query=f"""
|
||||
SELECT
|
||||
"market"."Agents".id,
|
||||
"market"."Agents"."createdAt",
|
||||
"market"."Agents"."updatedAt",
|
||||
"market"."Agents".version,
|
||||
"market"."Agents".name,
|
||||
LEFT("market"."Agents".description, 500) AS description,
|
||||
"market"."Agents".author,
|
||||
"market"."Agents".keywords,
|
||||
"market"."Agents".categories,
|
||||
"market"."Agents".graph,
|
||||
"market"."Agents"."submissionStatus",
|
||||
"market"."Agents"."submissionDate",
|
||||
"market"."Agents".search::text AS search
|
||||
FROM "market"."Agents"
|
||||
LEFT JOIN "market"."FeaturedAgent" ON "market"."Agents"."id" = "market"."FeaturedAgent"."agentId"
|
||||
WHERE ("market"."FeaturedAgent"."agentId" IS NULL OR "market"."FeaturedAgent"."featuredCategories" = '{{}}')
|
||||
AND "market"."Agents"."submissionStatus" = 'APPROVED'
|
||||
ORDER BY "market"."Agents"."createdAt" DESC
|
||||
LIMIT {page_size} OFFSET {page_size * (page - 1)}
|
||||
""",
|
||||
model=prisma.models.Agents,
|
||||
)
|
||||
return agents
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_all_categories() -> market.model.CategoriesResponse:
|
||||
"""
|
||||
Retrieve all unique categories from the database.
|
||||
|
||||
Returns:
|
||||
CategoriesResponse: A list of unique categories.
|
||||
"""
|
||||
try:
|
||||
agents = await prisma.models.Agents.prisma().find_many(distinct=["categories"])
|
||||
|
||||
# Aggregate categories on the Python side
|
||||
all_categories = set()
|
||||
for agent in agents:
|
||||
all_categories.update(agent.categories)
|
||||
|
||||
unique_categories = sorted(list(all_categories))
|
||||
|
||||
return market.model.CategoriesResponse(unique_categories=unique_categories)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception:
|
||||
# Return an empty list of categories in case of unexpected errors
|
||||
return market.model.CategoriesResponse(unique_categories=[])
|
||||
|
||||
|
||||
async def create_agent_installed_event(
|
||||
event_data: market.model.AgentInstalledFromMarketplaceEventData,
|
||||
):
|
||||
try:
|
||||
await prisma.models.InstallTracker.prisma().create(
|
||||
data={
|
||||
"installedAgentId": event_data.installed_agent_id,
|
||||
"marketplaceAgentId": event_data.marketplace_agent_id,
|
||||
"installationLocation": prisma.enums.InstallationLocation(
|
||||
event_data.installation_location.name
|
||||
),
|
||||
}
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
@@ -1,161 +0,0 @@
|
||||
import datetime
|
||||
import typing
|
||||
from enum import Enum
|
||||
from typing import Generic, Literal, TypeVar, Union
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
|
||||
class InstallationLocation(str, Enum):
|
||||
LOCAL = "local"
|
||||
CLOUD = "cloud"
|
||||
|
||||
|
||||
class AgentInstalledFromMarketplaceEventData(pydantic.BaseModel):
|
||||
marketplace_agent_id: str
|
||||
installed_agent_id: str
|
||||
installation_location: InstallationLocation
|
||||
|
||||
|
||||
class AgentInstalledFromTemplateEventData(pydantic.BaseModel):
|
||||
template_id: str
|
||||
installed_agent_id: str
|
||||
installation_location: InstallationLocation
|
||||
|
||||
|
||||
class AgentInstalledFromMarketplaceEvent(pydantic.BaseModel):
|
||||
event_name: Literal["agent_installed_from_marketplace"]
|
||||
event_data: AgentInstalledFromMarketplaceEventData
|
||||
|
||||
|
||||
class AgentInstalledFromTemplateEvent(pydantic.BaseModel):
|
||||
event_name: Literal["agent_installed_from_template"]
|
||||
event_data: AgentInstalledFromTemplateEventData
|
||||
|
||||
|
||||
AnalyticsEvent = Union[
|
||||
AgentInstalledFromMarketplaceEvent, AgentInstalledFromTemplateEvent
|
||||
]
|
||||
|
||||
|
||||
class AnalyticsRequest(pydantic.BaseModel):
|
||||
event: AnalyticsEvent
|
||||
|
||||
|
||||
class AddAgentRequest(pydantic.BaseModel):
|
||||
graph: dict[str, typing.Any]
|
||||
author: str
|
||||
keywords: list[str]
|
||||
categories: list[str]
|
||||
|
||||
|
||||
class SubmissionReviewRequest(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
version: int
|
||||
status: prisma.enums.SubmissionStatus
|
||||
comments: str | None
|
||||
|
||||
|
||||
class AgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response from an agent.
|
||||
|
||||
Attributes:
|
||||
id (str): The ID of the agent.
|
||||
name (str, optional): The name of the agent.
|
||||
description (str, optional): The description of the agent.
|
||||
author (str, optional): The author of the agent.
|
||||
keywords (list[str]): The keywords associated with the agent.
|
||||
categories (list[str]): The categories the agent belongs to.
|
||||
version (int): The version of the agent.
|
||||
createdAt (str): The creation date of the agent.
|
||||
updatedAt (str): The last update date of the agent.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: typing.Optional[str]
|
||||
description: typing.Optional[str]
|
||||
author: typing.Optional[str]
|
||||
keywords: list[str]
|
||||
categories: list[str]
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
submissionStatus: str
|
||||
views: int = 0
|
||||
downloads: int = 0
|
||||
|
||||
|
||||
class AgentDetailResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for an agent detail.
|
||||
|
||||
Attributes:
|
||||
id (str): The ID of the agent.
|
||||
name (Optional[str]): The name of the agent.
|
||||
description (Optional[str]): The description of the agent.
|
||||
author (Optional[str]): The author of the agent.
|
||||
keywords (List[str]): The keywords associated with the agent.
|
||||
categories (List[str]): The categories the agent belongs to.
|
||||
version (int): The version of the agent.
|
||||
createdAt (str): The creation date of the agent.
|
||||
updatedAt (str): The last update date of the agent.
|
||||
graph (Dict[str, Any]): The graph data of the agent.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: typing.Optional[str]
|
||||
description: typing.Optional[str]
|
||||
author: typing.Optional[str]
|
||||
keywords: list[str]
|
||||
categories: list[str]
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
graph: dict[str, typing.Any]
|
||||
|
||||
|
||||
class FeaturedAgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for an agent detail.
|
||||
"""
|
||||
|
||||
agentId: str
|
||||
featuredCategories: list[str]
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
isActive: bool
|
||||
|
||||
|
||||
class CategoriesResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for a list of categories.
|
||||
|
||||
Attributes:
|
||||
unique_categories (list[str]): The list of unique categories.
|
||||
"""
|
||||
|
||||
unique_categories: list[str]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ListResponse(pydantic.BaseModel, Generic[T]):
|
||||
"""
|
||||
Represents a list response.
|
||||
|
||||
Attributes:
|
||||
items (list[T]): The list of items.
|
||||
total_count (int): The total count of items.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of items per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
items: list[T]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
@@ -1,286 +0,0 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import prisma
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
|
||||
logger = logging.getLogger("marketplace")
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.delete("/agent/{agent_id}", response_model=market.model.AgentResponse)
|
||||
async def delete_agent(
|
||||
agent_id: str,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
Delete an agent and all related records from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to delete.
|
||||
|
||||
Returns:
|
||||
market.model.AgentResponse: The deleted agent's data.
|
||||
|
||||
Raises:
|
||||
fastapi.HTTPException: If the agent is not found or if there's an error during deletion.
|
||||
"""
|
||||
try:
|
||||
deleted_agent = await market.db.delete_agent(agent_id)
|
||||
if deleted_agent:
|
||||
return market.model.AgentResponse(**deleted_agent.dict())
|
||||
else:
|
||||
raise fastapi.HTTPException(status_code=404, detail="Agent not found")
|
||||
except market.db.AgentQueryError as e:
|
||||
logger.error(f"Error deleting agent: {e}")
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting agent: {e}")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail="An unexpected error occurred"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent", response_model=market.model.AgentResponse)
|
||||
async def create_agent_entry(
|
||||
request: market.model.AddAgentRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to create a new agent entry in the database.
|
||||
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.create_agent_entry(
|
||||
request.graph["name"],
|
||||
request.graph["description"],
|
||||
request.author,
|
||||
request.keywords,
|
||||
request.categories,
|
||||
prisma.Json(request.graph),
|
||||
)
|
||||
|
||||
return fastapi.responses.PlainTextResponse(agent.model_dump_json())
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/agent/featured/{agent_id}")
|
||||
async def set_agent_featured(
|
||||
agent_id: str,
|
||||
categories: list[str] = fastapi.Query(
|
||||
default=["featured"],
|
||||
description="The categories to set the agent as featured in",
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse:
|
||||
"""
|
||||
A basic endpoint to set an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.set_agent_featured(
|
||||
agent_id, is_active=True, featured_categories=categories
|
||||
)
|
||||
return market.model.FeaturedAgentResponse(**agent.model_dump())
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/agent/featured/{agent_id}")
|
||||
async def get_agent_featured(
|
||||
agent_id: str,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse | None:
|
||||
"""
|
||||
A basic endpoint to get an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_featured(agent_id)
|
||||
if agent:
|
||||
return market.model.FeaturedAgentResponse(**agent.model_dump())
|
||||
else:
|
||||
return None
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/agent/featured/{agent_id}")
|
||||
async def unset_agent_featured(
|
||||
agent_id: str,
|
||||
category: str = "featured",
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse | None:
|
||||
"""
|
||||
A basic endpoint to unset an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
featured = await market.db.remove_featured_category(agent_id, category=category)
|
||||
if featured:
|
||||
return market.model.FeaturedAgentResponse(**featured.model_dump())
|
||||
else:
|
||||
return None
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/agent/not-featured")
|
||||
async def get_not_featured_agents(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
"""
|
||||
A basic endpoint to get all not featured agents in the database.
|
||||
"""
|
||||
try:
|
||||
agents = await market.db.get_not_featured_agents(page=page, page_size=page_size)
|
||||
return market.model.ListResponse(
|
||||
items=[
|
||||
market.model.AgentResponse(**agent.model_dump()) for agent in agents
|
||||
],
|
||||
total_count=len(agents),
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=999,
|
||||
)
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent/submissions",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
async def get_agent_submissions(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
name: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by agent name"
|
||||
),
|
||||
keyword: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by keyword"
|
||||
),
|
||||
category: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by category"
|
||||
),
|
||||
description: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Fuzzy search in description"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, ge=0, le=100, description="Fuzzy search threshold"
|
||||
),
|
||||
sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
|
||||
sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
|
||||
"desc", description="Sort order (asc or desc)"
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
logger.info("Getting agent submissions")
|
||||
try:
|
||||
result = await market.db.get_agents(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
name=name,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
description=description,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=prisma.enums.SubmissionStatus.PENDING,
|
||||
)
|
||||
|
||||
agents = [
|
||||
market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
|
||||
]
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=agents,
|
||||
total_count=result["total_count"],
|
||||
page=result["page"],
|
||||
page_size=result["page_size"],
|
||||
total_pages=result["total_pages"],
|
||||
)
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent/submissions")
|
||||
async def review_submission(
|
||||
review_request: market.model.SubmissionReviewRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> prisma.models.Agents | None:
|
||||
"""
|
||||
A basic endpoint to review a submission in the database.
|
||||
"""
|
||||
logger.info(
|
||||
f"Reviewing submission: {review_request.agent_id}, {review_request.version}"
|
||||
)
|
||||
try:
|
||||
agent = await market.db.update_agent_entry(
|
||||
agent_id=review_request.agent_id,
|
||||
version=review_request.version,
|
||||
submission_state=review_request.status,
|
||||
comments=review_request.comments,
|
||||
)
|
||||
return agent
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_categories() -> market.model.CategoriesResponse:
|
||||
"""
|
||||
A basic endpoint to get all available categories.
|
||||
"""
|
||||
try:
|
||||
categories = await market.db.get_all_categories()
|
||||
return categories
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
@@ -1,76 +0,0 @@
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.app
|
||||
|
||||
client = fastapi.testclient.TestClient(market.app.app)
|
||||
|
||||
|
||||
async def override_auth_middleware(request: fastapi.Request):
|
||||
return {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
|
||||
|
||||
|
||||
market.app.app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
|
||||
override_auth_middleware
|
||||
)
|
||||
|
||||
|
||||
def test_get_submissions():
|
||||
with mock.patch("market.db.get_agents") as mock_get_agents:
|
||||
mock_get_agents.return_value = {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
response = client.get(
|
||||
"/api/v1/market/admin/agent/submissions?page=1&page_size=10&description_threshold=60&sort_by=createdAt&sort_order=desc",
|
||||
headers={"Bearer": ""},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_review_submission():
|
||||
with mock.patch("market.db.update_agent_entry") as mock_update_agent_entry:
|
||||
mock_update_agent_entry.return_value = prisma.models.Agents(
|
||||
id="aaa-bbb-ccc",
|
||||
version=1,
|
||||
createdAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
updatedAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
|
||||
submissionDate=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionReviewComments="Looks good",
|
||||
submissionReviewDate=datetime.datetime.fromisoformat(
|
||||
"2021-10-01T00:00:00+00:00"
|
||||
),
|
||||
keywords=["test"],
|
||||
categories=["test"],
|
||||
graph='{"name": "test", "description": "test"}', # type: ignore
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1/market/admin/agent/submissions",
|
||||
headers={
|
||||
"Authorization": "Bearer token"
|
||||
}, # Assuming you need an authorization token
|
||||
json={
|
||||
"agent_id": "aaa-bbb-ccc",
|
||||
"version": 1,
|
||||
"status": "APPROVED",
|
||||
"comments": "Looks good",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -1,368 +0,0 @@
|
||||
import json
|
||||
import tempfile
|
||||
import typing
|
||||
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import prisma
|
||||
import prisma.enums
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.analytics
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents", response_model=market.model.ListResponse[market.model.AgentResponse]
|
||||
)
|
||||
async def list_agents(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
name: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by agent name"
|
||||
),
|
||||
keyword: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by keyword"
|
||||
),
|
||||
category: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by category"
|
||||
),
|
||||
description: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Fuzzy search in description"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, ge=0, le=100, description="Fuzzy search threshold"
|
||||
),
|
||||
sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
|
||||
sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
|
||||
"desc", description="Sort order (asc or desc)"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve a list of agents based on the provided filters.
|
||||
|
||||
Args:
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
name (str, optional): Filter by agent name.
|
||||
keyword (str, optional): Filter by keyword.
|
||||
category (str, optional): Filter by category.
|
||||
description (str, optional): Fuzzy search in description.
|
||||
description_threshold (int): Fuzzy search threshold (default: 60, min: 0, max: 100).
|
||||
sort_by (str): Field to sort by (default: "createdAt").
|
||||
sort_order (str): Sort order (asc or desc) (default: "desc").
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
try:
|
||||
result = await market.db.get_agents(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
name=name,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
description=description,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
|
||||
agents = [
|
||||
market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
|
||||
]
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=agents,
|
||||
total_count=result["total_count"],
|
||||
page=result["page"],
|
||||
page_size=result["page_size"],
|
||||
total_pages=result["total_pages"],
|
||||
)
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}", response_model=market.model.AgentDetailResponse)
|
||||
async def get_agent_details_endpoint(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve details of a specific agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to retrieve.
|
||||
version (Optional[int]): Specific version of the agent (default: None).
|
||||
|
||||
Returns:
|
||||
market.model.AgentDetailResponse: The response containing the agent details.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
background_tasks.add_task(market.utils.analytics.track_view, agent_id)
|
||||
return market.model.AgentDetailResponse(**agent.model_dump())
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}/download")
|
||||
async def download_agent(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Download details of a specific agent.
|
||||
|
||||
NOTE: This is the same as agent details, however it also triggers
|
||||
the "download" tracking. We don't actually want to download a file though
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to retrieve.
|
||||
version (Optional[int]): Specific version of the agent (default: None).
|
||||
|
||||
Returns:
|
||||
market.model.AgentDetailResponse: The response containing the agent details.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
background_tasks.add_task(market.utils.analytics.track_download, agent_id)
|
||||
return market.model.AgentDetailResponse(**agent.model_dump())
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}/download-file")
|
||||
async def download_agent_file(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to download"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
) -> fastapi.responses.FileResponse:
|
||||
"""
|
||||
Download the agent file by streaming its content.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to download.
|
||||
version (Optional[int]): Specific version of the agent to download.
|
||||
|
||||
Returns:
|
||||
StreamingResponse: A streaming response containing the agent's graph data.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
|
||||
graph_data: prisma.Json = agent.graph
|
||||
|
||||
background_tasks.add_task(market.utils.analytics.track_download, agent_id)
|
||||
|
||||
file_name = f"agent_{agent_id}_v{version or 'latest'}.json"
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False
|
||||
) as tmp_file:
|
||||
tmp_file.write(json.dumps(graph_data))
|
||||
tmp_file.flush()
|
||||
|
||||
return fastapi.responses.FileResponse(
|
||||
tmp_file.name, filename=file_name, media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
# top agents by downloads
|
||||
@router.get(
|
||||
"/top-downloads/agents",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
async def top_agents_by_downloads(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
"""
|
||||
Retrieve a list of top agents based on the number of downloads.
|
||||
|
||||
Args:
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of top agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
try:
|
||||
result = await market.db.get_top_agents_by_downloads(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
|
||||
ret = market.model.ListResponse(
|
||||
total_count=result.total_count,
|
||||
page=result.page,
|
||||
page_size=result.page_size,
|
||||
total_pages=result.total_pages,
|
||||
items=[
|
||||
market.model.AgentResponse(
|
||||
id=item.agent.id,
|
||||
name=item.agent.name,
|
||||
description=item.agent.description,
|
||||
author=item.agent.author,
|
||||
keywords=item.agent.keywords,
|
||||
categories=item.agent.categories,
|
||||
version=item.agent.version,
|
||||
createdAt=item.agent.createdAt,
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=item.views,
|
||||
downloads=item.downloads,
|
||||
submissionStatus=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.items
|
||||
if item.agent is not None
|
||||
],
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get(
|
||||
"/featured/agents",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
async def get_featured_agents(
|
||||
category: str = fastapi.Query(
|
||||
"featured", description="Category of featured agents"
|
||||
),
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve a list of featured agents based on the provided category.
|
||||
|
||||
Args:
|
||||
category (str): Category of featured agents (default: "featured").
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of featured agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
try:
|
||||
result = await market.db.get_featured_agents(
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
|
||||
ret = market.model.ListResponse(
|
||||
total_count=result.total_count,
|
||||
page=result.page,
|
||||
page_size=result.page_size,
|
||||
total_pages=result.total_pages,
|
||||
items=[
|
||||
market.model.AgentResponse(
|
||||
id=item.agent.id,
|
||||
name=item.agent.name,
|
||||
description=item.agent.description,
|
||||
author=item.agent.author,
|
||||
keywords=item.agent.keywords,
|
||||
categories=item.agent.categories,
|
||||
version=item.agent.version,
|
||||
createdAt=item.agent.createdAt,
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=(
|
||||
item.agent.AnalyticsTracker[0].views
|
||||
if item.agent.AnalyticsTracker
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
downloads=(
|
||||
item.agent.AnalyticsTracker[0].downloads
|
||||
if item.agent.AnalyticsTracker
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
submissionStatus=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.featured_agents
|
||||
if item.agent is not None
|
||||
],
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
) from e
|
||||
@@ -1,26 +0,0 @@
|
||||
import fastapi
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post("/agent-installed")
|
||||
async def agent_installed_endpoint(
|
||||
event_data: market.model.AgentInstalledFromMarketplaceEventData,
|
||||
):
|
||||
"""
|
||||
Endpoint to track agent installation events from the marketplace.
|
||||
|
||||
Args:
|
||||
event_data (market.model.AgentInstalledFromMarketplaceEventData): The event data.
|
||||
"""
|
||||
try:
|
||||
await market.db.create_agent_installed_event(event_data)
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
@@ -1,56 +0,0 @@
|
||||
import typing
|
||||
|
||||
import fastapi
|
||||
import prisma.enums
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.extension_types
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def search(
|
||||
query: str,
|
||||
page: int = fastapi.Query(1, description="The pagination page to start on"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, description="The number of items to return per page"
|
||||
),
|
||||
categories: typing.List[str] = fastapi.Query(
|
||||
None, description="The categories to filter by"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, description="The number of characters to return from the description"
|
||||
),
|
||||
sort_by: str = fastapi.Query("rank", description="Sorting by column"),
|
||||
sort_order: typing.Literal["desc", "asc"] = fastapi.Query(
|
||||
"desc", description="The sort order based on sort_by"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="The submission status to filter by",
|
||||
),
|
||||
) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
|
||||
"""searches endpoint for agents
|
||||
|
||||
Args:
|
||||
query (str): the search query
|
||||
page (int, optional): the pagination page to start on. Defaults to 1.
|
||||
page_size (int, optional): the number of items to return per page. Defaults to 10.
|
||||
category (str | None, optional): the agent category to filter by. None is no filter. Defaults to None.
|
||||
description_threshold (int, optional): the number of characters to return from the description. Defaults to 60.
|
||||
sort_by (str, optional): Sorting by column. Defaults to "rank".
|
||||
sort_order ('asc' | 'desc', optional): the sort order based on sort_by. Defaults to "desc".
|
||||
"""
|
||||
agents = await market.db.search_db(
|
||||
query=query,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
categories=categories,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
return agents
|
||||
@@ -1,35 +0,0 @@
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import prisma
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.analytics
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post("/agents/submit", response_model=market.model.AgentResponse)
|
||||
async def submit_agent(
|
||||
request: market.model.AddAgentRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(autogpt_libs.auth.requires_user),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to create a new agent entry in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.create_agent_entry(
|
||||
request.graph["name"],
|
||||
request.graph["description"],
|
||||
request.author,
|
||||
request.keywords,
|
||||
request.categories,
|
||||
prisma.Json(request.graph),
|
||||
)
|
||||
|
||||
return fastapi.responses.PlainTextResponse(agent.model_dump_json())
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
@@ -1,47 +0,0 @@
|
||||
import prisma.models
|
||||
|
||||
|
||||
async def track_download(agent_id: str):
|
||||
"""
|
||||
Track the download event in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an error tracking the download event.
|
||||
"""
|
||||
try:
|
||||
await prisma.models.AnalyticsTracker.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {"downloads": {"increment": 1}},
|
||||
"create": {"agentId": agent_id, "downloads": 1, "views": 0},
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error tracking download event: {str(e)}")
|
||||
|
||||
|
||||
async def track_view(agent_id: str):
|
||||
"""
|
||||
Track the view event in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an error tracking the view event.
|
||||
"""
|
||||
try:
|
||||
await prisma.models.AnalyticsTracker.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {"views": {"increment": 1}},
|
||||
"create": {"agentId": agent_id, "downloads": 0, "views": 1},
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error tracking view event: {str(e)}")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user