Merge remote-tracking branch 'origin/dev' into pr/8754

This commit is contained in:
Nicholas Tindle
2025-01-07 12:49:31 -06:00
82 changed files with 2592 additions and 4344 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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."

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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()
@@ -138,17 +138,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)
@@ -157,32 +178,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)
@@ -255,7 +251,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,
@@ -297,10 +293,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

View File

@@ -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={

View File

@@ -424,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 --------------------- #
@@ -609,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(

View File

@@ -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):

View File

@@ -246,7 +246,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]):
@@ -255,21 +256,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
@@ -283,11 +284,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]):

View File

@@ -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 ------- #

View File

@@ -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(

View File

@@ -1,10 +1,11 @@
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"/webhooks/{webhook_id}/ingress"

View File

@@ -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__)
@@ -786,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

View File

@@ -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
@@ -575,3 +579,66 @@ async def generate_image(
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"
)

View File

@@ -33,20 +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:
"""
Normalizes the URL by:
1. Stripping whitespace and trailing slashes.
2. Ensuring the scheme is http:// or https:// if missing.
3. Replacing backslashes with forward slashes.
"""
url = url.strip().strip("/")
if not url.startswith(("http://", "https://")):
url = "http://" + url
url = url.replace("\\", "/")
return url
def _is_ip_blocked(ip: str) -> bool:
"""
Checks if the IP address is in a blocked network.
@@ -61,9 +47,12 @@ def validate_url(url: str, trusted_origins: list[str]) -> str:
to a private, link-local, or otherwise blocked IP address — unless
the hostname is explicitly trusted.
"""
# Normalize/canonicalize input
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:

View File

@@ -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}")

View File

@@ -0,0 +1,2 @@
-- Add "TERMINATED" to execution status enum type
ALTER TYPE "AgentExecutionStatus" ADD VALUE 'TERMINATED';

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -216,6 +216,7 @@ enum AgentExecutionStatus {
QUEUED
RUNNING
COMPLETED
TERMINATED
FAILED
}
@@ -638,4 +639,4 @@ enum APIKeyStatus {
ACTIVE
REVOKED
SUSPENDED
}
}

View File

@@ -160,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"] == ""

View File

@@ -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: ../

View File

@@ -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:

View File

@@ -81,7 +81,7 @@
"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,7 +92,7 @@
"@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/react": "^18",
@@ -100,9 +100,9 @@
"@types/react-modal": "^3.16.3",
"axe-playwright": "^2.0.3",
"chromatic": "^11.12.5",
"concurrently": "^9.0.1",
"concurrently": "^9.1.1",
"eslint": "^8",
"eslint-config-next": "15.1.0",
"eslint-config-next": "15.1.3",
"eslint-plugin-storybook": "^0.11.0",
"msw": "^2.7.0",
"msw-storybook-addon": "^2.0.3",
@@ -110,7 +110,7 @@
"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",

View File

@@ -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);
@@ -267,7 +268,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)}
@@ -732,13 +734,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 ? (
@@ -787,7 +786,6 @@ export function CustomNode({
<Switch
onCheckedChange={toggleAdvancedSettings}
checked={isAdvancedOpen}
className="mr-5"
/>
</div>
</>
@@ -796,7 +794,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)}
@@ -856,8 +854,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":

View File

@@ -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}

View File

@@ -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 */}

View File

