Merge branch 'dev' of github.com:Significant-Gravitas/AutoGPT into fix/copilot-p0-cli-internals

This commit is contained in:
Zamil Majdy
2026-04-03 13:07:01 +02:00
2351 changed files with 48619 additions and 822116 deletions

10
.claude/settings.json Normal file
View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allowedTools": [
"Read", "Grep", "Glob",
"Bash(ls:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(find:*)",
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", "Bash(git worktree:*)",
"Bash(tmux:*)", "Bash(sleep:*)", "Bash(branchlet:*)"
]
}
}

View File

@@ -6,11 +6,19 @@ on:
paths:
- '.github/workflows/classic-autogpt-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/direct_benchmark/**'
- 'classic/forge/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
pull_request:
branches: [ master, dev, release-* ]
paths:
- '.github/workflows/classic-autogpt-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/direct_benchmark/**'
- 'classic/forge/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
concurrency:
group: ${{ format('classic-autogpt-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -19,47 +27,22 @@ concurrency:
defaults:
run:
shell: bash
working-directory: classic/original_autogpt
working-directory: classic
jobs:
test:
permissions:
contents: read
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
runs-on: ubuntu-latest
steps:
# Quite slow on macOS (2~4 minutes to set up Docker)
# - name: Set up Docker (macOS)
# if: runner.os == 'macOS'
# uses: crazy-max/ghaction-setup-docker@v3
- name: Start MinIO service (Linux)
if: runner.os == 'Linux'
- name: Start MinIO service
working-directory: '.'
run: |
docker pull minio/minio:edge-cicd
docker run -d -p 9000:9000 minio/minio:edge-cicd
- name: Start MinIO service (macOS)
if: runner.os == 'macOS'
working-directory: ${{ runner.temp }}
run: |
brew install minio/stable/minio
mkdir data
minio server ./data &
# No MinIO on Windows:
# - Windows doesn't support running Linux Docker containers
# - It doesn't seem possible to start background processes on Windows. They are
# killed after the step returns.
# See: https://github.com/actions/runner/issues/598#issuecomment-2011890429
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -71,41 +54,23 @@ jobs:
git config --global user.name "Auto-GPT-Bot"
git config --global user.email "github-bot@agpt.co"
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: "3.12"
- id: get_date
name: Get date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Set up Python dependency cache
# On Windows, unpacking cached dependencies takes longer than just installing them
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
key: poetry-${{ runner.os }}-${{ hashFiles('classic/original_autogpt/poetry.lock') }}
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry (Unix)
if: runner.os != 'Windows'
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 Poetry (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
$env:PATH += ";$env:APPDATA\Python\Scripts"
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Install Python dependencies
run: poetry install
@@ -116,12 +81,13 @@ jobs:
--cov=autogpt --cov-branch --cov-report term-missing --cov-report xml \
--numprocesses=logical --durations=10 \
--junitxml=junit.xml -o junit_family=legacy \
tests/unit tests/integration
original_autogpt/tests/unit original_autogpt/tests/integration
env:
CI: true
PLAIN_OUTPUT: True
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
S3_ENDPOINT_URL: ${{ runner.os != 'Windows' && 'http://127.0.0.1:9000' || '' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
S3_ENDPOINT_URL: http://127.0.0.1:9000
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
@@ -135,11 +101,11 @@ jobs:
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: autogpt-agent,${{ runner.os }}
flags: autogpt-agent
- name: Upload logs to artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: test-logs
path: classic/original_autogpt/logs/
path: classic/logs/

View File

@@ -148,7 +148,7 @@ jobs:
--entrypoint poetry ${{ env.IMAGE_NAME }} run \
pytest -v --cov=autogpt --cov-branch --cov-report term-missing \
--numprocesses=4 --durations=10 \
tests/unit tests/integration 2>&1 | tee test_output.txt
original_autogpt/tests/unit original_autogpt/tests/integration 2>&1 | tee test_output.txt
test_failure=${PIPESTATUS[0]}

View File

@@ -10,10 +10,9 @@ on:
- '.github/workflows/classic-autogpts-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- 'classic/benchmark/**'
- 'classic/run'
- 'classic/cli.py'
- 'classic/setup.py'
- 'classic/direct_benchmark/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
- '!**/*.md'
pull_request:
branches: [ master, dev, release-* ]
@@ -21,10 +20,9 @@ on:
- '.github/workflows/classic-autogpts-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- 'classic/benchmark/**'
- 'classic/run'
- 'classic/cli.py'
- 'classic/setup.py'
- 'classic/direct_benchmark/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
- '!**/*.md'
defaults:
@@ -35,13 +33,9 @@ defaults:
jobs:
serve-agent-protocol:
runs-on: ubuntu-latest
strategy:
matrix:
agent-name: [ original_autogpt ]
fail-fast: false
timeout-minutes: 20
env:
min-python-version: '3.10'
min-python-version: '3.12'
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -55,22 +49,22 @@ jobs:
python-version: ${{ env.min-python-version }}
- name: Install Poetry
working-directory: ./classic/${{ matrix.agent-name }}/
run: |
curl -sSL https://install.python-poetry.org | python -
- name: Run regression tests
- name: Install dependencies
run: poetry install
- name: Run smoke tests with direct-benchmark
run: |
./run agent start ${{ matrix.agent-name }}
cd ${{ matrix.agent-name }}
poetry run agbenchmark --mock --test=BasicRetrieval --test=Battleship --test=WebArenaTask_0
poetry run agbenchmark --test=WriteFile
poetry run direct-benchmark run \
--strategies one_shot \
--models claude \
--tests ReadFile,WriteFile \
--json
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
AGENT_NAME: ${{ matrix.agent-name }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
HELICONE_CACHE_ENABLED: false
HELICONE_PROPERTY_AGENT: ${{ matrix.agent-name }}
REPORTS_FOLDER: ${{ format('../../reports/{0}', matrix.agent-name) }}
TELEMETRY_ENVIRONMENT: autogpt-ci
TELEMETRY_OPT_IN: ${{ github.ref_name == 'master' }}
NONINTERACTIVE_MODE: "true"
CI: true

View File

@@ -1,18 +1,24 @@
name: Classic - AGBenchmark CI
name: Classic - Direct Benchmark CI
on:
push:
branches: [ master, dev, ci-test* ]
paths:
- 'classic/benchmark/**'
- '!classic/benchmark/reports/**'
- 'classic/direct_benchmark/**'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- .github/workflows/classic-benchmark-ci.yml
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
pull_request:
branches: [ master, dev, release-* ]
paths:
- 'classic/benchmark/**'
- '!classic/benchmark/reports/**'
- 'classic/direct_benchmark/**'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- .github/workflows/classic-benchmark-ci.yml
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
concurrency:
group: ${{ format('benchmark-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -23,95 +29,16 @@ defaults:
shell: bash
env:
min-python-version: '3.10'
min-python-version: '3.12'
jobs:
test:
permissions:
contents: read
benchmark-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
defaults:
run:
shell: bash
working-directory: classic/benchmark
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: Set up Python dependency cache
# On Windows, unpacking cached dependencies takes longer than just installing them
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
key: poetry-${{ runner.os }}-${{ hashFiles('classic/benchmark/poetry.lock') }}
- name: Install Poetry (Unix)
if: runner.os != 'Windows'
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 Poetry (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
$env:PATH += ";$env:APPDATA\Python\Scripts"
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
- name: Install Python dependencies
run: poetry install
- name: Run pytest with coverage
run: |
poetry run pytest -vv \
--cov=agbenchmark --cov-branch --cov-report term-missing --cov-report xml \
--durations=10 \
--junitxml=junit.xml -o junit_family=legacy \
tests
env:
CI: true
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }} # Run even if tests fail
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: agbenchmark,${{ runner.os }}
self-test-with-agent:
runs-on: ubuntu-latest
strategy:
matrix:
agent-name: [forge]
fail-fast: false
timeout-minutes: 20
working-directory: classic
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -124,53 +51,120 @@ jobs:
with:
python-version: ${{ env.min-python-version }}
- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python -
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
- name: Run basic benchmark tests
run: |
echo "Testing ReadFile challenge with one_shot strategy..."
poetry run direct-benchmark run \
--fresh \
--strategies one_shot \
--models claude \
--tests ReadFile \
--json
echo "Testing WriteFile challenge..."
poetry run direct-benchmark run \
--fresh \
--strategies one_shot \
--models claude \
--tests WriteFile \
--json
env:
CI: true
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NONINTERACTIVE_MODE: "true"
- name: Test category filtering
run: |
echo "Testing coding category..."
poetry run direct-benchmark run \
--fresh \
--strategies one_shot \
--models claude \
--categories coding \
--tests ReadFile,WriteFile \
--json
env:
CI: true
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NONINTERACTIVE_MODE: "true"
- name: Test multiple strategies
run: |
echo "Testing multiple strategies..."
poetry run direct-benchmark run \
--fresh \
--strategies one_shot,plan_execute \
--models claude \
--tests ReadFile \
--parallel 2 \
--json
env:
CI: true
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NONINTERACTIVE_MODE: "true"
# Run regression tests on maintain challenges
regression-tests:
runs-on: ubuntu-latest
timeout-minutes: 45
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
defaults:
run:
shell: bash
working-directory: classic
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Set up Python ${{ env.min-python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.min-python-version }}
- name: Set up Python dependency cache
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
- name: Run regression tests
working-directory: classic
run: |
./run agent start ${{ matrix.agent-name }}
cd ${{ matrix.agent-name }}
set +e # Ignore non-zero exit codes and continue execution
echo "Running the following command: poetry run agbenchmark --maintain --mock"
poetry run agbenchmark --maintain --mock
EXIT_CODE=$?
set -e # Stop ignoring non-zero exit codes
# Check if the exit code was 5, and if so, exit with 0 instead
if [ $EXIT_CODE -eq 5 ]; then
echo "regression_tests.json is empty."
fi
echo "Running the following command: poetry run agbenchmark --mock"
poetry run agbenchmark --mock
echo "Running the following command: poetry run agbenchmark --mock --category=data"
poetry run agbenchmark --mock --category=data
echo "Running the following command: poetry run agbenchmark --mock --category=coding"
poetry run agbenchmark --mock --category=coding
# echo "Running the following command: poetry run agbenchmark --test=WriteFile"
# poetry run agbenchmark --test=WriteFile
cd ../benchmark
poetry install
echo "Adding the BUILD_SKILL_TREE environment variable. This will attempt to add new elements in the skill tree. If new elements are added, the CI fails because they should have been pushed"
export BUILD_SKILL_TREE=true
# poetry run agbenchmark --mock
# CHANGED=$(git diff --name-only | grep -E '(agbenchmark/challenges)|(../classic/frontend/assets)') || echo "No diffs"
# if [ ! -z "$CHANGED" ]; then
# echo "There are unstaged changes please run agbenchmark and commit those changes since they are needed."
# echo "$CHANGED"
# exit 1
# else
# echo "No unstaged changes."
# fi
echo "Running regression tests (previously beaten challenges)..."
poetry run direct-benchmark run \
--fresh \
--strategies one_shot \
--models claude \
--maintain \
--parallel 4 \
--json
env:
CI: true
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
TELEMETRY_ENVIRONMENT: autogpt-benchmark-ci
TELEMETRY_OPT_IN: ${{ github.ref_name == 'master' }}
NONINTERACTIVE_MODE: "true"

View File

@@ -6,13 +6,15 @@ on:
paths:
- '.github/workflows/classic-forge-ci.yml'
- 'classic/forge/**'
- '!classic/forge/tests/vcr_cassettes'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
pull_request:
branches: [ master, dev, release-* ]
paths:
- '.github/workflows/classic-forge-ci.yml'
- 'classic/forge/**'
- '!classic/forge/tests/vcr_cassettes'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
concurrency:
group: ${{ format('forge-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
@@ -21,131 +23,60 @@ concurrency:
defaults:
run:
shell: bash
working-directory: classic/forge
working-directory: classic
jobs:
test:
permissions:
contents: read
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
runs-on: ubuntu-latest
steps:
# Quite slow on macOS (2~4 minutes to set up Docker)
# - name: Set up Docker (macOS)
# if: runner.os == 'macOS'
# uses: crazy-max/ghaction-setup-docker@v3
- name: Start MinIO service (Linux)
if: runner.os == 'Linux'
- name: Start MinIO service
working-directory: '.'
run: |
docker pull minio/minio:edge-cicd
docker run -d -p 9000:9000 minio/minio:edge-cicd
- name: Start MinIO service (macOS)
if: runner.os == 'macOS'
working-directory: ${{ runner.temp }}
run: |
brew install minio/stable/minio
mkdir data
minio server ./data &
# No MinIO on Windows:
# - Windows doesn't support running Linux Docker containers
# - It doesn't seem possible to start background processes on Windows. They are
# killed after the step returns.
# See: https://github.com/actions/runner/issues/598#issuecomment-2011890429
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Checkout cassettes
if: ${{ startsWith(github.event_name, 'pull_request') }}
env:
PR_BASE: ${{ github.event.pull_request.base.ref }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
cassette_base_branch="${PR_BASE}"
cd tests/vcr_cassettes
if ! git ls-remote --exit-code --heads origin $cassette_base_branch ; then
cassette_base_branch="master"
fi
if git ls-remote --exit-code --heads origin $cassette_branch ; then
git fetch origin $cassette_branch
git fetch origin $cassette_base_branch
git checkout $cassette_branch
# Pick non-conflicting cassette updates from the base branch
git merge --no-commit --strategy-option=ours origin/$cassette_base_branch
echo "Using cassettes from mirror branch '$cassette_branch'," \
"synced to upstream branch '$cassette_base_branch'."
else
git checkout -b $cassette_branch
echo "Branch '$cassette_branch' does not exist in cassette submodule." \
"Using cassettes from '$cassette_base_branch'."
fi
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: "3.12"
- name: Set up Python dependency cache
# On Windows, unpacking cached dependencies takes longer than just installing them
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
key: poetry-${{ runner.os }}-${{ hashFiles('classic/forge/poetry.lock') }}
path: ~/.cache/pypoetry
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry (Unix)
if: runner.os != 'Windows'
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 Poetry (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
$env:PATH += ";$env:APPDATA\Python\Scripts"
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Install Python dependencies
run: poetry install
- name: Install Playwright browsers
run: poetry run playwright install chromium
- name: Run pytest with coverage
run: |
poetry run pytest -vv \
--cov=forge --cov-branch --cov-report term-missing --cov-report xml \
--durations=10 \
--junitxml=junit.xml -o junit_family=legacy \
forge
forge/forge forge/tests
env:
CI: true
PLAIN_OUTPUT: True
# API keys - tests that need these will skip if not available
# Secrets are not available to fork PRs (GitHub security feature)
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
S3_ENDPOINT_URL: ${{ runner.os != 'Windows' && 'http://127.0.0.1:9000' || '' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
S3_ENDPOINT_URL: http://127.0.0.1:9000
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
@@ -159,85 +90,11 @@ jobs:
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: forge,${{ runner.os }}
- id: setup_git_auth
name: Set up git token authentication
# Cassettes may be pushed even when tests fail
if: success() || failure()
run: |
config_key="http.${{ github.server_url }}/.extraheader"
if [ "${{ runner.os }}" = 'macOS' ]; then
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64)
else
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64 -w0)
fi
git config "$config_key" \
"Authorization: Basic $base64_pat"
cd tests/vcr_cassettes
git config "$config_key" \
"Authorization: Basic $base64_pat"
echo "config_key=$config_key" >> $GITHUB_OUTPUT
- id: push_cassettes
name: Push updated cassettes
# For pull requests, push updated cassettes even when tests fail
if: github.event_name == 'push' || (! github.event.pull_request.head.repo.fork && (success() || failure()))
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [ "${{ startsWith(github.event_name, 'pull_request') }}" = "true" ]; then
is_pull_request=true
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
else
cassette_branch="${{ github.ref_name }}"
fi
cd tests/vcr_cassettes
# Commit & push changes to cassettes if any
if ! git diff --quiet; then
git add .
git commit -m "Auto-update cassettes"
git push origin HEAD:$cassette_branch
if [ ! $is_pull_request ]; then
cd ../..
git add tests/vcr_cassettes
git commit -m "Update cassette submodule"
git push origin HEAD:$cassette_branch
fi
echo "updated=true" >> $GITHUB_OUTPUT
else
echo "updated=false" >> $GITHUB_OUTPUT
echo "No cassette changes to commit"
fi
- name: Post Set up git token auth
if: steps.setup_git_auth.outcome == 'success'
run: |
git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
git submodule foreach git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
- name: Apply "behaviour change" label and comment on PR
if: ${{ startsWith(github.event_name, 'pull_request') }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
TOKEN="${{ secrets.PAT_REVIEW }}"
REPO="${{ github.repository }}"
if [[ "${{ steps.push_cassettes.outputs.updated }}" == "true" ]]; then
echo "Adding label and comment..."
echo $TOKEN | gh auth login --with-token
gh issue edit $PR_NUMBER --add-label "behaviour change"
gh issue comment $PR_NUMBER --body "You changed AutoGPT's behaviour on ${{ runner.os }}. The cassettes have been updated and will be merged to the submodule when this Pull Request gets merged."
fi
flags: forge
- name: Upload logs to artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: test-logs
path: classic/forge/logs/
path: classic/logs/

View File

@@ -1,60 +0,0 @@
name: Classic - Frontend CI/CD
on:
push:
branches:
- master
- dev
- 'ci-test*' # This will match any branch that starts with "ci-test"
paths:
- 'classic/frontend/**'
- '.github/workflows/classic-frontend-ci.yml'
pull_request:
paths:
- 'classic/frontend/**'
- '.github/workflows/classic-frontend-ci.yml'
jobs:
build:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
env:
BUILD_BRANCH: ${{ format('classic-frontend-build/{0}', github.ref_name) }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.13.2'
- name: Build Flutter to Web
run: |
cd classic/frontend
flutter build web --base-href /app/
# - name: Commit and Push to ${{ env.BUILD_BRANCH }}
# if: github.event_name == 'push'
# run: |
# git config --local user.email "action@github.com"
# git config --local user.name "GitHub Action"
# git add classic/frontend/build/web
# git checkout -B ${{ env.BUILD_BRANCH }}
# git commit -m "Update frontend build to ${GITHUB_SHA:0:7}" -a
# git push -f origin ${{ env.BUILD_BRANCH }}
- name: Create PR ${{ env.BUILD_BRANCH }} -> ${{ github.ref_name }}
if: github.event_name == 'push'
uses: peter-evans/create-pull-request@v8
with:
add-paths: classic/frontend/build/web
base: ${{ github.ref_name }}
branch: ${{ env.BUILD_BRANCH }}
delete-branch: true
title: "Update frontend build in `${{ github.ref_name }}`"
body: "This PR updates the frontend build based on commit ${{ github.sha }}."
commit-message: "Update frontend build based on commit ${{ github.sha }}"

View File

@@ -7,7 +7,9 @@ on:
- '.github/workflows/classic-python-checks-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- 'classic/benchmark/**'
- 'classic/direct_benchmark/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
- '**.py'
- '!classic/forge/tests/vcr_cassettes'
pull_request:
@@ -16,7 +18,9 @@ on:
- '.github/workflows/classic-python-checks-ci.yml'
- 'classic/original_autogpt/**'
- 'classic/forge/**'
- 'classic/benchmark/**'
- 'classic/direct_benchmark/**'
- 'classic/pyproject.toml'
- 'classic/poetry.lock'
- '**.py'
- '!classic/forge/tests/vcr_cassettes'
@@ -27,44 +31,13 @@ concurrency:
defaults:
run:
shell: bash
working-directory: classic
jobs:
get-changed-parts:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- id: changes-in
name: Determine affected subprojects
uses: dorny/paths-filter@v3
with:
filters: |
original_autogpt:
- classic/original_autogpt/autogpt/**
- classic/original_autogpt/tests/**
- classic/original_autogpt/poetry.lock
forge:
- classic/forge/forge/**
- classic/forge/tests/**
- classic/forge/poetry.lock
benchmark:
- classic/benchmark/agbenchmark/**
- classic/benchmark/tests/**
- classic/benchmark/poetry.lock
outputs:
changed-parts: ${{ steps.changes-in.outputs.changes }}
lint:
needs: get-changed-parts
runs-on: ubuntu-latest
env:
min-python-version: "3.10"
strategy:
matrix:
sub-package: ${{ fromJson(needs.get-changed-parts.outputs.changed-parts) }}
fail-fast: false
min-python-version: "3.12"
steps:
- name: Checkout repository
@@ -81,42 +54,31 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles(format('{0}/poetry.lock', matrix.sub-package)) }}
key: ${{ runner.os }}-poetry-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
# Install dependencies
- name: Install Python dependencies
run: poetry -C classic/${{ matrix.sub-package }} install
run: poetry install
# Lint
- name: Lint (isort)
run: poetry run isort --check .
working-directory: classic/${{ matrix.sub-package }}
- name: Lint (Black)
if: success() || failure()
run: poetry run black --check .
working-directory: classic/${{ matrix.sub-package }}
- name: Lint (Flake8)
if: success() || failure()
run: poetry run flake8 .
working-directory: classic/${{ matrix.sub-package }}
types:
needs: get-changed-parts
runs-on: ubuntu-latest
env:
min-python-version: "3.10"
strategy:
matrix:
sub-package: ${{ fromJson(needs.get-changed-parts.outputs.changed-parts) }}
fail-fast: false
min-python-version: "3.12"
steps:
- name: Checkout repository
@@ -133,19 +95,16 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles(format('{0}/poetry.lock', matrix.sub-package)) }}
key: ${{ runner.os }}-poetry-${{ hashFiles('classic/poetry.lock') }}
- name: Install Poetry
run: curl -sSL https://install.python-poetry.org | python3 -
# Install dependencies
- name: Install Python dependencies
run: poetry -C classic/${{ matrix.sub-package }} install
run: poetry install
# Typecheck
- name: Typecheck
if: success() || failure()
run: poetry run pyright
working-directory: classic/${{ matrix.sub-package }}

View File

@@ -269,12 +269,14 @@ jobs:
DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
DIRECT_URL: ${{ steps.supabase.outputs.DB_URL }}
- name: Run pytest
- 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
poetry run pytest -s -vv -o log_cli=true -o log_cli_level=DEBUG \
--cov=backend --cov-branch --cov-report term-missing --cov-report xml
else
poetry run pytest -s -vv
poetry run pytest -s -vv \
--cov=backend --cov-branch --cov-report term-missing --cov-report xml
fi
env:
LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
@@ -287,11 +289,13 @@ jobs:
REDIS_PORT: "6379"
ENCRYPTION_KEY: "dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw=" # DO NOT USE IN PRODUCTION!!
# - name: Upload coverage reports to Codecov
# uses: codecov/codecov-action@v4
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# flags: backend,${{ runner.os }}
- name: Upload coverage reports to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: platform-backend
files: ./autogpt_platform/backend/coverage.xml
env:
CI: true

View File

@@ -148,3 +148,11 @@ jobs:
- name: Run Integration Tests
run: pnpm test:unit
- name: Upload coverage reports to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: platform-frontend
files: ./autogpt_platform/frontend/coverage/cobertura-coverage.xml

9
.gitignore vendored
View File

@@ -3,6 +3,7 @@
classic/original_autogpt/keys.py
classic/original_autogpt/*.json
auto_gpt_workspace/*
.autogpt/
*.mpeg
.env
# Root .env files
@@ -159,6 +160,10 @@ CURRENT_BULLETIN.md
# AgBenchmark
classic/benchmark/agbenchmark/reports/
classic/reports/
classic/direct_benchmark/reports/
classic/.benchmark_workspaces/
classic/direct_benchmark/.benchmark_workspaces/
# Nodejs
package-lock.json
@@ -177,9 +182,13 @@ autogpt_platform/backend/settings.py
*.ign.*
.test-contents
**/.claude/settings.local.json
.claude/settings.local.json
CLAUDE.local.md
/autogpt_platform/backend/logs
# Test database
test.db
.next
# Implementation plans (generated by AI agents)
plans/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "classic/forge/tests/vcr_cassettes"]
path = classic/forge/tests/vcr_cassettes
url = https://github.com/Significant-Gravitas/Auto-GPT-test-cassettes

View File

@@ -84,51 +84,16 @@ repos:
stages: [pre-commit, post-checkout]
- id: poetry-install
name: Check & Install dependencies - Classic - AutoGPT
alias: poetry-install-classic-autogpt
name: Check & Install dependencies - Classic
alias: poetry-install-classic
entry: >
bash -c '
if [ -n "$PRE_COMMIT_FROM_REF" ]; then
git diff --name-only "$PRE_COMMIT_FROM_REF" "$PRE_COMMIT_TO_REF"
else
git diff --cached --name-only
fi | grep -qE "^classic/(original_autogpt|forge)/poetry\.lock$" || exit 0;
poetry -C classic/original_autogpt install
'
# include forge source (since it's a path dependency)
always_run: true
language: system
pass_filenames: false
stages: [pre-commit, post-checkout]
- id: poetry-install
name: Check & Install dependencies - Classic - Forge
alias: poetry-install-classic-forge
entry: >
bash -c '
if [ -n "$PRE_COMMIT_FROM_REF" ]; then
git diff --name-only "$PRE_COMMIT_FROM_REF" "$PRE_COMMIT_TO_REF"
else
git diff --cached --name-only
fi | grep -qE "^classic/forge/poetry\.lock$" || exit 0;
poetry -C classic/forge install
'
always_run: true
language: system
pass_filenames: false
stages: [pre-commit, post-checkout]
- id: poetry-install
name: Check & Install dependencies - Classic - Benchmark
alias: poetry-install-classic-benchmark
entry: >
bash -c '
if [ -n "$PRE_COMMIT_FROM_REF" ]; then
git diff --name-only "$PRE_COMMIT_FROM_REF" "$PRE_COMMIT_TO_REF"
else
git diff --cached --name-only
fi | grep -qE "^classic/benchmark/poetry\.lock$" || exit 0;
poetry -C classic/benchmark install
fi | grep -qE "^classic/poetry\.lock$" || exit 0;
poetry -C classic install
'
always_run: true
language: system
@@ -223,26 +188,10 @@ repos:
language: system
- id: isort
name: Lint (isort) - Classic - AutoGPT
alias: isort-classic-autogpt
entry: poetry -P classic/original_autogpt run isort -p autogpt
files: ^classic/original_autogpt/
types: [file, python]
language: system
- id: isort
name: Lint (isort) - Classic - Forge
alias: isort-classic-forge
entry: poetry -P classic/forge run isort -p forge
files: ^classic/forge/
types: [file, python]
language: system
- id: isort
name: Lint (isort) - Classic - Benchmark
alias: isort-classic-benchmark
entry: poetry -P classic/benchmark run isort -p agbenchmark
files: ^classic/benchmark/
name: Lint (isort) - Classic
alias: isort-classic
entry: bash -c 'cd classic && poetry run isort $(echo "$@" | sed "s|classic/||g")' --
files: ^classic/(original_autogpt|forge|direct_benchmark)/
types: [file, python]
language: system
@@ -256,26 +205,13 @@ repos:
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
# To have flake8 load the config of the individual subprojects, we have to call
# them separately.
# Use consolidated flake8 config at classic/.flake8
hooks:
- id: flake8
name: Lint (Flake8) - Classic - AutoGPT
alias: flake8-classic-autogpt
files: ^classic/original_autogpt/(autogpt|scripts|tests)/
args: [--config=classic/original_autogpt/.flake8]
- id: flake8
name: Lint (Flake8) - Classic - Forge
alias: flake8-classic-forge
files: ^classic/forge/(forge|tests)/
args: [--config=classic/forge/.flake8]
- id: flake8
name: Lint (Flake8) - Classic - Benchmark
alias: flake8-classic-benchmark
files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.]
args: [--config=classic/benchmark/.flake8]
name: Lint (Flake8) - Classic
alias: flake8-classic
files: ^classic/(original_autogpt|forge|direct_benchmark)/
args: [--config=classic/.flake8]
- repo: local
hooks:
@@ -311,29 +247,10 @@ repos:
pass_filenames: false
- id: pyright
name: Typecheck - Classic - AutoGPT
alias: pyright-classic-autogpt
entry: poetry -C classic/original_autogpt run pyright
# 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]
language: system
pass_filenames: false
- id: pyright
name: Typecheck - Classic - Forge
alias: pyright-classic-forge
entry: poetry -C classic/forge run pyright
files: ^classic/forge/(forge/|poetry\.lock$)
types: [file]
language: system
pass_filenames: false
- id: pyright
name: Typecheck - Classic - Benchmark
alias: pyright-classic-benchmark
entry: poetry -C classic/benchmark run pyright
files: ^classic/benchmark/(agbenchmark/|tests/|poetry\.lock$)
name: Typecheck - Classic
alias: pyright-classic
entry: poetry -C classic run pyright
files: ^classic/(original_autogpt|forge|direct_benchmark)/.*\.py$|^classic/poetry\.lock$
types: [file]
language: system
pass_filenames: false
@@ -360,26 +277,9 @@ repos:
# pass_filenames: false
# - id: pytest
# name: Run tests - Classic - AutoGPT (excl. slow tests)
# alias: pytest-classic-autogpt
# entry: bash -c 'cd classic/original_autogpt && poetry run pytest --cov=autogpt -m "not slow" tests/unit tests/integration'
# # include forge source (since it's a path dependency) but exclude *_test.py files:
# files: ^(classic/original_autogpt/((autogpt|tests)/|poetry\.lock$)|classic/forge/(forge/.*(?<!_test)\.py|poetry\.lock)$)
# language: system
# pass_filenames: false
# - id: pytest
# name: Run tests - Classic - Forge (excl. slow tests)
# alias: pytest-classic-forge
# entry: bash -c 'cd classic/forge && poetry run pytest --cov=forge -m "not slow"'
# files: ^classic/forge/(forge/|tests/|poetry\.lock$)
# language: system
# pass_filenames: false
# - id: pytest
# name: Run tests - Classic - Benchmark
# alias: pytest-classic-benchmark
# entry: bash -c 'cd classic/benchmark && poetry run pytest --cov=benchmark'
# files: ^classic/benchmark/(agbenchmark/|tests/|poetry\.lock$)
# name: Run tests - Classic (excl. slow tests)
# alias: pytest-classic
# entry: bash -c 'cd classic && poetry run pytest -m "not slow"'
# files: ^classic/(original_autogpt|forge|direct_benchmark)/
# language: system
# pass_filenames: false

View File

@@ -0,0 +1,61 @@
from unittest.mock import AsyncMock
import fastapi
import fastapi.testclient
import pytest
from backend.api.features.v1 import v1_router
app = fastapi.FastAPI()
app.include_router(v1_router)
client = fastapi.testclient.TestClient(app)
@pytest.fixture(autouse=True)
def setup_app_auth(mock_jwt_user):
from autogpt_libs.auth.jwt_utils import get_jwt_payload
app.dependency_overrides[get_jwt_payload] = mock_jwt_user["get_jwt_payload"]
yield
app.dependency_overrides.clear()
def test_onboarding_profile_success(mocker):
mock_extract = mocker.patch(
"backend.api.features.v1.extract_business_understanding",
new_callable=AsyncMock,
)
mock_upsert = mocker.patch(
"backend.api.features.v1.upsert_business_understanding",
new_callable=AsyncMock,
)
from backend.data.understanding import BusinessUnderstandingInput
mock_extract.return_value = BusinessUnderstandingInput.model_construct(
user_name="John",
user_role="Founder/CEO",
pain_points=["Finding leads"],
suggested_prompts={"Learn": ["How do I automate lead gen?"]},
)
mock_upsert.return_value = AsyncMock()
response = client.post(
"/onboarding/profile",
json={
"user_name": "John",
"user_role": "Founder/CEO",
"pain_points": ["Finding leads", "Email & outreach"],
},
)
assert response.status_code == 200
mock_extract.assert_awaited_once()
mock_upsert.assert_awaited_once()
def test_onboarding_profile_missing_fields():
response = client.post(
"/onboarding/profile",
json={"user_name": "John"},
)
assert response.status_code == 422

View File

@@ -63,12 +63,17 @@ from backend.data.onboarding import (
UserOnboardingUpdate,
complete_onboarding_step,
complete_re_run_agent,
format_onboarding_for_extraction,
get_recommended_agents,
get_user_onboarding,
onboarding_enabled,
reset_user_onboarding,
update_user_onboarding,
)
from backend.data.tally import extract_business_understanding
from backend.data.understanding import (
BusinessUnderstandingInput,
upsert_business_understanding,
)
from backend.data.user import (
get_or_create_user,
get_user_by_id,
@@ -282,35 +287,33 @@ async def get_onboarding_agents(
return await get_recommended_agents(user_id)
class OnboardingStatusResponse(pydantic.BaseModel):
"""Response for onboarding status check."""
class OnboardingProfileRequest(pydantic.BaseModel):
"""Request body for onboarding profile submission."""
is_onboarding_enabled: bool
is_chat_enabled: bool
user_name: str = pydantic.Field(min_length=1, max_length=100)
user_role: str = pydantic.Field(min_length=1, max_length=100)
pain_points: list[str] = pydantic.Field(default_factory=list, max_length=20)
class OnboardingStatusResponse(pydantic.BaseModel):
"""Response for onboarding completion check."""
is_completed: bool
@v1_router.get(
"/onboarding/enabled",
summary="Is onboarding enabled",
"/onboarding/completed",
summary="Check if onboarding is completed",
tags=["onboarding", "public"],
response_model=OnboardingStatusResponse,
dependencies=[Security(requires_user)],
)
async def is_onboarding_enabled(
async def is_onboarding_completed(
user_id: Annotated[str, Security(get_user_id)],
) -> OnboardingStatusResponse:
# Check if chat is enabled for user
is_chat_enabled = await is_feature_enabled(Flag.CHAT, user_id, False)
# If chat is enabled, skip legacy onboarding
if is_chat_enabled:
return OnboardingStatusResponse(
is_onboarding_enabled=False,
is_chat_enabled=True,
)
user_onboarding = await get_user_onboarding(user_id)
return OnboardingStatusResponse(
is_onboarding_enabled=await onboarding_enabled(),
is_chat_enabled=False,
is_completed=OnboardingStep.VISIT_COPILOT in user_onboarding.completedSteps,
)
@@ -325,6 +328,38 @@ async def reset_onboarding(user_id: Annotated[str, Security(get_user_id)]):
return await reset_user_onboarding(user_id)
@v1_router.post(
"/onboarding/profile",
summary="Submit onboarding profile",
tags=["onboarding"],
dependencies=[Security(requires_user)],
)
async def submit_onboarding_profile(
data: OnboardingProfileRequest,
user_id: Annotated[str, Security(get_user_id)],
):
formatted = format_onboarding_for_extraction(
user_name=data.user_name,
user_role=data.user_role,
pain_points=data.pain_points,
)
try:
understanding_input = await extract_business_understanding(formatted)
except Exception:
understanding_input = BusinessUnderstandingInput.model_construct()
# Ensure the direct fields are set even if LLM missed them
understanding_input.user_name = data.user_name
understanding_input.user_role = data.user_role
if not understanding_input.pain_points:
understanding_input.pain_points = data.pain_points
await upsert_business_understanding(user_id, understanding_input)
return {"status": "ok"}
########################################################
##################### Blocks ###########################
########################################################

View File

@@ -0,0 +1,323 @@
import asyncio
from typing import Any, Literal
from pydantic import SecretStr
from sqlalchemy.engine.url import URL
from sqlalchemy.exc import DBAPIError, OperationalError, ProgrammingError
from backend.blocks._base import (
Block,
BlockCategory,
BlockOutput,
BlockSchemaInput,
BlockSchemaOutput,
)
from backend.blocks.sql_query_helpers import (
_DATABASE_TYPE_DEFAULT_PORT,
_DATABASE_TYPE_TO_DRIVER,
DatabaseType,
_execute_query,
_sanitize_error,
_validate_query_is_read_only,
_validate_single_statement,
)
from backend.data.model import (
CredentialsField,
CredentialsMetaInput,
SchemaField,
UserPasswordCredentials,
)
from backend.integrations.providers import ProviderName
from backend.util.request import resolve_and_check_blocked
TEST_CREDENTIALS = UserPasswordCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="database",
username=SecretStr("test_user"),
password=SecretStr("test_pass"),
title="Mock Database credentials",
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}
DatabaseCredentials = UserPasswordCredentials
DatabaseCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.DATABASE],
Literal["user_password"],
]
def DatabaseCredentialsField() -> DatabaseCredentialsInput:
return CredentialsField(
description="Database username and password",
)
class SQLQueryBlock(Block):
class Input(BlockSchemaInput):
database_type: DatabaseType = SchemaField(
default=DatabaseType.POSTGRES,
description="Database engine",
advanced=False,
)
host: SecretStr = SchemaField(
description=(
"Database hostname or IP address. "
"Treated as a secret to avoid leaking infrastructure details. "
"Private/internal IPs are blocked (SSRF protection)."
),
placeholder="db.example.com",
secret=True,
)
port: int | None = SchemaField(
default=None,
description=(
"Database port (leave empty for default: "
"PostgreSQL: 5432, MySQL: 3306, MSSQL: 1433)"
),
ge=1,
le=65535,
)
database: str = SchemaField(
description="Name of the database to connect to",
placeholder="my_database",
)
query: str = SchemaField(
description="SQL query to execute",
placeholder="SELECT * FROM analytics.daily_active_users LIMIT 10",
)
read_only: bool = SchemaField(
default=True,
description=(
"When enabled (default), only SELECT queries are allowed "
"and the database session is set to read-only mode. "
"Disable to allow write operations (INSERT, UPDATE, DELETE, etc.)."
),
)
timeout: int = SchemaField(
default=30,
description="Query timeout in seconds (max 120)",
ge=1,
le=120,
)
max_rows: int = SchemaField(
default=1000,
description="Maximum number of rows to return (max 10000)",
ge=1,
le=10000,
)
credentials: DatabaseCredentialsInput = DatabaseCredentialsField()
class Output(BlockSchemaOutput):
results: list[dict[str, Any]] = SchemaField(
description="Query results as a list of row dictionaries"
)
columns: list[str] = SchemaField(
description="Column names from the query result"
)
row_count: int = SchemaField(description="Number of rows returned")
truncated: bool = SchemaField(
description=(
"True when the result set was capped by max_rows, "
"indicating additional rows exist in the database"
)
)
affected_rows: int = SchemaField(
description="Number of rows affected by a write query (INSERT/UPDATE/DELETE)"
)
error: str = SchemaField(description="Error message if the query failed")
def __init__(self):
super().__init__(
id="4dc35c0f-4fd8-465e-9616-5a216f1ba2bc",
description=(
"Execute a SQL query. Read-only by default for safety "
"-- disable to allow write operations. "
"Supports PostgreSQL, MySQL, and MSSQL via SQLAlchemy."
),
categories={BlockCategory.DATA},
input_schema=SQLQueryBlock.Input,
output_schema=SQLQueryBlock.Output,
test_input={
"query": "SELECT 1 AS test_col",
"database_type": DatabaseType.POSTGRES,
"host": "localhost",
"database": "test_db",
"timeout": 30,
"max_rows": 1000,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("results", [{"test_col": 1}]),
("columns", ["test_col"]),
("row_count", 1),
("truncated", False),
],
test_mock={
"execute_query": lambda *_args, **_kwargs: (
[{"test_col": 1}],
["test_col"],
-1,
False,
),
"check_host_allowed": lambda *_args, **_kwargs: ["127.0.0.1"],
},
)
@staticmethod
async def check_host_allowed(host: str) -> list[str]:
"""Validate that the given host is not a private/blocked address.
Returns the list of resolved IP addresses so the caller can pin the
connection to the validated IP (preventing DNS rebinding / TOCTOU).
Raises ValueError or OSError if the host is blocked.
Extracted as a method so it can be mocked during block tests.
"""
return await resolve_and_check_blocked(host)
@staticmethod
def execute_query(
connection_url: URL | str,
query: str,
timeout: int,
max_rows: int,
read_only: bool = True,
database_type: DatabaseType = DatabaseType.POSTGRES,
) -> tuple[list[dict[str, Any]], list[str], int, bool]:
"""Execute a SQL query and return (rows, columns, affected_rows, truncated).
Delegates to ``_execute_query`` in ``sql_query_helpers``.
Extracted as a method so it can be mocked during block tests.
"""
return _execute_query(
connection_url=connection_url,
query=query,
timeout=timeout,
max_rows=max_rows,
read_only=read_only,
database_type=database_type,
)
async def run(
self,
input_data: Input,
*,
credentials: DatabaseCredentials,
**_kwargs: Any,
) -> BlockOutput:
# Validate query structure and read-only constraints.
error = self._validate_query(input_data)
if error:
yield "error", error
return
# Validate host and resolve for SSRF protection.
host, pinned_host, error = await self._resolve_host(input_data)
if error:
yield "error", error
return
# Build connection URL and execute.
port = input_data.port or _DATABASE_TYPE_DEFAULT_PORT[input_data.database_type]
username = credentials.username.get_secret_value()
connection_url = URL.create(
drivername=_DATABASE_TYPE_TO_DRIVER[input_data.database_type],
username=username,
password=credentials.password.get_secret_value(),
host=pinned_host,
port=port,
database=input_data.database,
)
conn_str = connection_url.render_as_string(hide_password=True)
db_name = input_data.database
def _sanitize(err: Exception) -> str:
return _sanitize_error(
str(err).strip(),
conn_str,
host=pinned_host,
original_host=host,
username=username,
port=port,
database=db_name,
)
try:
results, columns, affected, truncated = await asyncio.to_thread(
self.execute_query,
connection_url=connection_url,
query=input_data.query,
timeout=input_data.timeout,
max_rows=input_data.max_rows,
read_only=input_data.read_only,
database_type=input_data.database_type,
)
yield "results", results
yield "columns", columns
yield "row_count", len(results)
yield "truncated", truncated
if affected >= 0:
yield "affected_rows", affected
except OperationalError as e:
yield (
"error",
self._classify_operational_error(
_sanitize(e),
input_data.timeout,
),
)
except ProgrammingError as e:
yield "error", f"SQL error: {_sanitize(e)}"
except DBAPIError as e:
yield "error", f"Database error: {_sanitize(e)}"
except ModuleNotFoundError:
yield (
"error",
(
f"Database driver not available for "
f"{input_data.database_type.value}. "
f"Please contact the platform administrator."
),
)
@staticmethod
def _validate_query(input_data: "SQLQueryBlock.Input") -> str | None:
"""Validate query structure and read-only constraints."""
stmt_error, parsed_stmt = _validate_single_statement(input_data.query)
if stmt_error:
return stmt_error
assert parsed_stmt is not None
if input_data.read_only:
return _validate_query_is_read_only(parsed_stmt)
return None
async def _resolve_host(
self, input_data: "SQLQueryBlock.Input"
) -> tuple[str, str, str | None]:
"""Validate and resolve the database host. Returns (host, pinned_ip, error)."""
host = input_data.host.get_secret_value().strip()
if not host:
return "", "", "Database host is required."
if host.startswith("/"):
return host, "", "Unix socket connections are not allowed."
try:
resolved_ips = await self.check_host_allowed(host)
except (ValueError, OSError) as e:
return host, "", f"Blocked host: {str(e).strip()}"
return host, resolved_ips[0], None
@staticmethod
def _classify_operational_error(sanitized_msg: str, timeout: int) -> str:
"""Classify an already-sanitized OperationalError for user display."""
lower = sanitized_msg.lower()
if "timeout" in lower or "cancel" in lower:
return f"Query timed out after {timeout}s."
if "connect" in lower:
return f"Failed to connect to database: {sanitized_msg}"
return f"Database error: {sanitized_msg}"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,430 @@
import re
from datetime import date, datetime, time
from decimal import Decimal
from enum import Enum
from typing import Any
import sqlparse
from sqlalchemy import create_engine, text
from sqlalchemy.engine.url import URL
class DatabaseType(str, Enum):
POSTGRES = "postgres"
MYSQL = "mysql"
MSSQL = "mssql"
# Defense-in-depth: reject queries containing data-modifying keywords.
# These are checked against parsed SQL tokens (not raw text) so column names
# and string literals do not cause false positives.
_DISALLOWED_KEYWORDS = {
"INSERT",
"UPDATE",
"DELETE",
"DROP",
"ALTER",
"CREATE",
"TRUNCATE",
"GRANT",
"REVOKE",
"COPY",
"EXECUTE",
"CALL",
"SET",
"RESET",
"DISCARD",
"NOTIFY",
"DO",
# MySQL file exfiltration: LOAD DATA LOCAL INFILE reads server/client files
"LOAD",
# MySQL REPLACE is INSERT-or-UPDATE; data modification
"REPLACE",
# ANSI MERGE (UPSERT) modifies data
"MERGE",
# MSSQL BULK INSERT loads external files into tables
"BULK",
# MSSQL EXEC / EXEC sp_name runs stored procedures (arbitrary code)
"EXEC",
}
# Map DatabaseType enum values to the expected SQLAlchemy driver prefix.
_DATABASE_TYPE_TO_DRIVER = {
DatabaseType.POSTGRES: "postgresql",
DatabaseType.MYSQL: "mysql+pymysql",
DatabaseType.MSSQL: "mssql+pymssql",
}
# Connection timeout in seconds passed to the DBAPI driver (connect_timeout /
# login_timeout). This bounds how long the driver waits to establish a TCP
# connection to the database server. It is separate from the per-statement
# timeout configured via SET commands inside _configure_session().
_CONNECT_TIMEOUT_SECONDS = 10
# Default ports for each database type.
_DATABASE_TYPE_DEFAULT_PORT = {
DatabaseType.POSTGRES: 5432,
DatabaseType.MYSQL: 3306,
DatabaseType.MSSQL: 1433,
}
def _sanitize_error(
error_msg: str,
connection_string: str,
*,
host: str = "",
original_host: str = "",
username: str = "",
port: int = 0,
database: str = "",
) -> str:
"""Remove connection string, credentials, and infrastructure details
from error messages so they are safe to expose to the LLM.
Scrubs:
- The full connection string
- URL-embedded credentials (``://user:pass@``)
- ``password=<value>`` key-value pairs
- The database hostname / IP used for the connection
- The original (pre-resolution) hostname provided by the user
- Any IPv4 addresses that appear in the message
- Any bracketed IPv6 addresses (e.g. ``[::1]``, ``[fe80::1%eth0]``)
- The database username
- The database port number
- The database name
"""
sanitized = error_msg.replace(connection_string, "<connection_string>")
sanitized = re.sub(r"password=[^\s&]+", "password=***", sanitized)
sanitized = re.sub(r"://[^@]+@", "://***:***@", sanitized)
# Replace the known host (may be an IP already) before the generic IP pass.
# Also replace the original (pre-DNS-resolution) hostname if it differs.
if original_host and original_host != host:
sanitized = sanitized.replace(original_host, "<host>")
if host:
sanitized = sanitized.replace(host, "<host>")
# Replace any remaining IPv4 addresses (e.g. resolved IPs the driver logs)
sanitized = re.sub(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", "<ip>", sanitized)
# Replace bracketed IPv6 addresses (e.g. "[::1]", "[fe80::1%eth0]")
sanitized = re.sub(r"\[[0-9a-fA-F:]+(?:%[^\]]+)?\]", "<ip>", sanitized)
# Replace the database username (handles double-quoted, single-quoted,
# and unquoted formats across PostgreSQL, MySQL, and MSSQL error messages).
if username:
sanitized = re.sub(
r"""for user ["']?""" + re.escape(username) + r"""["']?""",
"for user <user>",
sanitized,
)
# Catch remaining bare occurrences in various quote styles:
# - PostgreSQL: "FATAL: role "myuser" does not exist"
# - MySQL: "Access denied for user 'myuser'@'host'"
# - MSSQL: "Login failed for user 'myuser'"
sanitized = sanitized.replace(f'"{username}"', "<user>")
sanitized = sanitized.replace(f"'{username}'", "<user>")
# Replace the port number (handles "port 5432" and ":5432" formats)
if port:
port_str = re.escape(str(port))
sanitized = re.sub(
r"(?:port |:)" + port_str + r"(?![0-9])",
lambda m: ("port " if m.group().startswith("p") else ":") + "<port>",
sanitized,
)
# Replace the database name to avoid leaking internal infrastructure names.
# Use word-boundary regex to prevent mangling when the database name is a
# common substring (e.g. "test", "data", "on").
if database:
sanitized = re.sub(r"\b" + re.escape(database) + r"\b", "<database>", sanitized)
return sanitized
def _extract_keyword_tokens(parsed: sqlparse.sql.Statement) -> list[str]:
"""Extract keyword tokens from a parsed SQL statement.
Uses sqlparse token type classification to collect Keyword/DML/DDL/DCL
tokens. String literals and identifiers have different token types, so
they are naturally excluded from the result.
"""
return [
token.normalized.upper()
for token in parsed.flatten()
if token.ttype
in (
sqlparse.tokens.Keyword,
sqlparse.tokens.Keyword.DML,
sqlparse.tokens.Keyword.DDL,
sqlparse.tokens.Keyword.DCL,
)
]
def _has_disallowed_into(stmt: sqlparse.sql.Statement) -> bool:
"""Check if a statement contains a disallowed ``INTO`` clause.
``SELECT ... INTO @variable`` is a valid read-only MySQL syntax that stores
a query result into a session-scoped user variable. All other forms of
``INTO`` are data-modifying or file-writing and must be blocked:
* ``SELECT ... INTO new_table`` (PostgreSQL / MSSQL creates a table)
* ``SELECT ... INTO OUTFILE`` (MySQL writes to the filesystem)
* ``SELECT ... INTO DUMPFILE`` (MySQL writes to the filesystem)
* ``INSERT INTO ...`` (already blocked by INSERT being in the
disallowed set, but we reject INTO as well for defense-in-depth)
Returns ``True`` if the statement contains a disallowed ``INTO``.
"""
flat = list(stmt.flatten())
for i, token in enumerate(flat):
if not (
token.ttype in (sqlparse.tokens.Keyword,)
and token.normalized.upper() == "INTO"
):
continue
# Look at the first non-whitespace token after INTO.
j = i + 1
while j < len(flat) and flat[j].ttype is sqlparse.tokens.Text.Whitespace:
j += 1
if j >= len(flat):
# INTO at the very end malformed, block it.
return True
next_token = flat[j]
# MySQL user variable: either a single Name starting with "@"
# (e.g. ``@total``) or a bare ``@`` Operator token followed by a Name.
if next_token.ttype is sqlparse.tokens.Name and next_token.value.startswith(
"@"
):
continue
if next_token.ttype is sqlparse.tokens.Operator and next_token.value == "@":
continue
# Everything else (table name, OUTFILE, DUMPFILE, etc.) is disallowed.
return True
return False
def _validate_query_is_read_only(stmt: sqlparse.sql.Statement) -> str | None:
"""Validate that a parsed SQL statement is read-only (SELECT/WITH only).
Accepts an already-parsed statement from ``_validate_single_statement``
to avoid re-parsing. Checks:
1. Statement type must be SELECT (sqlparse classifies WITH...SELECT as SELECT)
2. No disallowed keywords (INSERT, UPDATE, DELETE, DROP, etc.)
3. No disallowed INTO clauses (allows MySQL ``SELECT ... INTO @variable``)
Returns an error message if the query is not read-only, None otherwise.
"""
# sqlparse returns 'SELECT' for SELECT and WITH...SELECT queries
if stmt.get_type() != "SELECT":
return "Only SELECT queries are allowed."
# Defense-in-depth: check parsed keyword tokens for disallowed keywords
for kw in _extract_keyword_tokens(stmt):
# Normalize multi-word tokens (e.g. "SET LOCAL" -> "SET")
base_kw = kw.split()[0] if " " in kw else kw
if base_kw in _DISALLOWED_KEYWORDS:
return f"Disallowed SQL keyword: {kw}"
# Contextual check for INTO: allow MySQL @variable syntax, block everything else
if _has_disallowed_into(stmt):
return "Disallowed SQL keyword: INTO"
return None
def _validate_single_statement(
query: str,
) -> tuple[str | None, sqlparse.sql.Statement | None]:
"""Validate that the query contains exactly one non-empty SQL statement.
Returns (error_message, parsed_statement). If error_message is not None,
the query is invalid and parsed_statement will be None.
"""
stripped = query.strip().rstrip(";").strip()
if not stripped:
return "Query is empty.", None
# Parse the SQL using sqlparse for proper tokenization
statements = sqlparse.parse(stripped)
# Filter out empty statements and comment-only statements
statements = [
s
for s in statements
if s.tokens
and str(s).strip()
and not all(
t.is_whitespace or t.ttype in sqlparse.tokens.Comment for t in s.flatten()
)
]
if not statements:
return "Query is empty.", None
# Reject multiple statements -- prevents injection via semicolons
if len(statements) > 1:
return "Only single statements are allowed.", None
return None, statements[0]
def _serialize_value(value: Any) -> Any:
"""Convert database-specific types to JSON-serializable Python types."""
if isinstance(value, Decimal):
# NaN / Infinity are not valid JSON numbers; serialize as strings.
if value.is_nan() or value.is_infinite():
return str(value)
# Use int for whole numbers; use str for fractional to preserve exact
# precision (float would silently round high-precision analytics values).
if value == value.to_integral_value():
return int(value)
return str(value)
if isinstance(value, (datetime, date, time)):
return value.isoformat()
if isinstance(value, memoryview):
return bytes(value).hex()
if isinstance(value, bytes):
return value.hex()
return value
def _configure_session(
conn: Any,
dialect_name: str,
timeout_ms: str,
read_only: bool,
) -> None:
"""Set session-level timeout and read-only mode for the given dialect.
Timeout limitations by database:
* **PostgreSQL** ``statement_timeout`` reliably cancels any running
statement (SELECT or DML) after the configured duration.
* **MySQL** ``MAX_EXECUTION_TIME`` only applies to **read-only SELECT**
statements. DML (INSERT/UPDATE/DELETE) and DDL are *not* bounded by
this hint; they rely on the server's ``wait_timeout`` /
``interactive_timeout`` instead. There is no session-level setting in
MySQL that reliably cancels long-running writes.
* **MSSQL** ``SET LOCK_TIMEOUT`` only limits how long the server waits
to acquire a **lock**. CPU-bound queries (e.g. large scans, hash
joins) that do not block on locks will *not* be cancelled. MSSQL has
no session-level ``statement_timeout`` equivalent; the closest
mechanism is Resource Governor (requires sysadmin configuration) or
``CONTEXT_INFO``-based external monitoring.
Note: SQLite is not supported by this block. The ``_configure_session``
function is a no-op for unrecognised dialect names, so an SQLite engine
would skip all SET commands silently. The block's ``DatabaseType`` enum
intentionally excludes SQLite.
"""
if dialect_name == "postgresql":
conn.execute(text("SET statement_timeout = " + timeout_ms))
if read_only:
conn.execute(text("SET default_transaction_read_only = ON"))
elif dialect_name == "mysql":
# NOTE: MAX_EXECUTION_TIME only applies to SELECT statements.
# Write queries (INSERT/UPDATE/DELETE) are not bounded by this
# setting; they rely on the database's wait_timeout instead.
# See docstring above for full limitations.
conn.execute(text("SET SESSION MAX_EXECUTION_TIME = " + timeout_ms))
if read_only:
conn.execute(text("SET SESSION TRANSACTION READ ONLY"))
elif dialect_name == "mssql":
# MSSQL: SET LOCK_TIMEOUT limits lock-wait time (ms) only.
# CPU-bound queries without lock contention are NOT cancelled.
# See docstring above for full limitations.
conn.execute(text("SET LOCK_TIMEOUT " + timeout_ms))
# MSSQL lacks a session-level read-only mode like
# PostgreSQL/MySQL. Read-only enforcement is handled by
# the SQL validation layer (_validate_query_is_read_only)
# and the ROLLBACK in the finally block.
def _run_in_transaction(
conn: Any,
dialect_name: str,
query: str,
max_rows: int,
read_only: bool,
) -> tuple[list[dict[str, Any]], list[str], int, bool]:
"""Execute a query inside an explicit transaction, returning results.
Returns ``(rows, columns, affected_rows, truncated)`` where *truncated*
is ``True`` when ``fetchmany`` returned exactly ``max_rows`` rows,
indicating that additional rows may exist in the result set.
"""
# MSSQL uses T-SQL "BEGIN TRANSACTION"; others use "BEGIN".
begin_stmt = "BEGIN TRANSACTION" if dialect_name == "mssql" else "BEGIN"
conn.execute(text(begin_stmt))
try:
result = conn.execute(text(query))
affected = result.rowcount if not result.returns_rows else -1
columns = list(result.keys()) if result.returns_rows else []
rows = result.fetchmany(max_rows) if result.returns_rows else []
truncated = len(rows) == max_rows
results = [
{col: _serialize_value(val) for col, val in zip(columns, row)}
for row in rows
]
except Exception:
try:
conn.execute(text("ROLLBACK"))
except Exception:
pass
raise
else:
conn.execute(text("ROLLBACK" if read_only else "COMMIT"))
return results, columns, affected, truncated
def _execute_query(
connection_url: URL | str,
query: str,
timeout: int,
max_rows: int,
read_only: bool = True,
database_type: DatabaseType = DatabaseType.POSTGRES,
) -> tuple[list[dict[str, Any]], list[str], int, bool]:
"""Execute a SQL query and return (rows, columns, affected_rows, truncated).
Uses SQLAlchemy to connect to any supported database.
For SELECT queries, rows are limited to ``max_rows`` via DBAPI fetchmany.
``truncated`` is ``True`` when the result set was capped by ``max_rows``.
For write queries, affected_rows contains the rowcount from the driver.
When ``read_only`` is True, the database session is set to read-only
mode and the transaction is always rolled back.
"""
# Determine driver-specific connection timeout argument.
# pymssql uses "login_timeout", while PostgreSQL/MySQL use "connect_timeout".
timeout_key = (
"login_timeout" if database_type == DatabaseType.MSSQL else "connect_timeout"
)
engine = create_engine(
connection_url, connect_args={timeout_key: _CONNECT_TIMEOUT_SECONDS}
)
try:
with engine.connect() as conn:
# Use AUTOCOMMIT so SET commands take effect immediately.
conn = conn.execution_options(isolation_level="AUTOCOMMIT")
# Compute timeout in milliseconds. The value is Pydantic-validated
# (ge=1, le=120), but we use int() as defense-in-depth.
# NOTE: SET commands do not support bind parameters in most
# databases, so we use str(int(...)) for safe interpolation.
timeout_ms = str(int(timeout * 1000))
_configure_session(conn, engine.dialect.name, timeout_ms, read_only)
return _run_in_transaction(
conn, engine.dialect.name, query, max_rows, read_only
)
finally:
engine.dispose()

View File

@@ -436,6 +436,28 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
return [StoreAgentDetails.from_db(agent) for agent in recommended_agents]
def format_onboarding_for_extraction(
user_name: str,
user_role: str,
pain_points: list[str],
) -> str:
"""Format onboarding wizard answers as Q&A text for LLM extraction."""
def normalize(value: str) -> str:
return " ".join(value.strip().split())
name = normalize(user_name)
role = normalize(user_role)
points = [normalize(p) for p in pain_points if normalize(p)]
lines = [
f"Q: What is your name?\nA: {name}",
f"Q: What best describes your role?\nA: {role}",
f"Q: What tasks are eating your time?\nA: {', '.join(points)}",
]
return "\n\n".join(lines)
@cached(maxsize=1, ttl_seconds=300) # Cache for 5 minutes since this rarely changes
async def onboarding_enabled() -> bool:
"""

View File

@@ -0,0 +1,27 @@
from backend.data.onboarding import format_onboarding_for_extraction
def test_format_onboarding_for_extraction_basic():
result = format_onboarding_for_extraction(
user_name="John",
user_role="Founder/CEO",
pain_points=["Finding leads", "Email & outreach"],
)
assert "Q: What is your name?" in result
assert "A: John" in result
assert "Q: What best describes your role?" in result
assert "A: Founder/CEO" in result
assert "Q: What tasks are eating your time?" in result
assert "Finding leads" in result
assert "Email & outreach" in result
def test_format_onboarding_for_extraction_with_other():
result = format_onboarding_for_extraction(
user_name="Jane",
user_role="Data Scientist",
pain_points=["Research", "Building dashboards"],
)
assert "A: Jane" in result
assert "A: Data Scientist" in result
assert "Research, Building dashboards" in result

View File

@@ -15,6 +15,7 @@ class ProviderName(str, Enum):
ANTHROPIC = "anthropic"
APOLLO = "apollo"
COMPASS = "compass"
DATABASE = "database"
DISCORD = "discord"
D_ID = "d_id"
E2B = "e2b"

View File

@@ -974,6 +974,128 @@ files = [
]
markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "coverage"
version = "7.13.5"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"},
{file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"},
{file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"},
{file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"},
{file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"},
{file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"},
{file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"},
{file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"},
{file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"},
{file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"},
{file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"},
{file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"},
{file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"},
{file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"},
{file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"},
{file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"},
{file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"},
{file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"},
{file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"},
{file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
{file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
{file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
{file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
{file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
{file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
{file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
{file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
{file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
{file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
{file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
{file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
{file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
{file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
{file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
{file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
{file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
{file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
{file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
{file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
{file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
{file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
{file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
{file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
{file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
{file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "crashtest"
version = "0.4.1"
@@ -5720,6 +5842,75 @@ dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pyt
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"]
[[package]]
name = "pymssql"
version = "2.3.13"
description = "DB-API interface to Microsoft SQL Server for Python. (new Cython-based version)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pymssql-2.3.13-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:476f6f06b2ae5dfbfa0b169a6ecdd0d9ddfedb07f2d6dc97d2dd630ff2d6789a"},
{file = "pymssql-2.3.13-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:17942dc9474693ab2229a8a6013e5b9cb1312a5251207552141bb85fcce8c131"},
{file = "pymssql-2.3.13-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d87237500def5f743a52e415cd369d632907212154fcc7b4e13f264b4e30021"},
{file = "pymssql-2.3.13-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:612ac062027d2118879f11a5986e9d9d82d07ca3545bb98c93200b68826ea687"},
{file = "pymssql-2.3.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1897c1b767cc143e77d285123ae5fd4fa7379a1bfec5c515d38826caf084eb6"},
{file = "pymssql-2.3.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48631c7b9fd14a1bd5675c521b6082590bf700b7961c65638d237817b3fde735"},
{file = "pymssql-2.3.13-cp310-cp310-win_amd64.whl", hash = "sha256:79c759db6e991eeae473b000c2e0a7fb8da799b2da469fe5a10d30916315e0b5"},
{file = "pymssql-2.3.13-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:152be40c0d7f5e4b1323f7728b0a01f3ee0082190cfbadf84b2c2e930d57e00e"},
{file = "pymssql-2.3.13-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:d94da3a55545c5b6926cb4d1c6469396f0ae32ad5d6932c513f7a0bf569b4799"},
{file = "pymssql-2.3.13-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51e42c5defc3667f0803c7ade85db0e6f24b9a1c5a18fcdfa2d09c36bff9b065"},
{file = "pymssql-2.3.13-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aa18944a121f996178e26cadc598abdbf73759f03dc3cd74263fdab1b28cd96"},
{file = "pymssql-2.3.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:910404e0ec85c4cc7c633ec3df9b04a35f23bb74a844dd377a387026ae635e3a"},
{file = "pymssql-2.3.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4b834c34e7600369eee7bc877948b53eb0fe6f3689f0888d005ae47dd53c0a66"},
{file = "pymssql-2.3.13-cp311-cp311-win_amd64.whl", hash = "sha256:5c2e55b6513f9c5a2f58543233ed40baaa7f91c79e64a5f961ea3fc57a700b80"},
{file = "pymssql-2.3.13-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733"},
{file = "pymssql-2.3.13-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131"},
{file = "pymssql-2.3.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c"},
{file = "pymssql-2.3.13-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be"},
{file = "pymssql-2.3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0"},
{file = "pymssql-2.3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d"},
{file = "pymssql-2.3.13-cp312-cp312-win_amd64.whl", hash = "sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb"},
{file = "pymssql-2.3.13-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c"},
{file = "pymssql-2.3.13-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e"},
{file = "pymssql-2.3.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230"},
{file = "pymssql-2.3.13-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68"},
{file = "pymssql-2.3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9"},
{file = "pymssql-2.3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5"},
{file = "pymssql-2.3.13-cp313-cp313-win_amd64.whl", hash = "sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0"},
{file = "pymssql-2.3.13-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2"},
{file = "pymssql-2.3.13-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47"},
{file = "pymssql-2.3.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1"},
{file = "pymssql-2.3.13-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac"},
{file = "pymssql-2.3.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b"},
{file = "pymssql-2.3.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193"},
{file = "pymssql-2.3.13-cp314-cp314-win_amd64.whl", hash = "sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d"},
{file = "pymssql-2.3.13-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:b0af51904764811da0bfe4b057b1d72dee11a399ce9ed5770875162772740c8a"},
{file = "pymssql-2.3.13-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:0a7e6431925572bc75fb47929ae8ca5b0aac26abfe8b98d4c08daf117b5657f1"},
{file = "pymssql-2.3.13-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9b1d5aef2b5f47a7f9d9733caee4d66772681e8f798a0f5e4739a8bdab408c"},
{file = "pymssql-2.3.13-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c690f1869dadbf4201b7f51317fceff6e5d8f5175cec6a4a813e06b0dca2d6ed"},
{file = "pymssql-2.3.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e7c31f192da9d30f0e03ad99e548120a8740a675302e2f04fa8c929f7cbee771"},
{file = "pymssql-2.3.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f5d995a80996235ed32102a93067ce6a7143cce3bfd4e5042bf600020fc08456"},
{file = "pymssql-2.3.13-cp39-cp39-win_amd64.whl", hash = "sha256:6a6c0783d97f57133573a03aad3017917dbdf7831a65e0d84ccf2a85e183ca66"},
{file = "pymssql-2.3.13.tar.gz", hash = "sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce"},
]
[[package]]
name = "pymysql"
version = "1.1.2"
description = "Pure Python MySQL Driver"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9"},
{file = "pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03"},
]
[package.extras]
ed25519 = ["PyNaCl (>=1.4.0)"]
rsa = ["cryptography"]
[[package]]
name = "pyparsing"
version = "3.3.2"
@@ -5919,6 +6110,26 @@ typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""}
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-cov"
version = "7.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
{file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
]
[package.dependencies]
coverage = {version = ">=7.10.6", extras = ["toml"]}
pluggy = ">=1.2"
pytest = ">=7"
[package.extras]
testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-mock"
version = "3.15.1"
@@ -7009,6 +7220,22 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "sqlparse"
version = "0.5.5"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
{file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
]
[package.extras]
dev = ["build"]
doc = ["sphinx"]
[[package]]
name = "sse-starlette"
version = "3.2.0"
@@ -8630,4 +8857,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.14"
content-hash = "1dd10577184ebff0d10997f4c6ba49484de79b7fa090946e8e5ce5c5bac3cdeb"
content-hash = "8dd9db689a2dd57fc3cccea02e596a522f334f6b5ed18e92252555f61835d71d"

View File

@@ -94,7 +94,10 @@ posthog = "^7.6.0"
fpdf2 = "^2.8.6"
langsmith = "^0.7.7"
openpyxl = "^3.1.5"
pymssql = "^2.3.2"
pymysql = "^1.1.1"
pyarrow = "^23.0.0"
sqlparse = "^0.5.5"
[tool.poetry.group.dev.dependencies]
aiohappyeyeballs = "^2.6.1"
@@ -105,6 +108,7 @@ isort = "^5.13.2"
poethepoet = "^0.41.0"
pre-commit = "^4.4.0"
pyright = "^1.1.407"
pytest-cov = "^7.1.0"
pytest-mock = "^3.15.1"
pytest-watcher = "^0.6.3"
requests = "^2.32.5"

View File

@@ -15,7 +15,7 @@
"types": "tsc --noEmit",
"test": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test",
"test-ui": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test --ui",
"test:unit": "vitest run",
"test:unit": "vitest run --coverage",
"test:unit:watch": "vitest",
"test:no-build": "playwright test",
"gentests": "playwright codegen http://localhost:3000",
@@ -122,6 +122,7 @@
"tailwind-merge": "2.6.0",
"tailwind-scrollbar": "3.1.0",
"tailwindcss-animate": "1.0.7",
"twemoji": "14.0.2",
"use-stick-to-bottom": "1.1.2",
"uuid": "11.1.0",
"vaul": "1.1.2",
@@ -150,6 +151,7 @@
"@types/react-modal": "3.16.3",
"@types/react-window": "2.0.0",
"@vitejs/plugin-react": "5.1.2",
"@vitest/coverage-v8": "4.0.17",
"axe-playwright": "2.2.2",
"chromatic": "13.3.3",
"concurrently": "9.2.1",

View File

@@ -288,6 +288,9 @@ importers:
tailwindcss-animate:
specifier: 1.0.7
version: 1.0.7(tailwindcss@3.4.17)
twemoji:
specifier: 14.0.2
version: 14.0.2
use-stick-to-bottom:
specifier: 1.1.2
version: 1.1.2(react@18.3.1)
@@ -367,6 +370,9 @@ importers:
'@vitejs/plugin-react':
specifier: 5.1.2
version: 5.1.2(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
'@vitest/coverage-v8':
specifier: 4.0.17
version: 4.0.17(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@24.10.0)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2))
axe-playwright:
specifier: 2.2.2
version: 2.2.2(playwright@1.56.1)
@@ -629,6 +635,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.29.2':
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5':
resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==}
engines: {node: '>=6.9.0'}
@@ -1098,6 +1109,14 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@braintree/sanitize-url@7.1.2':
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
@@ -3917,6 +3936,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vitest/coverage-v8@4.0.17':
resolution: {integrity: sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==}
peerDependencies:
'@vitest/browser': 4.0.17
vitest: 4.0.17
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
@@ -4229,6 +4257,9 @@ packages:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
ast-v8-to-istanbul@0.3.12:
resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==}
astring@1.9.0:
resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
hasBin: true
@@ -5470,6 +5501,10 @@ packages:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fs-extra@8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
fs-monkey@1.1.0:
resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==}
@@ -5709,6 +5744,9 @@ packages:
html-entities@2.6.0:
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-minifier-terser@6.1.0:
resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
engines: {node: '>=12'}
@@ -6004,6 +6042,18 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
@@ -6044,6 +6094,9 @@ packages:
react:
optional: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -6103,6 +6156,12 @@ packages:
jsonc-parser@2.2.1:
resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==}
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
jsonfile@5.0.0:
resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
@@ -6299,10 +6358,17 @@ packages:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}
magicast@0.5.2:
resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==}
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
@@ -8165,6 +8231,12 @@ packages:
tty-browserify@0.0.1:
resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==}
twemoji-parser@14.0.0:
resolution: {integrity: sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==}
twemoji@14.0.2:
resolution: {integrity: sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -8289,6 +8361,10 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -8998,6 +9074,10 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@babel/parser@7.29.2':
dependencies:
'@babel/types': 7.29.0
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.5)':
dependencies:
'@babel/core': 7.28.5
@@ -9599,6 +9679,13 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@1.0.2': {}
'@braintree/sanitize-url@7.1.2': {}
'@chevrotain/cst-dts-gen@11.0.3':
@@ -12628,6 +12715,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@4.0.17(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@24.10.0)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.17
ast-v8-to-istanbul: 0.3.12
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.2
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@24.10.0)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2)
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
@@ -13019,6 +13120,12 @@ snapshots:
dependencies:
tslib: 2.8.1
ast-v8-to-istanbul@0.3.12:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
astring@1.9.0: {}
async-function@1.0.0: {}
@@ -14114,8 +14221,8 @@ snapshots:
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -14134,7 +14241,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -14145,22 +14252,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -14171,7 +14278,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -14511,6 +14618,12 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@8.1.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-monkey@1.1.0: {}
fs.realpath@1.0.0: {}
@@ -14848,6 +14961,8 @@ snapshots:
html-entities@2.6.0: {}
html-escaper@2.0.2: {}
html-minifier-terser@6.1.0:
dependencies:
camel-case: 4.1.2
@@ -15135,6 +15250,19 @@ snapshots:
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@@ -15169,6 +15297,8 @@ snapshots:
'@types/react': 18.3.17
react: 18.3.1
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -15232,6 +15362,16 @@ snapshots:
jsonc-parser@2.2.1: {}
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@5.0.0:
dependencies:
universalify: 0.1.2
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
@@ -15420,10 +15560,20 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.2:
dependencies:
'@babel/parser': 7.29.2
'@babel/types': 7.29.0
source-map-js: 1.2.1
make-dir@3.1.0:
dependencies:
semver: 6.3.1
make-dir@4.0.0:
dependencies:
semver: 7.7.3
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
@@ -17782,6 +17932,15 @@ snapshots:
tty-browserify@0.0.1: {}
twemoji-parser@14.0.0: {}
twemoji@14.0.2:
dependencies:
fs-extra: 8.1.0
jsonfile: 5.0.0
twemoji-parser: 14.0.0
universalify: 0.1.2
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -17919,6 +18078,8 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
universalify@0.1.2: {}
universalify@2.0.1: {}
unplugin@1.0.1:

View File

@@ -1,33 +0,0 @@
"use client";
import { OnboardingText } from "../components/OnboardingText";
import OnboardingButton from "../components/OnboardingButton";
import Image from "next/image";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
export default function Page() {
useOnboarding(1);
return (
<>
<Image
src="/gpt_dark_RGB.svg"
alt="GPT Dark Logo"
className="-mb-2"
width={300}
height={300}
/>
<OnboardingText className="mb-3" variant="header" center>
Welcome to AutoGPT
</OnboardingText>
<OnboardingText className="mb-12" center>
Think of AutoGPT as your digital teammate, working intelligently to
<br />
complete tasks based on your directions. Let&apos;s learn a bit about
you to
<br />
tailor your experience.
</OnboardingText>
<OnboardingButton href="/onboarding/2-reason">Continue</OnboardingButton>
</>
);
}

View File

@@ -1,69 +0,0 @@
"use client";
import OnboardingButton from "../components/OnboardingButton";
import {
OnboardingFooter,
OnboardingHeader,
OnboardingStep,
} from "../components/OnboardingStep";
import { OnboardingText } from "../components/OnboardingText";
import OnboardingList from "../components/OnboardingList";
import { isEmptyOrWhitespace } from "@/lib/utils";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
const reasons = [
{
label: "Content & Marketing",
text: "Content creation, social media management, blogging, creative writing",
id: "content_marketing",
},
{
label: "Business & Workflow Automation",
text: "Operations, task management, productivity",
id: "business_workflow_automation",
},
{
label: "Data & Research",
text: "Data analysis, insights, research, financial operation",
id: "data_research",
},
{
label: "AI & Innovation",
text: "AI experimentation, automation testing, advanced AI applications",
id: "ai_innovation",
},
{
label: "Personal productivity",
text: "Automating daily tasks, organizing information, personal workflows",
id: "personal_productivity",
},
];
export default function Page() {
const { state, updateState } = useOnboarding(2, "WELCOME");
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/1-welcome"}>
<OnboardingText className="mt-4" variant="header" center>
What&apos;s your main reason for using AutoGPT?
</OnboardingText>
<OnboardingText className="mt-1" center>
Select the option that best matches your needs
</OnboardingText>
</OnboardingHeader>
<OnboardingList
elements={reasons}
selectedId={state?.usageReason}
onSelect={(usageReason) => updateState({ usageReason })}
/>
<OnboardingFooter>
<OnboardingButton
href="/onboarding/3-services"
disabled={isEmptyOrWhitespace(state?.usageReason)}
>
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -1,171 +0,0 @@
"use client";
import OnboardingButton from "../components/OnboardingButton";
import {
OnboardingStep,
OnboardingHeader,
OnboardingFooter,
} from "../components/OnboardingStep";
import { OnboardingText } from "../components/OnboardingText";
import { OnboardingGrid } from "../components/OnboardingGrid";
import { useCallback } from "react";
import OnboardingInput from "../components/OnboardingInput";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
const services = [
{
name: "D-ID",
text: "Generate AI-powered avatars and videos for dynamic content creation.",
icon: "/integrations/d-id.png",
},
{
name: "Discord",
text: "A chat platform for communities and teams, supporting text, voice, and video.",
icon: "/integrations/discord.png",
},
{
name: "GitHub",
text: "AutoGPT can track issues, manage repos, and automate workflows with GitHub.",
icon: "/integrations/github.png",
},
{
name: "Google Workspace",
text: "Automate emails, calendar events, and document management in AutoGPT with Google Workspace.",
icon: "/integrations/google.png",
},
{
name: "Google Maps",
text: "Fetch locations, directions, and real-time geodata for navigation.",
icon: "/integrations/maps.png",
},
{
name: "HubSpot",
text: "Manage customer relationships, automate marketing, and track sales.",
icon: "/integrations/hubspot.png",
},
{
name: "Linear",
text: "Streamline project management and issue tracking with a modern workflow.",
icon: "/integrations/linear.png",
},
{
name: "Medium",
text: "Publish and explore insightful content with a powerful writing platform.",
icon: "/integrations/medium.png",
},
{
name: "Mem0",
text: "AI-powered memory assistant for smarter data organization and recall.",
icon: "/integrations/mem0.png",
},
{
name: "Notion",
text: "Organize work, notes, and databases in an all-in-one workspace.",
icon: "/integrations/notion.png",
},
{
name: "NVIDIA",
text: "Accelerate AI, graphics, and computing with cutting-edge technology.",
icon: "/integrations/nvidia.jpg",
},
{
name: "OpenWeatherMap",
text: "Access real-time weather data and forecasts worldwide.",
icon: "/integrations/openweathermap.png",
},
{
name: "Pinecone",
text: "Store and search vector data for AI-driven applications.",
icon: "/integrations/pinecone.png",
},
{
name: "Reddit",
text: "Explore trending discussions and engage with online communities.",
icon: "/integrations/reddit.png",
},
{
name: "Slant3D",
text: "Automate and optimize 3D printing workflows with AI.",
icon: "/integrations/slant3d.jpeg",
},
{
name: "SMTP",
text: "Send and manage emails with secure and reliable delivery.",
icon: "/integrations/smtp.png",
},
{
name: "Todoist",
text: "Organize tasks and projects with a simple, intuitive to-do list.",
icon: "/integrations/todoist.png",
},
{
name: "Twitter (X)",
text: "Stay connected and share updates on the world's biggest conversation platform.",
icon: "/integrations/x.png",
},
{
name: "Unreal Speech",
text: "Generate natural-sounding AI voices for speech applications.",
icon: "/integrations/unreal-speech.png",
},
];
export default function Page() {
const { state, updateState } = useOnboarding(3, "USAGE_REASON");
const switchIntegration = useCallback(
(name: string) => {
if (!state) {
return;
}
const integrations = state.integrations.includes(name)
? state.integrations.filter((i) => i !== name)
: [...state.integrations, name];
updateState({ integrations });
},
[state, updateState],
);
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/2-reason"}>
<OnboardingText className="mt-4" variant="header" center>
What platforms or services would you like AutoGPT to work with?
</OnboardingText>
<OnboardingText className="mt-1" center>
You can select more than one option
</OnboardingText>
</OnboardingHeader>
<div className="w-fit">
<OnboardingText className="my-4" variant="subheader">
Available integrations
</OnboardingText>
<OnboardingGrid
elements={services}
selected={state?.integrations}
onSelect={switchIntegration}
/>
<OnboardingText className="mt-12" variant="subheader">
Help us grow our integrations
</OnboardingText>
<OnboardingText className="my-4">
Let us know which partnerships you&apos;d like to see next
</OnboardingText>
<OnboardingInput
className="mb-4"
placeholder="Others (please specify)"
value={state?.otherIntegrations || ""}
onChange={(otherIntegrations) => updateState({ otherIntegrations })}
/>
</div>
<OnboardingFooter>
<OnboardingButton className="mb-2" href="/onboarding/4-agent">
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -1,104 +0,0 @@
"use client";
import { isEmptyOrWhitespace } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
import OnboardingAgentCard from "../components/OnboardingAgentCard";
import OnboardingButton from "../components/OnboardingButton";
import {
OnboardingFooter,
OnboardingHeader,
OnboardingStep,
} from "../components/OnboardingStep";
import { OnboardingText } from "../components/OnboardingText";
import { getV1RecommendedOnboardingAgents } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { resolveResponse } from "@/app/api/helpers";
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
export default function Page() {
const { state, updateState, completeStep } = useOnboarding(4, "INTEGRATIONS");
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
const router = useRouter();
useEffect(() => {
resolveResponse(getV1RecommendedOnboardingAgents()).then((agents) => {
if (agents.length < 2) {
completeStep("CONGRATS");
router.replace("/");
}
setAgents(agents);
});
}, []);
useEffect(() => {
// Deselect agent if it's not in the list of agents
if (
state?.selectedStoreListingVersionId &&
agents.length > 0 &&
!agents.some(
(agent) =>
agent.store_listing_version_id ===
state.selectedStoreListingVersionId,
)
) {
updateState({
selectedStoreListingVersionId: null,
agentInput: {},
});
}
}, [state?.selectedStoreListingVersionId, updateState, agents]);
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/3-services"}>
<OnboardingText className="mt-4" variant="header" center>
Choose an agent
</OnboardingText>
<OnboardingText className="mt-1" center>
We think these agents are a good match for you based on your answers
</OnboardingText>
</OnboardingHeader>
<div className="my-12 flex items-center justify-between gap-5">
<OnboardingAgentCard
agent={agents[0]}
selected={
agents[0] !== undefined
? state?.selectedStoreListingVersionId ==
agents[0]?.store_listing_version_id
: false
}
onClick={() =>
updateState({
selectedStoreListingVersionId: agents[0].store_listing_version_id,
agentInput: {},
})
}
/>
<OnboardingAgentCard
agent={agents[1]}
selected={
agents[1] !== undefined
? state?.selectedStoreListingVersionId ==
agents[1]?.store_listing_version_id
: false
}
onClick={() =>
updateState({
selectedStoreListingVersionId: agents[1].store_listing_version_id,
})
}
/>
</div>
<OnboardingFooter>
<OnboardingButton
href="/onboarding/5-run"
disabled={isEmptyOrWhitespace(state?.selectedStoreListingVersionId)}
>
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -1,62 +0,0 @@
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
import { useState } from "react";
import { getSchemaDefaultCredentials } from "../../helpers";
import { areAllCredentialsSet, getCredentialFields } from "./helpers";
type Credential = CredentialsMetaInput | undefined;
type Credentials = Record<string, Credential>;
type Props = {
agent: GraphModel | null;
siblingInputs?: Record<string, any>;
onCredentialsChange: (
credentials: Record<string, CredentialsMetaInput>,
) => void;
onValidationChange: (isValid: boolean) => void;
onLoadingChange: (isLoading: boolean) => void;
};
export function AgentOnboardingCredentials(props: Props) {
const [inputCredentials, setInputCredentials] = useState<Credentials>({});
const fields = getCredentialFields(props.agent);
const required = Object.keys(fields || {}).length > 0;
if (!required) return null;
function handleSelectCredentials(key: string, value: Credential) {
const updated = { ...inputCredentials, [key]: value };
setInputCredentials(updated);
const sanitized: Record<string, CredentialsMetaInput> = {};
for (const [k, v] of Object.entries(updated)) {
if (v) sanitized[k] = v;
}
props.onCredentialsChange(sanitized);
const isValid = !required || areAllCredentialsSet(fields, updated);
props.onValidationChange(isValid);
}
return (
<>
{Object.entries(fields).map(([key, inputSubSchema]) => (
<div key={key} className="mt-4">
<CredentialsInput
schema={inputSubSchema}
selectedCredentials={
inputCredentials[key] ??
getSchemaDefaultCredentials(inputSubSchema)
}
onSelectCredentials={(value) => handleSelectCredentials(key, value)}
siblingInputs={props.siblingInputs}
onLoaded={(loaded) => props.onLoadingChange(!loaded)}
/>
</div>
))}
</>
);
}

View File

@@ -1,32 +0,0 @@
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
export function getCredentialFields(
agent: GraphModel | null,
): AgentCredentialsFields {
if (!agent) return {};
const hasNoInputs =
!agent.credentials_input_schema ||
typeof agent.credentials_input_schema !== "object" ||
!("properties" in agent.credentials_input_schema) ||
!agent.credentials_input_schema.properties;
if (hasNoInputs) return {};
return agent.credentials_input_schema.properties as AgentCredentialsFields;
}
export type AgentCredentialsFields = Record<
string,
BlockIOCredentialsSubSchema
>;
export function areAllCredentialsSet(
fields: AgentCredentialsFields,
inputs: Record<string, CredentialsMetaInput | undefined>,
) {
const required = Object.keys(fields || {});
return required.every((k) => Boolean(inputs[k]));
}

View File

@@ -1,45 +0,0 @@
import { cn } from "@/lib/utils";
import { OnboardingText } from "../../components/OnboardingText";
type RunAgentHintProps = {
handleNewRun: () => void;
};
export function RunAgentHint(props: RunAgentHintProps) {
return (
<div className="ml-[104px] w-[481px] pl-5">
<div className="flex flex-col">
<OnboardingText variant="header">Run your first agent</OnboardingText>
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
A &apos;run&apos; is when your agent starts working on a task
</span>
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
Click on <b>New Run</b> below to try it out
</span>
<div
onClick={props.handleNewRun}
className={cn(
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
"cursor-pointer transition-all duration-200 ease-in-out hover:bg-violet-50",
)}
>
<svg
width="38"
height="38"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g stroke="#6d28d9" strokeWidth="1.2" strokeLinecap="round">
<line x1="16" y1="8" x2="16" y2="24" />
<line x1="8" y1="16" x2="24" y2="16" />
</g>
</svg>
<span className="ml-3 font-sans text-[19px] font-medium leading-normal text-violet-700">
New run
</span>
</div>
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import StarRating from "../../components/StarRating";
import SmartImage from "@/components/__legacy__/SmartImage";
type Props = {
storeAgent: StoreAgentDetails | null;
};
export function SelectedAgentCard(props: Props) {
return (
<div className="fixed left-1/4 top-1/2 w-[481px] -translate-x-1/2 -translate-y-1/2">
<div className="h-[156px] w-[481px] rounded-xl bg-white px-6 pb-5 pt-4">
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
SELECTED AGENT
</span>
{props.storeAgent ? (
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-3">
{/* Left image */}
<SmartImage
src={props.storeAgent.agent_image[0]}
alt="Agent cover"
className="w-[350px] rounded-lg"
/>
{/* Right content */}
<div className="ml-3 flex flex-1 flex-col">
<div className="mb-2 flex flex-col items-start">
<span className="data-sentry-unmask w-[292px] truncate font-sans text-[14px] font-medium leading-tight text-zinc-800">
{props.storeAgent.agent_name}
</span>
<span className="data-sentry-unmask font-norma w-[292px] truncate font-sans text-xs text-zinc-600">
by {props.storeAgent.creator}
</span>
</div>
<div className="flex w-[292px] items-center justify-between">
<span className="truncate font-sans text-xs font-normal leading-tight text-zinc-600">
{props.storeAgent.runs.toLocaleString("en-US")} runs
</span>
<StarRating
className="font-sans text-xs font-normal leading-tight text-zinc-600"
starSize={12}
rating={props.storeAgent.rating || 0}
/>
</div>
</div>
</div>
) : (
<div className="mt-4 flex h-20 animate-pulse rounded-lg bg-gray-300 p-2" />
)}
</div>
</div>
);
}

View File

@@ -1,57 +0,0 @@
import type {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import type { InputValues } from "./types";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
export function computeInitialAgentInputs(
agent: GraphModel | null,
existingInputs?: InputValues | null,
): InputValues {
const properties = agent?.input_schema?.properties || {};
const result: InputValues = {};
Object.entries(properties).forEach(([key, subSchema]) => {
if (
existingInputs &&
key in existingInputs &&
existingInputs[key] != null
) {
result[key] = existingInputs[key];
return;
}
const def = (subSchema as unknown as { default?: string | number }).default;
result[key] = def ?? "";
});
return result;
}
type IsRunDisabledParams = {
agent: GraphModel | null;
isRunning: boolean;
agentInputs: InputValues | null | undefined;
};
export function isRunDisabled({
agent,
isRunning,
agentInputs,
}: IsRunDisabledParams) {
const hasEmptyInput = Object.values(agentInputs || {}).some(
(value) => String(value).trim() === "",
);
if (hasEmptyInput) return true;
if (!agent) return true;
if (isRunning) return true;
return false;
}
export function getSchemaDefaultCredentials(
schema: BlockIOCredentialsSubSchema,
): CredentialsMetaInput | undefined {
return schema.default as CredentialsMetaInput | undefined;
}

View File

@@ -1,124 +0,0 @@
"use client";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/__legacy__/ui/card";
import { RunAgentInputs } from "@/components/contextual/RunAgentInputs/RunAgentInputs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { Play } from "lucide-react";
import OnboardingButton from "../components/OnboardingButton";
import { OnboardingHeader, OnboardingStep } from "../components/OnboardingStep";
import { OnboardingText } from "../components/OnboardingText";
import { AgentOnboardingCredentials } from "./components/AgentOnboardingCredentials/AgentOnboardingCredentials";
import { RunAgentHint } from "./components/RunAgentHint";
import { SelectedAgentCard } from "./components/SelectedAgentCard";
import { isRunDisabled } from "./helpers";
import type { InputValues } from "./types";
import { useOnboardingRunStep } from "./useOnboardingRunStep";
export default function Page() {
const {
ready,
error,
showInput,
agentGraph,
onboarding,
storeAgent,
runningAgent,
handleSetAgentInput,
handleRunAgent,
handleNewRun,
handleCredentialsChange,
handleCredentialsValidationChange,
handleCredentialsLoadingChange,
} = useOnboardingRunStep();
if (error) {
return <ErrorCard responseError={error} />;
}
if (!ready) {
return (
<div className="flex flex-col gap-8">
<CircleNotchIcon className="size-10 animate-spin" />
</div>
);
}
return (
<OnboardingStep dotted>
<OnboardingHeader backHref={"/onboarding/4-agent"} transparent />
<div className="flex min-h-[80vh] items-center justify-center">
<SelectedAgentCard storeAgent={storeAgent} />
<div className="w-[481px]" />
{!showInput ? (
<RunAgentHint handleNewRun={handleNewRun} />
) : (
<div className="ml-[104px] w-[481px] pl-5">
<div className="flex flex-col">
<OnboardingText variant="header">
Provide details for your agent
</OnboardingText>
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
Give your agent the details it needs to workjust enter <br />
the key information and get started.
</span>
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
When you&apos;re done, click <b>Run Agent</b>.
</span>
<Card className="agpt-box mt-4">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{Object.entries(
agentGraph?.input_schema.properties || {},
).map(([key, inputSubSchema]) => (
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
))}
<AgentOnboardingCredentials
agent={agentGraph}
siblingInputs={
(onboarding.state?.agentInput as Record<string, any>) ||
undefined
}
onCredentialsChange={handleCredentialsChange}
onValidationChange={handleCredentialsValidationChange}
onLoadingChange={handleCredentialsLoadingChange}
/>
</CardContent>
</Card>
<OnboardingButton
variant="violet"
className="mt-8 w-[136px]"
loading={runningAgent}
disabled={isRunDisabled({
agent: agentGraph,
isRunning: runningAgent,
agentInputs:
(onboarding.state?.agentInput as unknown as InputValues) ||
null,
})}
onClick={handleRunAgent}
icon={<Play className="mr-2" size={18} />}
>
Run agent
</OnboardingButton>
</div>
</div>
)}
</div>
</OnboardingStep>
);
}

View File

@@ -1,2 +0,0 @@
export type InputPrimitive = string | number;
export type InputValues = Record<string, InputPrimitive>;

View File

@@ -1,157 +0,0 @@
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { computeInitialAgentInputs } from "./helpers";
import { InputValues } from "./types";
import { okData, resolveResponse } from "@/app/api/helpers";
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import {
useGetV2GetAgentByVersion,
useGetV2GetAgentGraph,
} from "@/app/api/__generated__/endpoints/store/store";
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphID } from "@/lib/autogpt-server-api";
export function useOnboardingRunStep() {
const onboarding = useOnboarding(undefined, "AGENT_CHOICE");
const [showInput, setShowInput] = useState(false);
const [runningAgent, setRunningAgent] = useState(false);
const [inputCredentials, setInputCredentials] = useState<
Record<string, CredentialsMetaInput>
>({});
const [credentialsValid, setCredentialsValid] = useState(true);
const [credentialsLoaded, setCredentialsLoaded] = useState(false);
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
const currentAgentVersion =
onboarding.state?.selectedStoreListingVersionId ?? "";
const {
data: storeAgent,
error: storeAgentQueryError,
isSuccess: storeAgentQueryIsSuccess,
} = useGetV2GetAgentByVersion(currentAgentVersion, {
query: {
enabled: !!currentAgentVersion,
select: okData,
},
});
const {
data: agentGraphMeta,
error: agentGraphQueryError,
isSuccess: agentGraphQueryIsSuccess,
} = useGetV2GetAgentGraph(currentAgentVersion, {
query: {
enabled: !!currentAgentVersion,
select: okData,
},
});
useEffect(() => {
onboarding.setStep(5);
}, []);
useEffect(() => {
if (agentGraphMeta && onboarding.state) {
const initialAgentInputs = computeInitialAgentInputs(
agentGraphMeta,
(onboarding.state.agentInput as unknown as InputValues) || null,
);
onboarding.updateState({ agentInput: initialAgentInputs });
}
}, [agentGraphMeta]);
function handleNewRun() {
if (!onboarding.state) return;
setShowInput(true);
onboarding.setStep(6);
onboarding.completeStep("AGENT_NEW_RUN");
}
function handleSetAgentInput(key: string, value: string) {
if (!onboarding.state) return;
onboarding.updateState({
agentInput: {
...onboarding.state.agentInput,
[key]: value,
},
});
}
async function handleRunAgent() {
if (!agentGraphMeta || !storeAgent || !onboarding.state) {
toast({
title: "Error getting agent",
description:
"Either the agent is not available or there was an error getting it.",
variant: "destructive",
});
return;
}
setRunningAgent(true);
try {
const libraryAgent = await resolveResponse(
postV2AddMarketplaceAgent({
store_listing_version_id: storeAgent?.store_listing_version_id || "",
source: "onboarding",
}),
);
const { id: runID } = await api.executeGraph(
libraryAgent.graph_id as GraphID,
libraryAgent.graph_version,
onboarding.state.agentInput || {},
inputCredentials,
"onboarding",
);
onboarding.updateState({ onboardingAgentExecutionId: runID });
router.push("/onboarding/6-congrats");
} catch (error) {
console.error("Error running agent:", error);
toast({
title: "Error running agent",
description:
"There was an error running your agent. Please try again or try choosing a different agent if it still fails.",
variant: "destructive",
});
setRunningAgent(false);
}
}
return {
ready: agentGraphQueryIsSuccess && storeAgentQueryIsSuccess,
error: agentGraphQueryError || storeAgentQueryError,
agentGraph: agentGraphMeta || null,
onboarding,
showInput,
storeAgent: storeAgent || null,
runningAgent,
credentialsValid,
credentialsLoaded,
handleSetAgentInput,
handleRunAgent,
handleNewRun,
handleCredentialsChange: setInputCredentials,
handleCredentialsValidationChange: setCredentialsValid,
handleCredentialsLoadingChange: (v: boolean) => setCredentialsLoaded(!v),
};
}

View File

@@ -1,127 +0,0 @@
"use client";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
import { resolveResponse } from "@/app/api/helpers";
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import { Confetti } from "@/components/molecules/Confetti/Confetti";
import type { ConfettiRef } from "@/components/molecules/Confetti/Confetti";
export default function Page() {
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
const router = useRouter();
const api = useBackendAPI();
const [showText, setShowText] = useState(false);
const [showSubtext, setShowSubtext] = useState(false);
const confettiRef = useRef<ConfettiRef>(null);
useEffect(() => {
// Fire side cannons for a celebratory effect
const duration = 1500;
const end = Date.now() + duration;
function frame() {
confettiRef.current?.fire({
particleCount: 4,
angle: 60,
spread: 70,
origin: { x: 0, y: 0.6 },
shapes: ["square"],
scalar: 0.8,
gravity: 0.6,
decay: 0.93,
});
confettiRef.current?.fire({
particleCount: 4,
angle: 120,
spread: 70,
origin: { x: 1, y: 0.6 },
shapes: ["square"],
scalar: 0.8,
gravity: 0.6,
decay: 0.93,
});
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}
frame();
const timer0 = setTimeout(() => {
setShowText(true);
}, 100);
const timer1 = setTimeout(() => {
setShowSubtext(true);
}, 500);
const timer2 = setTimeout(async () => {
completeStep("CONGRATS");
try {
const onboarding = await resolveResponse(getV1OnboardingState());
if (onboarding?.selectedStoreListingVersionId) {
try {
const libraryAgent = await resolveResponse(
postV2AddMarketplaceAgent({
store_listing_version_id:
onboarding.selectedStoreListingVersionId,
source: "onboarding",
}),
);
router.replace(`/library/agents/${libraryAgent.id}`);
} catch (error) {
console.error("Failed to add agent to library:", error);
router.replace("/library");
}
} else {
router.replace("/library");
}
} catch (error) {
console.error("Failed to get onboarding data:", error);
router.replace("/library");
}
}, 3000);
return () => {
clearTimeout(timer0);
clearTimeout(timer1);
clearTimeout(timer2);
};
}, [completeStep, router, api]);
return (
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
<Confetti ref={confettiRef} manualstart />
<div
className={cn(
"z-10 -mb-16 text-9xl duration-500",
showText ? "opacity-100" : "opacity-0",
)}
>
🎉
</div>
<h1
className={cn(
"font-poppins text-9xl font-medium tracking-tighter text-violet-700 duration-500",
showText ? "opacity-100" : "opacity-0",
)}
>
Congrats!
</h1>
<p
className={cn(
"mb-16 mt-4 font-poppins text-2xl font-medium text-violet-800 transition-opacity duration-500",
showSubtext ? "opacity-100" : "opacity-0",
)}
>
You earned 3$ for running your first agent
</p>
</div>
);
}

View File

@@ -1,105 +0,0 @@
import { cn } from "@/lib/utils";
import StarRating from "./StarRating";
import SmartImage from "@/components/__legacy__/SmartImage";
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
type OnboardingAgentCardProps = {
agent?: StoreAgentDetails;
selected?: boolean;
onClick: () => void;
};
export default function OnboardingAgentCard({
agent,
selected,
onClick,
}: OnboardingAgentCardProps) {
if (!agent) {
return (
<div
className={cn(
"relative animate-pulse",
"h-[394px] w-[368px] rounded-[20px] border border-transparent bg-zinc-200",
)}
/>
);
}
const {
agent_image,
creator_avatar,
agent_name,
description,
creator,
runs,
rating,
} = agent;
return (
<div
className={cn(
"relative cursor-pointer transition-all duration-200 ease-in-out",
"h-[394px] w-[368px] rounded-[20px] border border-transparent bg-white",
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
)}
onClick={onClick}
>
{/* Image container */}
<div className="relative">
<SmartImage
src={agent_image?.[0]}
alt="Agent cover"
className="m-2 h-[196px] w-[350px] rounded-[16px]"
/>
{/* Profile picture overlay */}
<div className="absolute bottom-2 left-4">
<SmartImage
src={creator_avatar}
alt="Profile picture"
className="h-[50px] w-[50px] rounded-full border border-white"
/>
</div>
</div>
{/* Content container */}
<div className="flex h-[180px] flex-col justify-between px-4 pb-3">
{/* Text content wrapper */}
<div>
{/* Title - 2 lines max */}
<p className="data-sentry-unmask text-md line-clamp-2 max-h-[50px] font-sans text-base font-medium leading-normal text-zinc-800">
{agent_name}
</p>
{/* Author - single line with truncate */}
<p className="data-sentry-unmask truncate text-sm font-normal leading-normal text-zinc-600">
by {creator}
</p>
{/* Description - 3 lines max */}
<p
className={cn(
"mt-2 line-clamp-3 text-sm leading-5",
selected ? "text-zinc-500" : "text-zinc-400",
)}
>
{description}
</p>
</div>
{/* Bottom stats */}
<div className="flex w-full items-center justify-between">
<span className="mt-1 font-sans text-sm font-medium text-zinc-800">
{runs?.toLocaleString("en-US")} runs
</span>
<StarRating rating={rating} />
</div>
</div>
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-[20px] border-2 transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</div>
);
}

View File

@@ -1,20 +0,0 @@
import { ChevronLeft } from "lucide-react";
import Link from "next/link";
interface OnboardingBackButtonProps {
href: string;
}
export default function OnboardingBackButton({
href,
}: OnboardingBackButtonProps) {
return (
<Link
className="flex items-center gap-2 font-sans text-base font-medium text-zinc-700 transition-colors duration-200 hover:text-zinc-800"
href={href}
>
<ChevronLeft size={24} className="-mr-1" />
<span>Back</span>
</Link>
);
}

View File

@@ -1,76 +0,0 @@
import { useCallback, useMemo, useState } from "react";
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
import { cn } from "@/lib/utils";
import Link from "next/link";
const variants = {
default: "bg-zinc-700 hover:bg-zinc-800",
violet: "bg-violet-600 hover:bg-violet-700",
};
type OnboardingButtonProps = {
className?: string;
variant?: keyof typeof variants;
children?: React.ReactNode;
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
href?: string;
icon?: React.ReactNode;
};
export default function OnboardingButton({
className,
variant = "default",
children,
loading,
disabled,
onClick,
href,
icon,
}: OnboardingButtonProps) {
const [internalLoading, setInternalLoading] = useState(false);
const isLoading = loading !== undefined ? loading : internalLoading;
const buttonClasses = useMemo(
() =>
cn(
"font-sans text-white text-sm font-medium",
"inline-flex justify-center items-center",
"h-12 min-w-[100px] rounded-full py-3 px-5",
"transition-colors duration-200",
className,
disabled ? "bg-zinc-300 cursor-not-allowed" : variants[variant],
),
[disabled, variant, className],
);
const onClickInternal = useCallback(() => {
setInternalLoading(true);
if (onClick) {
onClick();
}
}, [setInternalLoading, onClick]);
if (href && !disabled) {
return (
<Link href={href} onClick={onClickInternal} className={buttonClasses}>
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
{icon && !isLoading && <>{icon}</>}
{children}
</Link>
);
}
return (
<button
onClick={onClickInternal}
disabled={disabled}
className={buttonClasses}
>
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
{icon && !isLoading && <>{icon}</>}
{children}
</button>
);
}

View File

@@ -1,86 +0,0 @@
import { cn } from "@/lib/utils";
import SmartImage from "@/components/__legacy__/SmartImage";
type OnboardingGridElementProps = {
name: string;
text: string;
icon: string;
selected: boolean;
onClick: () => void;
};
function OnboardingGridElement({
name,
text,
icon,
selected,
onClick,
}: OnboardingGridElementProps) {
return (
<button
className={cn(
"relative flex h-[236px] w-[200px] flex-col items-start gap-2 rounded-xl border border-transparent bg-white p-[15px] font-sans",
"transition-all duration-200 ease-in-out",
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
)}
onClick={onClick}
>
<SmartImage
src={icon}
alt={`Logo of ${name}`}
imageContain
className="h-12 w-12 rounded-lg"
/>
<span className="text-md mt-4 w-full text-left font-medium leading-normal text-[#121212]">
{name}
</span>
<span className="w-full text-left text-[11.5px] font-normal leading-5 text-zinc-500">
{text}
</span>
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</button>
);
}
type OnboardingGridProps = {
className?: string;
elements: Array<{
name: string;
text: string;
icon: string;
}>;
selected?: string[];
onSelect: (name: string) => void;
};
export function OnboardingGrid({
className,
elements,
selected,
onSelect,
}: OnboardingGridProps) {
return (
<div
className={cn(
className,
"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4",
)}
>
{elements.map((element) => (
<OnboardingGridElement
key={element.name}
name={element.name}
text={element.text}
icon={element.icon}
selected={selected?.includes(element.name) || false}
onClick={() => onSelect(element.name)}
/>
))}
</div>
);
}

View File

@@ -1,29 +0,0 @@
import { cn } from "@/lib/utils";
interface OnboardingInputProps {
className?: string;
placeholder: string;
value: string;
onChange: (value: string) => void;
}
export default function OnboardingInput({
className,
placeholder,
value,
onChange,
}: OnboardingInputProps) {
return (
<input
className={cn(
className,
"font-poppin relative h-[50px] w-[512px] rounded-[25px] border border-transparent bg-white px-4 text-sm font-normal leading-normal text-zinc-900",
"transition-all duration-200 ease-in-out placeholder:text-zinc-400",
"focus:border-transparent focus:bg-[#F5F3FF80] focus:outline-none focus:ring-2 focus:ring-violet-700",
)}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@@ -1,135 +0,0 @@
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
type OnboardingListElementProps = {
label: string;
text: string;
selected?: boolean;
custom?: boolean;
onClick: (content: string) => void;
};
export function OnboardingListElement({
label,
text,
selected,
custom,
onClick,
}: OnboardingListElementProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [content, setContent] = useState(text);
useEffect(() => {
if (selected && custom && inputRef.current) {
inputRef.current.focus();
}
}, [selected, custom]);
const setCustomText = (e: React.ChangeEvent<HTMLInputElement>) => {
setContent(e.target.value);
onClick(e.target.value);
};
return (
<button
onClick={() => onClick(content)}
className={cn(
"relative flex h-[78px] w-[530px] items-center rounded-xl border border-transparent px-5 py-4 transition-all duration-200 ease-in-out",
selected ? "bg-[#F5F3FF80]" : "bg-white hover:border-zinc-400",
)}
>
<div className="flex w-full flex-col items-start gap-1">
<span className="text-sm font-medium text-zinc-700">{label}</span>
{custom && selected ? (
<input
ref={inputRef}
className={cn(
selected ? "text-zinc-600" : "text-zinc-400",
"font-poppin w-full border-0 bg-[#F5F3FF80] text-sm focus:outline-none",
)}
placeholder="Please specify"
value={content}
onChange={setCustomText}
/>
) : (
<span
className={cn(
selected ? "text-zinc-600" : "text-zinc-400",
"text-sm",
)}
>
{custom ? "Please specify" : text}
</span>
)}
</div>
{!custom && (
<div className="absolute right-4">
<Check
size={24}
className={cn(
"transition-all duration-200 ease-in-out",
selected ? "text-violet-700" : "text-transparent",
)}
/>
</div>
)}
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</button>
);
}
type OnboardingListProps = {
className?: string;
elements: Array<{
label: string;
text: string;
id: string;
}>;
selectedId?: string | null;
onSelect: (id: string) => void;
};
function OnboardingList({
className,
elements,
selectedId,
onSelect,
}: OnboardingListProps) {
const isCustom = useCallback(() => {
return (
selectedId !== null &&
!elements.some((element) => element.id === selectedId)
);
}, [selectedId, elements]);
return (
<div className={cn(className, "flex flex-col gap-2")}>
{elements.map((element) => (
<OnboardingListElement
key={element.id}
label={element.label}
text={element.text}
selected={element.id === selectedId}
onClick={() => onSelect(element.id)}
/>
))}
<OnboardingListElement
label="Other"
text={isCustom() ? selectedId! : ""}
selected={isCustom()}
custom
onClick={(c) => {
onSelect(c);
}}
/>
</div>
);
}
export default OnboardingList;

View File

@@ -1,45 +0,0 @@
import { useState, useEffect, useRef } from "react";
interface OnboardingProgressProps {
totalSteps: number;
toStep: number;
}
export default function OnboardingProgress({
totalSteps,
toStep,
}: OnboardingProgressProps) {
const [animatedStep, setAnimatedStep] = useState(toStep - 1);
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
// On initial mount, just set the position without animation
isInitialMount.current = false;
return;
}
// After initial mount, animate position changes
setAnimatedStep(toStep - 1);
}, [toStep]);
return (
<div className="relative flex items-center justify-center gap-3">
{/* Background circles */}
{Array.from({ length: totalSteps + 1 }).map((_, index) => (
<div key={index} className="h-2 w-2 rounded-full bg-zinc-400" />
))}
{/* Animated progress indicator */}
<div
className={`absolute left-0 h-2 w-7 rounded-full bg-zinc-400 ${
!isInitialMount.current
? "transition-all duration-300 ease-in-out"
: ""
}`}
style={{
transform: `translateX(${animatedStep * 20}px)`,
}}
/>
</div>
);
}

View File

@@ -1,66 +0,0 @@
"use client";
import { ReactNode } from "react";
import OnboardingBackButton from "./OnboardingBackButton";
import { cn } from "@/lib/utils";
import OnboardingProgress from "./OnboardingProgress";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
export function OnboardingStep({
dotted,
children,
}: {
dotted?: boolean;
children: ReactNode;
}) {
return (
<div className="relative flex min-h-screen w-full flex-col">
{dotted && (
<div className="absolute left-1/2 h-full w-1/2 bg-white bg-[radial-gradient(#e5e7eb77_1px,transparent_1px)] [background-size:10px_10px]"></div>
)}
<div className="z-10 flex flex-col items-center">{children}</div>
</div>
);
}
interface OnboardingHeaderProps {
backHref: string;
transparent?: boolean;
children?: ReactNode;
}
export function OnboardingHeader({
backHref,
transparent,
children,
}: OnboardingHeaderProps) {
const { step } = useOnboarding();
return (
<div className="sticky top-0 z-10 w-full">
<div
className={cn(transparent ? "bg-transparent" : "bg-gray-100", "pb-5")}
>
<div className="flex w-full items-center justify-between px-5 py-4">
<OnboardingBackButton href={backHref} />
<OnboardingProgress totalSteps={5} toStep={(step || 1) - 1} />
</div>
{children}
</div>
{!transparent && (
<div className="h-4 w-full bg-gradient-to-b from-gray-100 via-gray-100/50 to-transparent" />
)}
</div>
);
}
export function OnboardingFooter({ children }: { children?: ReactNode }) {
return (
<div className="sticky bottom-0 z-10 w-full">
<div className="h-4 w-full bg-gradient-to-t from-gray-100 via-gray-100/50 to-transparent" />
<div className="flex justify-center bg-gray-100">
<div className="px-5 py-5">{children}</div>
</div>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { cn } from "@/lib/utils";
import { ReactNode } from "react";
const variants = {
header: "font-poppin text-xl font-medium leading-7 text-zinc-900",
subheader: "font-sans text-sm font-medium leading-6 text-zinc-800",
default: "font-sans text-sm font-normal leading-6 text-zinc-500",
};
export function OnboardingText({
className,
center,
variant = "default",
children,
}: {
className?: string;
center?: boolean;
variant?: keyof typeof variants;
children: ReactNode;
}) {
return (
<div
className={cn(
"w-full",
center ? "text-center" : "text-left",
variants[variant] || variants.default,
className,
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,17 @@
interface Props {
currentStep: number;
totalSteps: number;
}
export function ProgressBar({ currentStep, totalSteps }: Props) {
const percent = (currentStep / totalSteps) * 100;
return (
<div className="absolute left-0 top-0 h-[0.625rem] w-full bg-neutral-300">
<div
className="h-full bg-purple-400 shadow-[0_0_4px_2px_rgba(168,85,247,0.5)] transition-all duration-500 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
interface Props {
icon: React.ReactNode;
label: string;
selected: boolean;
onClick: () => void;
className?: string;
}
export function SelectableCard({
icon,
label,
selected,
onClick,
className,
}: Props) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={selected}
className={cn(
"flex h-[9rem] w-[10.375rem] shrink-0 flex-col items-center justify-center gap-3 rounded-xl border-2 bg-white px-6 py-5 transition-all hover:shadow-sm md:shrink lg:gap-2 lg:px-10 lg:py-8",
className,
selected
? "border-purple-500 bg-purple-50 shadow-sm"
: "border-transparent",
)}
>
<Text
variant="lead"
as="span"
className={selected ? "text-neutral-900" : "text-purple-600"}
>
{icon}
</Text>
<Text variant="body-medium" as="span" className="whitespace-nowrap">
{label}
</Text>
</button>
);
}

View File

@@ -1,63 +0,0 @@
import { cn } from "@/lib/utils";
import { useMemo } from "react";
import { FaRegStar, FaStar, FaStarHalfAlt } from "react-icons/fa";
export default function StarRating({
className,
starSize,
rating,
}: {
className?: string;
starSize?: number;
rating: number;
}) {
// Round to 1 decimal place
const roundedRating = Math.round(rating * 10) / 10;
starSize ??= 15;
// Generate array of 5 star values
const stars = useMemo(
() =>
Array(5)
.fill(0)
.map((_, index) => {
const difference = roundedRating - index;
if (difference >= 1) {
return "full";
} else if (difference > 0) {
// Half star for values between 0.2 and 0.8
return difference >= 0.8
? "full"
: difference >= 0.2
? "half"
: "empty";
}
return "empty";
}),
[roundedRating],
);
return (
<div
className={cn(
"flex items-center gap-0.5 text-sm font-medium text-zinc-800",
className,
)}
>
{/* Display numerical rating */}
<span className="mr-1 mt-0.5">{roundedRating}</span>
{/* Display stars */}
{stars.map((starType, index) => {
if (starType === "full") {
return <FaStar size={starSize} key={index} />;
} else if (starType === "half") {
return <FaStarHalfAlt size={starSize} key={index} />;
} else {
return <FaRegStar size={starSize} key={index} />;
}
})}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { cn } from "@/lib/utils";
interface Props {
totalSteps: number;
currentStep: number;
}
export function StepIndicator({ totalSteps, currentStep }: Props) {
return (
<div className="flex items-center gap-2">
{Array.from({ length: totalSteps }, (_, i) => (
<div
key={i}
className={cn(
"h-2 rounded-full transition-all",
i + 1 === currentStep ? "w-6 bg-foreground" : "w-2 bg-gray-300",
)}
/>
))}
</div>
);
}

View File

@@ -6,8 +6,8 @@ export default function OnboardingLayout({
children: ReactNode;
}) {
return (
<div className="flex min-h-screen w-full items-center justify-center bg-gray-100">
<main className="mx-auto flex w-full flex-col items-center">
<div className="relative flex min-h-screen w-full flex-col bg-gray-100">
<main className="flex w-full flex-1 flex-col items-center justify-center">
{children}
</main>
</div>

View File

@@ -1,73 +1,59 @@
"use client";
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { getOnboardingStatus, resolveResponse } from "@/app/api/helpers";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { CaretLeft } from "@phosphor-icons/react";
import { ProgressBar } from "./components/ProgressBar";
import { StepIndicator } from "./components/StepIndicator";
import { PainPointsStep } from "./steps/PainPointsStep";
import { PreparingStep } from "./steps/PreparingStep";
import { RoleStep } from "./steps/RoleStep";
import { WelcomeStep } from "./steps/WelcomeStep";
import { useOnboardingWizardStore } from "./store";
import { useOnboardingPage } from "./useOnboardingPage";
export default function OnboardingPage() {
const router = useRouter();
const { currentStep, isLoading, handlePreparingComplete } =
useOnboardingPage();
const prevStep = useOnboardingWizardStore((s) => s.prevStep);
useEffect(() => {
async function redirectToStep() {
try {
// Check if onboarding is enabled (also gets chat flag for redirect)
const { shouldShowOnboarding } = await getOnboardingStatus();
if (isLoading) return null;
if (!shouldShowOnboarding) {
router.replace("/");
return;
}
const totalSteps = 4;
const showDots = currentStep <= 3;
const showBack = currentStep > 1 && currentStep <= 3;
const onboarding = await resolveResponse(getV1OnboardingState());
const showProgressBar = currentStep <= 3;
// Handle completed onboarding
if (onboarding.completedSteps.includes("GET_RESULTS")) {
router.replace("/");
return;
}
return (
<div className="flex min-h-screen w-full flex-col items-center">
{showProgressBar && (
<ProgressBar currentStep={currentStep} totalSteps={totalSteps} />
)}
// Redirect to appropriate step based on completed steps
if (onboarding.completedSteps.includes("AGENT_INPUT")) {
router.push("/onboarding/5-run");
return;
}
{showBack && (
<button
type="button"
onClick={prevStep}
className="text-md absolute left-6 top-6 flex items-center gap-1 text-zinc-500 transition-colors duration-200 hover:text-zinc-900"
>
<CaretLeft size={16} />
Back
</button>
)}
if (onboarding.completedSteps.includes("AGENT_NEW_RUN")) {
router.push("/onboarding/5-run");
return;
}
<div className="flex flex-1 items-center py-16">
{currentStep === 1 && <WelcomeStep />}
{currentStep === 2 && <RoleStep />}
{currentStep === 3 && <PainPointsStep />}
{currentStep === 4 && (
<PreparingStep onComplete={handlePreparingComplete} />
)}
</div>
if (onboarding.completedSteps.includes("AGENT_CHOICE")) {
router.push("/onboarding/5-run");
return;
}
if (onboarding.completedSteps.includes("INTEGRATIONS")) {
router.push("/onboarding/4-agent");
return;
}
if (onboarding.completedSteps.includes("USAGE_REASON")) {
router.push("/onboarding/3-services");
return;
}
if (onboarding.completedSteps.includes("WELCOME")) {
router.push("/onboarding/2-reason");
return;
}
// Default: redirect to first step
router.push("/onboarding/1-welcome");
} catch (error) {
console.error("Failed to determine onboarding step:", error);
router.replace("/");
}
}
redirectToStep();
}, [router]);
return <LoadingSpinner size="large" cover />;
{showDots && (
<div className="pb-8">
<StepIndicator totalSteps={3} currentStep={currentStep} />
</div>
)}
</div>
);
}

View File

@@ -1,33 +0,0 @@
"use client";
import { postV1ResetOnboardingProgress } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function OnboardingResetPage() {
const { toast } = useToast();
const router = useRouter();
useEffect(() => {
postV1ResetOnboardingProgress()
.then(() => {
toast({
title: "Onboarding reset successfully",
description: "You can now start the onboarding process again",
variant: "success",
});
router.push("/onboarding");
})
.catch(() => {
toast({
title: "Failed to reset onboarding",
description: "Please try again later",
variant: "destructive",
});
});
}, [toast, router]);
return <LoadingSpinner cover />;
}

View File

@@ -0,0 +1,141 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { ReactNode } from "react";
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
import { SelectableCard } from "../components/SelectableCard";
import { usePainPointsStep } from "./usePainPointsStep";
import { Emoji } from "@/components/atoms/Emoji/Emoji";
const ALL_PAIN_POINTS: { id: string; label: string; icon: ReactNode }[] = [
{
id: "Finding leads",
label: "Finding leads",
icon: <Emoji text="🔍" size={32} />,
},
{
id: "Email & outreach",
label: "Email & outreach",
icon: <Emoji text="📧" size={32} />,
},
{
id: "Reports & data",
label: "Reports & data",
icon: <Emoji text="📊" size={32} />,
},
{
id: "Customer support",
label: "Customer support",
icon: <Emoji text="💬" size={32} />,
},
{
id: "Social media",
label: "Social media",
icon: <Emoji text="📱" size={32} />,
},
{
id: "CRM & data entry",
label: "CRM & data entry",
icon: <Emoji text="📝" size={32} />,
},
{
id: "Scheduling",
label: "Scheduling",
icon: <Emoji text="🗓️" size={32} />,
},
{ id: "Research", label: "Research", icon: <Emoji text="🔬" size={32} /> },
{
id: "Something else",
label: "Something else",
icon: <Emoji text="🚩" size={32} />,
},
];
function orderPainPoints(topIDs: string[]) {
const top = topIDs
.map((id) => ALL_PAIN_POINTS.find((p) => p.id === id))
.filter((p): p is (typeof ALL_PAIN_POINTS)[number] => p != null);
const rest = ALL_PAIN_POINTS.filter(
(p) => !topIDs.includes(p.id) && p.id !== "Something else",
);
const somethingElse = ALL_PAIN_POINTS.find((p) => p.id === "Something else")!;
return [...top, ...rest, somethingElse];
}
export function PainPointsStep() {
const {
topIDs,
painPoints,
otherPainPoint,
togglePainPoint,
setOtherPainPoint,
hasSomethingElse,
canContinue,
handleLaunch,
} = usePainPointsStep();
const orderedPainPoints = orderPainPoints(topIDs);
return (
<FadeIn>
<div className="flex w-full flex-col items-center gap-12 px-4">
<div className="flex max-w-lg flex-col items-center gap-2 px-4 text-center">
<Text
variant="h3"
className="!text-[1.5rem] !leading-[2rem] md:!text-[1.75rem] md:!leading-[2.5rem]"
>
What&apos;s eating your time?
</Text>
<Text variant="lead" className="!text-zinc-500">
Pick the tasks you&apos;d love to hand off to Autopilot
</Text>
</div>
<div className="flex w-full flex-col items-center gap-4">
<div className="flex w-full max-w-[100vw] flex-nowrap gap-4 overflow-x-auto px-8 scrollbar-none md:grid md:grid-cols-3 md:overflow-hidden md:px-0">
{orderedPainPoints.map((p) => (
<SelectableCard
key={p.id}
icon={p.icon}
label={p.label}
selected={painPoints.includes(p.id)}
onClick={() => togglePainPoint(p.id)}
className="p-8"
/>
))}
</div>
{!hasSomethingElse ? (
<Text variant="small" className="!text-zinc-500">
Pick as many as you want you can always change later
</Text>
) : null}
</div>
{hasSomethingElse && (
<div className="-mb-5 w-full px-8 md:px-0">
<Input
id="other-pain-point"
label="Other pain point"
hideLabel
placeholder="What else takes up your time?"
value={otherPainPoint}
onChange={(e) => setOtherPainPoint(e.target.value)}
autoFocus
/>
</div>
)}
<Button
onClick={handleLaunch}
disabled={!canContinue}
className="w-full max-w-xs"
>
Launch Autopilot
</Button>
</div>
</FadeIn>
);
}

View File

@@ -0,0 +1,112 @@
"use client";
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
import { Text } from "@/components/atoms/Text/Text";
import { TypingText } from "@/components/molecules/TypingText/TypingText";
import { cn } from "@/lib/utils";
import { Check } from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react";
const CHECKLIST = [
"Personalizing your experience",
"Connecting automation engines",
"Building your space",
] as const;
const STEP_DURATION_MS = 4000;
const STEP_INTERVAL = STEP_DURATION_MS / CHECKLIST.length;
interface Props {
onComplete: () => void;
}
export function PreparingStep({ onComplete }: Props) {
const [started, setStarted] = useState(false);
const [completedItems, setCompletedItems] = useState(0);
const [progress, setProgress] = useState(0);
const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete;
useEffect(() => {
const timer = setTimeout(() => setStarted(true), 300);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!started) return;
const startTime = Date.now();
const progressInterval = setInterval(() => {
const elapsed = Date.now() - startTime;
const pct = Math.min(100, (elapsed / STEP_DURATION_MS) * 100);
setProgress(pct);
const items = Math.min(
CHECKLIST.length,
Math.floor(elapsed / STEP_INTERVAL) + 1,
);
setCompletedItems(items);
if (elapsed >= STEP_DURATION_MS) {
clearInterval(progressInterval);
onCompleteRef.current();
}
}, 50);
return () => clearInterval(progressInterval);
}, [started]);
return (
<div className="flex w-full max-w-md flex-col items-center gap-8 px-4">
<div className="flex flex-col items-center gap-4">
<AutoGPTLogo
className="relative right-[3rem] h-24 w-[12rem]"
hideText
/>
<Text variant="h3" className="text-center">
<TypingText
text="Preparing your workspace..."
active={started}
delay={400}
speed={60}
/>
</Text>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-purple-500 transition-all duration-100 ease-linear"
style={{ width: `${progress}%` }}
/>
</div>
<ul className="flex flex-col gap-3">
{CHECKLIST.map((item, i) => (
<li key={item} className="flex items-center gap-3">
<div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full transition-colors",
i < completedItems
? "bg-neutral-900 text-white"
: "bg-gray-200 text-gray-400",
)}
>
<Check size={14} weight="bold" />
</div>
<Text
variant="body"
as="span"
className={cn(
"transition-colors",
i < completedItems ? "!text-black" : "!text-zinc-500",
)}
>
{item}
</Text>
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,122 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
import { SelectableCard } from "../components/SelectableCard";
import { useOnboardingWizardStore } from "../store";
import { Emoji } from "@/components/atoms/Emoji/Emoji";
const IMG_SIZE = 42;
const ROLES = [
{
id: "Founder/CEO",
label: "Founder / CEO",
icon: <Emoji text="🎯" size={IMG_SIZE} />,
},
{
id: "Operations",
label: "Operations",
icon: <Emoji text="⚙️" size={IMG_SIZE} />,
},
{
id: "Sales/BD",
label: "Sales / BD",
icon: <Emoji text="📈" size={IMG_SIZE} />,
},
{
id: "Marketing",
label: "Marketing",
icon: <Emoji text="📢" size={IMG_SIZE} />,
},
{
id: "Product/PM",
label: "Product / PM",
icon: <Emoji text="🔨" size={IMG_SIZE} />,
},
{
id: "Engineering",
label: "Engineering",
icon: <Emoji text="💻" size={IMG_SIZE} />,
},
{
id: "HR/People",
label: "HR / People",
icon: <Emoji text="👤" size={IMG_SIZE} />,
},
{ id: "Other", label: "Other", icon: <Emoji text="🚩" size={IMG_SIZE} /> },
] as const;
export function RoleStep() {
const name = useOnboardingWizardStore((s) => s.name);
const role = useOnboardingWizardStore((s) => s.role);
const otherRole = useOnboardingWizardStore((s) => s.otherRole);
const setRole = useOnboardingWizardStore((s) => s.setRole);
const setOtherRole = useOnboardingWizardStore((s) => s.setOtherRole);
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
const isOther = role === "Other";
const canContinue = role && (!isOther || otherRole.trim());
function handleContinue() {
if (canContinue) {
nextStep();
}
}
return (
<FadeIn>
<div className="flex w-full flex-col items-center gap-12 px-4">
<div className="mx-auto flex w-full max-w-lg flex-col items-center gap-2 px-4 text-center">
<Text
variant="h3"
className="!text-[1.5rem] !leading-[2rem] md:!text-[1.75rem] md:!leading-[2.5rem]"
>
What best describes you, {name}?
</Text>
<Text variant="lead" className="!text-zinc-500">
Autopilot will tailor automations to your world
</Text>
</div>
<div className="flex w-full max-w-[100vw] flex-nowrap gap-4 overflow-x-auto px-8 scrollbar-none md:grid md:grid-cols-4 md:overflow-hidden md:px-0">
{ROLES.map((r) => (
<SelectableCard
key={r.id}
icon={r.icon}
label={r.label}
selected={role === r.id}
onClick={() => setRole(r.id)}
className="p-8"
/>
))}
</div>
{isOther && (
<div className="-mb-5 w-full px-8 md:px-0">
<Input
id="other-role"
label="Other role"
hideLabel
placeholder="Describe your role..."
value={otherRole}
onChange={(e) => setOtherRole(e.target.value)}
autoFocus
/>
</div>
)}
<Button
onClick={handleContinue}
disabled={!canContinue}
className="w-full max-w-xs"
>
Continue
</Button>
</div>
</FadeIn>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { Question } from "@phosphor-icons/react";
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
import { useOnboardingWizardStore } from "../store";
export function WelcomeStep() {
const name = useOnboardingWizardStore((s) => s.name);
const setName = useOnboardingWizardStore((s) => s.setName);
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (name.trim()) {
nextStep();
}
}
return (
<FadeIn>
<form
onSubmit={handleSubmit}
className="flex w-full max-w-lg flex-col items-center gap-4 px-4 md:gap-8"
>
<div className="mb-8 flex flex-col items-center gap-3 text-center md:mb-0">
<AutoGPTLogo
className="relative right-[3rem] h-24 w-[12rem]"
hideText
/>
<Text variant="h3">Welcome to AutoGPT</Text>
<Text variant="lead" as="span" className="!text-zinc-500">
Let&apos;s personalize your experience so{" "}
<span className="relative mr-3 inline-block bg-gradient-to-r from-purple-500 to-indigo-500 bg-clip-text text-transparent">
Autopilot
<span className="absolute -right-4 top-0">
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="What is Autopilot?"
className="inline-flex text-purple-500"
>
<Question size={14} />
</button>
</TooltipTrigger>
<TooltipContent>
Autopilot is AutoGPT&apos;s AI assistant that watches your
connected apps, spots repetitive tasks you do every day
and runs them for you automatically.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
</span>{" "}
can start saving you time right away
</Text>
</div>
<Input
id="first-name"
label="Your first name"
placeholder="e.g. John"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full"
autoFocus
/>
<Button
type="submit"
disabled={!name.trim()}
className="w-full max-w-xs"
>
Continue
</Button>
</form>
</FadeIn>
);
}

View File

@@ -0,0 +1,54 @@
import { useOnboardingWizardStore } from "../store";
const ROLE_TOP_PICKS: Record<string, string[]> = {
"Founder/CEO": [
"Finding leads",
"Reports & data",
"Email & outreach",
"Scheduling",
],
Operations: ["CRM & data entry", "Scheduling", "Reports & data"],
"Sales/BD": ["Finding leads", "Email & outreach", "CRM & data entry"],
Marketing: ["Social media", "Email & outreach", "Research"],
"Product/PM": ["Research", "Reports & data", "Scheduling"],
Engineering: ["Research", "Reports & data", "CRM & data entry"],
"HR/People": ["Scheduling", "Email & outreach", "CRM & data entry"],
};
export function getTopPickIDs(role: string) {
return ROLE_TOP_PICKS[role] ?? [];
}
export function usePainPointsStep() {
const role = useOnboardingWizardStore((s) => s.role);
const painPoints = useOnboardingWizardStore((s) => s.painPoints);
const otherPainPoint = useOnboardingWizardStore((s) => s.otherPainPoint);
const togglePainPoint = useOnboardingWizardStore((s) => s.togglePainPoint);
const setOtherPainPoint = useOnboardingWizardStore(
(s) => s.setOtherPainPoint,
);
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
const topIDs = getTopPickIDs(role);
const hasSomethingElse = painPoints.includes("Something else");
const canContinue =
painPoints.length > 0 &&
(!hasSomethingElse || Boolean(otherPainPoint.trim()));
function handleLaunch() {
if (canContinue) {
nextStep();
}
}
return {
topIDs,
painPoints,
otherPainPoint,
togglePainPoint,
setOtherPainPoint,
hasSomethingElse,
canContinue,
handleLaunch,
};
}

View File

@@ -0,0 +1,77 @@
import { create } from "zustand";
export type Step = 1 | 2 | 3 | 4;
interface OnboardingWizardState {
currentStep: Step;
name: string;
role: string;
otherRole: string;
painPoints: string[];
otherPainPoint: string;
setName(name: string): void;
setRole(role: string): void;
setOtherRole(otherRole: string): void;
togglePainPoint(painPoint: string): void;
setOtherPainPoint(otherPainPoint: string): void;
nextStep(): void;
prevStep(): void;
goToStep(step: Step): void;
reset(): void;
}
export const useOnboardingWizardStore = create<OnboardingWizardState>(
(set) => ({
currentStep: 1,
name: "",
role: "",
otherRole: "",
painPoints: [],
otherPainPoint: "",
setName(name) {
set({ name });
},
setRole(role) {
set({ role });
},
setOtherRole(otherRole) {
set({ otherRole });
},
togglePainPoint(painPoint) {
set((state) => {
const exists = state.painPoints.includes(painPoint);
return {
painPoints: exists
? state.painPoints.filter((p) => p !== painPoint)
: [...state.painPoints, painPoint],
};
});
},
setOtherPainPoint(otherPainPoint) {
set({ otherPainPoint });
},
nextStep() {
set((state) => ({
currentStep: Math.min(4, state.currentStep + 1) as Step,
}));
},
prevStep() {
set((state) => ({
currentStep: Math.max(1, state.currentStep - 1) as Step,
}));
},
goToStep(step) {
set({ currentStep: step });
},
reset() {
set({
currentStep: 1,
name: "",
role: "",
otherRole: "",
painPoints: [],
otherPainPoint: "",
});
},
}),
);

View File

@@ -0,0 +1,109 @@
import {
getV1OnboardingState,
postV1CompleteOnboardingStep,
postV1SubmitOnboardingProfile,
} from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { resolveResponse } from "@/app/api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { Step, useOnboardingWizardStore } from "./store";
function parseStep(value: string | null): Step {
const n = Number(value);
if (n >= 1 && n <= 4) return n as Step;
return 1;
}
export function useOnboardingPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { isLoggedIn } = useSupabase();
const currentStep = useOnboardingWizardStore((s) => s.currentStep);
const goToStep = useOnboardingWizardStore((s) => s.goToStep);
const reset = useOnboardingWizardStore((s) => s.reset);
const [isLoading, setIsLoading] = useState(true);
const hasSubmitted = useRef(false);
const hasInitialized = useRef(false);
// Initialise store from URL on mount, reset form data
useEffect(() => {
if (hasInitialized.current) return;
hasInitialized.current = true;
const urlStep = parseStep(searchParams.get("step"));
reset();
goToStep(urlStep);
}, [searchParams, reset, goToStep]);
// Sync store → URL when step changes
useEffect(() => {
const urlStep = parseStep(searchParams.get("step"));
if (currentStep !== urlStep) {
router.replace(`/onboarding?step=${currentStep}`, { scroll: false });
}
}, [currentStep, router, searchParams]);
// Check if onboarding already completed
useEffect(() => {
if (!isLoggedIn) return;
async function checkCompletion() {
try {
const onboarding = await resolveResponse(getV1OnboardingState());
if (onboarding.completedSteps.includes("VISIT_COPILOT")) {
router.replace("/copilot");
return;
}
} catch {
// If we can't check, show onboarding anyway
}
setIsLoading(false);
}
checkCompletion();
}, [isLoggedIn, router]);
// Submit profile when entering step 4
useEffect(() => {
if (currentStep !== 4 || hasSubmitted.current) return;
hasSubmitted.current = true;
const { name, role, otherRole, painPoints, otherPainPoint } =
useOnboardingWizardStore.getState();
const resolvedRole = role === "Other" ? otherRole : role;
const resolvedPainPoints = painPoints
.filter((p) => p !== "Something else")
.concat(
painPoints.includes("Something else") && otherPainPoint.trim()
? [otherPainPoint.trim()]
: [],
);
postV1SubmitOnboardingProfile({
user_name: name,
user_role: resolvedRole,
pain_points: resolvedPainPoints,
}).catch(() => {
// Best effort — profile data is non-critical for accessing copilot
});
}, [currentStep]);
async function handlePreparingComplete() {
for (let attempt = 0; attempt < 3; attempt++) {
try {
await postV1CompleteOnboardingStep({ step: "VISIT_COPILOT" });
router.replace("/copilot");
return;
} catch {
if (attempt < 2) await new Promise((r) => setTimeout(r, 1000));
}
}
router.replace("/copilot");
}
return {
currentStep,
isLoading,
handlePreparingComplete,
};
}

View File

@@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
let next = "/";
let next = "/copilot";
if (code) {
const supabase = await getServerSupabase();
@@ -25,15 +25,9 @@ export async function GET(request: Request) {
const api = new BackendAPI();
await api.createUser();
// Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus();
if (shouldShowOnboarding) {
next = "/onboarding";
revalidatePath("/onboarding", "layout");
} else {
next = "/";
revalidatePath(next, "layout");
}
next = shouldShowOnboarding ? "/onboarding" : "/copilot";
revalidatePath(next, "layout");
} catch (createUserError) {
console.error("Error creating user:", createUserError);

View File

@@ -1,13 +1,7 @@
"use client";
import { FeatureFlagPage } from "@/services/feature-flags/FeatureFlagPage";
import { Flag } from "@/services/feature-flags/use-get-flag";
import { CopilotPage } from "./CopilotPage";
export default function Page() {
return (
<FeatureFlagPage flag={Flag.CHAT} whenDisabled="/library">
<CopilotPage />
</FeatureFlagPage>
);
return <CopilotPage />;
}

View File

@@ -36,13 +36,11 @@ export async function login(email: string, password: string) {
const api = new BackendAPI();
await api.createUser();
// Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus();
const next = shouldShowOnboarding ? "/onboarding" : "/";
return {
success: true,
next,
next: shouldShowOnboarding ? "/onboarding" : "/copilot",
};
} catch (err) {
Sentry.captureException(err);

View File

@@ -112,6 +112,7 @@ export function useLoginPage() {
: "Unexpected error during login",
variant: "destructive",
});
setIsLoading(false);
setIsLoggingIn(false);
}

View File

@@ -69,11 +69,12 @@ export async function signup(
};
}
// Get onboarding status from backend (includes chat flag evaluated for this user)
const { shouldShowOnboarding } = await getOnboardingStatus();
const next = shouldShowOnboarding ? "/onboarding" : "/";
return { success: true, next };
return {
success: true,
next: shouldShowOnboarding ? "/onboarding" : "/copilot",
};
} catch (err) {
Sentry.captureException(err);
return {

View File

@@ -106,8 +106,6 @@ export function useSignupPage() {
data.agreeToTerms,
);
setIsLoading(false);
if (!result.success) {
if (result.error === "user_already_exists") {
setFeedback("User with this email already exists");
@@ -141,6 +139,10 @@ export function useSignupPage() {
: "Unexpected error during signup",
variant: "destructive",
});
} finally {
setTimeout(() => {
setIsLoading(false);
}, 3000);
}
}

View File

@@ -1,8 +1,5 @@
import type { InfiniteData } from "@tanstack/react-query";
import {
getV1IsOnboardingEnabled,
getV1OnboardingState,
} from "./__generated__/endpoints/onboarding/onboarding";
import { getV1CheckIfOnboardingIsCompleted } from "./__generated__/endpoints/onboarding/onboarding";
import { Pagination } from "./__generated__/models/pagination";
export type OKData<TResponse extends { status: number; data?: any }> =
@@ -178,10 +175,8 @@ export async function resolveResponse<
}
export async function getOnboardingStatus() {
const status = await resolveResponse(getV1IsOnboardingEnabled());
const onboarding = await resolveResponse(getV1OnboardingState());
const isCompleted = onboarding.completedSteps.includes("CONGRATS");
const status = await resolveResponse(getV1CheckIfOnboardingIsCompleted());
return {
shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted,
shouldShowOnboarding: !status.is_completed,
};
}

View File

@@ -5348,11 +5348,11 @@
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/onboarding/enabled": {
"/api/onboarding/completed": {
"get": {
"tags": ["v1", "onboarding", "public"],
"summary": "Is onboarding enabled",
"operationId": "getV1Is onboarding enabled",
"summary": "Check if onboarding is completed",
"operationId": "getV1Check if onboarding is completed",
"responses": {
"200": {
"description": "Successful Response",
@@ -5371,6 +5371,41 @@
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/onboarding/profile": {
"post": {
"tags": ["v1", "onboarding"],
"summary": "Submit onboarding profile",
"operationId": "postV1Submit onboarding profile",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OnboardingProfileRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
},
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/onboarding/reset": {
"post": {
"tags": ["v1", "onboarding"],
@@ -11182,18 +11217,40 @@
"title": "OAuthApplicationPublicInfo",
"description": "Public information about an OAuth application (for consent screen)"
},
"OnboardingStatusResponse": {
"OnboardingProfileRequest": {
"properties": {
"is_onboarding_enabled": {
"type": "boolean",
"title": "Is Onboarding Enabled"
"user_name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"title": "User Name"
},
"is_chat_enabled": { "type": "boolean", "title": "Is Chat Enabled" }
"user_role": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"title": "User Role"
},
"pain_points": {
"items": { "type": "string" },
"type": "array",
"maxItems": 20,
"title": "Pain Points"
}
},
"type": "object",
"required": ["is_onboarding_enabled", "is_chat_enabled"],
"required": ["user_name", "user_role"],
"title": "OnboardingProfileRequest",
"description": "Request body for onboarding profile submission."
},
"OnboardingStatusResponse": {
"properties": {
"is_completed": { "type": "boolean", "title": "Is Completed" }
},
"type": "object",
"required": ["is_completed"],
"title": "OnboardingStatusResponse",
"description": "Response for onboarding status check."
"description": "Response for onboarding completion check."
},
"OnboardingStep": {
"type": "string",

View File

@@ -46,14 +46,6 @@ export default async function RootLayout({
className={`${fonts.poppins.variable} ${fonts.sans.variable} ${fonts.mono.variable}`}
suppressHydrationWarning
>
<head>
<SetupAnalytics
host={host}
ga={{
gaId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
}}
/>
</head>
<body className="min-h-screen">
<ErrorBoundary context="application">
<Providers
@@ -63,6 +55,13 @@ export default async function RootLayout({
// enableSystem
disableTransitionOnChange
>
<SetupAnalytics
host={host}
ga={{
gaId:
process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
}}
/>
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
{children}
<TallyPopupSimple />

View File

@@ -0,0 +1,121 @@
interface Props extends React.SVGProps<SVGSVGElement> {
hideText?: boolean;
}
export function AutoGPTLogo({ hideText = false, className, ...props }: Props) {
return (
<svg
viewBox="0 0 89 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="AutoGPT Logo"
className={className ?? "h-10 w-[5.5rem]"}
{...props}
>
<g id="AutoGPT-logo 1" clipPath="url(#clip0_3364_2463)">
<path
id="Vector"
d="M69.1364 28.8681V38.6414C69.1364 39.3617 68.5471 39.951 67.8301 39.951C67.0541 39.951 66.4124 39.4599 66.4124 38.6414V24.0584C66.4124 20.9644 68.9236 18.4531 72.0177 18.4531C75.1117 18.4531 77.623 20.9644 77.623 24.0584C77.623 27.1525 75.1117 29.6637 72.0177 29.6637C70.9634 29.6637 69.9812 29.3723 69.1397 28.8681H69.1364ZM70.2856 22.3231C71.2417 22.3231 72.0177 23.0991 72.0177 24.0552C72.0177 25.0112 71.2417 25.7872 70.2856 25.7872C70.1088 25.7872 69.9353 25.761 69.7749 25.7119C70.2824 26.3994 71.0976 26.8447 72.0177 26.8447C73.5565 26.8447 74.8039 25.5973 74.8039 24.0584C74.8039 22.5196 73.5565 21.2721 72.0177 21.2721C71.0976 21.2721 70.2824 21.7174 69.7749 22.405C69.9353 22.3559 70.1088 22.3297 70.2856 22.3297V22.3231Z"
fill="url(#paint0_linear_3364_2463)"
/>
<path
id="Vector_2"
d="M62.133 28.8675V35.144C62.133 35.7137 61.9005 36.2343 61.524 36.6075C60.6989 37.4326 59.1699 37.4326 58.3448 36.6075C57.2611 35.5238 58.2891 33.6903 56.3509 31.752C54.4126 29.8137 51.1974 29.8694 49.318 31.752C48.4504 32.6196 47.9102 33.8212 47.9102 35.144C47.9102 35.8643 48.4995 36.4536 49.2198 36.4536C49.999 36.4536 50.6375 35.9625 50.6375 35.144C50.6375 34.5743 50.87 34.057 51.2465 33.6805C52.0716 32.8554 53.6006 32.8554 54.4257 33.6805C55.6076 34.8624 54.4126 36.5289 56.4196 38.536C58.3022 40.4186 61.5731 40.4186 63.4524 38.536C64.3201 37.6683 64.8603 36.4667 64.8603 35.144V24.0545C64.8603 20.9605 62.3491 18.4492 59.255 18.4492C56.161 18.4492 53.6497 20.9605 53.6497 24.0545C53.6497 27.1486 56.161 29.6598 59.255 29.6598C60.3093 29.6598 61.2948 29.3684 62.133 28.8642V28.8675ZM59.255 26.8441C58.335 26.8441 57.5197 26.3988 57.0122 25.7112C57.1727 25.7603 57.3462 25.7865 57.523 25.7865C58.479 25.7865 59.255 25.0106 59.255 24.0545C59.255 23.0985 58.479 22.3225 57.523 22.3225C57.3462 22.3225 57.1727 22.3487 57.0122 22.3978C57.5197 21.7103 58.335 21.265 59.255 21.265C60.7938 21.265 62.0413 22.5124 62.0413 24.0512C62.0413 25.5901 60.7938 26.8375 59.255 26.8375V26.8441Z"
fill="url(#paint1_linear_3364_2463)"
/>
<path
id="Vector_3"
d="M81.709 12.959C81.709 9.51134 80.3371 6.24048 77.9045 3.80453C75.4685 1.36858 72.1977 0 68.75 0C65.3024 0 62.0315 1.37186 59.5956 3.80453C57.1596 6.24048 55.791 9.51461 55.791 12.959V13.5451C55.791 14.2948 56.4 14.9038 57.1498 14.9038C57.8996 14.9038 58.5085 14.2948 58.5085 13.5451V12.959C58.5085 10.2349 59.5956 7.64836 61.5175 5.72645C63.4394 3.80453 66.0259 2.71425 68.75 2.71425C71.4741 2.71425 74.0574 3.80126 75.9826 5.72645C77.9045 7.64836 78.9948 10.2349 78.9948 12.959C78.9948 13.7088 79.6037 14.3178 80.3535 14.3178C81.1033 14.3178 81.7123 13.7088 81.7123 12.959H81.709Z"
fill="url(#paint2_linear_3364_2463)"
/>
<path
id="Vector_4"
d="M81.7092 17.061V18.7341H83.8963C84.6232 18.7341 85.2191 19.33 85.2191 20.0569C85.2191 20.7837 84.6952 21.4582 83.8963 21.4582H81.7092V35.1964C81.7092 35.7661 81.9417 36.2834 82.3182 36.6599C83.1433 37.485 84.6723 37.485 85.4974 36.6599C85.8739 36.2834 86.1064 35.7661 86.1064 35.1964V34.738C86.1064 33.9228 86.7481 33.4284 87.5241 33.4284C88.2444 33.4284 88.8337 34.0177 88.8337 34.738V35.1964C88.8337 36.5192 88.2935 37.7208 87.4258 38.5884C85.5432 40.471 82.2822 40.471 80.3996 38.5884C79.5319 37.7208 78.9917 36.5192 78.9917 35.1964V17.061C78.9917 16.272 79.6171 15.7383 80.3832 15.7383C81.1493 15.7383 81.706 16.3342 81.706 17.061H81.7092Z"
fill="url(#paint3_linear_3364_2463)"
/>
<path
id="Vector_5"
d="M75.4293 38.6377C75.4293 39.358 74.8399 39.9441 74.1196 39.9441C73.3436 39.9441 72.7019 39.453 72.7019 38.6377V34.2013C72.7019 33.4809 73.2912 32.8916 74.0116 32.8916C74.7875 32.8916 75.4293 33.3827 75.4293 34.2013V38.6377Z"
fill="url(#paint4_linear_3364_2463)"
/>
{!hideText && (
<path
id="Vector_6"
d="M11.7672 22.2907V31.6252H8.94164V26.9399H2.82557V31.6252H0V22.2907C0 14.5998 11.7672 14.4983 11.7672 22.2907ZM44.3808 31.6252C48.5618 31.6252 51.9506 28.2365 51.9506 24.0554C51.9506 19.8744 48.5618 16.4857 44.3808 16.4857C40.1997 16.4857 36.811 19.8744 36.811 24.0554C36.811 28.2365 40.1997 31.6252 44.3808 31.6252ZM44.3808 28.7309C41.8008 28.7309 39.7086 26.6387 39.7086 24.0587C39.7086 21.4787 41.8008 19.3865 44.3808 19.3865C46.9608 19.3865 49.053 21.4787 49.053 24.0587C49.053 26.6387 46.9608 28.7309 44.3808 28.7309ZM37.3218 16.4857V19.2097H33.2095V31.6252H30.4854V19.2097H26.3731V16.4857H37.3218ZM25.0111 25.8202V16.4857H22.1855V25.8202C22.1855 30.0242 16.0661 29.9489 16.0661 25.8202V16.4857H13.2406V25.8202C13.2406 33.5111 25.0078 33.6126 25.0078 25.8202H25.0111ZM8.94164 24.2159V22.294C8.94164 18.09 2.8223 18.1653 2.8223 22.294V24.2159H8.94164Z"
fill="#000030"
/>
)}
<path
id="Vector_7"
d="M87.4713 32.257C88.2434 32.257 88.8693 31.6311 88.8693 30.859C88.8693 30.0869 88.2434 29.4609 87.4713 29.4609C86.6992 29.4609 86.0732 30.0869 86.0732 30.859C86.0732 31.6311 86.6992 32.257 87.4713 32.257Z"
fill="#669CF6"
/>
<path
id="Vector_8"
d="M49.2167 39.9475C49.9888 39.9475 50.6147 39.3215 50.6147 38.5494C50.6147 37.7773 49.9888 37.1514 49.2167 37.1514C48.4445 37.1514 47.8186 37.7773 47.8186 38.5494C47.8186 39.3215 48.4445 39.9475 49.2167 39.9475Z"
fill="#669CF6"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_3364_2463"
x1="62.7328"
y1="20.9589"
x2="62.7328"
y2="33.2932"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#000030" />
<stop offset="1" stopColor="#9900FF" />
</linearGradient>
<linearGradient
id="paint1_linear_3364_2463"
x1="47.5336"
y1="20.947"
x2="47.5336"
y2="33.2951"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#000030" />
<stop offset="1" stopColor="#4285F4" />
</linearGradient>
<linearGradient
id="paint2_linear_3364_2463"
x1="69.4138"
y1="6.17402"
x2="48.0898"
y2="-3.94009"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#4285F4" />
<stop offset="1" stopColor="#9900FF" />
</linearGradient>
<linearGradient
id="paint3_linear_3364_2463"
x1="74.2976"
y1="15.7136"
x2="74.2976"
y2="34.5465"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#000030" />
<stop offset="1" stopColor="#4285F4" />
</linearGradient>
<linearGradient
id="paint4_linear_3364_2463"
x1="64.3579"
y1="24.1914"
x2="65.0886"
y2="30.9756"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#4285F4" />
<stop offset="1" stopColor="#9900FF" />
</linearGradient>
<clipPath id="clip0_3364_2463">
<rect width="88.8696" height="40" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { Emoji } from "./Emoji";
const meta: Meta<typeof Emoji> = {
title: "Atoms/Emoji",
tags: ["autodocs"],
component: Emoji,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Renders emoji text as cross-platform SVG images using Twemoji.",
},
},
},
argTypes: {
text: {
control: "text",
description: "Emoji character(s) to render",
},
size: {
control: { type: "number", min: 12, max: 96, step: 4 },
description: "Size in pixels (width and height)",
},
},
args: {
text: "🚀",
size: 24,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
text: "🚀",
},
};
export const Small: Story = {
args: {
text: "✨",
size: 16,
},
};
export const Large: Story = {
args: {
text: "🎉",
size: 48,
},
};
export const AllSizes: Story = {
render: renderAllSizes,
};
function renderAllSizes() {
const sizes = [16, 24, 32, 48, 64];
return (
<div className="flex items-end gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<Emoji text="🔥" size={size} />
<span className="text-xs text-muted-foreground">{size}px</span>
</div>
))}
</div>
);
}
export const MultipleEmojis: Story = {
render: renderMultipleEmojis,
};
function renderMultipleEmojis() {
const emojis = ["😀", "🎯", "⚡", "🌈", "🤖", "💡", "🔧", "📦"];
return (
<div className="flex flex-wrap gap-3">
{emojis.map((emoji) => (
<Emoji key={emoji} text={emoji} size={32} />
))}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import twemoji from "twemoji";
interface Props {
text: string;
size?: number;
}
export function Emoji({ text, size = 24 }: Props) {
return (
<span
className="inline-flex items-center"
style={{ width: size, height: size }}
dangerouslySetInnerHTML={{
// twemoji.parse only converts emoji codepoints to <img> tags
// pointing to Twitter's CDN SVGs — it does not inject arbitrary HTML
__html: twemoji.parse(text, {
folder: "svg",
ext: ".svg",
attributes: () => ({
width: String(size),
height: String(size),
}),
}),
}}
/>
);
}

View File

@@ -0,0 +1,33 @@
import { FadeIn } from "./FadeIn";
import type { Meta, StoryObj } from "@storybook/nextjs";
const meta: Meta<typeof FadeIn> = {
title: "Atoms/FadeIn",
tags: ["autodocs"],
component: FadeIn,
parameters: {
layout: "centered",
docs: {
description: {
component:
"A wrapper that fades in its children with a subtle upward slide animation using framer-motion.",
},
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<FadeIn>
<div className="rounded-lg border border-zinc-200 bg-white p-8 text-center">
<p className="text-lg font-medium">This content fades in</p>
<p className="text-sm text-zinc-500">
With a subtle upward slide animation
</p>
</div>
</FadeIn>
),
};

View File

@@ -0,0 +1,22 @@
"use client";
import { motion } from "framer-motion";
import { ReactNode } from "react";
interface Props {
children: ReactNode;
onComplete?: () => void;
}
export function FadeIn({ children, onComplete }: Props) {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
onAnimationComplete={onComplete}
>
{children}
</motion.div>
);
}

View File

@@ -225,7 +225,7 @@ export function Input({
return hideLabel ? (
inputWithError
) : (
<label htmlFor={props.id} className="flex flex-col gap-2">
<label htmlFor={props.id} className="flex w-full flex-col gap-2">
<div className="flex items-center justify-between">
<Text variant="large-medium" as="span" className="text-black">
{label}

View File

@@ -18,6 +18,7 @@ export const providerIcons: Partial<
aiml_api: fallbackIcon,
anthropic: fallbackIcon,
apollo: fallbackIcon,
database: fallbackIcon,
e2b: fallbackIcon,
github: FaGithub,
google: FaGoogle,

View File

@@ -2,14 +2,14 @@
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import { IconAutoGPTLogo, IconType } from "@/components/__legacy__/ui/icons";
import { IconType } from "@/components/__legacy__/ui/icons";
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { FeedbackButton } from "./components/FeedbackButton";
import { AgentActivityDropdown } from "./components/AgentActivityDropdown/AgentActivityDropdown";
@@ -25,7 +25,6 @@ export function Navbar() {
const breakpoint = useBreakpoint();
const isSmallScreen = breakpoint === "sm" || breakpoint === "base";
const dynamicMenuItems = getAccountMenuItems(user?.role);
const isChatEnabled = useGetFlag(Flag.CHAT);
const previewBranchName = environment.getPreviewStealingDev();
const logoutInProgress = isLogoutInProgress();
@@ -44,11 +43,9 @@ export function Navbar() {
const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName);
const homeHref = isChatEnabled === true ? "/copilot" : "/library";
const actualLoggedInLinks = [
{ name: "Home", href: homeHref },
...(isChatEnabled === true ? [{ name: "Agents", href: "/library" }] : []),
{ name: "Home", href: "/copilot" },
{ name: "Agents", href: "/library" },
...loggedInLinks,
];
@@ -88,8 +85,8 @@ export function Navbar() {
) : null}
{/* Centered logo */}
<div className="static h-auto w-[4.5rem] md:absolute md:left-1/2 md:top-1/2 md:w-[5.5rem] md:-translate-x-1/2 md:-translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
<div className="static md:absolute md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2">
<AutoGPTLogo className="h-auto w-[4.5rem] md:w-[5.5rem]" />
</div>
{/* Right section */}

View File

@@ -1,7 +1,6 @@
"use client";
import { cn } from "@/lib/utils";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { Laptop, ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -22,12 +21,10 @@ interface Props {
export function NavbarLink({ name, href }: Props) {
const pathname = usePathname();
const isChatEnabled = useGetFlag(Flag.CHAT);
const expectedHomeRoute = isChatEnabled ? "/copilot" : "/library";
const isActive =
href === expectedHomeRoute
? pathname === "/" || pathname.startsWith(expectedHomeRoute)
href === "/copilot"
? pathname === "/" || pathname.startsWith("/copilot")
: pathname.includes(href);
return (
@@ -77,24 +74,14 @@ export function NavbarLink({ name, href }: Props) {
<HomepageIcon />
</div>
)}
{href === "/library" &&
(isChatEnabled ? (
<ListChecksIcon
className={cn(
"h-5 w-5 shrink-0",
isActive && "text-white dark:text-black",
)}
/>
) : (
<div
className={cn(
iconNudgedClass,
isActive && "text-white dark:text-black",
)}
>
<HomepageIcon />
</div>
))}
{href === "/library" && (
<ListChecksIcon
className={cn(
"h-5 w-5 shrink-0",
isActive && "text-white dark:text-black",
)}
/>
)}
<Text
variant="h5"
className={cn(

View File

@@ -0,0 +1,56 @@
import { TypingText } from "./TypingText";
import type { Meta, StoryObj } from "@storybook/nextjs";
const meta: Meta<typeof TypingText> = {
title: "Molecules/TypingText",
tags: ["autodocs"],
component: TypingText,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Animates text appearing character by character with a blinking cursor. Useful for loading states and onboarding flows.",
},
},
},
argTypes: {
text: { control: "text" },
active: { control: "boolean" },
speed: { control: { type: "range", min: 10, max: 100, step: 5 } },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
text: "Personalizing your experience...",
active: true,
speed: 30,
},
};
export const Fast: Story = {
args: {
text: "This types out quickly!",
active: true,
speed: 15,
},
};
export const Slow: Story = {
args: {
text: "This types out slowly...",
active: true,
speed: 80,
},
};
export const Inactive: Story = {
args: {
text: "This text is hidden because active is false",
active: false,
},
};

View File

@@ -0,0 +1,55 @@
"use client";
import { useEffect, useState } from "react";
interface Props {
text: string;
active: boolean;
speed?: number;
delay?: number;
}
export function TypingText({ text, active, speed = 30, delay = 0 }: Props) {
const [charCount, setCharCount] = useState(0);
useEffect(() => {
if (!active) {
setCharCount(0);
return;
}
setCharCount(0);
const timeout = setTimeout(() => {
const interval = setInterval(() => {
setCharCount((prev) => {
if (prev >= text.length) {
clearInterval(interval);
return prev;
}
return prev + 1;
});
}, speed);
cleanupRef = () => clearInterval(interval);
}, delay);
let cleanupRef = () => {};
return () => {
clearTimeout(timeout);
cleanupRef();
};
}, [active, text, speed, delay]);
if (!active) return null;
return (
<>
{text.slice(0, charCount)}
{charCount < text.length && (
<span className="inline-block h-4 w-[2px] animate-pulse bg-current" />
)}
</>
);
}

View File

@@ -36,7 +36,7 @@ export function shouldRedirectFromOnboarding(
pathname: string,
): boolean {
return (
completedSteps.includes("CONGRATS") &&
completedSteps.includes("VISIT_COPILOT") &&
!pathname.startsWith("/onboarding/reset")
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import {
getV1IsOnboardingEnabled,
getV1CheckIfOnboardingIsCompleted,
getV1OnboardingState,
patchV1UpdateOnboardingState,
postV1CompleteOnboardingStep,
@@ -142,13 +142,16 @@ export default function OnboardingProvider({
async function initializeOnboarding() {
try {
// Check onboarding enabled only for onboarding routes
if (isOnOnboardingRoute) {
const enabled = await resolveResponse(getV1IsOnboardingEnabled());
if (!enabled) {
router.push("/");
return;
}
const { is_completed } = await resolveResponse(
getV1CheckIfOnboardingIsCompleted(),
);
if (!is_completed && !isOnOnboardingRoute) {
router.replace("/onboarding");
return;
} else if (is_completed && isOnOnboardingRoute) {
router.replace("/copilot");
return;
}
const onboarding = await fetchOnboarding();
@@ -158,7 +161,7 @@ export default function OnboardingProvider({
isOnOnboardingRoute &&
shouldRedirectFromOnboarding(onboarding.completedSteps, pathname)
) {
router.push("/");
router.replace("/copilot");
}
} catch (error) {
console.error("Failed to initialize onboarding:", error);

View File

@@ -13,7 +13,6 @@ export enum Flag {
AGENT_FAVORITING = "agent-favoriting",
MARKETPLACE_SEARCH_TERMS = "marketplace-search-terms",
ENABLE_PLATFORM_PAYMENT = "enable-platform-payment",
CHAT = "chat",
}
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
@@ -27,7 +26,6 @@ const defaultFlags = {
[Flag.AGENT_FAVORITING]: false,
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
[Flag.ENABLE_PLATFORM_PAYMENT]: false,
[Flag.CHAT]: false,
};
type FlagValues = typeof defaultFlags;

View File

@@ -0,0 +1,120 @@
import test, { expect } from "@playwright/test";
import { signupTestUser } from "./utils/signup";
import { completeOnboardingWizard } from "./utils/onboarding";
import { getSelectors } from "./utils/selectors";
test("new user completes full onboarding wizard", async ({ page }) => {
// Signup WITHOUT skipping onboarding (ignoreOnboarding=false)
await signupTestUser(page, undefined, undefined, false);
// Should be on onboarding
await expect(page).toHaveURL(/\/onboarding/);
// Complete the wizard
await completeOnboardingWizard(page, {
name: "Alice",
role: "Marketing",
painPoints: ["Social media", "Email & outreach"],
});
// Should have been redirected to /copilot
await expect(page).toHaveURL(/\/copilot/);
// User should be authenticated
await page
.getByTestId("profile-popout-menu-trigger")
.waitFor({ state: "visible", timeout: 10000 });
});
test("onboarding wizard step navigation works", async ({ page }) => {
await signupTestUser(page, undefined, undefined, false);
await expect(page).toHaveURL(/\/onboarding/);
// Step 1: Welcome
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
await page.getByLabel("Your first name").fill("Bob");
await page.getByRole("button", { name: "Continue" }).click();
// Step 2: Role — verify we're here, then go back
await expect(page.getByText("What best describes you")).toBeVisible();
await page.getByText("Back").click();
// Should be back on step 1 with name preserved
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
await expect(page.getByLabel("Your first name")).toHaveValue("Bob");
});
test("onboarding wizard validates required fields", async ({ page }) => {
await signupTestUser(page, undefined, undefined, false);
await expect(page).toHaveURL(/\/onboarding/);
// Step 1: Continue should be disabled without a name
const continueButton = page.getByRole("button", { name: "Continue" });
await expect(continueButton).toBeDisabled();
// Fill name — continue should become enabled
await page.getByLabel("Your first name").fill("Charlie");
await expect(continueButton).toBeEnabled();
await continueButton.click();
// Step 2: Continue should be disabled without a role
const step2Continue = page.getByRole("button", { name: "Continue" });
await expect(step2Continue).toBeDisabled();
// Select role — continue should become enabled
await page.getByText("Engineering").click();
await expect(step2Continue).toBeEnabled();
await step2Continue.click();
// Step 3: Launch Autopilot should be disabled without any pain points
const launchButton = page.getByRole("button", { name: "Launch Autopilot" });
await expect(launchButton).toBeDisabled();
// Select a pain point — button should become enabled
await page.getByText("Research", { exact: true }).click();
await expect(launchButton).toBeEnabled();
});
test("completed onboarding redirects away from /onboarding", async ({
page,
}) => {
// Create user and complete onboarding
await signupTestUser(page, undefined, undefined, false);
await completeOnboardingWizard(page);
// Try to navigate back to onboarding — should be redirected to /copilot
await page.goto("http://localhost:3000/onboarding");
await page.waitForURL(/\/copilot/, { timeout: 10000 });
});
test("onboarding URL params sync with steps", async ({ page }) => {
await signupTestUser(page, undefined, undefined, false);
await expect(page).toHaveURL(/\/onboarding/);
// Step 1: URL may or may not include step=1 on initial load (no param is equivalent to step 1)
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
// Fill name and go to step 2
await page.getByLabel("Your first name").fill("Test");
await page.getByRole("button", { name: "Continue" }).click();
// URL should show step=2
await expect(page).toHaveURL(/step=2/);
});
test("role-based pain point ordering works", async ({ page }) => {
await signupTestUser(page, undefined, undefined, false);
// Complete step 1
await page.getByLabel("Your first name").fill("Test");
await page.getByRole("button", { name: "Continue" }).click();
// Select Sales/BD role
await page.getByText("Sales / BD").click();
await page.getByRole("button", { name: "Continue" }).click();
// On pain points step, "Finding leads" should be visible (top pick for Sales)
await expect(page.getByText("What's eating your time?")).toBeVisible();
const { getText } = getSelectors(page);
await expect(getText("Finding leads")).toBeVisible();
});

View File

@@ -1,4 +1,5 @@
import { Page } from "@playwright/test";
import { skipOnboardingIfPresent } from "../utils/onboarding";
export class LoginPage {
constructor(private page: Page) {}
@@ -64,6 +65,9 @@ export class LoginPage {
await new Promise((resolve) => setTimeout(resolve, 200)); // allow time for client-side redirect
await this.page.waitForLoadState("load", { timeout: 10_000 });
// If redirected to onboarding, complete it via API so tests can proceed
await skipOnboardingIfPresent(this.page, "/marketplace");
console.log("➡️ Navigating to /marketplace ...");
await this.page.goto("/marketplace", { timeout: 20_000 });
console.log("✅ Login process complete");

View File

@@ -182,10 +182,10 @@ test("logged in user is redirected from /login to /copilot", async ({
await hasUrl(page, "/marketplace");
await page.goto("/login");
await hasUrl(page, "/library?sort=updatedAt");
await hasUrl(page, "/copilot");
});
test("logged in user is redirected from /signup to /library", async ({
test("logged in user is redirected from /signup to /copilot", async ({
page,
}) => {
const testUser = await getTestUser();
@@ -195,5 +195,5 @@ test("logged in user is redirected from /signup to /library", async ({
await hasUrl(page, "/marketplace");
await page.goto("/signup");
await hasUrl(page, "/library?sort=updatedAt");
await hasUrl(page, "/copilot");
});

View File

@@ -0,0 +1,81 @@
import { Page, expect } from "@playwright/test";
/**
* Complete the onboarding wizard via API.
* Use this when a test needs an authenticated user who has already finished onboarding
* (e.g., tests that navigate to marketplace, library, or build pages).
*
* The function sends a POST request to the onboarding completion endpoint using
* the page's request context, which inherits the browser's auth cookies.
*/
export async function completeOnboardingViaAPI(page: Page) {
await page.request.post(
"http://localhost:3000/api/proxy/api/onboarding/step?step=VISIT_COPILOT",
{ headers: { "Content-Type": "application/json" } },
);
}
/**
* Handle the onboarding redirect that occurs after login/signup.
* If the page is on /onboarding, completes onboarding via API and navigates
* to the given destination. If already past onboarding, does nothing.
*/
export async function skipOnboardingIfPresent(
page: Page,
destination: string = "/marketplace",
) {
const url = page.url();
if (!url.includes("/onboarding")) return;
await completeOnboardingViaAPI(page);
await page.goto(`http://localhost:3000${destination}`);
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
}
/**
* Walk through the full 4-step onboarding wizard in the browser.
* Returns the data that was entered so tests can verify it was submitted.
*/
export async function completeOnboardingWizard(
page: Page,
options?: {
name?: string;
role?: string;
painPoints?: string[];
},
) {
const name = options?.name ?? "TestUser";
const role = options?.role ?? "Engineering";
const painPoints = options?.painPoints ?? ["Research", "Reports & data"];
// Step 1: Welcome — enter name
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible({
timeout: 10000,
});
await page.getByLabel("Your first name").fill(name);
await page.getByRole("button", { name: "Continue" }).click();
// Step 2: Role — select a role
await expect(page.getByText("What best describes you")).toBeVisible({
timeout: 5000,
});
await page.getByText(role, { exact: false }).click();
await page.getByRole("button", { name: "Continue" }).click();
// Step 3: Pain points — select tasks
await expect(page.getByText("What's eating your time?")).toBeVisible({
timeout: 5000,
});
for (const point of painPoints) {
await page.getByText(point, { exact: true }).click();
}
await page.getByRole("button", { name: "Launch Autopilot" }).click();
// Step 4: Preparing — wait for animation to complete and redirect to /copilot
await expect(page.getByText("Preparing your workspace")).toBeVisible({
timeout: 5000,
});
await page.waitForURL(/\/copilot/, { timeout: 15000 });
return { name, role, painPoints };
}

View File

@@ -1,5 +1,6 @@
import { Page } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
import { skipOnboardingIfPresent } from "./onboarding";
type TestUser = {
email: string;
@@ -25,8 +26,15 @@ export class SigninUtils {
await this.page.goto("/login");
await this.loginPage.login(testUser.email, testUser.password);
// Verify we're on marketplace
await this.page.waitForURL("/marketplace");
// Wait for redirect — could land on /onboarding, /marketplace, or /copilot
await this.page.waitForURL(
(url: URL) =>
/\/(onboarding|marketplace|copilot|library)/.test(url.pathname),
{ timeout: 15000 },
);
// Skip onboarding if present
await skipOnboardingIfPresent(this.page, "/marketplace");
// Verify profile menu is visible (user is authenticated)
await this.page.getByTestId("profile-popout-menu-trigger").waitFor({

View File

@@ -2,6 +2,7 @@ import { TestUser } from "./auth";
import { getSelectors } from "./selectors";
import { isVisible } from "./assertion";
import { BuildPage } from "../pages/build.page";
import { skipOnboardingIfPresent } from "./onboarding";
export async function signupTestUser(
page: any,
@@ -58,18 +59,15 @@ export async function signupTestUser(
// Handle onboarding redirect if needed
if (currentUrl.includes("/onboarding") && ignoreOnboarding) {
await page.goto("http://localhost:3000/marketplace");
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
await skipOnboardingIfPresent(page, "/marketplace");
}
// Verify we're on an expected final page and user is authenticated
if (currentUrl.includes("/copilot") || currentUrl.includes("/library")) {
// For copilot/library landing pages, just verify user is authenticated
await page
.getByTestId("profile-popout-menu-trigger")
.waitFor({ state: "visible", timeout: 10000 });
} else if (ignoreOnboarding || currentUrl.includes("/marketplace")) {
// Verify we're on marketplace
await page
.getByText(
"Bringing you AI agents designed by thinkers from around the world",
@@ -77,7 +75,6 @@ export async function signupTestUser(
.first()
.waitFor({ state: "visible", timeout: 10000 });
// Verify user is authenticated (profile menu visible)
await page
.getByTestId("profile-popout-menu-trigger")
.waitFor({ state: "visible", timeout: 10000 });

View File

@@ -8,5 +8,16 @@ export default defineConfig({
environment: "happy-dom",
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
setupFiles: ["./src/tests/integrations/vitest.setup.tsx"],
coverage: {
provider: "v8",
reporter: ["text", "cobertura"],
reportsDirectory: "./coverage",
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.stories.{ts,tsx}",
"src/tests/**",
],
},
},
});

View File

@@ -1,12 +1,15 @@
[flake8]
max-line-length = 88
extend-ignore = E203
exclude =
.tox,
__pycache__,
*.pyc,
.env
venv*/*,
.venv/*,
reports/*,
dist/*,
data/*,
.env,
venv*,
.venv,
reports,
dist,
data,
.benchmark_workspaces,
.autogpt,

291
classic/CLAUDE.md Normal file
View File

@@ -0,0 +1,291 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
AutoGPT Classic is an experimental, **unsupported** project demonstrating autonomous GPT-4 operation. Dependencies will not be updated, and the codebase contains known vulnerabilities. This is preserved for educational/historical purposes.
## Repository Structure
```
classic/
├── pyproject.toml # Single consolidated Poetry project
├── poetry.lock # Single lock file
├── forge/
│ └── forge/ # Core agent framework package
├── original_autogpt/
│ └── autogpt/ # AutoGPT agent package
├── direct_benchmark/
│ └── direct_benchmark/ # Benchmark harness package
└── benchmark/ # Challenge definitions (data, not code)
```
All packages are managed by a single `pyproject.toml` at the classic/ root.
## Common Commands
### Setup & Install
```bash
# Install everything from classic/ directory
cd classic
poetry install
```
### Running Agents
```bash
# Run forge agent
poetry run python -m forge
# Run original autogpt server
poetry run serve --debug
# Run autogpt CLI
poetry run autogpt
```
Agents run on `http://localhost:8000` by default.
### Benchmarking
```bash
# Run benchmarks
poetry run direct-benchmark run
# Run specific strategies and models
poetry run direct-benchmark run \
--strategies one_shot,rewoo \
--models claude \
--parallel 4
# Run a single test
poetry run direct-benchmark run --tests ReadFile
# List available commands
poetry run direct-benchmark --help
```
### Testing
```bash
poetry run pytest # All tests
poetry run pytest forge/tests/ # Forge tests only
poetry run pytest original_autogpt/tests/ # AutoGPT tests only
poetry run pytest -k test_name # Single test by name
poetry run pytest path/to/test.py # Specific test file
poetry run pytest --cov # With coverage
```
### Linting & Formatting
Run from the classic/ directory:
```bash
# Format everything (recommended to run together)
poetry run black . && poetry run isort .
# Check formatting (CI-style, no changes)
poetry run black --check . && poetry run isort --check-only .
# Lint
poetry run flake8 # Style linting
# Type check
poetry run pyright # Type checking (some errors are expected in infrastructure code)
```
Note: Always run linters over the entire directory, not specific files, for best results.
## Architecture
### Forge (Core Framework)
The `forge` package is the foundation that other components depend on:
- `forge/agent/` - Agent implementation and protocols
- `forge/llm/` - Multi-provider LLM integrations (OpenAI, Anthropic, Groq, LiteLLM)
- `forge/components/` - Reusable agent components
- `forge/file_storage/` - File system abstraction
- `forge/config/` - Configuration management
### Original AutoGPT
- `original_autogpt/autogpt/app/` - CLI application entry points
- `original_autogpt/autogpt/agents/` - Agent implementations
- `original_autogpt/autogpt/agent_factory/` - Agent creation logic
### Direct Benchmark
Benchmark harness for testing agent performance:
- `direct_benchmark/direct_benchmark/` - CLI and harness code
- `benchmark/agbenchmark/challenges/` - Test cases organized by category (code, retrieval, data, etc.)
- Reports generated in `direct_benchmark/reports/`
### Package Structure
All three packages are included in a single Poetry project. Imports are fully qualified:
- `from forge.agent.base import BaseAgent`
- `from autogpt.agents.agent import Agent`
- `from direct_benchmark.harness import BenchmarkHarness`
## Code Style
- Python 3.12 target
- Line length: 88 characters (Black default)
- Black for formatting, isort for imports (profile="black")
- Type hints with Pyright checking
## Testing Patterns
- Async support via pytest-asyncio
- Fixtures defined in `conftest.py` files provide: `tmp_project_root`, `storage`, `config`, `llm_provider`, `agent`
- Tests requiring API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY) will skip if not set
## Environment Setup
Copy `.env.example` to `.env` in the relevant directory and add your API keys:
```bash
cp .env.example .env
# Edit .env with your OPENAI_API_KEY, etc.
```
## Workspaces
Agents operate within a **workspace** - a directory containing all agent data and files. The workspace root defaults to the current working directory.
### Workspace Structure
```
{workspace}/
├── .autogpt/
│ ├── autogpt.yaml # Workspace-level permissions
│ ├── ap_server.db # Agent Protocol database (server mode)
│ └── agents/
│ └── AutoGPT-{agent_id}/
│ ├── state.json # Agent profile, directives, action history
│ ├── permissions.yaml # Agent-specific permission overrides
│ └── workspace/ # Agent's sandboxed working directory
```
### Key Concepts
- **Multiple agents** can coexist in the same workspace (each gets its own subdirectory)
- **File access** is sandboxed to the agent's `workspace/` directory by default
- **State persistence** - agent state saves to `state.json` and survives across sessions
- **Storage backends** - supports local filesystem, S3, and GCS (via `FILE_STORAGE_BACKEND` env var)
### Specifying a Workspace
```bash
# Default: uses current directory
cd /path/to/my/project && poetry run autogpt
# Or specify explicitly via CLI (if supported)
poetry run autogpt --workspace /path/to/workspace
```
## Settings Location
Configuration uses a **layered system** with three levels (in order of precedence):
### 1. Environment Variables (Global)
Loaded from `.env` file in the working directory:
```bash
# Required
OPENAI_API_KEY=sk-...
# Optional LLM settings
SMART_LLM=gpt-4o # Model for complex reasoning
FAST_LLM=gpt-4o-mini # Model for simple tasks
EMBEDDING_MODEL=text-embedding-3-small
# Optional search providers (for web search component)
TAVILY_API_KEY=tvly-...
SERPER_API_KEY=...
GOOGLE_API_KEY=...
GOOGLE_CUSTOM_SEARCH_ENGINE_ID=...
# Optional infrastructure
LOG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR
DATABASE_STRING=sqlite:///agent.db # Agent Protocol database
PORT=8000 # Server port
FILE_STORAGE_BACKEND=local # local, s3, or gcs
```
### 2. Workspace Settings (`{workspace}/.autogpt/autogpt.yaml`)
Workspace-wide permissions that apply to **all agents** in this workspace:
```yaml
allow:
- read_file({workspace}/**)
- write_to_file({workspace}/**)
- list_folder({workspace}/**)
- web_search(*)
deny:
- read_file(**.env)
- read_file(**.env.*)
- read_file(**.key)
- read_file(**.pem)
- execute_shell(rm -rf:*)
- execute_shell(sudo:*)
```
Auto-generated with sensible defaults if missing.
### 3. Agent Settings (`{workspace}/.autogpt/agents/{id}/permissions.yaml`)
Agent-specific permission overrides:
```yaml
allow:
- execute_python(*)
- web_search(*)
deny:
- execute_shell(*)
```
## Permissions
The permission system uses **pattern matching** with a **first-match-wins** evaluation order.
### Permission Check Order
1. Agent deny list → **Block**
2. Workspace deny list → **Block**
3. Agent allow list → **Allow**
4. Workspace allow list → **Allow**
5. Session denied list → **Block** (commands denied during this session)
6. **Prompt user** → Interactive approval (if in interactive mode)
### Pattern Syntax
Format: `command_name(glob_pattern)`
| Pattern | Description |
|---------|-------------|
| `read_file({workspace}/**)` | Read any file in workspace (recursive) |
| `write_to_file({workspace}/*.txt)` | Write only .txt files in workspace root |
| `execute_shell(python:**)` | Execute Python commands only |
| `execute_shell(git:*)` | Execute any git command |
| `web_search(*)` | Allow all web searches |
Special tokens:
- `{workspace}` - Replaced with actual workspace path
- `**` - Matches any path including `/`
- `*` - Matches any characters except `/`
### Interactive Approval Scopes
When prompted for permission, users can choose:
| Scope | Effect |
|-------|--------|
| **Once** | Allow this one time only (not saved) |
| **Agent** | Always allow for this agent (saves to agent `permissions.yaml`) |
| **Workspace** | Always allow for all agents (saves to `autogpt.yaml`) |
| **Deny** | Deny this command (saves to appropriate deny list) |
### Default Security
Out of the box, the following are **denied by default**:
- Reading sensitive files (`.env`, `.key`, `.pem`)
- Destructive shell commands (`rm -rf`, `sudo`)
- Operations outside the workspace directory

View File

@@ -1,182 +0,0 @@
## CLI Documentation
This document describes how to interact with the project's CLI (Command Line Interface). It includes the types of outputs you can expect from each command. Note that the `agents stop` command will terminate any process running on port 8000.
### 1. Entry Point for the CLI
Running the `./run` command without any parameters will display the help message, which provides a list of available commands and options. Additionally, you can append `--help` to any command to view help information specific to that command.
```sh
./run
```
**Output**:
```
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
agent Commands to create, start and stop agents
benchmark Commands to start the benchmark and list tests and categories
setup Installs dependencies needed for your system.
```
If you need assistance with any command, simply add the `--help` parameter to the end of your command, like so:
```sh
./run COMMAND --help
```
This will display a detailed help message regarding that specific command, including a list of any additional options and arguments it accepts.
### 2. Setup Command
```sh
./run setup
```
**Output**:
```
Setup initiated
Installation has been completed.
```
This command initializes the setup of the project.
### 3. Agents Commands
**a. List All Agents**
```sh
./run agent list
```
**Output**:
```
Available agents: 🤖
🐙 forge
🐙 autogpt
```
Lists all the available agents.
**b. Create a New Agent**
```sh
./run agent create my_agent
```
**Output**:
```
🎉 New agent 'my_agent' created and switched to the new directory in agents folder.
```
Creates a new agent named 'my_agent'.
**c. Start an Agent**
```sh
./run agent start my_agent
```
**Output**:
```
... (ASCII Art representing the agent startup)
[Date and Time] [forge.sdk.db] [DEBUG] 🐛 Initializing AgentDB with database_string: sqlite:///agent.db
[Date and Time] [forge.sdk.agent] [INFO] 📝 Agent server starting on http://0.0.0.0:8000
```
Starts the 'my_agent' and displays startup ASCII art and logs.
**d. Stop an Agent**
```sh
./run agent stop
```
**Output**:
```
Agent stopped
```
Stops the running agent.
### 4. Benchmark Commands
**a. List Benchmark Categories**
```sh
./run benchmark categories list
```
**Output**:
```
Available categories: 📚
📖 code
📖 safety
📖 memory
... (and so on)
```
Lists all available benchmark categories.
**b. List Benchmark Tests**
```sh
./run benchmark tests list
```
**Output**:
```
Available tests: 📚
📖 interface
🔬 Search - TestSearch
🔬 Write File - TestWriteFile
... (and so on)
```
Lists all available benchmark tests.
**c. Show Details of a Benchmark Test**
```sh
./run benchmark tests details TestWriteFile
```
**Output**:
```
TestWriteFile
-------------
Category: interface
Task: Write the word 'Washington' to a .txt file
... (and other details)
```
Displays the details of the 'TestWriteFile' benchmark test.
**d. Start Benchmark for the Agent**
```sh
./run benchmark start my_agent
```
**Output**:
```
(more details about the testing process shown whilst the test are running)
============= 13 failed, 1 passed in 0.97s ============...
```
Displays the results of the benchmark tests on 'my_agent'.

View File

@@ -2,7 +2,7 @@
ARG BUILD_TYPE=dev
# Use an official Python base image from the Docker Hub
FROM python:3.10-slim AS autogpt-base
FROM python:3.12-slim AS autogpt-base
# Install browsers
RUN apt-get update && apt-get install -y \
@@ -28,14 +28,13 @@ RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN poetry config installer.max-workers 10
WORKDIR /app/autogpt
COPY original_autogpt/pyproject.toml original_autogpt/poetry.lock ./
WORKDIR /app
COPY pyproject.toml poetry.lock README.md ./
# Include forge so it can be used as a path dependency
COPY forge/ ../forge
# Include frontend
COPY frontend/ ../frontend
# Include all package directories so poetry install can resolve path dependencies
COPY forge/ ./forge/
COPY original_autogpt/ ./original_autogpt/
COPY direct_benchmark/ ./direct_benchmark/
# Set the entrypoint
ENTRYPOINT ["poetry", "run", "autogpt"]
@@ -45,15 +44,12 @@ CMD []
FROM autogpt-base AS autogpt-dev
RUN poetry install --no-cache --no-root \
&& rm -rf $(poetry env info --path)/src
ONBUILD COPY original_autogpt/ ./
ONBUILD RUN mkdir -p ./data
# release build -> include bare minimum
FROM autogpt-base AS autogpt-release
RUN poetry install --no-cache --no-root --without dev \
&& rm -rf $(poetry env info --path)/src
ONBUILD COPY original_autogpt/ ./autogpt
ONBUILD COPY original_autogpt/README.md ./README.md
ONBUILD RUN mkdir -p ./data
FROM autogpt-${BUILD_TYPE} AS autogpt

View File

@@ -1,173 +0,0 @@
# Quickstart Guide
> For the complete getting started [tutorial series](https://aiedge.medium.com/autogpt-forge-e3de53cc58ec) <- click here
Welcome to the Quickstart Guide! This guide will walk you through setting up, building, and running your own AutoGPT agent. Whether you're a seasoned AI developer or just starting out, this guide will provide you with the steps to jumpstart your journey in AI development with AutoGPT.
## System Requirements
This project supports Linux (Debian-based), Mac, and Windows Subsystem for Linux (WSL). If you use a Windows system, you must install WSL. You can find the installation instructions for WSL [here](https://learn.microsoft.com/en-us/windows/wsl/).
## Getting Setup
1. **Fork the Repository**
To fork the repository, follow these steps:
- Navigate to the main page of the repository.
![Repository](../docs/content/imgs/quickstart/001_repo.png)
- In the top-right corner of the page, click Fork.
![Create Fork UI](../docs/content/imgs/quickstart/002_fork.png)
- On the next page, select your GitHub account to create the fork.
- Wait for the forking process to complete. You now have a copy of the repository in your GitHub account.
2. **Clone the Repository**
To clone the repository, you need to have Git installed on your system. If you don't have Git installed, download it from [here](https://git-scm.com/downloads). Once you have Git installed, follow these steps:
- Open your terminal.
- Navigate to the directory where you want to clone the repository.
- Run the git clone command for the fork you just created
![Clone the Repository](../docs/content/imgs/quickstart/003_clone.png)
- Then open your project in your ide
![Open the Project in your IDE](../docs/content/imgs/quickstart/004_ide.png)
4. **Setup the Project**
Next, we need to set up the required dependencies. We have a tool to help you perform all the tasks on the repo.
It can be accessed by running the `run` command by typing `./run` in the terminal.
The first command you need to use is `./run setup.` This will guide you through setting up your system.
Initially, you will get instructions for installing Flutter and Chrome and setting up your GitHub access token like the following image:
![Setup the Project](../docs/content/imgs/quickstart/005_setup.png)
### For Windows Users
If you're a Windows user and experience issues after installing WSL, follow the steps below to resolve them.
#### Update WSL
Run the following command in Powershell or Command Prompt:
1. Enable the optional WSL and Virtual Machine Platform components.
2. Download and install the latest Linux kernel.
3. Set WSL 2 as the default.
4. Download and install the Ubuntu Linux distribution (a reboot may be required).
```shell
wsl --install
```
For more detailed information and additional steps, refer to [Microsoft's WSL Setup Environment Documentation](https://learn.microsoft.com/en-us/windows/wsl/setup/environment).
#### Resolve FileNotFoundError or "No such file or directory" Errors
When you run `./run setup`, if you encounter errors like `No such file or directory` or `FileNotFoundError`, it might be because Windows-style line endings (CRLF - Carriage Return Line Feed) are not compatible with Unix/Linux style line endings (LF - Line Feed).
To resolve this, you can use the `dos2unix` utility to convert the line endings in your script from CRLF to LF. Heres how to install and run `dos2unix` on the script:
```shell
sudo apt update
sudo apt install dos2unix
dos2unix ./run
```
After executing the above commands, running `./run setup` should work successfully.
#### Store Project Files within the WSL File System
If you continue to experience issues, consider storing your project files within the WSL file system instead of the Windows file system. This method avoids path translations and permissions issues and provides a more consistent development environment.
You can keep running the command to get feedback on where you are up to with your setup.
When setup has been completed, the command will return an output like this:
![Setup Complete](../docs/content/imgs/quickstart/006_setup_complete.png)
## Creating Your Agent
After completing the setup, the next step is to create your agent template.
Execute the command `./run agent create YOUR_AGENT_NAME`, where `YOUR_AGENT_NAME` should be replaced with your chosen name.
Tips for naming your agent:
* Give it its own unique name, or name it after yourself
* Include an important aspect of your agent in the name, such as its purpose
Examples: `SwiftyosAssistant`, `PwutsPRAgent`, `MySuperAgent`
![Create an Agent](../docs/content/imgs/quickstart/007_create_agent.png)
## Running your Agent
Your agent can be started using the command: `./run agent start YOUR_AGENT_NAME`
This starts the agent on the URL: `http://localhost:8000/`
![Start the Agent](../docs/content/imgs/quickstart/009_start_agent.png)
The front end can be accessed from `http://localhost:8000/`; first, you must log in using either a Google account or your GitHub account.
![Login](../docs/content/imgs/quickstart/010_login.png)
Upon logging in, you will get a page that looks something like this: your task history down the left-hand side of the page, and the 'chat' window to send tasks to your agent.
![Login](../docs/content/imgs/quickstart/011_home.png)
When you have finished with your agent or just need to restart it, use Ctl-C to end the session. Then, you can re-run the start command.
If you are having issues and want to ensure the agent has been stopped, there is a `./run agent stop` command, which will kill the process using port 8000, which should be the agent.
## Benchmarking your Agent
The benchmarking system can also be accessed using the CLI too:
```bash
agpt % ./run benchmark
Usage: cli.py benchmark [OPTIONS] COMMAND [ARGS]...
Commands to start the benchmark and list tests and categories
Options:
--help Show this message and exit.
Commands:
categories Benchmark categories group command
start Starts the benchmark command
tests Benchmark tests group command
agpt % ./run benchmark categories
Usage: cli.py benchmark categories [OPTIONS] COMMAND [ARGS]...
Benchmark categories group command
Options:
--help Show this message and exit.
Commands:
list List benchmark categories command
agpt % ./run benchmark tests
Usage: cli.py benchmark tests [OPTIONS] COMMAND [ARGS]...
Benchmark tests group command
Options:
--help Show this message and exit.
Commands:
details Benchmark test details command
list List benchmark tests command
```
The benchmark has been split into different categories of skills you can test your agent on. You can see what categories are available with
```bash
./run benchmark categories list
# And what tests are available with
./run benchmark tests list
```
![Login](../docs/content/imgs/quickstart/012_tests.png)
Finally, you can run the benchmark with
```bash
./run benchmark start YOUR_AGENT_NAME
```
>

View File

@@ -4,7 +4,7 @@ AutoGPT Classic was an experimental project to demonstrate autonomous GPT-4 oper
## Project Status
⚠️ **This project is unsupported, and dependencies will not be updated. It was an experiment that has concluded its initial research phase. If you want to use AutoGPT, you should use the [AutoGPT Platform](/autogpt_platform)**
**This project is unsupported, and dependencies will not be updated.** It was an experiment that has concluded its initial research phase. If you want to use AutoGPT, you should use the [AutoGPT Platform](/autogpt_platform).
For those interested in autonomous AI agents, we recommend exploring more actively maintained alternatives or referring to this codebase for educational purposes only.
@@ -16,37 +16,171 @@ AutoGPT Classic was one of the first implementations of autonomous AI agents - A
- Learn from the results and adjust its approach
- Chain multiple actions together to achieve an objective
## Key Features
- 🔄 Autonomous task chaining
- 🛠 Tool and API integration capabilities
- 💾 Memory management for context retention
- 🔍 Web browsing and information gathering
- 📝 File operations and content creation
- 🔄 Self-prompting and task breakdown
## Structure
The project is organized into several key components:
- `/benchmark` - Performance testing tools
- `/forge` - Core autonomous agent framework
- `/frontend` - User interface components
- `/original_autogpt` - Original implementation
```
classic/
├── pyproject.toml # Single consolidated Poetry project
├── poetry.lock # Single lock file
├── forge/ # Core autonomous agent framework
├── original_autogpt/ # Original implementation
├── direct_benchmark/ # Benchmark harness
└── benchmark/ # Challenge definitions (data)
```
## Getting Started
While this project is no longer actively maintained, you can still explore the codebase:
### Prerequisites
- Python 3.12+
- [Poetry](https://python-poetry.org/docs/#installation)
### Installation
1. Clone the repository:
```bash
# Clone the repository
git clone https://github.com/Significant-Gravitas/AutoGPT.git
cd classic
# Install everything
poetry install
```
2. Review the documentation:
- For reference, see the [documentation](https://docs.agpt.co). You can browse at the same point in time as this commit so the docs don't change.
- Check `CLI-USAGE.md` for command-line interface details
- Refer to `TROUBLESHOOTING.md` for common issues
### Configuration
Configuration uses a layered system:
1. **Environment variables** (`.env` file)
2. **Workspace settings** (`.autogpt/autogpt.yaml`)
3. **Agent settings** (`.autogpt/agents/{id}/permissions.yaml`)
Copy the example environment file and add your API keys:
```bash
cp .env.example .env
```
Key environment variables:
```bash
# Required
OPENAI_API_KEY=sk-...
# Optional LLM settings
SMART_LLM=gpt-4o # Model for complex reasoning
FAST_LLM=gpt-4o-mini # Model for simple tasks
# Optional search providers
TAVILY_API_KEY=tvly-...
SERPER_API_KEY=...
# Optional infrastructure
LOG_LEVEL=DEBUG
PORT=8000
FILE_STORAGE_BACKEND=local # local, s3, or gcs
```
### Running
All commands run from the `classic/` directory:
```bash
# Run forge agent
poetry run python -m forge
# Run original autogpt server
poetry run serve --debug
# Run autogpt CLI
poetry run autogpt
```
Agents run on `http://localhost:8000` by default.
### Benchmarking
```bash
poetry run direct-benchmark run
```
### Testing
```bash
poetry run pytest # All tests
poetry run pytest forge/tests/ # Forge tests only
poetry run pytest original_autogpt/tests/ # AutoGPT tests only
```
## Workspaces
Agents operate within a **workspace** directory that contains all agent data and files:
```
{workspace}/
├── .autogpt/
│ ├── autogpt.yaml # Workspace-level permissions
│ ├── ap_server.db # Agent Protocol database (server mode)
│ └── agents/
│ └── AutoGPT-{agent_id}/
│ ├── state.json # Agent profile, directives, history
│ ├── permissions.yaml # Agent-specific permissions
│ └── workspace/ # Agent's sandboxed working directory
```
- The workspace defaults to the current working directory
- Multiple agents can coexist in the same workspace
- Agent file access is sandboxed to their `workspace/` subdirectory
- State persists across sessions via `state.json`
## Permissions
AutoGPT uses a **layered permission system** with pattern matching:
### Permission Files
| File | Scope | Location |
|------|-------|----------|
| `autogpt.yaml` | All agents in workspace | `.autogpt/autogpt.yaml` |
| `permissions.yaml` | Single agent | `.autogpt/agents/{id}/permissions.yaml` |
### Permission Format
```yaml
allow:
- read_file({workspace}/**) # Read any file in workspace
- write_to_file({workspace}/**) # Write any file in workspace
- web_search(*) # All web searches
deny:
- read_file(**.env) # Block .env files
- execute_shell(sudo:*) # Block sudo commands
```
### Check Order (First Match Wins)
1. Agent deny → Block
2. Workspace deny → Block
3. Agent allow → Allow
4. Workspace allow → Allow
5. Prompt user → Interactive approval
### Interactive Approval
When prompted, users can approve commands with different scopes:
- **Once** - Allow this one time only
- **Agent** - Always allow for this agent
- **Workspace** - Always allow for all agents
- **Deny** - Block this command
### Default Security
Denied by default:
- Sensitive files (`.env`, `.key`, `.pem`)
- Destructive commands (`rm -rf`, `sudo`)
- Operations outside the workspace
## Security Notice
This codebase has **known vulnerabilities** and issues with its dependencies. It will not be updated to new dependencies. Use for educational purposes only.
## License
@@ -55,27 +189,3 @@ This project segment is licensed under the MIT License - see the [LICENSE](LICEN
## Documentation
Please refer to the [documentation](https://docs.agpt.co) for more detailed information about the project's architecture and concepts.
You can browse at the same point in time as this commit so the docs don't change.
## Historical Impact
AutoGPT Classic played a significant role in advancing the field of autonomous AI agents:
- Demonstrated practical implementation of AI autonomy
- Inspired numerous derivative projects and research
- Contributed to the development of AI agent architectures
- Helped identify key challenges in AI autonomy
## Security Notice
If you're studying this codebase, please understand this has KNOWN vulnerabilities and issues with its dependencies. It will not be updated to new dependencies.
## Community & Support
While active development has concluded:
- The codebase remains available for study and reference
- Historical discussions can be found in project issues
- Related research and developments continue in the broader AI agent community
## Acknowledgments
Thanks to all contributors who participated in this experimental project and helped advance the field of autonomous AI agents.

View File

@@ -1,4 +0,0 @@
AGENT_NAME=mini-agi
REPORTS_FOLDER="reports/mini-agi"
OPENAI_API_KEY="sk-" # for LLM eval
BUILD_SKILL_TREE=false # set to true to build the skill tree.

View File

@@ -1,12 +0,0 @@
[flake8]
max-line-length = 88
# Ignore rules that conflict with Black code style
extend-ignore = E203, W503
exclude =
__pycache__/,
*.pyc,
.pytest_cache/,
venv*/,
.venv/,
reports/,
agbenchmark/reports/,

View File

@@ -1,174 +0,0 @@
agbenchmark_config/workspace/
backend/backend_stdout.txt
reports/df*.pkl
reports/raw*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.DS_Store
```
secrets.json
agbenchmark_config/challenges_already_beaten.json
agbenchmark_config/challenges/pri_*
agbenchmark_config/updates.json
agbenchmark_config/reports/*
agbenchmark_config/reports/success_rate.json
agbenchmark_config/reports/regression_tests.json

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