@@ -15,15 +15,11 @@
.custom-node [data-id^="date-picker"],
.custom-node [data-list-container],
.custom-node [data-add-item],
.custom-node [data-content-settings] {
min-width: calc(100% - 2.5rem);
max-width: 400px;
}
.array-item-container {
.custom-node [data-content-settings]. .array-item-container {
display: flex;
align-items: center;
min-width: calc(100% - 2.5rem);
max-width: 100%;
}
.custom-node .custom-switch {

View File

@@ -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";
@@ -95,12 +95,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);
@@ -217,6 +218,7 @@ export const CredentialsInput: FC<{
<>
{supportsApiKey && (
<APIKeyCredentialsModal
credentialsFieldName={selfKey}
open={isAPICredentialsModalOpen}
onClose={() => setAPICredentialsModalOpen(false)}
onCredentialsCreate={(credsMeta) => {
@@ -250,7 +252,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)}>
@@ -318,7 +322,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} />
@@ -361,11 +370,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"),

View File

@@ -203,7 +203,7 @@ export const NodeGenericInputField: FC<{
className,
displayName,
}) => {
className = cn(className, "my-2");
className = cn(className);
displayName ||= propSchema.title || beautifyString(propKey);
if ("allOf" in propSchema) {
@@ -753,6 +753,7 @@ const NodeCredentialsInput: FC<{
return (
<div className={cn("flex flex-col", className)}>
<CredentialsInput
selfKey={selfKey}
onSelectCredentials={(credsMeta) =>
handleInputChange(selfKey, credsMeta)
}
@@ -983,18 +984,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}

View File

@@ -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 {

View File

@@ -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 &&

View File

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

View File

@@ -197,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;
};
@@ -247,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;

View File

@@ -1018,7 +1018,7 @@
"@types/tough-cookie" "^4.0.5"
tough-cookie "^4.1.4"
"@chromatic-com/storybook@^3.2.2":
"@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==
@@ -1774,10 +1774,10 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.22.tgz#8898ae47595badbfacebfc1585f42a4e06a97301"
integrity sha512-EQ6y1QeNQglNmNIXvwP/Bb+lf7n9WtgcWvtoFsHquVLCJUuxRs+6SfZ5EK0/EqkkLex4RrDySvKgKNN7PXip7Q==
"@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"
@@ -3296,10 +3296,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"
@@ -5274,10 +5274,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.2"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
concurrently@^9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.1.tgz#609dde2ce12f4f12d6a5ea6eace4c38bb7ab2ead"
integrity sha512-6VX8lrBIycgZKTwBsWS+bLrmkGRkDmvtGsYylRN9b93CygN6CbK46HmnQ3rdSOR8HRjdahDrxb5MqD9cEFOg5Q==
dependencies:
chalk "^4.1.2"
lodash "^4.17.21"
@@ -6213,12 +6213,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"
@@ -11284,7 +11284,7 @@ tailwindcss-animate@^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:
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==

View File

@@ -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

View File

@@ -1,6 +0,0 @@
database.db
database.db-journal
build/
config.json
secrets/*
!secrets/.gitkeep

View File

@@ -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"]

View File

@@ -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.

View File

@@ -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"

View File

@@ -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)

View File

@@ -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},
}

View File

@@ -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)}")

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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}"
)

View File

@@ -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

View File

@@ -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))

View File

@@ -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)}")

View File

@@ -1,5 +0,0 @@
import prisma.models
class AgentsWithRank(prisma.models.Agents):
rank: float

View File

@@ -1,6 +0,0 @@
import prisma.models
prisma.models.Agents.create_partial(
"AgentOnlyDescriptionNameAuthorIdCategories",
include={"name", "author", "id", "categories"},
)

View File

@@ -1,61 +0,0 @@
-- CreateTable
CREATE TABLE "Agents" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"name" TEXT,
"description" TEXT,
"author" TEXT,
"keywords" TEXT[],
"categories" TEXT[],
"search" tsvector DEFAULT ''::tsvector,
"graph" JSONB NOT NULL,
CONSTRAINT "Agents_pkey" PRIMARY KEY ("id","version")
);
-- CreateTable
CREATE TABLE "AnalyticsTracker" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"agentId" UUID NOT NULL,
"views" INTEGER NOT NULL,
"downloads" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Agents_id_key" ON "Agents"("id");
-- CreateIndex
CREATE UNIQUE INDEX "AnalyticsTracker_id_key" ON "AnalyticsTracker"("id");
-- AddForeignKey
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Add trigger to update the search column with the tsvector of the agent
-- Function to be invoked by trigger
CREATE OR REPLACE FUNCTION update_tsvector_column() RETURNS TRIGGER AS $$
BEGIN
NEW.search := to_tsvector('english', COALESCE(NEW.description, '')|| ' ' ||COALESCE(NEW.name, '')|| ' ' ||COALESCE(NEW.author, ''));
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY definer SET search_path = public, pg_temp;
-- Trigger that keeps the TSVECTOR up to date
DROP TRIGGER IF EXISTS "update_tsvector" ON "Agents";
CREATE TRIGGER "update_tsvector"
BEFORE INSERT OR UPDATE ON "Agents"
FOR EACH ROW
EXECUTE FUNCTION update_tsvector_column ();

View File

@@ -1,11 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_pkey" PRIMARY KEY ("id");
-- CreateIndex
CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId");

View File

@@ -1,20 +0,0 @@
-- CreateTable
CREATE TABLE "FeaturedAgent" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"agentId" UUID NOT NULL,
"is_featured" BOOLEAN NOT NULL,
"category" TEXT NOT NULL DEFAULT 'featured',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FeaturedAgent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "FeaturedAgent_id_key" ON "FeaturedAgent"("id");
-- CreateIndex
CREATE UNIQUE INDEX "FeaturedAgent_agentId_key" ON "FeaturedAgent"("agentId");
-- AddForeignKey
ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "FeaturedAgent" ALTER COLUMN "is_featured" SET DEFAULT false;

View File

@@ -1,8 +0,0 @@
-- CreateEnum
CREATE TYPE "SubmissionStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- AlterTable
ALTER TABLE "Agents" ADD COLUMN "submissionDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "submissionReviewComments" TEXT,
ADD COLUMN "submissionReviewDate" TIMESTAMP(3),
ADD COLUMN "submissionStatus" "SubmissionStatus" NOT NULL DEFAULT 'PENDING';

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `category` on the `FeaturedAgent` table. All the data in the column will be lost.
- You are about to drop the column `is_featured` on the `FeaturedAgent` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "FeaturedAgent" DROP COLUMN "category",
DROP COLUMN "is_featured",
ADD COLUMN "featuredCategories" TEXT[],
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,19 +0,0 @@
-- CreateEnum
CREATE TYPE "InstallationLocation" AS ENUM ('LOCAL', 'CLOUD');
-- CreateTable
CREATE TABLE "InstallTracker" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"marketplaceAgentId" UUID NOT NULL,
"installedAgentId" UUID NOT NULL,
"installationLocation" "InstallationLocation" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InstallTracker_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "InstallTracker_marketplaceAgentId_installedAgentId_key" ON "InstallTracker"("marketplaceAgentId", "installedAgentId");
-- AddForeignKey
ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,20 +0,0 @@
-- DropForeignKey
ALTER TABLE "AnalyticsTracker" DROP CONSTRAINT "AnalyticsTracker_agentId_fkey";
-- DropForeignKey
ALTER TABLE "FeaturedAgent" DROP CONSTRAINT "FeaturedAgent_agentId_fkey";
-- DropForeignKey
ALTER TABLE "InstallTracker" DROP CONSTRAINT "InstallTracker_marketplaceAgentId_fkey";
-- DropIndex
DROP INDEX "AnalyticsTracker_agentId_key";
-- AddForeignKey
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

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

View File

@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
[tool.poetry]
name = "market"
version = "0.1.0"
description = ""
authors = [
"SwiftyOS <craigswift13@gmail.com>",
"Nicholas Tindle <spam@ntindle.com>",
]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
prisma = "^0.15.0"
python-dotenv = "^1.0.1"
uvicorn = "^0.34.0"
fastapi = "^0.115.6"
sentry-sdk = { extras = ["fastapi"], version = "^2.19.2" }
fuzzywuzzy = "^0.18.0"
python-levenshtein = "^0.26.1"
# autogpt-platform-backend = { path = "../backend", develop = true }
prometheus-fastapi-instrumentator = "^7.0.0"
autogpt-libs = {path = "../autogpt_libs"}
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.4"
pytest-asyncio = "^0.25.0"
pytest-watcher = "^0.4.3"
requests = "^2.32.3"
ruff = "^0.8.3"
pyright = "^1.1.390"
isort = "^5.13.2"
black = "^24.10.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
format = "scripts:format"
lint = "scripts:lint"
app = "scripts:app"
setup = "scripts:setup"
populate = "scripts:populate_database"
[tool.pytest-watcher]
now = false
clear = true
delay = 0.2
runner = "pytest"
runner_args = []
patterns = ["*.py"]
ignore_patterns = []
[tool.pytest.ini_options]
asyncio_mode = "auto"

View File

@@ -1,80 +0,0 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-py"
recursive_type_depth = 5
interface = "asyncio"
previewFeatures = ["fullTextSearch"]
partial_type_generator = "market/utils/partial_types.py"
}
enum SubmissionStatus {
PENDING
APPROVED
REJECTED
}
model Agents {
id String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submissionDate DateTime @default(now())
submissionReviewDate DateTime?
submissionStatus SubmissionStatus @default(PENDING)
submissionReviewComments String?
name String?
description String?
author String?
keywords String[]
categories String[]
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
graph Json
AnalyticsTracker AnalyticsTracker[]
FeaturedAgent FeaturedAgent?
InstallTracker InstallTracker[]
@@id(name: "graphVersionId", [id, version])
}
model AnalyticsTracker {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
agentId String @unique @db.Uuid
agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
views Int
downloads Int
}
enum InstallationLocation {
LOCAL
CLOUD
}
model InstallTracker {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
marketplaceAgentId String @db.Uuid
marketplaceAgent Agents @relation(fields: [marketplaceAgentId], references: [id], onDelete: Cascade)
installedAgentId String @db.Uuid
installationLocation InstallationLocation
createdAt DateTime @default(now())
@@unique([marketplaceAgentId, installedAgentId])
}
model FeaturedAgent {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
agentId String @unique @db.Uuid
agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
isActive Boolean @default(false)
featuredCategories String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -1,65 +0,0 @@
import os
import subprocess
directory = os.path.dirname(os.path.realpath(__file__))
def run(*command: str) -> None:
print(f">>>>> Running poetry run {' '.join(command)}")
subprocess.run(["poetry", "run"] + list(command), cwd=directory, check=True)
def lint():
try:
run("ruff", "check", ".", "--exit-zero")
run("isort", "--diff", "--check", "--profile", "black", ".")
run("black", "--diff", "--check", ".")
run("pyright")
except subprocess.CalledProcessError as e:
print("Lint failed, try running `poetry run format` to fix the issues: ", e)
raise e
def populate_database():
import glob
import json
import pathlib
import requests
import market.model
templates = pathlib.Path(__file__).parent.parent / "graph_templates"
all_files = glob.glob(str(templates / "*.json"))
for file in all_files:
with open(file, "r") as f:
data = f.read()
req = market.model.AddAgentRequest(
graph=json.loads(data),
author="Populate DB",
categories=["Pre-Populated"],
keywords=["test"],
)
response = requests.post(
"http://localhost:8015/api/v1/market/admin/agent", json=req.model_dump()
)
print(response.text)
def format():
run("ruff", "check", "--fix", ".")
run("isort", "--profile", "black", ".")
run("black", ".")
run("pyright", ".")
def app():
port = os.getenv("PORT", "8015")
run("uvicorn", "market.app:app", "--reload", "--port", port, "--host", "0.0.0.0")
def setup():
run("prisma", "generate")
run("prisma", "migrate", "deploy")

View File

@@ -1,79 +0,0 @@
from datetime import datetime, timezone
import pytest
from fastapi.testclient import TestClient
from market.app import app
from market.db import AgentQueryError
@pytest.fixture
def test_client():
return TestClient(app)
# Mock data
mock_agents = [
{
"id": "1",
"name": "Agent 1",
"description": "Description 1",
"author": "Author 1",
"keywords": ["AI", "chatbot"],
"categories": ["general"],
"version": 1,
"createdAt": datetime.now(timezone.utc),
"updatedAt": datetime.now(timezone.utc),
"graph": {"node1": "value1"},
},
{
"id": "2",
"name": "Agent 2",
"description": "Description 2",
"author": "Author 2",
"keywords": ["ML", "NLP"],
"categories": ["specialized"],
"version": 1,
"createdAt": datetime.now(timezone.utc),
"updatedAt": datetime.now(timezone.utc),
"graph": {"node2": "value2"},
},
]
# TODO: Need to mock prisma somehow
@pytest.mark.asyncio
async def test_list_agents(test_client):
response = test_client.get("/agents")
assert response.status_code == 200
data = response.json()
assert len(data["agents"]) == 2
assert data["total_count"] == 2
@pytest.mark.asyncio
async def test_list_agents_with_filters(test_client):
response = await test_client.get("/agents?name=Agent 1&keyword=AI&category=general")
assert response.status_code == 200
data = response.json()
assert len(data["agents"]) == 1
assert data["agents"][0]["name"] == "Agent 1"
@pytest.mark.asyncio
async def test_get_agent_details(test_client, mock_get_agent_details):
response = await test_client.get("/agents/1")
assert response.status_code == 200
data = response.json()
assert data["id"] == "1"
assert data["name"] == "Agent 1"
assert "graph" in data
@pytest.mark.asyncio
async def test_get_nonexistent_agent(test_client, mock_get_agent_details):
mock_get_agent_details.side_effect = AgentQueryError("Agent not found")
response = await test_client.get("/agents/999")
assert response.status_code == 404

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
[[package]]
name = "agent-protocol-client"
@@ -6,6 +6,8 @@ version = "1.1.0"
description = "Agent Communication Protocol Client"
optional = false
python-versions = "^3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = []
develop = false
@@ -28,6 +30,8 @@ version = "3.9.3"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"},
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"},
@@ -124,6 +128,8 @@ version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
@@ -138,6 +144,8 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -149,6 +157,8 @@ version = "4.2.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
{file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
@@ -171,6 +181,8 @@ version = "2.4.1"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
@@ -189,6 +201,8 @@ version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@@ -200,6 +214,8 @@ version = "23.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
@@ -219,6 +235,8 @@ version = "23.12.1"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"},
{file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"},
@@ -265,6 +283,8 @@ version = "5.3.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
{file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
@@ -276,6 +296,8 @@ version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
@@ -287,6 +309,8 @@ version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
@@ -351,6 +375,8 @@ version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
@@ -362,6 +388,8 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -461,6 +489,8 @@ version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
@@ -475,6 +505,8 @@ version = "1.2.4"
description = "click_default_group"
optional = false
python-versions = ">=2.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"},
{file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"},
@@ -492,6 +524,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -503,6 +537,8 @@ version = "1.2.0"
description = "Python library for calculating contours of 2D quadrilateral grids"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"},
{file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"},
@@ -566,6 +602,8 @@ version = "7.5.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
{file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
@@ -633,6 +671,8 @@ version = "0.12.1"
description = "Composable style cycles"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
{file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
@@ -648,6 +688,8 @@ version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
@@ -659,6 +701,8 @@ version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
@@ -670,6 +714,8 @@ version = "1.9.0"
description = "Distro - an OS platform information API"
optional = false
python-versions = ">=3.6"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
@@ -681,6 +727,8 @@ version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
@@ -695,6 +743,8 @@ version = "2.0.1"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.5"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
{file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
@@ -709,6 +759,8 @@ version = "0.109.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
{file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
@@ -728,6 +780,8 @@ version = "3.13.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
@@ -744,6 +798,8 @@ version = "7.0.0"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.8.1"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"},
{file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"},
@@ -760,6 +816,8 @@ version = "4.47.2"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df"},
{file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1"},
@@ -825,6 +883,8 @@ version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
@@ -911,6 +971,8 @@ version = "4.0.11"
description = "Git Object Database"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
{file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
@@ -925,6 +987,8 @@ version = "3.1.41"
description = "GitPython is a Python library used to interact with Git repositories"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"},
{file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"},
@@ -942,6 +1006,8 @@ version = "2.26.2"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "google-auth-2.26.2.tar.gz", hash = "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81"},
{file = "google_auth-2.26.2-py2.py3-none-any.whl", hash = "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424"},
@@ -965,6 +1031,8 @@ version = "1.2.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "google-auth-oauthlib-1.2.0.tar.gz", hash = "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8"},
{file = "google_auth_oauthlib-1.2.0-py2.py3-none-any.whl", hash = "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf"},
@@ -983,6 +1051,8 @@ version = "5.12.4"
description = "Google Spreadsheets Python API"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "gspread-5.12.4-py3-none-any.whl", hash = "sha256:1e453d87e0fde23bc5546e33eb684cf8b8c26540615f2f1ae004a9084a29051d"},
{file = "gspread-5.12.4.tar.gz", hash = "sha256:3fcef90183f15d3c9233b4caa021a83682f2b2ee678340c42d7ca7d8be98c6d1"},
@@ -998,6 +1068,8 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -1009,6 +1081,8 @@ version = "0.17.3"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
{file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
@@ -1030,6 +1104,8 @@ version = "0.22.0"
description = "A comprehensive HTTP client library."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"},
{file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"},
@@ -1044,6 +1120,8 @@ version = "0.24.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
{file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
@@ -1067,6 +1145,8 @@ version = "2.5.33"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
{file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
@@ -1081,6 +1161,8 @@ version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
@@ -1092,6 +1174,8 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@@ -1103,6 +1187,8 @@ version = "8.20.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "ipython-8.20.0-py3-none-any.whl", hash = "sha256:bc9716aad6f29f36c449e30821c9dd0c1c1a7b59ddcc26931685b87b4c569619"},
{file = "ipython-8.20.0.tar.gz", hash = "sha256:2f21bd3fc1d51550c89ee3944ae04bbc7bc79e129ea0937da6e6c68bfdbf117a"},
@@ -1139,6 +1225,8 @@ version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
@@ -1153,6 +1241,8 @@ version = "0.19.1"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
@@ -1172,6 +1262,8 @@ version = "3.1.3"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
@@ -1189,6 +1281,8 @@ version = "3.0.2"
description = "Python library for serializing any arbitrary object graph into JSON"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"},
{file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"},
@@ -1205,6 +1299,8 @@ version = "1.4.5"
description = "A fast implementation of the Cassowary constraint solver"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"},
{file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"},
@@ -1318,6 +1414,8 @@ version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
@@ -1387,6 +1485,8 @@ version = "3.8.2"
description = "Python plotting package"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7"},
{file = "matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367"},
@@ -1435,6 +1535,8 @@ version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.5"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
{file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
@@ -1449,6 +1551,8 @@ version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
@@ -1460,6 +1564,8 @@ version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
@@ -1559,6 +1665,8 @@ version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -1570,6 +1678,8 @@ version = "3.2.1"
description = "Python package for creating and manipulating graphs and networks"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"},
{file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"},
@@ -1588,6 +1698,8 @@ version = "1.8.0"
description = "Node.js virtual environment builder"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
@@ -1602,6 +1714,8 @@ version = "1.26.3"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"},
{file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"},
@@ -1647,6 +1761,8 @@ version = "4.1.3"
description = "OAuth 2.0 client library"
optional = false
python-versions = "*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"},
{file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"},
@@ -1665,6 +1781,8 @@ version = "3.2.2"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
{file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
@@ -1681,6 +1799,8 @@ version = "1.7.2"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.7.1"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "openai-1.7.2-py3-none-any.whl", hash = "sha256:8f41b90a762f5fd9d182b45851041386fed94c8ad240a70abefee61a68e0ef53"},
{file = "openai-1.7.2.tar.gz", hash = "sha256:c73c78878258b07f1b468b0602c6591f25a1478f49ecb90b9bd44b7cc80bce73"},
@@ -1704,6 +1824,8 @@ version = "1.3.0.post0"
description = "Capture the outcome of Python function calls."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"},
{file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"},
@@ -1718,6 +1840,8 @@ version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
@@ -1729,6 +1853,8 @@ version = "2.1.4"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"},
{file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"},
@@ -1797,6 +1923,8 @@ version = "0.8.3"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
@@ -1812,6 +1940,8 @@ version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
@@ -1823,6 +1953,8 @@ version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
@@ -1837,6 +1969,8 @@ version = "10.2.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
@@ -1922,6 +2056,8 @@ version = "4.1.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
{file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
@@ -1937,6 +2073,8 @@ version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
@@ -1952,6 +2090,8 @@ version = "3.6.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"},
{file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"},
@@ -1970,6 +2110,8 @@ version = "3.0.43"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"},
{file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"},
@@ -1984,6 +2126,8 @@ version = "5.9.7"
description = "Cross-platform lib for process and system monitoring in Python."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "psutil-5.9.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6"},
{file = "psutil-5.9.7-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056"},
@@ -2012,6 +2156,8 @@ version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
@@ -2023,6 +2169,8 @@ version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
@@ -2037,6 +2185,8 @@ version = "0.5.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"},
{file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"},
@@ -2048,6 +2198,8 @@ version = "0.3.0"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"},
{file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"},
@@ -2062,6 +2214,8 @@ version = "2.11.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
{file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
@@ -2073,6 +2227,8 @@ version = "2.21"
description = "C parser in Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
markers = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"
files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
@@ -2084,6 +2240,8 @@ version = "2.7.4"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
{file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
@@ -2103,6 +2261,8 @@ version = "2.18.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
@@ -2194,6 +2354,8 @@ version = "2.3.4"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"},
{file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"},
@@ -2213,6 +2375,8 @@ version = "3.2.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
@@ -2224,6 +2388,8 @@ version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
{file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
@@ -2239,6 +2405,8 @@ version = "3.1.1"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.6.8"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
@@ -2253,6 +2421,8 @@ version = "1.1.366"
description = "Command line wrapper for pyright"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyright-1.1.366-py3-none-any.whl", hash = "sha256:c09e73ccc894976bcd6d6a5784aa84d724dbd9ceb7b873b39d475ca61c2de071"},
{file = "pyright-1.1.366.tar.gz", hash = "sha256:10e4d60be411f6d960cd39b0b58bf2ff76f2c83b9aeb102ffa9d9fda2e1303cb"},
@@ -2271,6 +2441,8 @@ version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
@@ -2283,6 +2455,8 @@ version = "7.4.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
@@ -2305,6 +2479,8 @@ version = "0.23.7"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
@@ -2323,6 +2499,8 @@ version = "5.0.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
@@ -2341,6 +2519,8 @@ version = "2.8.2"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
@@ -2355,6 +2535,8 @@ version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
@@ -2369,6 +2551,8 @@ version = "0.0.7"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "python_multipart-0.0.7-py3-none-any.whl", hash = "sha256:b1fef9a53b74c795e2347daac8c54b252d9e0df9c619712691c1cc8021bd3c49"},
{file = "python_multipart-0.0.7.tar.gz", hash = "sha256:288a6c39b06596c1b988bb6794c6fbc80e6c369e35e5062637df256bee0c9af9"},
@@ -2383,6 +2567,8 @@ version = "2023.3.post1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
@@ -2394,6 +2580,8 @@ version = "0.3.2"
description = "A Python network graph visualization library"
optional = false
python-versions = ">3.6"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pyvis-0.3.2-py3-none-any.whl", hash = "sha256:5720c4ca8161dc5d9ab352015723abb7a8bb8fb443edeb07f7a322db34a97555"},
]
@@ -2410,6 +2598,8 @@ version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
@@ -2470,6 +2660,8 @@ version = "2.31.0"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
@@ -2491,6 +2683,8 @@ version = "1.3.1"
description = "OAuthlib authentication support for Requests."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
{file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
@@ -2509,6 +2703,8 @@ version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
@@ -2523,6 +2719,8 @@ version = "4.16.0"
description = ""
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "selenium-4.16.0-py3-none-any.whl", hash = "sha256:aec71f4e6ed6cb3ec25c9c1b5ed56ae31b6da0a7f17474c7566d303f84e6219f"},
{file = "selenium-4.16.0.tar.gz", hash = "sha256:b2e987a445306151f7be0e6dfe2aa72a479c2ac6a91b9d5ef2d6dd4e49ad0435"},
@@ -2540,6 +2738,8 @@ version = "69.0.3"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
@@ -2556,6 +2756,8 @@ version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -2567,6 +2769,8 @@ version = "5.0.1"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
{file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
@@ -2578,6 +2782,8 @@ version = "1.3.0"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
@@ -2589,6 +2795,8 @@ version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
@@ -2600,6 +2808,8 @@ version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
@@ -2619,6 +2829,8 @@ version = "0.36.3"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
{file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
@@ -2636,6 +2848,8 @@ version = "0.9.0"
description = "Pretty-print tabular data"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
{file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
@@ -2650,6 +2864,8 @@ version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -2661,6 +2877,8 @@ version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -2672,6 +2890,8 @@ version = "4.66.1"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
@@ -2692,6 +2912,8 @@ version = "5.14.1"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"},
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"},
@@ -2707,6 +2929,8 @@ version = "0.24.0"
description = "A friendly Python library for async concurrency and I/O"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c"},
{file = "trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d"},
@@ -2727,6 +2951,8 @@ version = "0.11.1"
description = "WebSocket library for Trio"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"},
{file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"},
@@ -2743,10 +2969,12 @@ version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
markers = {main = "python_version <= \"3.11\" or python_version >= \"3.12\"", dev = "python_version < \"3.11\""}
[[package]]
name = "tzdata"
@@ -2754,6 +2982,8 @@ version = "2023.4"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
@@ -2765,6 +2995,8 @@ version = "1.26.18"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
{file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
@@ -2784,6 +3016,8 @@ version = "0.23.2"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"},
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
@@ -2803,6 +3037,8 @@ version = "20.25.0"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
@@ -2823,6 +3059,8 @@ version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
@@ -2834,6 +3072,8 @@ version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"},
{file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"},
@@ -2848,6 +3088,8 @@ version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
@@ -2946,6 +3188,6 @@ idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = "^3.10"
content-hash = "26bd75befe5223095b65be293086edf52f34f9043e49107c80a105dc0387dd6a"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -277,6 +277,16 @@ Naturally, to add an authenticated block for a new provider, you'll have to add
```
</details>
#### Multiple credentials inputs
Multiple credentials inputs are supported, under the following conditions:
- The name of each of the credentials input fields must end with `_credentials`.
- The names of the credentials input fields must match the names of the corresponding
parameters on the `run(..)` method of the block.
- If more than one of the credentials parameters are required, `test_credentials`
is a `dict[str, Credentials]`, with for each required credentials input the
parameter name as the key and suitable test credentials as the value.
#### Adding an OAuth2 service integration
To add support for a new OAuth2-authenticated service, you'll need to add an `OAuthHandler`.