diff --git a/.dockerignore b/.dockerignore index d7a007348296..94bf1742f1c4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,11 +9,13 @@ # Platform - Backend !autogpt_platform/backend/backend/ +!autogpt_platform/backend/test/e2e_test_data.py !autogpt_platform/backend/migrations/ !autogpt_platform/backend/schema.prisma !autogpt_platform/backend/pyproject.toml !autogpt_platform/backend/poetry.lock !autogpt_platform/backend/README.md +!autogpt_platform/backend/.env # Platform - Market !autogpt_platform/market/market/ @@ -26,6 +28,7 @@ # Platform - Frontend !autogpt_platform/frontend/src/ !autogpt_platform/frontend/public/ +!autogpt_platform/frontend/scripts/ !autogpt_platform/frontend/package.json !autogpt_platform/frontend/pnpm-lock.yaml !autogpt_platform/frontend/tsconfig.json @@ -33,6 +36,7 @@ ## config !autogpt_platform/frontend/*.config.* !autogpt_platform/frontend/.env.* +!autogpt_platform/frontend/.env # Classic - AutoGPT !classic/original_autogpt/autogpt/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9b348b557da0..2e37f67766ae 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,7 +24,8 @@ #### For configuration changes: -- [ ] `.env.example` is updated or already compatible with my changes + +- [ ] `.env.default` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..a0834a691321 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,244 @@ +# GitHub Copilot Instructions for AutoGPT + +This file provides comprehensive onboarding information for GitHub Copilot coding agent to work efficiently with the AutoGPT repository. + +## Repository Overview + +**AutoGPT** is a powerful platform for creating, deploying, and managing continuous AI agents that automate complex workflows. This is a large monorepo (~150MB) containing multiple components: + +- **AutoGPT Platform** (`autogpt_platform/`) - Main focus: Modern AI agent platform (Polyform Shield License) +- **Classic AutoGPT** (`classic/`) - Legacy agent system (MIT License) +- **Documentation** (`docs/`) - MkDocs-based documentation site +- **Infrastructure** - Docker configurations, CI/CD, and development tools + +**Primary Languages & Frameworks:** +- **Backend**: Python 3.10-3.13, FastAPI, Prisma ORM, PostgreSQL, RabbitMQ +- **Frontend**: TypeScript, Next.js 15, React, Tailwind CSS, Radix UI +- **Development**: Docker, Poetry, pnpm, Playwright, Storybook + +## Build and Validation Instructions + +### Essential Setup Commands + +**Always run these commands in the correct directory and in this order:** + +1. **Initial Setup** (required once): + ```bash + # Clone and enter repository + git clone && cd AutoGPT + + # Start all services (database, redis, rabbitmq, clamav) + cd autogpt_platform && docker compose --profile local up deps --build --detach + ``` + +2. **Backend Setup** (always run before backend development): + ```bash + cd autogpt_platform/backend + poetry install # Install dependencies + poetry run prisma migrate dev # Run database migrations + poetry run prisma generate # Generate Prisma client + ``` + +3. **Frontend Setup** (always run before frontend development): + ```bash + cd autogpt_platform/frontend + pnpm install # Install dependencies + ``` + +### Runtime Requirements + +**Critical:** Always ensure Docker services are running before starting development: +```bash +cd autogpt_platform && docker compose --profile local up deps --build --detach +``` + +**Python Version:** Use Python 3.11 (required; managed by Poetry via pyproject.toml) +**Node.js Version:** Use Node.js 21+ with pnpm package manager + +### Development Commands + +**Backend Development:** +```bash +cd autogpt_platform/backend +poetry run serve # Start development server (port 8000) +poetry run test # Run all tests (requires ~5 minutes) +poetry run pytest path/to/test.py # Run specific test +poetry run format # Format code (Black + isort) - always run first +poetry run lint # Lint code (ruff) - run after format +``` + +**Frontend Development:** +```bash +cd autogpt_platform/frontend +pnpm dev # Start development server (port 3000) - use for active development +pnpm build # Build for production (only needed for E2E tests or deployment) +pnpm test # Run Playwright E2E tests (requires build first) +pnpm test-ui # Run tests with UI +pnpm format # Format and lint code +pnpm storybook # Start component development server +``` + +### Testing Strategy + +**Backend Tests:** +- **Block Tests**: `poetry run pytest backend/blocks/test/test_block.py -xvs` (validates all blocks) +- **Specific Block**: `poetry run pytest 'backend/blocks/test/test_block.py::test_available_blocks[BlockName]' -xvs` +- **Snapshot Tests**: Use `--snapshot-update` when output changes, always review with `git diff` + +**Frontend Tests:** +- **E2E Tests**: Always run `pnpm dev` before `pnpm test` (Playwright requires running instance) +- **Component Tests**: Use Storybook for isolated component development + +### Critical Validation Steps + +**Before committing changes:** +1. Run `poetry run format` (backend) and `pnpm format` (frontend) +2. Ensure all tests pass in modified areas +3. Verify Docker services are still running +4. Check that database migrations apply cleanly + +**Common Issues & Workarounds:** +- **Prisma issues**: Run `poetry run prisma generate` after schema changes +- **Permission errors**: Ensure Docker has proper permissions +- **Port conflicts**: Check the `docker-compose.yml` file for the current list of exposed ports. You can list all mapped ports with: +- **Test timeouts**: Backend tests can take 5+ minutes, use `-x` flag to stop on first failure + +## Project Layout & Architecture + +### Core Architecture + +**AutoGPT Platform** (`autogpt_platform/`): +- `backend/` - FastAPI server with async support + - `backend/backend/` - Core API logic + - `backend/blocks/` - Agent execution blocks + - `backend/data/` - Database models and schemas + - `schema.prisma` - Database schema definition +- `frontend/` - Next.js application + - `src/app/` - App Router pages and layouts + - `src/components/` - Reusable React components + - `src/lib/` - Utilities and configurations +- `autogpt_libs/` - Shared Python utilities +- `docker-compose.yml` - Development stack orchestration + +**Key Configuration Files:** +- `pyproject.toml` - Python dependencies and tooling +- `package.json` - Node.js dependencies and scripts +- `schema.prisma` - Database schema and migrations +- `next.config.mjs` - Next.js configuration +- `tailwind.config.ts` - Styling configuration + +### Security & Middleware + +**Cache Protection**: Backend includes middleware preventing sensitive data caching in browsers/proxies +**Authentication**: JWT-based with Supabase integration +**User ID Validation**: All data access requires user ID checks - verify this for any `data/*.py` changes + +### Development Workflow + +**GitHub Actions**: Multiple CI/CD workflows in `.github/workflows/` +- `platform-backend-ci.yml` - Backend testing and validation +- `platform-frontend-ci.yml` - Frontend testing and validation +- `platform-fullstack-ci.yml` - End-to-end integration tests + +**Pre-commit Hooks**: Run linting and formatting checks +**Conventional Commits**: Use format `type(scope): description` (e.g., `feat(backend): add API`) + +### Key Source Files + +**Backend Entry Points:** +- `backend/backend/server/server.py` - FastAPI application setup +- `backend/backend/data/` - Database models and user management +- `backend/blocks/` - Agent execution blocks and logic + +**Frontend Entry Points:** +- `frontend/src/app/layout.tsx` - Root application layout +- `frontend/src/app/page.tsx` - Home page +- `frontend/src/lib/supabase/` - Authentication and database client + +**Protected Routes**: Update `frontend/lib/supabase/middleware.ts` when adding protected routes + +### Agent Block System + +Agents are built using a visual block-based system where each block performs a single action. Blocks are defined in `backend/blocks/` and must include: +- Block definition with input/output schemas +- Execution logic with proper error handling +- Tests validating functionality + +### Database & ORM + +**Prisma ORM** with PostgreSQL backend including pgvector for embeddings: +- Schema in `schema.prisma` +- Migrations in `backend/migrations/` +- Always run `prisma migrate dev` and `prisma generate` after schema changes + +## Environment Configuration + +### Configuration Files Priority Order +1. **Backend**: `/backend/.env.default` → `/backend/.env` (user overrides) +2. **Frontend**: `/frontend/.env.default` → `/frontend/.env` (user overrides) +3. **Platform**: `/.env.default` (Supabase/shared) → `/.env` (user overrides) +4. Docker Compose `environment:` sections override file-based config +5. Shell environment variables have highest precedence + +### Docker Environment Setup +- All services use hardcoded defaults (no `${VARIABLE}` substitutions) +- The `env_file` directive loads variables INTO containers at runtime +- Backend/Frontend services use YAML anchors for consistent configuration +- Copy `.env.default` files to `.env` for local development customization + +## Advanced Development Patterns + +### Adding New Blocks +1. Create file in `/backend/backend/blocks/` +2. Inherit from `Block` base class with input/output schemas +3. Implement `run` method with proper error handling +4. Generate block UUID using `uuid.uuid4()` +5. Register in block registry +6. Write tests alongside block implementation +7. Consider how inputs/outputs connect with other blocks in graph editor + +### API Development +1. Update routes in `/backend/backend/server/routers/` +2. Add/update Pydantic models in same directory +3. Write tests alongside route files +4. For `data/*.py` changes, validate user ID checks +5. Run `poetry run test` to verify changes + +### Frontend Development +1. Components in `/frontend/src/components/` +2. Use existing UI components from `/frontend/src/components/ui/` +3. Add Storybook stories for component development +4. Test user-facing features with Playwright E2E tests +5. Update protected routes in middleware when needed + +### Security Guidelines +**Cache Protection Middleware** (`/backend/backend/server/middleware/security.py`): +- Default: Disables caching for ALL endpoints with `Cache-Control: no-store, no-cache, must-revalidate, private` +- Uses allow list approach for cacheable paths (static assets, health checks, public pages) +- Prevents sensitive data caching in browsers/proxies +- Add new cacheable endpoints to `CACHEABLE_PATHS` + +### CI/CD Alignment +The repository has comprehensive CI workflows that test: +- **Backend**: Python 3.11-3.13, services (Redis/RabbitMQ/ClamAV), Prisma migrations, Poetry lock validation +- **Frontend**: Node.js 21, pnpm, Playwright with Docker Compose stack, API schema validation +- **Integration**: Full-stack type checking and E2E testing + +Match these patterns when developing locally - the copilot setup environment mirrors these CI configurations. + +## Collaboration with Other AI Assistants + +This repository is actively developed with assistance from Claude (via CLAUDE.md files). When working on this codebase: +- Check for existing CLAUDE.md files that provide additional context +- Follow established patterns and conventions already in the codebase +- Maintain consistency with existing code style and architecture +- Consider that changes may be reviewed and extended by both human developers and AI assistants + +## Trust These Instructions + +These instructions are comprehensive and tested. Only perform additional searches if: +1. Information here is incomplete for your specific task +2. You encounter errors not covered by the workarounds +3. You need to understand implementation details not covered above + +For detailed platform development patterns, refer to `autogpt_platform/CLAUDE.md` and `AGENTS.md` in the repository root. \ No newline at end of file diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000000..7af1ec436523 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,302 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + timeout-minutes: 45 + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + # Backend Python/Poetry setup (mirrors platform-backend-ci.yml) + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" # Use standard version matching CI + + - name: Set up Python dependency cache + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/backend/poetry.lock') }} + + - name: Install Poetry + run: | + # Extract Poetry version from backend/poetry.lock (matches CI) + cd autogpt_platform/backend + HEAD_POETRY_VERSION=$(python3 ../../.github/workflows/scripts/get_package_version_from_lockfile.py poetry) + echo "Found Poetry version ${HEAD_POETRY_VERSION} in backend/poetry.lock" + + # Install Poetry + curl -sSL https://install.python-poetry.org | POETRY_VERSION=$HEAD_POETRY_VERSION python3 - + + # Add Poetry to PATH + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Check poetry.lock + working-directory: autogpt_platform/backend + run: | + poetry lock + if ! git diff --quiet --ignore-matching-lines="^# " poetry.lock; then + echo "Warning: poetry.lock not up to date, but continuing for setup" + git checkout poetry.lock # Reset for clean setup + fi + + - name: Install Python dependencies + working-directory: autogpt_platform/backend + run: poetry install + + - name: Generate Prisma Client + working-directory: autogpt_platform/backend + run: poetry run prisma generate + + # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" + + - name: Enable corepack + run: corepack enable + + - name: Set pnpm store directory + run: | + pnpm config set store-dir ~/.pnpm-store + echo "PNPM_HOME=$HOME/.pnpm-store" >> $GITHUB_ENV + + - name: Cache frontend dependencies + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml', 'autogpt_platform/frontend/package.json') }} + restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} + ${{ runner.os }}-pnpm- + + - name: Install JavaScript dependencies + working-directory: autogpt_platform/frontend + run: pnpm install --frozen-lockfile + + # Install Playwright browsers for frontend testing + # NOTE: Disabled to save ~1 minute of setup time. Re-enable if Copilot needs browser automation (e.g., for MCP) + # - name: Install Playwright browsers + # working-directory: autogpt_platform/frontend + # run: pnpm playwright install --with-deps chromium + + # Docker setup for development environment + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Copy default environment files + working-directory: autogpt_platform + run: | + # Copy default environment files for development + cp .env.default .env + cp backend/.env.default backend/.env + cp frontend/.env.default frontend/.env + + # Phase 1: Cache and load Docker images for faster setup + - name: Set up Docker image cache + id: docker-cache + uses: actions/cache@v4 + with: + path: ~/docker-cache + # Use a versioned key for cache invalidation when image list changes + key: docker-images-v2-${{ runner.os }}-${{ hashFiles('.github/workflows/copilot-setup-steps.yml') }} + restore-keys: | + docker-images-v2-${{ runner.os }}- + docker-images-v1-${{ runner.os }}- + + - name: Load or pull Docker images + working-directory: autogpt_platform + run: | + mkdir -p ~/docker-cache + + # Define image list for easy maintenance + IMAGES=( + "redis:latest" + "rabbitmq:management" + "clamav/clamav-debian:latest" + "busybox:latest" + "kong:2.8.1" + "supabase/gotrue:v2.170.0" + "supabase/postgres:15.8.1.049" + "supabase/postgres-meta:v0.86.1" + "supabase/studio:20250224-d10db0f" + ) + + # Check if any cached tar files exist (more reliable than cache-hit) + if ls ~/docker-cache/*.tar 1> /dev/null 2>&1; then + echo "Docker cache found, loading images in parallel..." + for image in "${IMAGES[@]}"; do + # Convert image name to filename (replace : and / with -) + filename=$(echo "$image" | tr ':/' '--') + if [ -f ~/docker-cache/${filename}.tar ]; then + echo "Loading $image..." + docker load -i ~/docker-cache/${filename}.tar || echo "Warning: Failed to load $image from cache" & + fi + done + wait + echo "All cached images loaded" + else + echo "No Docker cache found, pulling images in parallel..." + # Pull all images in parallel + for image in "${IMAGES[@]}"; do + docker pull "$image" & + done + wait + + # Only save cache on main branches (not PRs) to avoid cache pollution + if [[ "${{ github.ref }}" == "refs/heads/master" ]] || [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "Saving Docker images to cache in parallel..." + for image in "${IMAGES[@]}"; do + # Convert image name to filename (replace : and / with -) + filename=$(echo "$image" | tr ':/' '--') + echo "Saving $image..." + docker save -o ~/docker-cache/${filename}.tar "$image" || echo "Warning: Failed to save $image" & + done + wait + echo "Docker image cache saved" + else + echo "Skipping cache save for PR/feature branch" + fi + fi + + echo "Docker images ready for use" + + # Phase 2: Build migrate service with GitHub Actions cache + - name: Build migrate Docker image with cache + working-directory: autogpt_platform + run: | + # Build the migrate image with buildx for GHA caching + docker buildx build \ + --cache-from type=gha \ + --cache-to type=gha,mode=max \ + --target migrate \ + --tag autogpt_platform-migrate:latest \ + --load \ + -f backend/Dockerfile \ + .. + + # Start services using pre-built images + - name: Start Docker services for development + working-directory: autogpt_platform + run: | + # Start essential services (migrate image already built with correct tag) + docker compose --profile local up deps --no-build --detach + echo "Waiting for services to be ready..." + + # Wait for database to be ready + echo "Checking database readiness..." + timeout 30 sh -c 'until docker compose exec -T db pg_isready -U postgres 2>/dev/null; do + echo " Waiting for database..." + sleep 2 + done' && echo "✅ Database is ready" || echo "⚠️ Database ready check timeout after 30s, continuing..." + + # Check migrate service status + echo "Checking migration status..." + docker compose ps migrate || echo " Migrate service not visible in ps output" + + # Wait for migrate service to complete + echo "Waiting for migrations to complete..." + timeout 30 bash -c ' + ATTEMPTS=0 + while [ $ATTEMPTS -lt 15 ]; do + ATTEMPTS=$((ATTEMPTS + 1)) + + # Check using docker directly (more reliable than docker compose ps) + CONTAINER_STATUS=$(docker ps -a --filter "label=com.docker.compose.service=migrate" --format "{{.Status}}" | head -1) + + if [ -z "$CONTAINER_STATUS" ]; then + echo " Attempt $ATTEMPTS: Migrate container not found yet..." + elif echo "$CONTAINER_STATUS" | grep -q "Exited (0)"; then + echo "✅ Migrations completed successfully" + docker compose logs migrate --tail=5 2>/dev/null || true + exit 0 + elif echo "$CONTAINER_STATUS" | grep -q "Exited ([1-9]"; then + EXIT_CODE=$(echo "$CONTAINER_STATUS" | grep -oE "Exited \([0-9]+\)" | grep -oE "[0-9]+") + echo "❌ Migrations failed with exit code: $EXIT_CODE" + echo "Migration logs:" + docker compose logs migrate --tail=20 2>/dev/null || true + exit 1 + elif echo "$CONTAINER_STATUS" | grep -q "Up"; then + echo " Attempt $ATTEMPTS: Migrate container is running... ($CONTAINER_STATUS)" + else + echo " Attempt $ATTEMPTS: Migrate container status: $CONTAINER_STATUS" + fi + + sleep 2 + done + + echo "⚠️ Timeout: Could not determine migration status after 30 seconds" + echo "Final container check:" + docker ps -a --filter "label=com.docker.compose.service=migrate" || true + echo "Migration logs (if available):" + docker compose logs migrate --tail=10 2>/dev/null || echo " No logs available" + ' || echo "⚠️ Migration check completed with warnings, continuing..." + + # Brief wait for other services to stabilize + echo "Waiting 5 seconds for other services to stabilize..." + sleep 5 + + # Verify installations and provide environment info + - name: Verify setup and show environment info + run: | + echo "=== Python Setup ===" + python --version + poetry --version + + echo "=== Node.js Setup ===" + node --version + pnpm --version + + echo "=== Additional Tools ===" + docker --version + docker compose version + gh --version || true + + echo "=== Services Status ===" + cd autogpt_platform + docker compose ps || true + + echo "=== Backend Dependencies ===" + cd backend + poetry show | head -10 || true + + echo "=== Frontend Dependencies ===" + cd ../frontend + pnpm list --depth=0 | head -10 || true + + echo "=== Environment Files ===" + ls -la ../.env* || true + ls -la .env* || true + ls -la ../backend/.env* || true + + echo "✅ AutoGPT Platform development environment setup complete!" + echo "🚀 Ready for development with Docker services running" + echo "📝 Backend server: poetry run serve (port 8000)" + echo "🌐 Frontend server: pnpm dev (port 3000)" \ No newline at end of file diff --git a/.github/workflows/platform-backend-ci.yml b/.github/workflows/platform-backend-ci.yml index 3492a00ea9a3..98a99de43f4a 100644 --- a/.github/workflows/platform-backend-ci.yml +++ b/.github/workflows/platform-backend-ci.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] + python-version: ["3.11", "3.12", "3.13"] runs-on: ubuntu-latest services: diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 749e5232c0c9..e9d720a4fb99 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -37,7 +37,7 @@ jobs: - name: Generate cache key id: cache-key - run: echo "key=${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}" >> $GITHUB_OUTPUT + run: echo "key=${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml', 'autogpt_platform/frontend/package.json') }}" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 @@ -45,6 +45,7 @@ jobs: path: ~/.pnpm-store key: ${{ steps.cache-key.outputs.key }} restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} ${{ runner.os }}-pnpm- - name: Install dependencies @@ -72,6 +73,7 @@ jobs: path: ~/.pnpm-store key: ${{ needs.setup.outputs.cache-key }} restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} ${{ runner.os }}-pnpm- - name: Install dependencies @@ -80,36 +82,6 @@ jobs: - name: Run lint run: pnpm lint - type-check: - runs-on: ubuntu-latest - needs: setup - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "21" - - - name: Enable corepack - run: corepack enable - - - name: Restore dependencies cache - uses: actions/cache@v4 - with: - path: ~/.pnpm-store - key: ${{ needs.setup.outputs.cache-key }} - restore-keys: | - ${{ runner.os }}-pnpm- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run tsc check - run: pnpm type-check - chromatic: runs-on: ubuntu-latest needs: setup @@ -136,6 +108,7 @@ jobs: path: ~/.pnpm-store key: ${{ needs.setup.outputs.cache-key }} restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} ${{ runner.os }}-pnpm- - name: Install dependencies @@ -151,7 +124,7 @@ jobs: exitOnceUploaded: true test: - runs-on: ubuntu-latest + runs-on: big-boi needs: setup strategy: fail-fast: false @@ -170,23 +143,67 @@ jobs: - name: Enable corepack run: corepack enable - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - large-packages: false # slow - docker-images: false # limited benefit - - name: Copy default supabase .env run: | - cp ../.env.example ../.env + cp ../.env.default ../.env - - name: Copy backend .env - run: | - cp ../backend/.env.example ../backend/.env + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-frontend-test-${{ hashFiles('autogpt_platform/docker-compose.yml', 'autogpt_platform/backend/Dockerfile', 'autogpt_platform/backend/pyproject.toml', 'autogpt_platform/backend/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-buildx-frontend-test- - name: Run docker compose run: | docker compose -f ../docker-compose.yml up -d + env: + DOCKER_BUILDKIT: 1 + BUILDX_CACHE_FROM: type=local,src=/tmp/.buildx-cache + BUILDX_CACHE_TO: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + if [ -d "/tmp/.buildx-cache-new" ]; then + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + fi + + - name: Wait for services to be ready + run: | + echo "Waiting for rest_server to be ready..." + timeout 60 sh -c 'until curl -f http://localhost:8006/health 2>/dev/null; do sleep 2; done' || echo "Rest server health check timeout, continuing..." + echo "Waiting for database to be ready..." + timeout 60 sh -c 'until docker compose -f ../docker-compose.yml exec -T db pg_isready -U postgres 2>/dev/null; do sleep 2; done' || echo "Database ready check timeout, continuing..." + + - name: Create E2E test data + run: | + echo "Creating E2E test data..." + # First try to run the script from inside the container + if docker compose -f ../docker-compose.yml exec -T rest_server test -f /app/autogpt_platform/backend/test/e2e_test_data.py; then + echo "✅ Found e2e_test_data.py in container, running it..." + docker compose -f ../docker-compose.yml exec -T rest_server sh -c "cd /app/autogpt_platform && python backend/test/e2e_test_data.py" || { + echo "❌ E2E test data creation failed!" + docker compose -f ../docker-compose.yml logs --tail=50 rest_server + exit 1 + } + else + echo "⚠️ e2e_test_data.py not found in container, copying and running..." + # Copy the script into the container and run it + docker cp ../backend/test/e2e_test_data.py $(docker compose -f ../docker-compose.yml ps -q rest_server):/tmp/e2e_test_data.py || { + echo "❌ Failed to copy script to container" + exit 1 + } + docker compose -f ../docker-compose.yml exec -T rest_server sh -c "cd /app/autogpt_platform && python /tmp/e2e_test_data.py" || { + echo "❌ E2E test data creation failed!" + docker compose -f ../docker-compose.yml logs --tail=50 rest_server + exit 1 + } + fi - name: Restore dependencies cache uses: actions/cache@v4 @@ -194,31 +211,25 @@ jobs: path: ~/.pnpm-store key: ${{ needs.setup.outputs.cache-key }} restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} ${{ runner.os }}-pnpm- - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Setup .env - run: cp .env.example .env - - - name: Build frontend - run: pnpm build --turbo - # uses Turbopack, much faster and safe enough for a test pipeline - - name: Install Browser 'chromium' run: pnpm playwright install --with-deps chromium - name: Run Playwright tests run: pnpm test:no-build + - name: Upload Playwright artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report + - name: Print Final Docker Compose logs if: always() run: docker compose -f ../docker-compose.yml logs - - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report-${{ matrix.browser }} - path: playwright-report/ - retention-days: 30 diff --git a/.github/workflows/platform-fullstack-ci.yml b/.github/workflows/platform-fullstack-ci.yml new file mode 100644 index 000000000000..d98a6598e0f9 --- /dev/null +++ b/.github/workflows/platform-fullstack-ci.yml @@ -0,0 +1,132 @@ +name: AutoGPT Platform - Frontend CI + +on: + push: + branches: [master, dev] + paths: + - ".github/workflows/platform-fullstack-ci.yml" + - "autogpt_platform/**" + pull_request: + paths: + - ".github/workflows/platform-fullstack-ci.yml" + - "autogpt_platform/**" + merge_group: + +defaults: + run: + shell: bash + working-directory: autogpt_platform/frontend + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" + + - name: Enable corepack + run: corepack enable + + - name: Generate cache key + id: cache-key + run: echo "key=${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml', 'autogpt_platform/frontend/package.json') }}" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + types: + runs-on: ubuntu-latest + needs: setup + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" + + - name: Enable corepack + run: corepack enable + + - name: Copy default supabase .env + run: | + cp ../.env.default ../.env + + - name: Copy backend .env + run: | + cp ../backend/.env.default ../backend/.env + + - name: Run docker compose + run: | + docker compose -f ../docker-compose.yml --profile local --profile deps_backend up -d + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ needs.setup.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup .env + run: cp .env.default .env + + - name: Wait for services to be ready + run: | + echo "Waiting for rest_server to be ready..." + timeout 60 sh -c 'until curl -f http://localhost:8006/health 2>/dev/null; do sleep 2; done' || echo "Rest server health check timeout, continuing..." + echo "Waiting for database to be ready..." + timeout 60 sh -c 'until docker compose -f ../docker-compose.yml exec -T db pg_isready -U postgres 2>/dev/null; do sleep 2; done' || echo "Database ready check timeout, continuing..." + + - name: Generate API queries + run: pnpm generate:api:force + + - name: Check for API schema changes + run: | + if ! git diff --exit-code src/app/api/openapi.json; then + echo "❌ API schema changes detected in src/app/api/openapi.json" + echo "" + echo "The openapi.json file has been modified after running 'pnpm generate:api-all'." + echo "This usually means changes have been made in the BE endpoints without updating the Frontend." + echo "The API schema is now out of sync with the Front-end queries." + echo "" + echo "To fix this:" + echo "1. Pull the backend 'docker compose pull && docker compose up -d --build --force-recreate'" + echo "2. Run 'pnpm generate:api' locally" + echo "3. Run 'pnpm types' locally" + echo "4. Fix any TypeScript errors that may have been introduced" + echo "5. Commit and push your changes" + echo "" + exit 1 + else + echo "✅ No API schema changes detected" + fi + + - name: Run Typescript checks + run: pnpm types diff --git a/.gitignore b/.gitignore index 1067dd921c15..15160be56ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ classic/original_autogpt/*.json auto_gpt_workspace/* *.mpeg .env +# Root .env files +/.env azure.yaml .vscode .idea/* @@ -121,7 +123,6 @@ celerybeat.pid # Environments .direnv/ -.env .venv env/ venv*/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 325ab9dcffec..39ec659ef9e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -235,7 +235,7 @@ repos: hooks: - id: tsc name: Typecheck - AutoGPT Platform - Frontend - entry: bash -c 'cd autogpt_platform/frontend && pnpm type-check' + entry: bash -c 'cd autogpt_platform/frontend && pnpm types' files: ^autogpt_platform/frontend/ types: [file] language: system diff --git a/LICENSE b/LICENSE index 52c6e9a8d528..f141042fa4c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,197 @@ -All portions of this repository are under one of two licenses. The majority of the AutoGPT repository is under the MIT License below. The autogpt_platform folder is under the -Polyform Shield License. +All portions of this repository are under one of two licenses. +- Everything inside the autogpt_platform folder is under the Polyform Shield License. +- Everything outside the autogpt_platform folder is under the MIT License. + +More info: + +**Polyform Shield License:** +Code and content within the `autogpt_platform` folder is licensed under the Polyform Shield License. This new project is our in-developlemt platform for building, deploying and managing agents. +Read more about this effort here: https://agpt.co/blog/introducing-the-autogpt-platform + +**MIT License:** +All other portions of the AutoGPT repository (i.e., everything outside the `autogpt_platform` folder) are licensed under the MIT License. This includes: +- The Original, stand-alone AutoGPT Agent +- Forge: https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/forge +- AG Benchmark: https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/benchmark +- AutoGPT Classic GUI: https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/frontend. + +We also publish additional work under the MIT Licence in other repositories, such as GravitasML (https://github.com/Significant-Gravitas/gravitasml) which is developed for and used in the AutoGPT Platform, and our [Code Ability](https://github.com/Significant-Gravitas/AutoGPT-Code-Ability) project. + +Both licences are available to read below: + +===================================================== +----------------------------------------------------- +===================================================== + +# PolyForm Shield License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncompete + +Any purpose is a permitted purpose, except for providing any +product that competes with the software or any product the +licensor or any of its affiliates provides using the software. + +## Competition + +Goods and services compete even when they provide functionality +through different kinds of interfaces or for different technical +platforms. Applications can compete with services, libraries +with plugins, frameworks with development tools, and so on, +even if they're written in different programming languages +or for different computer architectures. Goods and services +compete even when provided free of charge. If you market a +product as a practical substitute for the software or another +product, it definitely competes. + +## New Products + +If you are using the software to provide a product that does +not compete, but the licensor or any of its affiliates brings +your product into competition by providing a new version of +the software or another product using the software, you may +continue using versions of the software available under these +terms beforehand to provide your competing product, but not +any later versions. + +## Discontinued Products + +You may begin using the software to compete with a product +or service that the licensor or any of its affiliates has +stopped providing, unless the licensor includes a plain-text +line beginning with `Licensor Line of Business:` with the +software that mentions that line of business. For example: + +> Licensor Line of Business: YoyodyneCMS Content Management +System (http://example.com/cms) + +## Sales of Business + +If the licensor or any of its affiliates sells a line of +business developing the software or using the software +to provide a product, the buyer can also enforce +[Noncompete](#noncompete) for that product. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +A **product** can be a good or service, or a combination +of them. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +its affiliates. + +**Affiliates** means the other organizations than an +organization has control over, is under the control of, or is +under common control with. + +**Control** means ownership of substantially all the assets of +an entity, or the power to direct its management and policies +by vote, contract, or otherwise. Control can be direct or +indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. + +===================================================== +----------------------------------------------------- +===================================================== MIT License diff --git a/README.md b/README.md index 5044d2a21207..3572fe318be3 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,23 @@ [![Discord Follow](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fautogpt%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&label=total%20members&logo=discord&logoColor=white&color=7289da)](https://discord.gg/autogpt)   [![Twitter Follow](https://img.shields.io/twitter/follow/Auto_GPT?style=social)](https://twitter.com/Auto_GPT)   + +[Deutsch](https://zdoc.app/de/Significant-Gravitas/AutoGPT) | +[Español](https://zdoc.app/es/Significant-Gravitas/AutoGPT) | +[français](https://zdoc.app/fr/Significant-Gravitas/AutoGPT) | +[日本語](https://zdoc.app/ja/Significant-Gravitas/AutoGPT) | +[한국어](https://zdoc.app/ko/Significant-Gravitas/AutoGPT) | +[Português](https://zdoc.app/pt/Significant-Gravitas/AutoGPT) | +[Русский](https://zdoc.app/ru/Significant-Gravitas/AutoGPT) | +[中文](https://zdoc.app/zh/Significant-Gravitas/AutoGPT) + **AutoGPT** is a powerful platform that allows you to create, deploy, and manage continuous AI agents that automate complex workflows. ## Hosting Options - - Download to self-host - - [Join the Waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta + - Download to self-host (Free!) + - [Join the Waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta (Closed Beta - Public release Coming Soon!) -## How to Setup for Self-Hosting +## How to Self-Host the AutoGPT Platform > [!NOTE] > Setting up and hosting the AutoGPT Platform yourself is a technical process. > If you'd rather something that just works, we recommend [joining the waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta. @@ -113,7 +123,17 @@ Here are two examples of what you can do with AutoGPT: These examples show just a glimpse of what you can achieve with AutoGPT! You can create customized workflows to build agents for any use case. --- -### Mission and Licencing + +### **License Overview:** + +🛡️ **Polyform Shield License:** +All code and content within the `autogpt_platform` folder is licensed under the Polyform Shield License. This new project is our in-developlemt platform for building, deploying and managing agents.
_[Read more about this effort](https://agpt.co/blog/introducing-the-autogpt-platform)_ + +🦉 **MIT License:** +All other portions of the AutoGPT repository (i.e., everything outside the `autogpt_platform` folder) are licensed under the MIT License. This includes the original stand-alone AutoGPT Agent, along with projects such as [Forge](https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/forge), [agbenchmark](https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/benchmark) and the [AutoGPT Classic GUI](https://github.com/Significant-Gravitas/AutoGPT/tree/master/classic/frontend).
We also publish additional work under the MIT Licence in other repositories, such as [GravitasML](https://github.com/Significant-Gravitas/gravitasml) which is developed for and used in the AutoGPT Platform. See also our MIT Licenced [Code Ability](https://github.com/Significant-Gravitas/AutoGPT-Code-Ability) project. + +--- +### Mission Our mission is to provide the tools, so that you can focus on what matters: - 🏗️ **Building** - Lay the foundation for something amazing. @@ -126,14 +146,6 @@ Be part of the revolution! **AutoGPT** is here to stay, at the forefront of AI i  |  **🚀 [Contributing](CONTRIBUTING.md)** -**Licensing:** - -MIT License: The majority of the AutoGPT repository is under the MIT License. - -Polyform Shield License: This license applies to the autogpt_platform folder. - -For more information, see https://agpt.co/blog/introducing-the-autogpt-platform - --- ## 🤖 AutoGPT Classic > Below is information about the classic version of AutoGPT. diff --git a/autogpt_platform/.env.example b/autogpt_platform/.env.default similarity index 100% rename from autogpt_platform/.env.example rename to autogpt_platform/.env.default diff --git a/autogpt_platform/CLAUDE.md b/autogpt_platform/CLAUDE.md index 9e4fd69caae3..d6f69d67998b 100644 --- a/autogpt_platform/CLAUDE.md +++ b/autogpt_platform/CLAUDE.md @@ -1,9 +1,11 @@ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + ## Repository Overview AutoGPT Platform is a monorepo containing: + - **Backend** (`/backend`): Python FastAPI server with async support - **Frontend** (`/frontend`): Next.js React application - **Shared Libraries** (`/autogpt_libs`): Common Python utilities @@ -11,6 +13,7 @@ AutoGPT Platform is a monorepo containing: ## Essential Commands ### Backend Development + ```bash # Install dependencies cd backend && poetry install @@ -30,11 +33,18 @@ poetry run test # Run specific test poetry run pytest path/to/test_file.py::test_function_name +# Run block tests (tests that validate all blocks work correctly) +poetry run pytest backend/blocks/test/test_block.py -xvs + +# Run tests for a specific block (e.g., GetCurrentTimeBlock) +poetry run pytest 'backend/blocks/test/test_block.py::test_available_blocks[GetCurrentTimeBlock]' -xvs + # Lint and format # prefer format if you want to just "fix" it and only get the errors that can't be autofixed poetry run format # Black + isort poetry run lint # ruff ``` + More details can be found in TESTING.md #### Creating/Updating Snapshots @@ -47,8 +57,8 @@ poetry run pytest path/to/test.py --snapshot-update ⚠️ **Important**: Always review snapshot changes before committing! Use `git diff` to verify the changes are expected. - ### Frontend Development + ```bash # Install dependencies cd frontend && npm install @@ -66,12 +76,13 @@ npm run storybook npm run build # Type checking -npm run type-check +npm run types ``` ## Architecture Overview ### Backend Architecture + - **API Layer**: FastAPI with REST and WebSocket endpoints - **Database**: PostgreSQL with Prisma ORM, includes pgvector for embeddings - **Queue System**: RabbitMQ for async task processing @@ -80,6 +91,7 @@ npm run type-check - **Security**: Cache protection middleware prevents sensitive data caching in browsers/proxies ### Frontend Architecture + - **Framework**: Next.js App Router with React Server Components - **State Management**: React hooks + Supabase client for real-time updates - **Workflow Builder**: Visual graph editor using @xyflow/react @@ -87,6 +99,7 @@ npm run type-check - **Feature Flags**: LaunchDarkly integration ### Key Concepts + 1. **Agent Graphs**: Workflow definitions stored as JSON, executed by the backend 2. **Blocks**: Reusable components in `/backend/blocks/` that perform specific tasks 3. **Integrations**: OAuth and API connections stored per user @@ -94,13 +107,16 @@ npm run type-check 5. **Virus Scanning**: ClamAV integration for file upload security ### Testing Approach + - Backend uses pytest with snapshot testing for API responses - Test files are colocated with source files (`*_test.py`) - Frontend uses Playwright for E2E tests - Component testing via Storybook ### Database Schema + Key models (defined in `/backend/schema.prisma`): + - `User`: Authentication and profile data - `AgentGraph`: Workflow definitions with version control - `AgentGraphExecution`: Execution history and results @@ -108,13 +124,31 @@ Key models (defined in `/backend/schema.prisma`): - `StoreListing`: Marketplace listings for sharing agents ### Environment Configuration -- Backend: `.env` file in `/backend` -- Frontend: `.env.local` file in `/frontend` -- Both require Supabase credentials and API keys for various services + +#### Configuration Files + +- **Backend**: `/backend/.env.default` (defaults) → `/backend/.env` (user overrides) +- **Frontend**: `/frontend/.env.default` (defaults) → `/frontend/.env` (user overrides) +- **Platform**: `/.env.default` (Supabase/shared defaults) → `/.env` (user overrides) + +#### Docker Environment Loading Order + +1. `.env.default` files provide base configuration (tracked in git) +2. `.env` files provide user-specific overrides (gitignored) +3. Docker Compose `environment:` sections provide service-specific overrides +4. Shell environment variables have highest precedence + +#### Key Points + +- All services use hardcoded defaults in docker-compose files (no `${VARIABLE}` substitutions) +- The `env_file` directive loads variables INTO containers at runtime +- Backend/Frontend services use YAML anchors for consistent configuration +- Supabase services (`db/docker/docker-compose.yml`) follow the same pattern ### Common Development Tasks **Adding a new block:** + 1. Create new file in `/backend/backend/blocks/` 2. Inherit from `Block` base class 3. Define input/output schemas @@ -122,13 +156,18 @@ Key models (defined in `/backend/schema.prisma`): 5. Register in block registry 6. Generate the block uuid using `uuid.uuid4()` +Note: when making many new blocks analyze the interfaces for each of these blocks and picture if they would go well together in a graph based editor or would they struggle to connect productively? +ex: do the inputs and outputs tie well together? + **Modifying the API:** + 1. Update route in `/backend/backend/server/routers/` 2. Add/update Pydantic models in same directory 3. Write tests alongside the route file 4. Run `poetry run test` to verify **Frontend feature development:** + 1. Components go in `/frontend/src/components/` 2. Use existing UI components from `/frontend/src/components/ui/` 3. Add Storybook stories for new components @@ -137,6 +176,7 @@ Key models (defined in `/backend/schema.prisma`): ### Security Implementation **Cache Protection Middleware:** + - Located in `/backend/backend/server/middleware/security.py` - Default behavior: Disables caching for ALL endpoints with `Cache-Control: no-store, no-cache, must-revalidate, private` - Uses an allow list approach - only explicitly permitted paths can be cached @@ -144,3 +184,47 @@ Key models (defined in `/backend/schema.prisma`): - Prevents sensitive data (auth tokens, API keys, user data) from being cached by browsers/proxies - To allow caching for a new endpoint, add it to `CACHEABLE_PATHS` in the middleware - Applied to both main API server and external API applications + +### Creating Pull Requests + +- Create the PR aginst the `dev` branch of the repository. +- Ensure the branch name is descriptive (e.g., `feature/add-new-block`)/ +- Use conventional commit messages (see below)/ +- Fill out the .github/PULL_REQUEST_TEMPLATE.md template as the PR description/ +- Run the github pre-commit hooks to ensure code quality. + +### Reviewing/Revising Pull Requests + +- When the user runs /pr-comments or tries to fetch them, also run gh api /repos/Significant-Gravitas/AutoGPT/pulls/[issuenum]/reviews to get the reviews +- Use gh api /repos/Significant-Gravitas/AutoGPT/pulls/[issuenum]/reviews/[review_id]/comments to get the review contents +- Use gh api /repos/Significant-Gravitas/AutoGPT/issues/9924/comments to get the pr specific comments + +### Conventional Commits + +Use this format for commit messages and Pull Request titles: + +**Conventional Commit Types:** + +- `feat`: Introduces a new feature to the codebase +- `fix`: Patches a bug in the codebase +- `refactor`: Code change that neither fixes a bug nor adds a feature; also applies to removing features +- `ci`: Changes to CI configuration +- `docs`: Documentation-only changes +- `dx`: Improvements to the developer experience + +**Recommended Base Scopes:** + +- `platform`: Changes affecting both frontend and backend +- `frontend` +- `backend` +- `infra` +- `blocks`: Modifications/additions of individual blocks + +**Subscope Examples:** + +- `backend/executor` +- `backend/db` +- `frontend/builder` (includes changes to the block UI component) +- `infra/prod` + +Use these scopes and subscopes for clarity and consistency in commit messages. diff --git a/autogpt_platform/README.md b/autogpt_platform/README.md index 8422a29f0e82..ee2ff9f75933 100644 --- a/autogpt_platform/README.md +++ b/autogpt_platform/README.md @@ -8,7 +8,6 @@ Welcome to the AutoGPT Platform - a powerful system for creating and running AI - Docker - Docker Compose V2 (comes with Docker Desktop, or can be installed separately) -- Node.js & NPM (for running the frontend application) ### Running the System @@ -24,10 +23,10 @@ To run the AutoGPT Platform, follow these steps: 2. Run the following command: ``` - cp .env.example .env + cp .env.default .env ``` - This command will copy the `.env.example` file to `.env`. You can modify the `.env` file to add your own environment variables. + This command will copy the `.env.default` file to `.env`. You can modify the `.env` file to add your own environment variables. 3. Run the following command: @@ -37,44 +36,7 @@ To run the AutoGPT Platform, follow these steps: This command will start all the necessary backend services defined in the `docker-compose.yml` file in detached mode. -4. Navigate to `frontend` within the `autogpt_platform` directory: - - ``` - cd frontend - ``` - - You will need to run your frontend application separately on your local machine. - -5. Run the following command: - - ``` - cp .env.example .env.local - ``` - - This command will copy the `.env.example` file to `.env.local` in the `frontend` directory. You can modify the `.env.local` within this folder to add your own environment variables for the frontend application. - -6. Run the following command: - - Enable corepack and install dependencies by running: - - ``` - corepack enable - pnpm i - ``` - - Generate the API client (this step is required before running the frontend): - - ``` - pnpm generate:api-client - ``` - - Then start the frontend application in development mode: - - ``` - pnpm dev - ``` - -7. Open your browser and navigate to `http://localhost:3000` to access the AutoGPT Platform frontend. +4. After all the services are in ready state, open your browser and navigate to `http://localhost:3000` to access the AutoGPT Platform frontend. ### Docker Compose Commands @@ -177,20 +139,21 @@ The platform includes scripts for generating and managing the API client: - `pnpm fetch:openapi`: Fetches the OpenAPI specification from the backend service (requires backend to be running on port 8006) - `pnpm generate:api-client`: Generates the TypeScript API client from the OpenAPI specification using Orval -- `pnpm generate:api-all`: Runs both fetch and generate commands in sequence +- `pnpm generate:api`: Runs both fetch and generate commands in sequence #### Manual API Client Updates If you need to update the API client after making changes to the backend API: 1. Ensure the backend services are running: + ``` docker compose up -d ``` 2. Generate the updated API client: ``` - pnpm generate:api-all + pnpm generate:api ``` This will fetch the latest OpenAPI specification and regenerate the TypeScript client code. diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/auth/config.py b/autogpt_platform/autogpt_libs/autogpt_libs/auth/config.py index c143c78e6d9f..216aefc37d9b 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/auth/config.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/auth/config.py @@ -7,9 +7,5 @@ def __init__(self): self.ENABLE_AUTH: bool = os.getenv("ENABLE_AUTH", "false").lower() == "true" self.JWT_ALGORITHM: str = "HS256" - @property - def is_configured(self) -> bool: - return bool(self.JWT_SECRET_KEY) - settings = Settings() diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py index eb583ac1fc5a..68151452d2a1 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py @@ -10,8 +10,8 @@ from .config import settings from .jwt_utils import parse_jwt_token -security = HTTPBearer() logger = logging.getLogger(__name__) +bearer_auth = HTTPBearer(auto_error=False) async def auth_middleware(request: Request): @@ -20,11 +20,10 @@ async def auth_middleware(request: Request): logger.warning("Auth disabled") return {} - security = HTTPBearer() - credentials = await security(request) + credentials = await bearer_auth(request) if not credentials: - raise HTTPException(status_code=401, detail="Authorization header is missing") + raise HTTPException(status_code=401, detail="Not authenticated") try: payload = parse_jwt_token(credentials.credentials) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client.py b/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client.py deleted file mode 100644 index 9aed891706da..000000000000 --- a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client.py +++ /dev/null @@ -1,166 +0,0 @@ -import asyncio -import contextlib -import logging -from functools import wraps -from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, Union, cast - -import ldclient -from fastapi import HTTPException -from ldclient import Context, LDClient -from ldclient.config import Config -from typing_extensions import ParamSpec - -from .config import SETTINGS - -logger = logging.getLogger(__name__) - -P = ParamSpec("P") -T = TypeVar("T") - - -def get_client() -> LDClient: - """Get the LaunchDarkly client singleton.""" - return ldclient.get() - - -def initialize_launchdarkly() -> None: - sdk_key = SETTINGS.launch_darkly_sdk_key - logger.debug( - f"Initializing LaunchDarkly with SDK key: {'present' if sdk_key else 'missing'}" - ) - - if not sdk_key: - logger.warning("LaunchDarkly SDK key not configured") - return - - config = Config(sdk_key) - ldclient.set_config(config) - - if ldclient.get().is_initialized(): - logger.info("LaunchDarkly client initialized successfully") - else: - logger.error("LaunchDarkly client failed to initialize") - - -def shutdown_launchdarkly() -> None: - """Shutdown the LaunchDarkly client.""" - if ldclient.get().is_initialized(): - ldclient.get().close() - logger.info("LaunchDarkly client closed successfully") - - -def create_context( - user_id: str, additional_attributes: Optional[Dict[str, Any]] = None -) -> Context: - """Create LaunchDarkly context with optional additional attributes.""" - builder = Context.builder(str(user_id)).kind("user") - if additional_attributes: - for key, value in additional_attributes.items(): - builder.set(key, value) - return builder.build() - - -def feature_flag( - flag_key: str, - default: bool = False, -) -> Callable[ - [Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]] -]: - """ - Decorator for feature flag protected endpoints. - """ - - def decorator( - func: Callable[P, Union[T, Awaitable[T]]], - ) -> Callable[P, Union[T, Awaitable[T]]]: - @wraps(func) - async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - try: - user_id = kwargs.get("user_id") - if not user_id: - raise ValueError("user_id is required") - - if not get_client().is_initialized(): - logger.warning( - f"LaunchDarkly not initialized, using default={default}" - ) - is_enabled = default - else: - context = create_context(str(user_id)) - is_enabled = get_client().variation(flag_key, context, default) - - if not is_enabled: - raise HTTPException(status_code=404, detail="Feature not available") - - result = func(*args, **kwargs) - if asyncio.iscoroutine(result): - return await result - return cast(T, result) - except Exception as e: - logger.error(f"Error evaluating feature flag {flag_key}: {e}") - raise - - @wraps(func) - def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - try: - user_id = kwargs.get("user_id") - if not user_id: - raise ValueError("user_id is required") - - if not get_client().is_initialized(): - logger.warning( - f"LaunchDarkly not initialized, using default={default}" - ) - is_enabled = default - else: - context = create_context(str(user_id)) - is_enabled = get_client().variation(flag_key, context, default) - - if not is_enabled: - raise HTTPException(status_code=404, detail="Feature not available") - - return cast(T, func(*args, **kwargs)) - except Exception as e: - logger.error(f"Error evaluating feature flag {flag_key}: {e}") - raise - - return cast( - Callable[P, Union[T, Awaitable[T]]], - async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper, - ) - - return decorator - - -def percentage_rollout( - flag_key: str, - default: bool = False, -) -> Callable[ - [Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]] -]: - """Decorator for percentage-based rollouts.""" - return feature_flag(flag_key, default) - - -def beta_feature( - flag_key: Optional[str] = None, - unauthorized_response: Any = {"message": "Not available in beta"}, -) -> Callable[ - [Callable[P, Union[T, Awaitable[T]]]], Callable[P, Union[T, Awaitable[T]]] -]: - """Decorator for beta features.""" - actual_key = f"beta-{flag_key}" if flag_key else "beta" - return feature_flag(actual_key, False) - - -@contextlib.contextmanager -def mock_flag_variation(flag_key: str, return_value: Any): - """Context manager for testing feature flags.""" - original_variation = get_client().variation - get_client().variation = lambda key, context, default: ( - return_value if key == flag_key else original_variation(key, context, default) - ) - try: - yield - finally: - get_client().variation = original_variation diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client_test.py b/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client_test.py deleted file mode 100644 index 8fccfb28b501..000000000000 --- a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/client_test.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from ldclient import LDClient - -from autogpt_libs.feature_flag.client import feature_flag, mock_flag_variation - - -@pytest.fixture -def ld_client(mocker): - client = mocker.Mock(spec=LDClient) - mocker.patch("ldclient.get", return_value=client) - client.is_initialized.return_value = True - return client - - -@pytest.mark.asyncio -async def test_feature_flag_enabled(ld_client): - ld_client.variation.return_value = True - - @feature_flag("test-flag") - async def test_function(user_id: str): - return "success" - - result = test_function(user_id="test-user") - assert result == "success" - ld_client.variation.assert_called_once() - - -@pytest.mark.asyncio -async def test_feature_flag_unauthorized_response(ld_client): - ld_client.variation.return_value = False - - @feature_flag("test-flag") - async def test_function(user_id: str): - return "success" - - result = test_function(user_id="test-user") - assert result == {"error": "disabled"} - - -def test_mock_flag_variation(ld_client): - with mock_flag_variation("test-flag", True): - assert ld_client.variation("test-flag", None, False) - - with mock_flag_variation("test-flag", False): - assert ld_client.variation("test-flag", None, False) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/config.py b/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/config.py deleted file mode 100644 index e01c285d1e66..000000000000 --- a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/config.py +++ /dev/null @@ -1,15 +0,0 @@ -from pydantic import Field -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - launch_darkly_sdk_key: str = Field( - default="", - description="The Launch Darkly SDK key", - validation_alias="LAUNCH_DARKLY_SDK_KEY", - ) - - model_config = SettingsConfigDict(case_sensitive=True, extra="ignore") - - -SETTINGS = Settings() diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/logging/config.py b/autogpt_platform/autogpt_libs/autogpt_libs/logging/config.py index dda6c58547a5..d424e6ba8c7b 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/logging/config.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/logging/config.py @@ -1,6 +1,8 @@ """Logging module for Auto-GPT.""" import logging +import os +import socket import sys from pathlib import Path @@ -10,6 +12,15 @@ from .filters import BelowLevelFilter from .formatters import AGPTFormatter +# Configure global socket timeout and gRPC keepalive to prevent deadlocks +# This must be done at import time before any gRPC connections are established +socket.setdefaulttimeout(30) # 30-second socket timeout + +# Enable gRPC keepalive to detect dead connections faster +os.environ.setdefault("GRPC_KEEPALIVE_TIME_MS", "30000") # 30 seconds +os.environ.setdefault("GRPC_KEEPALIVE_TIMEOUT_MS", "5000") # 5 seconds +os.environ.setdefault("GRPC_KEEPALIVE_PERMIT_WITHOUT_CALLS", "true") + LOG_DIR = Path(__file__).parent.parent.parent.parent / "logs" LOG_FILE = "activity.log" DEBUG_LOG_FILE = "debug.log" @@ -79,7 +90,6 @@ def configure_logging(force_cloud_logging: bool = False) -> None: Note: This function is typically called at the start of the application to set up the logging infrastructure. """ - config = LoggingConfig() log_handlers: list[logging.Handler] = [] @@ -105,13 +115,17 @@ def configure_logging(force_cloud_logging: bool = False) -> None: if config.enable_cloud_logging or force_cloud_logging: import google.cloud.logging from google.cloud.logging.handlers import CloudLoggingHandler - from google.cloud.logging_v2.handlers.transports.sync import SyncTransport + from google.cloud.logging_v2.handlers.transports import ( + BackgroundThreadTransport, + ) client = google.cloud.logging.Client() + # Use BackgroundThreadTransport to prevent blocking the main thread + # and deadlocks when gRPC calls to Google Cloud Logging hang cloud_handler = CloudLoggingHandler( client, name="autogpt_logs", - transport=SyncTransport, + transport=BackgroundThreadTransport, ) cloud_handler.setLevel(config.level) log_handlers.append(cloud_handler) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/logging/utils.py b/autogpt_platform/autogpt_libs/autogpt_libs/logging/utils.py index 8abf04c2261b..0b92e2967d3a 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/logging/utils.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/logging/utils.py @@ -1,39 +1,5 @@ -import logging import re -from typing import Any - -import uvicorn.config -from colorama import Fore def remove_color_codes(s: str) -> str: return re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", s) - - -def fmt_kwargs(kwargs: dict) -> str: - return ", ".join(f"{n}={repr(v)}" for n, v in kwargs.items()) - - -def print_attribute( - title: str, value: Any, title_color: str = Fore.GREEN, value_color: str = "" -) -> None: - logger = logging.getLogger() - logger.info( - str(value), - extra={ - "title": f"{title.rstrip(':')}:", - "title_color": title_color, - "color": value_color, - }, - ) - - -def generate_uvicorn_config(): - """ - Generates a uvicorn logging config that silences uvicorn's default logging and tells it to use the native logging module. - """ - log_config = dict(uvicorn.config.LOGGING_CONFIG) - log_config["loggers"]["uvicorn"] = {"handlers": []} - log_config["loggers"]["uvicorn.error"] = {"handlers": []} - log_config["loggers"]["uvicorn.access"] = {"handlers": []} - return log_config diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache.py b/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache.py index 69858570abb7..23328e46a340 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache.py @@ -1,17 +1,34 @@ import inspect +import logging import threading -from typing import Awaitable, Callable, ParamSpec, TypeVar, cast, overload +import time +from functools import wraps +from typing import ( + Awaitable, + Callable, + ParamSpec, + Protocol, + Tuple, + TypeVar, + cast, + overload, + runtime_checkable, +) P = ParamSpec("P") R = TypeVar("R") +logger = logging.getLogger(__name__) + @overload -def thread_cached(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: ... +def thread_cached(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: + pass @overload -def thread_cached(func: Callable[P, R]) -> Callable[P, R]: ... +def thread_cached(func: Callable[P, R]) -> Callable[P, R]: + pass def thread_cached( @@ -57,3 +74,193 @@ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: def clear_thread_cache(func: Callable) -> None: if clear := getattr(func, "clear_cache", None): clear() + + +FuncT = TypeVar("FuncT") + + +R_co = TypeVar("R_co", covariant=True) + + +@runtime_checkable +class AsyncCachedFunction(Protocol[P, R_co]): + """Protocol for async functions with cache management methods.""" + + def cache_clear(self) -> None: + """Clear all cached entries.""" + return None + + def cache_info(self) -> dict[str, int | None]: + """Get cache statistics.""" + return {} + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: + """Call the cached function.""" + return None # type: ignore + + +def async_ttl_cache( + maxsize: int = 128, ttl_seconds: int | None = None +) -> Callable[[Callable[P, Awaitable[R]]], AsyncCachedFunction[P, R]]: + """ + TTL (Time To Live) cache decorator for async functions. + + Similar to functools.lru_cache but works with async functions and includes optional TTL. + + Args: + maxsize: Maximum number of cached entries + ttl_seconds: Time to live in seconds. If None, entries never expire (like lru_cache) + + Returns: + Decorator function + + Example: + # With TTL + @async_ttl_cache(maxsize=1000, ttl_seconds=300) + async def api_call(param: str) -> dict: + return {"result": param} + + # Without TTL (permanent cache like lru_cache) + @async_ttl_cache(maxsize=1000) + async def expensive_computation(param: str) -> dict: + return {"result": param} + """ + + def decorator( + async_func: Callable[P, Awaitable[R]], + ) -> AsyncCachedFunction[P, R]: + # Cache storage - use union type to handle both cases + cache_storage: dict[tuple, R | Tuple[R, float]] = {} + + @wraps(async_func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + # Create cache key from arguments + key = (args, tuple(sorted(kwargs.items()))) + current_time = time.time() + + # Check if we have a valid cached entry + if key in cache_storage: + if ttl_seconds is None: + # No TTL - return cached result directly + logger.debug( + f"Cache hit for {async_func.__name__} with key: {str(key)[:50]}" + ) + return cast(R, cache_storage[key]) + else: + # With TTL - check expiration + cached_data = cache_storage[key] + if isinstance(cached_data, tuple): + result, timestamp = cached_data + if current_time - timestamp < ttl_seconds: + logger.debug( + f"Cache hit for {async_func.__name__} with key: {str(key)[:50]}" + ) + return cast(R, result) + else: + # Expired entry + del cache_storage[key] + logger.debug( + f"Cache entry expired for {async_func.__name__}" + ) + + # Cache miss or expired - fetch fresh data + logger.debug( + f"Cache miss for {async_func.__name__} with key: {str(key)[:50]}" + ) + result = await async_func(*args, **kwargs) + + # Store in cache + if ttl_seconds is None: + cache_storage[key] = result + else: + cache_storage[key] = (result, current_time) + + # Simple cleanup when cache gets too large + if len(cache_storage) > maxsize: + # Remove oldest entries (simple FIFO cleanup) + cutoff = maxsize // 2 + oldest_keys = list(cache_storage.keys())[:-cutoff] if cutoff > 0 else [] + for old_key in oldest_keys: + cache_storage.pop(old_key, None) + logger.debug( + f"Cache cleanup: removed {len(oldest_keys)} entries for {async_func.__name__}" + ) + + return result + + # Add cache management methods (similar to functools.lru_cache) + def cache_clear() -> None: + cache_storage.clear() + + def cache_info() -> dict[str, int | None]: + return { + "size": len(cache_storage), + "maxsize": maxsize, + "ttl_seconds": ttl_seconds, + } + + # Attach methods to wrapper + setattr(wrapper, "cache_clear", cache_clear) + setattr(wrapper, "cache_info", cache_info) + + return cast(AsyncCachedFunction[P, R], wrapper) + + return decorator + + +@overload +def async_cache( + func: Callable[P, Awaitable[R]], +) -> AsyncCachedFunction[P, R]: + pass + + +@overload +def async_cache( + func: None = None, + *, + maxsize: int = 128, +) -> Callable[[Callable[P, Awaitable[R]]], AsyncCachedFunction[P, R]]: + pass + + +def async_cache( + func: Callable[P, Awaitable[R]] | None = None, + *, + maxsize: int = 128, +) -> ( + AsyncCachedFunction[P, R] + | Callable[[Callable[P, Awaitable[R]]], AsyncCachedFunction[P, R]] +): + """ + Process-level cache decorator for async functions (no TTL). + + Similar to functools.lru_cache but works with async functions. + This is a convenience wrapper around async_ttl_cache with ttl_seconds=None. + + Args: + func: The async function to cache (when used without parentheses) + maxsize: Maximum number of cached entries + + Returns: + Decorated function or decorator + + Example: + # Without parentheses (uses default maxsize=128) + @async_cache + async def get_data(param: str) -> dict: + return {"result": param} + + # With parentheses and custom maxsize + @async_cache(maxsize=1000) + async def expensive_computation(param: str) -> dict: + # Expensive computation here + return {"result": param} + """ + if func is None: + # Called with parentheses @async_cache() or @async_cache(maxsize=...) + return async_ttl_cache(maxsize=maxsize, ttl_seconds=None) + else: + # Called without parentheses @async_cache + decorator = async_ttl_cache(maxsize=maxsize, ttl_seconds=None) + return decorator(func) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache_test.py b/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache_test.py new file mode 100644 index 000000000000..e6ca3ecdfd2a --- /dev/null +++ b/autogpt_platform/autogpt_libs/autogpt_libs/utils/cache_test.py @@ -0,0 +1,705 @@ +"""Tests for the @thread_cached decorator. + +This module tests the thread-local caching functionality including: +- Basic caching for sync and async functions +- Thread isolation (each thread has its own cache) +- Cache clearing functionality +- Exception handling (exceptions are not cached) +- Argument handling (positional vs keyword arguments) +""" + +import asyncio +import threading +import time +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import Mock + +import pytest + +from autogpt_libs.utils.cache import ( + async_cache, + async_ttl_cache, + clear_thread_cache, + thread_cached, +) + + +class TestThreadCached: + def test_sync_function_caching(self): + call_count = 0 + + @thread_cached + def expensive_function(x: int, y: int = 0) -> int: + nonlocal call_count + call_count += 1 + return x + y + + assert expensive_function(1, 2) == 3 + assert call_count == 1 + + assert expensive_function(1, 2) == 3 + assert call_count == 1 + + assert expensive_function(1, y=2) == 3 + assert call_count == 2 + + assert expensive_function(2, 3) == 5 + assert call_count == 3 + + assert expensive_function(1) == 1 + assert call_count == 4 + + @pytest.mark.asyncio + async def test_async_function_caching(self): + call_count = 0 + + @thread_cached + async def expensive_async_function(x: int, y: int = 0) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) + return x + y + + assert await expensive_async_function(1, 2) == 3 + assert call_count == 1 + + assert await expensive_async_function(1, 2) == 3 + assert call_count == 1 + + assert await expensive_async_function(1, y=2) == 3 + assert call_count == 2 + + assert await expensive_async_function(2, 3) == 5 + assert call_count == 3 + + def test_thread_isolation(self): + call_count = 0 + results = {} + + @thread_cached + def thread_specific_function(x: int) -> str: + nonlocal call_count + call_count += 1 + return f"{threading.current_thread().name}-{x}" + + def worker(thread_id: int): + result1 = thread_specific_function(1) + result2 = thread_specific_function(1) + result3 = thread_specific_function(2) + results[thread_id] = (result1, result2, result3) + + with ThreadPoolExecutor(max_workers=3) as executor: + futures = [executor.submit(worker, i) for i in range(3)] + for future in futures: + future.result() + + assert call_count >= 2 + + for thread_id, (r1, r2, r3) in results.items(): + assert r1 == r2 + assert r1 != r3 + + @pytest.mark.asyncio + async def test_async_thread_isolation(self): + call_count = 0 + results = {} + + @thread_cached + async def async_thread_specific_function(x: int) -> str: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) + return f"{threading.current_thread().name}-{x}" + + async def async_worker(worker_id: int): + result1 = await async_thread_specific_function(1) + result2 = await async_thread_specific_function(1) + result3 = await async_thread_specific_function(2) + results[worker_id] = (result1, result2, result3) + + tasks = [async_worker(i) for i in range(3)] + await asyncio.gather(*tasks) + + for worker_id, (r1, r2, r3) in results.items(): + assert r1 == r2 + assert r1 != r3 + + def test_clear_cache_sync(self): + call_count = 0 + + @thread_cached + def clearable_function(x: int) -> int: + nonlocal call_count + call_count += 1 + return x * 2 + + assert clearable_function(5) == 10 + assert call_count == 1 + + assert clearable_function(5) == 10 + assert call_count == 1 + + clear_thread_cache(clearable_function) + + assert clearable_function(5) == 10 + assert call_count == 2 + + @pytest.mark.asyncio + async def test_clear_cache_async(self): + call_count = 0 + + @thread_cached + async def clearable_async_function(x: int) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) + return x * 2 + + assert await clearable_async_function(5) == 10 + assert call_count == 1 + + assert await clearable_async_function(5) == 10 + assert call_count == 1 + + clear_thread_cache(clearable_async_function) + + assert await clearable_async_function(5) == 10 + assert call_count == 2 + + def test_simple_arguments(self): + call_count = 0 + + @thread_cached + def simple_function(a: str, b: int, c: str = "default") -> str: + nonlocal call_count + call_count += 1 + return f"{a}-{b}-{c}" + + # First call with all positional args + result1 = simple_function("test", 42, "custom") + assert call_count == 1 + + # Same args, all positional - should hit cache + result2 = simple_function("test", 42, "custom") + assert call_count == 1 + assert result1 == result2 + + # Same values but last arg as keyword - creates different cache key + result3 = simple_function("test", 42, c="custom") + assert call_count == 2 + assert result1 == result3 # Same result, different cache entry + + # Different value - new cache entry + result4 = simple_function("test", 43, "custom") + assert call_count == 3 + assert result1 != result4 + + def test_positional_vs_keyword_args(self): + """Test that positional and keyword arguments create different cache entries.""" + call_count = 0 + + @thread_cached + def func(a: int, b: int = 10) -> str: + nonlocal call_count + call_count += 1 + return f"result-{a}-{b}" + + # All positional + result1 = func(1, 2) + assert call_count == 1 + assert result1 == "result-1-2" + + # Same values, but second arg as keyword + result2 = func(1, b=2) + assert call_count == 2 # Different cache key! + assert result2 == "result-1-2" # Same result + + # Verify both are cached separately + func(1, 2) # Uses first cache entry + assert call_count == 2 + + func(1, b=2) # Uses second cache entry + assert call_count == 2 + + def test_exception_handling(self): + call_count = 0 + + @thread_cached + def failing_function(x: int) -> int: + nonlocal call_count + call_count += 1 + if x < 0: + raise ValueError("Negative value") + return x * 2 + + assert failing_function(5) == 10 + assert call_count == 1 + + with pytest.raises(ValueError): + failing_function(-1) + assert call_count == 2 + + with pytest.raises(ValueError): + failing_function(-1) + assert call_count == 3 + + assert failing_function(5) == 10 + assert call_count == 3 + + @pytest.mark.asyncio + async def test_async_exception_handling(self): + call_count = 0 + + @thread_cached + async def async_failing_function(x: int) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) + if x < 0: + raise ValueError("Negative value") + return x * 2 + + assert await async_failing_function(5) == 10 + assert call_count == 1 + + with pytest.raises(ValueError): + await async_failing_function(-1) + assert call_count == 2 + + with pytest.raises(ValueError): + await async_failing_function(-1) + assert call_count == 3 + + def test_sync_caching_performance(self): + @thread_cached + def slow_function(x: int) -> int: + print(f"slow_function called with x={x}") + time.sleep(0.1) + return x * 2 + + start = time.time() + result1 = slow_function(5) + first_call_time = time.time() - start + print(f"First call took {first_call_time:.4f} seconds") + + start = time.time() + result2 = slow_function(5) + second_call_time = time.time() - start + print(f"Second call took {second_call_time:.4f} seconds") + + assert result1 == result2 == 10 + assert first_call_time > 0.09 + assert second_call_time < 0.01 + + @pytest.mark.asyncio + async def test_async_caching_performance(self): + @thread_cached + async def slow_async_function(x: int) -> int: + print(f"slow_async_function called with x={x}") + await asyncio.sleep(0.1) + return x * 2 + + start = time.time() + result1 = await slow_async_function(5) + first_call_time = time.time() - start + print(f"First async call took {first_call_time:.4f} seconds") + + start = time.time() + result2 = await slow_async_function(5) + second_call_time = time.time() - start + print(f"Second async call took {second_call_time:.4f} seconds") + + assert result1 == result2 == 10 + assert first_call_time > 0.09 + assert second_call_time < 0.01 + + def test_with_mock_objects(self): + mock = Mock(return_value=42) + + @thread_cached + def function_using_mock(x: int) -> int: + return mock(x) + + assert function_using_mock(1) == 42 + assert mock.call_count == 1 + + assert function_using_mock(1) == 42 + assert mock.call_count == 1 + + assert function_using_mock(2) == 42 + assert mock.call_count == 2 + + +class TestAsyncTTLCache: + """Tests for the @async_ttl_cache decorator.""" + + @pytest.mark.asyncio + async def test_basic_caching(self): + """Test basic caching functionality.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=60) + async def cached_function(x: int, y: int = 0) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) # Simulate async work + return x + y + + # First call + result1 = await cached_function(1, 2) + assert result1 == 3 + assert call_count == 1 + + # Second call with same args - should use cache + result2 = await cached_function(1, 2) + assert result2 == 3 + assert call_count == 1 # No additional call + + # Different args - should call function again + result3 = await cached_function(2, 3) + assert result3 == 5 + assert call_count == 2 + + @pytest.mark.asyncio + async def test_ttl_expiration(self): + """Test that cache entries expire after TTL.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=1) # Short TTL + async def short_lived_cache(x: int) -> int: + nonlocal call_count + call_count += 1 + return x * 2 + + # First call + result1 = await short_lived_cache(5) + assert result1 == 10 + assert call_count == 1 + + # Second call immediately - should use cache + result2 = await short_lived_cache(5) + assert result2 == 10 + assert call_count == 1 + + # Wait for TTL to expire + await asyncio.sleep(1.1) + + # Third call after expiration - should call function again + result3 = await short_lived_cache(5) + assert result3 == 10 + assert call_count == 2 + + @pytest.mark.asyncio + async def test_cache_info(self): + """Test cache info functionality.""" + + @async_ttl_cache(maxsize=5, ttl_seconds=300) + async def info_test_function(x: int) -> int: + return x * 3 + + # Check initial cache info + info = info_test_function.cache_info() + assert info["size"] == 0 + assert info["maxsize"] == 5 + assert info["ttl_seconds"] == 300 + + # Add an entry + await info_test_function(1) + info = info_test_function.cache_info() + assert info["size"] == 1 + + @pytest.mark.asyncio + async def test_cache_clear(self): + """Test cache clearing functionality.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=60) + async def clearable_function(x: int) -> int: + nonlocal call_count + call_count += 1 + return x * 4 + + # First call + result1 = await clearable_function(2) + assert result1 == 8 + assert call_count == 1 + + # Second call - should use cache + result2 = await clearable_function(2) + assert result2 == 8 + assert call_count == 1 + + # Clear cache + clearable_function.cache_clear() + + # Third call after clear - should call function again + result3 = await clearable_function(2) + assert result3 == 8 + assert call_count == 2 + + @pytest.mark.asyncio + async def test_maxsize_cleanup(self): + """Test that cache cleans up when maxsize is exceeded.""" + call_count = 0 + + @async_ttl_cache(maxsize=3, ttl_seconds=60) + async def size_limited_function(x: int) -> int: + nonlocal call_count + call_count += 1 + return x**2 + + # Fill cache to maxsize + await size_limited_function(1) # call_count: 1 + await size_limited_function(2) # call_count: 2 + await size_limited_function(3) # call_count: 3 + + info = size_limited_function.cache_info() + assert info["size"] == 3 + + # Add one more entry - should trigger cleanup + await size_limited_function(4) # call_count: 4 + + # Cache size should be reduced (cleanup removes oldest entries) + info = size_limited_function.cache_info() + assert info["size"] is not None and info["size"] <= 3 # Should be cleaned up + + @pytest.mark.asyncio + async def test_argument_variations(self): + """Test caching with different argument patterns.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=60) + async def arg_test_function(a: int, b: str = "default", *, c: int = 100) -> str: + nonlocal call_count + call_count += 1 + return f"{a}-{b}-{c}" + + # Different ways to call with same logical arguments + result1 = await arg_test_function(1, "test", c=200) + assert call_count == 1 + + # Same arguments, same order - should use cache + result2 = await arg_test_function(1, "test", c=200) + assert call_count == 1 + assert result1 == result2 + + # Different arguments - should call function + result3 = await arg_test_function(2, "test", c=200) + assert call_count == 2 + assert result1 != result3 + + @pytest.mark.asyncio + async def test_exception_handling(self): + """Test that exceptions are not cached.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=60) + async def exception_function(x: int) -> int: + nonlocal call_count + call_count += 1 + if x < 0: + raise ValueError("Negative value not allowed") + return x * 2 + + # Successful call - should be cached + result1 = await exception_function(5) + assert result1 == 10 + assert call_count == 1 + + # Same successful call - should use cache + result2 = await exception_function(5) + assert result2 == 10 + assert call_count == 1 + + # Exception call - should not be cached + with pytest.raises(ValueError): + await exception_function(-1) + assert call_count == 2 + + # Same exception call - should call again (not cached) + with pytest.raises(ValueError): + await exception_function(-1) + assert call_count == 3 + + @pytest.mark.asyncio + async def test_concurrent_calls(self): + """Test caching behavior with concurrent calls.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=60) + async def concurrent_function(x: int) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.05) # Simulate work + return x * x + + # Launch concurrent calls with same arguments + tasks = [concurrent_function(3) for _ in range(5)] + results = await asyncio.gather(*tasks) + + # All results should be the same + assert all(result == 9 for result in results) + + # Note: Due to race conditions, call_count might be up to 5 for concurrent calls + # This tests that the cache doesn't break under concurrent access + assert 1 <= call_count <= 5 + + +class TestAsyncCache: + """Tests for the @async_cache decorator (no TTL).""" + + @pytest.mark.asyncio + async def test_basic_caching_no_ttl(self): + """Test basic caching functionality without TTL.""" + call_count = 0 + + @async_cache(maxsize=10) + async def cached_function(x: int, y: int = 0) -> int: + nonlocal call_count + call_count += 1 + await asyncio.sleep(0.01) # Simulate async work + return x + y + + # First call + result1 = await cached_function(1, 2) + assert result1 == 3 + assert call_count == 1 + + # Second call with same args - should use cache + result2 = await cached_function(1, 2) + assert result2 == 3 + assert call_count == 1 # No additional call + + # Third call after some time - should still use cache (no TTL) + await asyncio.sleep(0.05) + result3 = await cached_function(1, 2) + assert result3 == 3 + assert call_count == 1 # Still no additional call + + # Different args - should call function again + result4 = await cached_function(2, 3) + assert result4 == 5 + assert call_count == 2 + + @pytest.mark.asyncio + async def test_no_ttl_vs_ttl_behavior(self): + """Test the difference between TTL and no-TTL caching.""" + ttl_call_count = 0 + no_ttl_call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=1) # Short TTL + async def ttl_function(x: int) -> int: + nonlocal ttl_call_count + ttl_call_count += 1 + return x * 2 + + @async_cache(maxsize=10) # No TTL + async def no_ttl_function(x: int) -> int: + nonlocal no_ttl_call_count + no_ttl_call_count += 1 + return x * 2 + + # First calls + await ttl_function(5) + await no_ttl_function(5) + assert ttl_call_count == 1 + assert no_ttl_call_count == 1 + + # Wait for TTL to expire + await asyncio.sleep(1.1) + + # Second calls after TTL expiry + await ttl_function(5) # Should call function again (TTL expired) + await no_ttl_function(5) # Should use cache (no TTL) + assert ttl_call_count == 2 # TTL function called again + assert no_ttl_call_count == 1 # No-TTL function still cached + + @pytest.mark.asyncio + async def test_async_cache_info(self): + """Test cache info for no-TTL cache.""" + + @async_cache(maxsize=5) + async def info_test_function(x: int) -> int: + return x * 3 + + # Check initial cache info + info = info_test_function.cache_info() + assert info["size"] == 0 + assert info["maxsize"] == 5 + assert info["ttl_seconds"] is None # No TTL + + # Add an entry + await info_test_function(1) + info = info_test_function.cache_info() + assert info["size"] == 1 + + +class TestTTLOptional: + """Tests for optional TTL functionality.""" + + @pytest.mark.asyncio + async def test_ttl_none_behavior(self): + """Test that ttl_seconds=None works like no TTL.""" + call_count = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=None) + async def no_ttl_via_none(x: int) -> int: + nonlocal call_count + call_count += 1 + return x**2 + + # First call + result1 = await no_ttl_via_none(3) + assert result1 == 9 + assert call_count == 1 + + # Wait (would expire if there was TTL) + await asyncio.sleep(0.1) + + # Second call - should still use cache + result2 = await no_ttl_via_none(3) + assert result2 == 9 + assert call_count == 1 # No additional call + + # Check cache info + info = no_ttl_via_none.cache_info() + assert info["ttl_seconds"] is None + + @pytest.mark.asyncio + async def test_cache_options_comparison(self): + """Test different cache options work as expected.""" + ttl_calls = 0 + no_ttl_calls = 0 + + @async_ttl_cache(maxsize=10, ttl_seconds=1) # With TTL + async def ttl_function(x: int) -> int: + nonlocal ttl_calls + ttl_calls += 1 + return x * 10 + + @async_cache(maxsize=10) # Process-level cache (no TTL) + async def process_function(x: int) -> int: + nonlocal no_ttl_calls + no_ttl_calls += 1 + return x * 10 + + # Both should cache initially + await ttl_function(3) + await process_function(3) + assert ttl_calls == 1 + assert no_ttl_calls == 1 + + # Immediate second calls - both should use cache + await ttl_function(3) + await process_function(3) + assert ttl_calls == 1 + assert no_ttl_calls == 1 + + # Wait for TTL to expire + await asyncio.sleep(1.1) + + # After TTL expiry + await ttl_function(3) # Should call function again + await process_function(3) # Should still use cache + assert ttl_calls == 2 # TTL cache expired, called again + assert no_ttl_calls == 1 # Process cache never expires diff --git a/autogpt_platform/autogpt_libs/poetry.lock b/autogpt_platform/autogpt_libs/poetry.lock index acf4a4088662..8f160b23df4c 100644 --- a/autogpt_platform/autogpt_libs/poetry.lock +++ b/autogpt_platform/autogpt_libs/poetry.lock @@ -41,7 +41,7 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main"] markers = "python_full_version < \"3.11.3\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, @@ -1196,22 +1196,23 @@ websockets = ">=11,<16" [[package]] name = "redis" -version = "5.2.1" +version = "6.2.0" description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=3.8" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, - {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, + {file = "redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e"}, + {file = "redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"}, ] [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>=3.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] [[package]] name = "requests" @@ -1252,30 +1253,31 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.12.3" +version = "0.12.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2"}, - {file = "ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041"}, - {file = "ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e"}, - {file = "ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311"}, - {file = "ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07"}, - {file = "ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12"}, - {file = "ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b"}, - {file = "ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f"}, - {file = "ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d"}, - {file = "ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7"}, - {file = "ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1"}, - {file = "ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77"}, + {file = "ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e"}, + {file = "ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f"}, + {file = "ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340"}, + {file = "ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66"}, + {file = "ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7"}, + {file = "ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93"}, + {file = "ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908"}, + {file = "ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089"}, + {file = "ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a"}, ] [[package]] @@ -1613,4 +1615,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "07bee2eb94b9e88e3ce87d35f1dbc77de0819b38a14d73e6b271ed4a8d1a4c29" +content-hash = "4cc687aabe5865665fb8c4ccc0ea7e0af80b41e401ca37919f57efa6e0b5be00" diff --git a/autogpt_platform/autogpt_libs/pyproject.toml b/autogpt_platform/autogpt_libs/pyproject.toml index 12ca7ab9f68c..4cc29e34a762 100644 --- a/autogpt_platform/autogpt_libs/pyproject.toml +++ b/autogpt_platform/autogpt_libs/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] name = "autogpt-libs" version = "0.2.0" -description = "Shared libraries across NextGen AutoGPT" -authors = ["Aarushi "] +description = "Shared libraries across AutoGPT Platform" +authors = ["AutoGPT team "] readme = "README.md" packages = [{ include = "autogpt_libs" }] @@ -10,20 +10,20 @@ packages = [{ include = "autogpt_libs" }] python = ">=3.10,<4.0" colorama = "^0.4.6" expiringdict = "^1.2.2" +fastapi = "^0.116.1" google-cloud-logging = "^3.12.1" +launchdarkly-server-sdk = "^9.12.0" pydantic = "^2.11.7" pydantic-settings = "^2.10.1" pyjwt = "^2.10.1" pytest-asyncio = "^1.1.0" pytest-mock = "^3.14.1" +redis = "^6.2.0" supabase = "^2.16.0" -launchdarkly-server-sdk = "^9.12.0" -fastapi = "^0.116.1" uvicorn = "^0.35.0" [tool.poetry.group.dev.dependencies] -redis = "^5.2.1" -ruff = "^0.12.3" +ruff = "^0.12.9" [build-system] requires = ["poetry-core"] diff --git a/autogpt_platform/backend/.dockerignore b/autogpt_platform/backend/.dockerignore new file mode 100644 index 000000000000..e66c25e657c8 --- /dev/null +++ b/autogpt_platform/backend/.dockerignore @@ -0,0 +1,52 @@ +# Development and testing files +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd +**/.Python +**/env/ +**/venv/ +**/.venv/ +**/pip-log.txt +**/.pytest_cache/ +**/test-results/ +**/snapshots/ +**/test/ + +# IDE and editor files +**/.vscode/ +**/.idea/ +**/*.swp +**/*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +**/*.log +**/logs/ + +# Git +.git/ +.gitignore + +# Documentation +**/*.md +!README.md + +# Local development files +.env +.env.local +**/.env.test + +# Build artifacts +**/dist/ +**/build/ +**/target/ + +# Docker files (avoid recursion) +Dockerfile* +docker-compose* +.dockerignore \ No newline at end of file diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.default similarity index 56% rename from autogpt_platform/backend/.env.example rename to autogpt_platform/backend/.env.default index e223efa52557..bcec6bf0a34d 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.default @@ -1,3 +1,9 @@ +# Backend Configuration +# This file contains environment variables that MUST be set for the AutoGPT platform +# Variables with working defaults in settings.py are not included here + +## ===== REQUIRED DATABASE CONFIGURATION ===== ## +# PostgreSQL Database Connection DB_USER=postgres DB_PASS=your-super-secret-and-long-postgres-password DB_NAME=postgres @@ -10,72 +16,50 @@ DB_SCHEMA=platform DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=${DB_SCHEMA}&connect_timeout=${DB_CONNECT_TIMEOUT}" DIRECT_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=${DB_SCHEMA}&connect_timeout=${DB_CONNECT_TIMEOUT}" PRISMA_SCHEMA="postgres/schema.prisma" +ENABLE_AUTH=true -# EXECUTOR -NUM_GRAPH_WORKERS=10 - -BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] - -# generate using `from cryptography.fernet import Fernet;Fernet.generate_key().decode()` -ENCRYPTION_KEY='dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw=' -UNSUBSCRIBE_SECRET_KEY = 'HlP8ivStJjmbf6NKi78m_3FnOogut0t5ckzjsIqeaio=' - +## ===== REQUIRED SERVICE CREDENTIALS ===== ## +# Redis Configuration REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=password -ENABLE_CREDIT=false -STRIPE_API_KEY= -STRIPE_WEBHOOK_SECRET= - -# What environment things should be logged under: local dev or prod -APP_ENV=local -# What environment to behave as: "local" or "cloud" -BEHAVE_AS=local -PYRO_HOST=localhost -SENTRY_DSN= - -# Email For Postmark so we can send emails -POSTMARK_SERVER_API_TOKEN= -POSTMARK_SENDER_EMAIL=invalid@invalid.com -POSTMARK_WEBHOOK_TOKEN= +# RabbitMQ Credentials +RABBITMQ_DEFAULT_USER=rabbitmq_user_default +RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 -## User auth with Supabase is required for any of the 3rd party integrations with auth to work. -ENABLE_AUTH=true +# Supabase Authentication SUPABASE_URL=http://localhost:8000 SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long -# RabbitMQ credentials -- Used for communication between services -RABBITMQ_HOST=localhost -RABBITMQ_PORT=5672 -RABBITMQ_DEFAULT_USER=rabbitmq_user_default -RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 - -## GCS bucket is required for marketplace and library functionality -MEDIA_GCS_BUCKET_NAME= +## ===== REQUIRED SECURITY KEYS ===== ## +# Generate using: from cryptography.fernet import Fernet;Fernet.generate_key().decode() +ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= +UNSUBSCRIBE_SECRET_KEY=HlP8ivStJjmbf6NKi78m_3FnOogut0t5ckzjsIqeaio= -## For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow -## for integrations to work. Defaults to the value of PLATFORM_BASE_URL if not set. -# FRONTEND_BASE_URL=http://localhost:3000 +## ===== IMPORTANT OPTIONAL CONFIGURATION ===== ## +# Platform URLs (set these for webhooks and OAuth to work) +PLATFORM_BASE_URL=http://localhost:8000 +FRONTEND_BASE_URL=http://localhost:3000 -## PLATFORM_BASE_URL must be set to a *publicly accessible* URL pointing to your backend -## to use the platform's webhook-related functionality. -## If you are developing locally, you can use something like ngrok to get a publc URL -## and tunnel it to your locally running backend. -PLATFORM_BASE_URL=http://localhost:3000 +# Media Storage (required for marketplace and library functionality) +MEDIA_GCS_BUCKET_NAME= -## Cloudflare Turnstile (CAPTCHA) Configuration -## Get these from the Cloudflare Turnstile dashboard: https://dash.cloudflare.com/?to=/:account/turnstile -## This is the backend secret key -TURNSTILE_SECRET_KEY= -## This is the verify URL -TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify +## ===== API KEYS AND OAUTH CREDENTIALS ===== ## +# All API keys below are optional - only add what you need -## == INTEGRATION CREDENTIALS == ## -# Each set of server side credentials is required for the corresponding 3rd party -# integration to work. +# AI/LLM Services +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +GROQ_API_KEY= +LLAMA_API_KEY= +AIML_API_KEY= +V0_API_KEY= +OPEN_ROUTER_API_KEY= +NVIDIA_API_KEY= +# OAuth Credentials # For the OAuth callback URL, use /auth/integrations/oauth_callback, # e.g. http://localhost:3000/auth/integrations/oauth_callback @@ -85,7 +69,6 @@ GITHUB_CLIENT_SECRET= # Google OAuth App server credentials - https://console.cloud.google.com/apis/credentials, and enable gmail api and set scopes # https://console.cloud.google.com/apis/credentials/consent ?project= - # You'll need to add/enable the following scopes (minimum): # https://console.developers.google.com/apis/api/gmail.googleapis.com/overview ?project= # https://console.cloud.google.com/apis/library/sheets.googleapis.com/ ?project= @@ -121,100 +104,75 @@ LINEAR_CLIENT_SECRET= TODOIST_CLIENT_ID= TODOIST_CLIENT_SECRET= -## ===== OPTIONAL API KEYS ===== ## +NOTION_CLIENT_ID= +NOTION_CLIENT_SECRET= -# LLM -OPENAI_API_KEY= -ANTHROPIC_API_KEY= -AIML_API_KEY= -GROQ_API_KEY= -OPEN_ROUTER_API_KEY= -LLAMA_API_KEY= +# Discord OAuth App credentials +# 1. Go to https://discord.com/developers/applications +# 2. Create a new application +# 3. Go to OAuth2 section and add redirect URI: http://localhost:3000/auth/integrations/oauth_callback +# 4. Copy Client ID and Client Secret below +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= -# Reddit -# Go to https://www.reddit.com/prefs/apps and create a new app -# Choose "script" for the type -# Fill in the redirect uri as /auth/integrations/oauth_callback, e.g. http://localhost:3000/auth/integrations/oauth_callback REDDIT_CLIENT_ID= REDDIT_CLIENT_SECRET= -REDDIT_USER_AGENT="AutoGPT:1.0 (by /u/autogpt)" - -# Discord -DISCORD_BOT_TOKEN= - -# SMTP/Email -SMTP_SERVER= -SMTP_PORT= -SMTP_USERNAME= -SMTP_PASSWORD= -# D-ID -DID_API_KEY= - -# Open Weather Map -OPENWEATHERMAP_API_KEY= - -# SMTP -SMTP_SERVER= -SMTP_PORT= -SMTP_USERNAME= -SMTP_PASSWORD= +# Payment Processing +STRIPE_API_KEY= +STRIPE_WEBHOOK_SECRET= -# Medium -MEDIUM_API_KEY= -MEDIUM_AUTHOR_ID= +# Email Service (for sending notifications and confirmations) +POSTMARK_SERVER_API_TOKEN= +POSTMARK_SENDER_EMAIL=invalid@invalid.com +POSTMARK_WEBHOOK_TOKEN= -# Google Maps -GOOGLE_MAPS_API_KEY= +# Error Tracking +SENTRY_DSN= -# Replicate -REPLICATE_API_KEY= +# Cloudflare Turnstile (CAPTCHA) Configuration +# Get these from the Cloudflare Turnstile dashboard: https://dash.cloudflare.com/?to=/:account/turnstile +# This is the backend secret key +TURNSTILE_SECRET_KEY= +# This is the verify URL +TURNSTILE_VERIFY_URL=https://challenges.cloudflare.com/turnstile/v0/siteverify -# Ideogram -IDEOGRAM_API_KEY= +# Feature Flags +LAUNCH_DARKLY_SDK_KEY= -# Fal +# Content Generation & Media +DID_API_KEY= FAL_API_KEY= +IDEOGRAM_API_KEY= +REPLICATE_API_KEY= +REVID_API_KEY= +SCREENSHOTONE_API_KEY= +UNREAL_SPEECH_API_KEY= -# Exa -EXA_API_KEY= - -# E2B +# Data & Search Services E2B_API_KEY= - -# Mem0 +EXA_API_KEY= +JINA_API_KEY= MEM0_API_KEY= +OPENWEATHERMAP_API_KEY= +GOOGLE_MAPS_API_KEY= -# Nvidia -NVIDIA_API_KEY= +# Communication Services +DISCORD_BOT_TOKEN= +MEDIUM_API_KEY= +MEDIUM_AUTHOR_ID= +SMTP_SERVER= +SMTP_PORT= +SMTP_USERNAME= +SMTP_PASSWORD= -# Apollo +# Business & Marketing Tools APOLLO_API_KEY= - -# SmartLead +ENRICHLAYER_API_KEY= +AYRSHARE_API_KEY= +AYRSHARE_JWT_KEY= SMARTLEAD_API_KEY= - -# ZeroBounce ZEROBOUNCE_API_KEY= -## ===== OPTIONAL API KEYS END ===== ## - -# Block Error Rate Monitoring -BLOCK_ERROR_RATE_THRESHOLD=0.5 -BLOCK_ERROR_RATE_CHECK_INTERVAL_SECS=86400 - -# Logging Configuration -LOG_LEVEL=INFO -ENABLE_CLOUD_LOGGING=false -ENABLE_FILE_LOGGING=false -# Use to manually set the log directory -# LOG_DIR=./logs - -# Example Blocks Configuration -# Set to true to enable example blocks in development -# These blocks are disabled by default in production -ENABLE_EXAMPLE_BLOCKS=false - -# Cloud Storage Configuration -# Cleanup interval for expired files (hours between cleanup runs, 1-24 hours) -CLOUD_STORAGE_CLEANUP_INTERVAL_HOURS=6 +# Other Services +AUTOMOD_API_KEY= \ No newline at end of file diff --git a/autogpt_platform/backend/.gitignore b/autogpt_platform/backend/.gitignore index 1ce7f628ee1b..197d29072b1b 100644 --- a/autogpt_platform/backend/.gitignore +++ b/autogpt_platform/backend/.gitignore @@ -1,3 +1,4 @@ +.env database.db database.db-journal dev.db diff --git a/autogpt_platform/backend/Dockerfile b/autogpt_platform/backend/Dockerfile index 5a0fadf9e48e..103812118775 100644 --- a/autogpt_platform/backend/Dockerfile +++ b/autogpt_platform/backend/Dockerfile @@ -1,31 +1,34 @@ -FROM python:3.11.10-slim-bookworm AS builder +FROM debian:13-slim AS builder # Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DEBIAN_FRONTEND=noninteractive WORKDIR /app RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy -RUN apt-get update --allow-releaseinfo-change --fix-missing - -# Install build dependencies -RUN apt-get install -y build-essential -RUN apt-get install -y libpq5 -RUN apt-get install -y libz-dev -RUN apt-get install -y libssl-dev -RUN apt-get install -y postgresql-client +# Update package list and install Python and build dependencies +RUN apt-get update --allow-releaseinfo-change --fix-missing \ + && apt-get install -y \ + python3.13 \ + python3.13-dev \ + python3.13-venv \ + python3-pip \ + build-essential \ + libpq5 \ + libz-dev \ + libssl-dev \ + postgresql-client ENV POETRY_HOME=/opt/poetry ENV POETRY_NO_INTERACTION=1 -ENV POETRY_VIRTUALENVS_CREATE=false +ENV POETRY_VIRTUALENVS_CREATE=true +ENV POETRY_VIRTUALENVS_IN_PROJECT=true ENV PATH=/opt/poetry/bin:$PATH -# Upgrade pip and setuptools to fix security vulnerabilities -RUN pip3 install --upgrade pip setuptools - -RUN pip3 install poetry +RUN pip3 install poetry --break-system-packages # Copy and install dependencies COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs @@ -37,27 +40,30 @@ RUN poetry install --no-ansi --no-root COPY autogpt_platform/backend/schema.prisma ./ RUN poetry run prisma generate -FROM python:3.11.10-slim-bookworm AS server_dependencies +FROM debian:13-slim AS server_dependencies WORKDIR /app ENV POETRY_HOME=/opt/poetry \ POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_CREATE=false + POETRY_VIRTUALENVS_CREATE=true \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + DEBIAN_FRONTEND=noninteractive ENV PATH=/opt/poetry/bin:$PATH -# Upgrade pip and setuptools to fix security vulnerabilities -RUN pip3 install --upgrade pip setuptools +# Install Python without upgrading system-managed packages +RUN apt-get update && apt-get install -y \ + python3.13 \ + python3-pip # Copy only necessary files from builder COPY --from=builder /app /app -COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11 -COPY --from=builder /usr/local/bin /usr/local/bin +COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3* +COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry # Copy Prisma binaries COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries - -ENV PATH="/app/.venv/bin:$PATH" +ENV PATH="/app/autogpt_platform/backend/.venv/bin:$PATH" RUN mkdir -p /app/autogpt_platform/autogpt_libs RUN mkdir -p /app/autogpt_platform/backend @@ -68,6 +74,12 @@ COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.tom WORKDIR /app/autogpt_platform/backend +FROM server_dependencies AS migrate + +# Migration stage only needs schema and migrations - much lighter than full backend +COPY autogpt_platform/backend/schema.prisma /app/autogpt_platform/backend/ +COPY autogpt_platform/backend/migrations /app/autogpt_platform/backend/migrations + FROM server_dependencies AS server COPY autogpt_platform/backend /app/autogpt_platform/backend diff --git a/autogpt_platform/backend/backend/app.py b/autogpt_platform/backend/backend/app.py index e605e227f435..596962ae0bd8 100644 --- a/autogpt_platform/backend/backend/app.py +++ b/autogpt_platform/backend/backend/app.py @@ -1,6 +1,10 @@ import logging from typing import TYPE_CHECKING +from dotenv import load_dotenv + +load_dotenv() + if TYPE_CHECKING: from backend.util.process import AppProcess @@ -38,12 +42,12 @@ def main(**kwargs): from backend.server.ws_api import WebsocketServer run_processes( - DatabaseManager(), - ExecutionManager(), + DatabaseManager().set_log_level("warning"), Scheduler(), NotificationManager(), WebsocketServer(), AgentServer(), + ExecutionManager(), **kwargs, ) diff --git a/autogpt_platform/backend/backend/blocks/__init__.py b/autogpt_platform/backend/backend/blocks/__init__.py index 4715d44611e8..f6299cbb53df 100644 --- a/autogpt_platform/backend/backend/blocks/__init__.py +++ b/autogpt_platform/backend/backend/blocks/__init__.py @@ -1,10 +1,14 @@ import functools import importlib +import logging import os import re from pathlib import Path from typing import TYPE_CHECKING, TypeVar +logger = logging.getLogger(__name__) + + if TYPE_CHECKING: from backend.data.block import Block @@ -99,7 +103,15 @@ def load_all_blocks() -> dict[str, type["Block"]]: available_blocks[block.id] = block_cls - return available_blocks + # Filter out blocks with incomplete auth configs, e.g. missing OAuth server secrets + from backend.data.block import is_block_auth_configured + + filtered_blocks = {} + for block_id, block_cls in available_blocks.items(): + if is_block_auth_configured(block_cls): + filtered_blocks[block_id] = block_cls + + return filtered_blocks __all__ = ["load_all_blocks"] diff --git a/autogpt_platform/backend/backend/blocks/agent.py b/autogpt_platform/backend/backend/blocks/agent.py index b8066bd50119..ac3eedb12bdc 100644 --- a/autogpt_platform/backend/backend/blocks/agent.py +++ b/autogpt_platform/backend/backend/blocks/agent.py @@ -1,4 +1,3 @@ -import asyncio import logging from typing import Any, Optional @@ -15,7 +14,8 @@ ) from backend.data.execution import ExecutionStatus from backend.data.model import NodeExecutionStats, SchemaField -from backend.util import json, retry +from backend.util.json import validate_with_jsonschema +from backend.util.retry import func_retry _logger = logging.getLogger(__name__) @@ -25,6 +25,9 @@ class Input(BlockSchema): user_id: str = SchemaField(description="User ID") graph_id: str = SchemaField(description="Graph ID") graph_version: int = SchemaField(description="Graph Version") + agent_name: Optional[str] = SchemaField( + default=None, description="Name to display in the Builder UI" + ) inputs: BlockInput = SchemaField(description="Input data for the graph") input_schema: dict = SchemaField(description="Input schema for the graph") @@ -49,7 +52,7 @@ def get_missing_input(cls, data: BlockInput) -> set[str]: @classmethod def get_mismatch_error(cls, data: BlockInput) -> str | None: - return json.validate_with_jsonschema(cls.get_input_schema(data), data) + return validate_with_jsonschema(cls.get_input_schema(data), data) class Output(BlockSchema): pass @@ -95,23 +98,14 @@ async def run(self, input_data: Input, **kwargs) -> BlockOutput: logger=logger, ): yield name, data - except asyncio.CancelledError: + except BaseException as e: await self._stop( graph_exec_id=graph_exec.id, user_id=input_data.user_id, logger=logger, ) logger.warning( - f"Execution of graph {input_data.graph_id}v{input_data.graph_version} was cancelled." - ) - except Exception as e: - await self._stop( - graph_exec_id=graph_exec.id, - user_id=input_data.user_id, - logger=logger, - ) - logger.error( - f"Execution of graph {input_data.graph_id}v{input_data.graph_version} failed: {e}, execution is stopped." + f"Execution of graph {input_data.graph_id}v{input_data.graph_version} failed: {e.__class__.__name__} {str(e)}; execution is stopped." ) raise @@ -131,6 +125,7 @@ async def _run( log_id = f"Graph #{graph_id}-V{graph_version}, exec-id: {graph_exec_id}" logger.info(f"Starting execution of {log_id}") + yielded_node_exec_ids = set() async for event in event_bus.listen( user_id=user_id, @@ -162,6 +157,14 @@ async def _run( f"Execution {log_id} produced input {event.input_data} output {event.output_data}" ) + if event.node_exec_id in yielded_node_exec_ids: + logger.warning( + f"{log_id} received duplicate event for node execution {event.node_exec_id}" + ) + continue + else: + yielded_node_exec_ids.add(event.node_exec_id) + if not event.block_id: logger.warning(f"{log_id} received event without block_id {event}") continue @@ -181,7 +184,7 @@ async def _run( ) yield output_name, output_data - @retry.func_retry + @func_retry async def _stop( self, graph_exec_id: str, @@ -197,7 +200,8 @@ async def _stop( await execution_utils.stop_graph_execution( graph_exec_id=graph_exec_id, user_id=user_id, + wait_timeout=3600, ) logger.info(f"Execution {log_id} stopped successfully.") - except Exception as e: - logger.error(f"Failed to stop execution {log_id}: {e}") + except TimeoutError as e: + logger.error(f"Execution {log_id} stop timed out: {e}") diff --git a/autogpt_platform/backend/backend/blocks/ai_music_generator.py b/autogpt_platform/backend/backend/blocks/ai_music_generator.py index b4561bd51302..92182fb16aee 100644 --- a/autogpt_platform/backend/backend/blocks/ai_music_generator.py +++ b/autogpt_platform/backend/backend/blocks/ai_music_generator.py @@ -166,7 +166,7 @@ async def run( output_format=input_data.output_format, normalization_strategy=input_data.normalization_strategy, ) - if result and result != "No output received": + if result and isinstance(result, str) and result.startswith("http"): yield "result", result return else: diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/__init__.py b/autogpt_platform/backend/backend/blocks/airtable/__init__.py similarity index 100% rename from autogpt_platform/autogpt_libs/autogpt_libs/feature_flag/__init__.py rename to autogpt_platform/backend/backend/blocks/airtable/__init__.py diff --git a/autogpt_platform/backend/backend/blocks/airtable/_api.py b/autogpt_platform/backend/backend/blocks/airtable/_api.py new file mode 100644 index 000000000000..4fb9602d9317 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/_api.py @@ -0,0 +1,1251 @@ +import base64 +from enum import Enum +from logging import getLogger +from typing import Any +from urllib.parse import quote, urlencode + +from backend.sdk import BaseModel, Credentials, Requests + +logger = getLogger(__name__) + + +def _convert_bools( + obj: Any, +) -> Any: # noqa: ANN401 – allow Any for deep conversion utility + """Recursively walk *obj* and coerce string booleans to real booleans.""" + if isinstance(obj, str): + lowered = obj.lower() + if lowered == "true": + return True + if lowered == "false": + return False + return obj + if isinstance(obj, list): + return [_convert_bools(item) for item in obj] + if isinstance(obj, dict): + return {k: _convert_bools(v) for k, v in obj.items()} + return obj + + +class WebhookFilters(BaseModel): + dataTypes: list[str] + changeTypes: list[str] | None = None + fromSources: list[str] | None = None + sourceOptions: dict | None = None + watchDataInFieldIds: list[str] | None = None + watchSchemasOfFieldIds: list[str] | None = None + + +class WebhookIncludes(BaseModel): + includeCellValuesInFieldIds: list[str] | str | None = None + includePreviousCellValues: bool | None = None + includePreviousFieldDefinitions: bool | None = None + + +class WebhookSpecification(BaseModel): + recordChangeScope: str | None = None + filters: WebhookFilters + includes: WebhookIncludes | None = None + + +class WebhookPayload(BaseModel): + actionMetadata: dict + baseTransactionNumber: int + payloadFormat: str + timestamp: str + changedTablesById: dict | None = None + createdTablesById: dict | None = None + destroyedTableIds: list[str] | None = None + error: bool | None = None + code: str | None = None + + +class ListWebhookPayloadsResponse(BaseModel): + payloads: list[WebhookPayload] + cursor: int | None = None + might_have_more: bool | None = None + payloadFormat: str + + +class TableFieldType(str, Enum): + SINGLE_LINE_TEXT = "singleLineText" + EMAIL = "email" + URL = "url" + MULTILINE_TEXT = "multilineText" + NUMBER = "number" + PERCENT = "percent" + CURRENCY = "currency" + SINGLE_SELECT = "singleSelect" + MULTIPLE_SELECTS = "multipleSelects" + SINGLE_COLLABORATOR = "singleCollaborator" + MULTIPLE_COLLABORATORS = "multipleCollaborators" + MULTIPLE_RECORD_LINKS = "multipleRecordLinks" + DATE = "date" + DATE_TIME = "dateTime" + PHONE_NUMBER = "phoneNumber" + MULTIPLE_ATTACHMENTS = "multipleAttachments" + CHECKBOX = "checkbox" + FORMULA = "formula" + CREATED_TIME = "createdTime" + ROLLUP = "rollup" + COUNT = "count" + LOOKUP = "lookup" + MULTIPLE_LOOKUP_VALUES = "multipleLookupValues" + AUTO_NUMBER = "autoNumber" + BARCODE = "barcode" + RATING = "rating" + RICH_TEXT = "richText" + DURATION = "duration" + LAST_MODIFIED_TIME = "lastModifiedTime" + BUTTON = "button" + CREATED_BY = "createdBy" + LAST_MODIFIED_BY = "lastModifiedBy" + EXTERNAL_SYNC_SOURCE = "externalSyncSource" + AI_TEXT = "aiText" + + +TABLE_FIELD_TYPES = set(type.value for type in TableFieldType) + + +class AirtableTimeZones(str, Enum): + UTC = "utc" + CLIENT = "client" + AFRICA_ABIDJAN = "Africa/Abidjan" + AFRICA_ACCRA = "Africa/Accra" + AFRICA_ADDIS_ABABA = "Africa/Addis_Ababa" + AFRICA_ALGIERS = "Africa/Algiers" + AFRICA_ASMARA = "Africa/Asmara" + AFRICA_BAMAKO = "Africa/Bamako" + AFRICA_BANGUI = "Africa/Bangui" + AFRICA_BANJUL = "Africa/Banjul" + AFRICA_BISSAU = "Africa/Bissau" + AFRICA_BLANTYRE = "Africa/Blantyre" + AFRICA_BRAZZAVILLE = "Africa/Brazzaville" + AFRICA_BUJUMBURA = "Africa/Bujumbura" + AFRICA_CAIRO = "Africa/Cairo" + AFRICA_CASABLANCA = "Africa/Casablanca" + AFRICA_CEUTA = "Africa/Ceuta" + AFRICA_CONAKRY = "Africa/Conakry" + AFRICA_DAKAR = "Africa/Dakar" + AFRICA_DAR_ES_SALAAM = "Africa/Dar_es_Salaam" + AFRICA_DJIBOUTI = "Africa/Djibouti" + AFRICA_DOUALA = "Africa/Douala" + AFRICA_EL_AAIUN = "Africa/El_Aaiun" + AFRICA_FREETOWN = "Africa/Freetown" + AFRICA_GABORONE = "Africa/Gaborone" + AFRICA_HARARE = "Africa/Harare" + AFRICA_JOHANNESBURG = "Africa/Johannesburg" + AFRICA_JUBA = "Africa/Juba" + AFRICA_KAMPALA = "Africa/Kampala" + AFRICA_KHARTOUM = "Africa/Khartoum" + AFRICA_KIGALI = "Africa/Kigali" + AFRICA_KINSHASA = "Africa/Kinshasa" + AFRICA_LAGOS = "Africa/Lagos" + AFRICA_LIBREVILLE = "Africa/Libreville" + AFRICA_LOME = "Africa/Lome" + AFRICA_LUANDA = "Africa/Luanda" + AFRICA_LUBUMBASHI = "Africa/Lubumbashi" + AFRICA_LUSAKA = "Africa/Lusaka" + AFRICA_MALABO = "Africa/Malabo" + AFRICA_MAPUTO = "Africa/Maputo" + AFRICA_MASERU = "Africa/Maseru" + AFRICA_MBABANE = "Africa/Mbabane" + AFRICA_MOGADISHU = "Africa/Mogadishu" + AFRICA_MONROVIA = "Africa/Monrovia" + AFRICA_NAIROBI = "Africa/Nairobi" + AFRICA_NDJAMENA = "Africa/Ndjamena" + AFRICA_NIAMEY = "Africa/Niamey" + AFRICA_NOUAKCHOTT = "Africa/Nouakchott" + AFRICA_OUAGADOUGOU = "Africa/Ouagadougou" + AFRICA_PORTO_NOVO = "Africa/Porto-Novo" + AFRICA_SAO_TOME = "Africa/Sao_Tome" + AFRICA_TRIPOLI = "Africa/Tripoli" + AFRICA_TUNIS = "Africa/Tunis" + AFRICA_WINDHOEK = "Africa/Windhoek" + AMERICA_ADAK = "America/Adak" + AMERICA_ANCHORAGE = "America/Anchorage" + AMERICA_ANGUILLA = "America/Anguilla" + AMERICA_ANTIGUA = "America/Antigua" + AMERICA_ARAGUAINA = "America/Araguaina" + AMERICA_ARGENTINA_BUENOS_AIRES = "America/Argentina/Buenos_Aires" + AMERICA_ARGENTINA_CATAMARCA = "America/Argentina/Catamarca" + AMERICA_ARGENTINA_CORDOBA = "America/Argentina/Cordoba" + AMERICA_ARGENTINA_JUJUY = "America/Argentina/Jujuy" + AMERICA_ARGENTINA_LA_RIOJA = "America/Argentina/La_Rioja" + AMERICA_ARGENTINA_MENDOZA = "America/Argentina/Mendoza" + AMERICA_ARGENTINA_RIO_GALLEGOS = "America/Argentina/Rio_Gallegos" + AMERICA_ARGENTINA_SALTA = "America/Argentina/Salta" + AMERICA_ARGENTINA_SAN_JUAN = "America/Argentina/San_Juan" + AMERICA_ARGENTINA_SAN_LUIS = "America/Argentina/San_Luis" + AMERICA_ARGENTINA_TUCUMAN = "America/Argentina/Tucuman" + AMERICA_ARGENTINA_USHUAIA = "America/Argentina/Ushuaia" + AMERICA_ARUBA = "America/Aruba" + AMERICA_ASUNCION = "America/Asuncion" + AMERICA_ATIKOKAN = "America/Atikokan" + AMERICA_BAHIA = "America/Bahia" + AMERICA_BAHIA_BANDERAS = "America/Bahia_Banderas" + AMERICA_BARBADOS = "America/Barbados" + AMERICA_BELEM = "America/Belem" + AMERICA_BELIZE = "America/Belize" + AMERICA_BLANC_SABLON = "America/Blanc-Sablon" + AMERICA_BOA_VISTA = "America/Boa_Vista" + AMERICA_BOGOTA = "America/Bogota" + AMERICA_BOISE = "America/Boise" + AMERICA_CAMBRIDGE_BAY = "America/Cambridge_Bay" + AMERICA_CAMPO_GRANDE = "America/Campo_Grande" + AMERICA_CANCUN = "America/Cancun" + AMERICA_CARACAS = "America/Caracas" + AMERICA_CAYENNE = "America/Cayenne" + AMERICA_CAYMAN = "America/Cayman" + AMERICA_CHICAGO = "America/Chicago" + AMERICA_CHIHUAHUA = "America/Chihuahua" + AMERICA_COSTA_RICA = "America/Costa_Rica" + AMERICA_CRESTON = "America/Creston" + AMERICA_CUIABA = "America/Cuiaba" + AMERICA_CURACAO = "America/Curacao" + AMERICA_DANMARKSHAVN = "America/Danmarkshavn" + AMERICA_DAWSON = "America/Dawson" + AMERICA_DAWSON_CREEK = "America/Dawson_Creek" + AMERICA_DENVER = "America/Denver" + AMERICA_DETROIT = "America/Detroit" + AMERICA_DOMINICA = "America/Dominica" + AMERICA_EDMONTON = "America/Edmonton" + AMERICA_EIRUNEPE = "America/Eirunepe" + AMERICA_EL_SALVADOR = "America/El_Salvador" + AMERICA_FORT_NELSON = "America/Fort_Nelson" + AMERICA_FORTALEZA = "America/Fortaleza" + AMERICA_GLACE_BAY = "America/Glace_Bay" + AMERICA_GODTHAB = "America/Godthab" + AMERICA_GOOSE_BAY = "America/Goose_Bay" + AMERICA_GRAND_TURK = "America/Grand_Turk" + AMERICA_GRENADA = "America/Grenada" + AMERICA_GUADELOUPE = "America/Guadeloupe" + AMERICA_GUATEMALA = "America/Guatemala" + AMERICA_GUAYAQUIL = "America/Guayaquil" + AMERICA_GUYANA = "America/Guyana" + AMERICA_HALIFAX = "America/Halifax" + AMERICA_HAVANA = "America/Havana" + AMERICA_HERMOSILLO = "America/Hermosillo" + AMERICA_INDIANA_INDIANAPOLIS = "America/Indiana/Indianapolis" + AMERICA_INDIANA_KNOX = "America/Indiana/Knox" + AMERICA_INDIANA_MARENGO = "America/Indiana/Marengo" + AMERICA_INDIANA_PETERSBURG = "America/Indiana/Petersburg" + AMERICA_INDIANA_TELL_CITY = "America/Indiana/Tell_City" + AMERICA_INDIANA_VEVAY = "America/Indiana/Vevay" + AMERICA_INDIANA_VINCENNES = "America/Indiana/Vincennes" + AMERICA_INDIANA_WINAMAC = "America/Indiana/Winamac" + AMERICA_INUVIK = "America/Inuvik" + AMERICA_IQALUIT = "America/Iqaluit" + AMERICA_JAMAICA = "America/Jamaica" + AMERICA_JUNEAU = "America/Juneau" + AMERICA_KENTUCKY_LOUISVILLE = "America/Kentucky/Louisville" + AMERICA_KENTUCKY_MONTICELLO = "America/Kentucky/Monticello" + AMERICA_KRALENDIJK = "America/Kralendijk" + AMERICA_LA_PAZ = "America/La_Paz" + AMERICA_LIMA = "America/Lima" + AMERICA_LOS_ANGELES = "America/Los_Angeles" + AMERICA_LOWER_PRINCES = "America/Lower_Princes" + AMERICA_MACEIO = "America/Maceio" + AMERICA_MANAGUA = "America/Managua" + AMERICA_MANAUS = "America/Manaus" + AMERICA_MARIGOT = "America/Marigot" + AMERICA_MARTINIQUE = "America/Martinique" + AMERICA_MATAMOROS = "America/Matamoros" + AMERICA_MAZATLAN = "America/Mazatlan" + AMERICA_MENOMINEE = "America/Menominee" + AMERICA_MERIDA = "America/Merida" + AMERICA_METLAKATLA = "America/Metlakatla" + AMERICA_MEXICO_CITY = "America/Mexico_City" + AMERICA_MIQUELON = "America/Miquelon" + AMERICA_MONCTON = "America/Moncton" + AMERICA_MONTERREY = "America/Monterrey" + AMERICA_MONTEVIDEO = "America/Montevideo" + AMERICA_MONTSERRAT = "America/Montserrat" + AMERICA_NASSAU = "America/Nassau" + AMERICA_NEW_YORK = "America/New_York" + AMERICA_NIPIGON = "America/Nipigon" + AMERICA_NOME = "America/Nome" + AMERICA_NORONHA = "America/Noronha" + AMERICA_NORTH_DAKOTA_BEULAH = "America/North_Dakota/Beulah" + AMERICA_NORTH_DAKOTA_CENTER = "America/North_Dakota/Center" + AMERICA_NORTH_DAKOTA_NEW_SALEM = "America/North_Dakota/New_Salem" + AMERICA_NUUK = "America/Nuuk" + AMERICA_OJINAGA = "America/Ojinaga" + AMERICA_PANAMA = "America/Panama" + AMERICA_PANGNIRTUNG = "America/Pangnirtung" + AMERICA_PARAMARIBO = "America/Paramaribo" + AMERICA_PHOENIX = "America/Phoenix" + AMERICA_PORT_AU_PRINCE = "America/Port-au-Prince" + AMERICA_PORT_OF_SPAIN = "America/Port_of_Spain" + AMERICA_PORTO_VELHO = "America/Porto_Velho" + AMERICA_PUERTO_RICO = "America/Puerto_Rico" + AMERICA_PUNTA_ARENAS = "America/Punta_Arenas" + AMERICA_RAINY_RIVER = "America/Rainy_River" + AMERICA_RANKIN_INLET = "America/Rankin_Inlet" + AMERICA_RECIFE = "America/Recife" + AMERICA_REGINA = "America/Regina" + AMERICA_RESOLUTE = "America/Resolute" + AMERICA_RIO_BRANCO = "America/Rio_Branco" + AMERICA_SANTAREM = "America/Santarem" + AMERICA_SANTIAGO = "America/Santiago" + AMERICA_SANTO_DOMINGO = "America/Santo_Domingo" + AMERICA_SAO_PAULO = "America/Sao_Paulo" + AMERICA_SCORESBYSUND = "America/Scoresbysund" + AMERICA_SITKA = "America/Sitka" + AMERICA_ST_BARTHELEMY = "America/St_Barthelemy" + AMERICA_ST_JOHNS = "America/St_Johns" + AMERICA_ST_KITTS = "America/St_Kitts" + AMERICA_ST_LUCIA = "America/St_Lucia" + AMERICA_ST_THOMAS = "America/St_Thomas" + AMERICA_ST_VINCENT = "America/St_Vincent" + AMERICA_SWIFT_CURRENT = "America/Swift_Current" + AMERICA_TEGUCIGALPA = "America/Tegucigalpa" + AMERICA_THULE = "America/Thule" + AMERICA_THUNDER_BAY = "America/Thunder_Bay" + AMERICA_TIJUANA = "America/Tijuana" + AMERICA_TORONTO = "America/Toronto" + AMERICA_TORTOLA = "America/Tortola" + AMERICA_VANCOUVER = "America/Vancouver" + AMERICA_WHITEHORSE = "America/Whitehorse" + AMERICA_WINNIPEG = "America/Winnipeg" + AMERICA_YAKUTAT = "America/Yakutat" + AMERICA_YELLOWKNIFE = "America/Yellowknife" + ANTARCTICA_CASEY = "Antarctica/Casey" + ANTARCTICA_DAVIS = "Antarctica/Davis" + ANTARCTICA_DUMONT_DURVILLE = "Antarctica/DumontDUrville" + ANTARCTICA_MACQUARIE = "Antarctica/Macquarie" + ANTARCTICA_MAWSON = "Antarctica/Mawson" + ANTARCTICA_MCMURDO = "Antarctica/McMurdo" + ANTARCTICA_PALMER = "Antarctica/Palmer" + ANTARCTICA_ROTHERA = "Antarctica/Rothera" + ANTARCTICA_SYOWA = "Antarctica/Syowa" + ANTARCTICA_TROLL = "Antarctica/Troll" + ANTARCTICA_VOSTOK = "Antarctica/Vostok" + ARCTIC_LONGYEARBYEN = "Arctic/Longyearbyen" + ASIA_ADEN = "Asia/Aden" + ASIA_ALMATY = "Asia/Almaty" + ASIA_AMMAN = "Asia/Amman" + ASIA_ANADYR = "Asia/Anadyr" + ASIA_AQTAU = "Asia/Aqtau" + ASIA_AQTOBE = "Asia/Aqtobe" + ASIA_ASHGABAT = "Asia/Ashgabat" + ASIA_ATYRAU = "Asia/Atyrau" + ASIA_BAGHDAD = "Asia/Baghdad" + ASIA_BAHRAIN = "Asia/Bahrain" + ASIA_BAKU = "Asia/Baku" + ASIA_BANGKOK = "Asia/Bangkok" + ASIA_BARNAUL = "Asia/Barnaul" + ASIA_BEIRUT = "Asia/Beirut" + ASIA_BISHKEK = "Asia/Bishkek" + ASIA_BRUNEI = "Asia/Brunei" + ASIA_CHITA = "Asia/Chita" + ASIA_CHOIBALSAN = "Asia/Choibalsan" + ASIA_COLOMBO = "Asia/Colombo" + ASIA_DAMASCUS = "Asia/Damascus" + ASIA_DHAKA = "Asia/Dhaka" + ASIA_DILI = "Asia/Dili" + ASIA_DUBAI = "Asia/Dubai" + ASIA_DUSHANBE = "Asia/Dushanbe" + ASIA_FAMAGUSTA = "Asia/Famagusta" + ASIA_GAZA = "Asia/Gaza" + ASIA_HEBRON = "Asia/Hebron" + ASIA_HO_CHI_MINH = "Asia/Ho_Chi_Minh" + ASIA_HONG_KONG = "Asia/Hong_Kong" + ASIA_HOVD = "Asia/Hovd" + ASIA_IRKUTSK = "Asia/Irkutsk" + ASIA_ISTANBUL = "Asia/Istanbul" + ASIA_JAKARTA = "Asia/Jakarta" + ASIA_JAYAPURA = "Asia/Jayapura" + ASIA_JERUSALEM = "Asia/Jerusalem" + ASIA_KABUL = "Asia/Kabul" + ASIA_KAMCHATKA = "Asia/Kamchatka" + ASIA_KARACHI = "Asia/Karachi" + ASIA_KATHMANDU = "Asia/Kathmandu" + ASIA_KHANDYGA = "Asia/Khandyga" + ASIA_KOLKATA = "Asia/Kolkata" + ASIA_KRASNOYARSK = "Asia/Krasnoyarsk" + ASIA_KUALA_LUMPUR = "Asia/Kuala_Lumpur" + ASIA_KUCHING = "Asia/Kuching" + ASIA_KUWAIT = "Asia/Kuwait" + ASIA_MACAU = "Asia/Macau" + ASIA_MAGADAN = "Asia/Magadan" + ASIA_MAKASSAR = "Asia/Makassar" + ASIA_MANILA = "Asia/Manila" + ASIA_MUSCAT = "Asia/Muscat" + ASIA_NICOSIA = "Asia/Nicosia" + ASIA_NOVOKUZNETSK = "Asia/Novokuznetsk" + ASIA_NOVOSIBIRSK = "Asia/Novosibirsk" + ASIA_OMSK = "Asia/Omsk" + ASIA_ORAL = "Asia/Oral" + ASIA_PHNOM_PENH = "Asia/Phnom_Penh" + ASIA_PONTIANAK = "Asia/Pontianak" + ASIA_PYONGYANG = "Asia/Pyongyang" + ASIA_QATAR = "Asia/Qatar" + ASIA_QOSTANAY = "Asia/Qostanay" + ASIA_QYZYLORDA = "Asia/Qyzylorda" + ASIA_RANGOON = "Asia/Rangoon" + ASIA_RIYADH = "Asia/Riyadh" + ASIA_SAKHALIN = "Asia/Sakhalin" + ASIA_SAMARKAND = "Asia/Samarkand" + ASIA_SEOUL = "Asia/Seoul" + ASIA_SHANGHAI = "Asia/Shanghai" + ASIA_SINGAPORE = "Asia/Singapore" + ASIA_SREDNEKOLYMSK = "Asia/Srednekolymsk" + ASIA_TAIPEI = "Asia/Taipei" + ASIA_TASHKENT = "Asia/Tashkent" + ASIA_TBILISI = "Asia/Tbilisi" + ASIA_TEHRAN = "Asia/Tehran" + ASIA_THIMPHU = "Asia/Thimphu" + ASIA_TOKYO = "Asia/Tokyo" + ASIA_TOMSK = "Asia/Tomsk" + ASIA_ULAANBAATAR = "Asia/Ulaanbaatar" + ASIA_URUMQI = "Asia/Urumqi" + ASIA_UST_NERA = "Asia/Ust-Nera" + ASIA_VIENTIANE = "Asia/Vientiane" + ASIA_VLADIVOSTOK = "Asia/Vladivostok" + ASIA_YAKUTSK = "Asia/Yakutsk" + ASIA_YANGON = "Asia/Yangon" + ASIA_YEKATERINBURG = "Asia/Yekaterinburg" + ASIA_YEREVAN = "Asia/Yerevan" + ATLANTIC_AZORES = "Atlantic/Azores" + ATLANTIC_BERMUDA = "Atlantic/Bermuda" + ATLANTIC_CANARY = "Atlantic/Canary" + ATLANTIC_CAPE_VERDE = "Atlantic/Cape_Verde" + ATLANTIC_FAROE = "Atlantic/Faroe" + ATLANTIC_MADEIRA = "Atlantic/Madeira" + ATLANTIC_REYKJAVIK = "Atlantic/Reykjavik" + ATLANTIC_SOUTH_GEORGIA = "Atlantic/South_Georgia" + ATLANTIC_ST_HELENA = "Atlantic/St_Helena" + ATLANTIC_STANLEY = "Atlantic/Stanley" + AUSTRALIA_ADELAIDE = "Australia/Adelaide" + AUSTRALIA_BRISBANE = "Australia/Brisbane" + AUSTRALIA_BROKEN_HILL = "Australia/Broken_Hill" + AUSTRALIA_CURRIE = "Australia/Currie" + AUSTRALIA_DARWIN = "Australia/Darwin" + AUSTRALIA_EUCLA = "Australia/Eucla" + AUSTRALIA_HOBART = "Australia/Hobart" + AUSTRALIA_LINDEMAN = "Australia/Lindeman" + AUSTRALIA_LORD_HOWE = "Australia/Lord_Howe" + AUSTRALIA_MELBOURNE = "Australia/Melbourne" + AUSTRALIA_PERTH = "Australia/Perth" + AUSTRALIA_SYDNEY = "Australia/Sydney" + EUROPE_AMSTERDAM = "Europe/Amsterdam" + EUROPE_ANDORRA = "Europe/Andorra" + EUROPE_ASTRAKHAN = "Europe/Astrakhan" + EUROPE_ATHENS = "Europe/Athens" + EUROPE_BELGRADE = "Europe/Belgrade" + EUROPE_BERLIN = "Europe/Berlin" + EUROPE_BRATISLAVA = "Europe/Bratislava" + EUROPE_BRUSSELS = "Europe/Brussels" + EUROPE_BUCHAREST = "Europe/Bucharest" + EUROPE_BUDAPEST = "Europe/Budapest" + EUROPE_BUSINGEN = "Europe/Busingen" + EUROPE_CHISINAU = "Europe/Chisinau" + EUROPE_COPENHAGEN = "Europe/Copenhagen" + EUROPE_DUBLIN = "Europe/Dublin" + EUROPE_GIBRALTAR = "Europe/Gibraltar" + EUROPE_GUERNSEY = "Europe/Guernsey" + EUROPE_HELSINKI = "Europe/Helsinki" + EUROPE_ISLE_OF_MAN = "Europe/Isle_of_Man" + EUROPE_ISTANBUL = "Europe/Istanbul" + EUROPE_JERSEY = "Europe/Jersey" + EUROPE_KALININGRAD = "Europe/Kaliningrad" + EUROPE_KIEV = "Europe/Kiev" + EUROPE_KIROV = "Europe/Kirov" + EUROPE_LISBON = "Europe/Lisbon" + EUROPE_LJUBLJANA = "Europe/Ljubljana" + EUROPE_LONDON = "Europe/London" + EUROPE_LUXEMBOURG = "Europe/Luxembourg" + EUROPE_MADRID = "Europe/Madrid" + EUROPE_MALTA = "Europe/Malta" + EUROPE_MARIEHAMN = "Europe/Mariehamn" + EUROPE_MINSK = "Europe/Minsk" + EUROPE_MONACO = "Europe/Monaco" + EUROPE_MOSCOW = "Europe/Moscow" + EUROPE_NICOSIA = "Europe/Nicosia" + EUROPE_OSLO = "Europe/Oslo" + EUROPE_PARIS = "Europe/Paris" + EUROPE_PODGORICA = "Europe/Podgorica" + EUROPE_PRAGUE = "Europe/Prague" + EUROPE_RIGA = "Europe/Riga" + EUROPE_ROME = "Europe/Rome" + EUROPE_SAMARA = "Europe/Samara" + EUROPE_SAN_MARINO = "Europe/San_Marino" + EUROPE_SARAJEVO = "Europe/Sarajevo" + EUROPE_SARATOV = "Europe/Saratov" + EUROPE_SIMFEROPOL = "Europe/Simferopol" + EUROPE_SKOPJE = "Europe/Skopje" + EUROPE_SOFIA = "Europe/Sofia" + EUROPE_STOCKHOLM = "Europe/Stockholm" + EUROPE_TALLINN = "Europe/Tallinn" + EUROPE_TIRANE = "Europe/Tirane" + EUROPE_ULYANOVSK = "Europe/Ulyanovsk" + EUROPE_UZHGOROD = "Europe/Uzhgorod" + EUROPE_VADUZ = "Europe/Vaduz" + EUROPE_VATICAN = "Europe/Vatican" + EUROPE_VIENNA = "Europe/Vienna" + EUROPE_VILNIUS = "Europe/Vilnius" + EUROPE_VOLGOGRAD = "Europe/Volgograd" + EUROPE_WARSAW = "Europe/Warsaw" + EUROPE_ZAGREB = "Europe/Zagreb" + EUROPE_ZAPOROZHYE = "Europe/Zaporozhye" + EUROPE_ZURICH = "Europe/Zurich" + INDIAN_ANTANANARIVO = "Indian/Antananarivo" + INDIAN_CHAGOS = "Indian/Chagos" + INDIAN_CHRISTMAS = "Indian/Christmas" + INDIAN_COCOS = "Indian/Cocos" + INDIAN_COMORO = "Indian/Comoro" + INDIAN_KERGUELEN = "Indian/Kerguelen" + INDIAN_MAHE = "Indian/Mahe" + INDIAN_MALDIVES = "Indian/Maldives" + INDIAN_MAURITIUS = "Indian/Mauritius" + INDIAN_MAYOTTE = "Indian/Mayotte" + INDIAN_REUNION = "Indian/Reunion" + PACIFIC_APIA = "Pacific/Apia" + PACIFIC_AUCKLAND = "Pacific/Auckland" + PACIFIC_BOUGAINVILLE = "Pacific/Bougainville" + PACIFIC_CHATHAM = "Pacific/Chatham" + PACIFIC_CHUUK = "Pacific/Chuuk" + PACIFIC_EASTER = "Pacific/Easter" + PACIFIC_EFATE = "Pacific/Efate" + PACIFIC_ENDERBURY = "Pacific/Enderbury" + PACIFIC_FAKAOFO = "Pacific/Fakaofo" + PACIFIC_FIJI = "Pacific/Fiji" + PACIFIC_FUNAFUTI = "Pacific/Funafuti" + PACIFIC_GALAPAGOS = "Pacific/Galapagos" + PACIFIC_GAMBIER = "Pacific/Gambier" + PACIFIC_GUADALCANAL = "Pacific/Guadalcanal" + PACIFIC_GUAM = "Pacific/Guam" + PACIFIC_HONOLULU = "Pacific/Honolulu" + PACIFIC_KANTON = "Pacific/Kanton" + PACIFIC_KIRITIMATI = "Pacific/Kiritimati" + PACIFIC_KOSRAE = "Pacific/Kosrae" + PACIFIC_KWAJALEIN = "Pacific/Kwajalein" + PACIFIC_MAJURO = "Pacific/Majuro" + PACIFIC_MARQUESAS = "Pacific/Marquesas" + PACIFIC_MIDWAY = "Pacific/Midway" + PACIFIC_NAURU = "Pacific/Nauru" + PACIFIC_NIUE = "Pacific/Niue" + PACIFIC_NORFOLK = "Pacific/Norfolk" + PACIFIC_NOUMEA = "Pacific/Noumea" + PACIFIC_PAGO_PAGO = "Pacific/Pago_Pago" + PACIFIC_PALAU = "Pacific/Palau" + PACIFIC_PITCAIRN = "Pacific/Pitcairn" + PACIFIC_POHNPEI = "Pacific/Pohnpei" + PACIFIC_PORT_MORESBY = "Pacific/Port_Moresby" + PACIFIC_RAROTONGA = "Pacific/Rarotonga" + PACIFIC_SAIPAN = "Pacific/Saipan" + PACIFIC_TAHITI = "Pacific/Tahiti" + PACIFIC_TARAWA = "Pacific/Tarawa" + PACIFIC_TONGATAPU = "Pacific/Tongatapu" + PACIFIC_WAKE = "Pacific/Wake" + PACIFIC_WALLIS = "Pacific/Wallis" + + +################################################################# +# Schema Management (Tables and Fields) +# NOTE: No delete operations are available in the Airtable API +################################################################# + + +async def create_table( + credentials: Credentials, + base_id: str, + table_name: str, + table_fields: list[dict], +) -> dict: + for field in table_fields: + assert field.get("name"), "Field name is required" + assert ( + field.get("type") in TABLE_FIELD_TYPES + ), f"Field type {field.get('type')} is not valid. Valid types are {TABLE_FIELD_TYPES}." + # Note fields have differnet options for different types we are not currently validating them + + response = await Requests().post( + f"https://api.airtable.com/v0/meta/bases/{base_id}/tables", + headers={"Authorization": credentials.auth_header()}, + json={ + "name": table_name, + "fields": table_fields, + }, + ) + + return response.json() + + +async def update_table( + credentials: Credentials, + base_id: str, + table_id: str, + table_name: str | None = None, + table_description: str | None = None, + date_dependency: dict | None = None, +) -> dict: + + assert ( + table_name or table_description or date_dependency + ), "At least one of table_name, table_description, or date_dependency must be provided" + + params: dict[str, str | dict[str, str]] = {} + if table_name: + params["name"] = table_name + if table_description: + params["description"] = table_description + if date_dependency: + params["dateDependency"] = date_dependency + + response = await Requests().patch( + f"https://api.airtable.com/v0/meta/bases/{base_id}/tables/{table_id}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + + return response.json() + + +async def create_field( + credentials: Credentials, + base_id: str, + table_id: str, + field_type: TableFieldType, + name: str, + description: str | None = None, + options: dict[str, str] | None = None, +) -> dict[str, str | dict[str, str]]: + + assert ( + field_type in TABLE_FIELD_TYPES + ), f"Field type {field_type} is not valid. Valid types are {TABLE_FIELD_TYPES}." + params: dict[str, str | dict[str, str]] = {} + params["type"] = field_type + params["name"] = name + if description: + params["description"] = description + if options: + params["options"] = options + + response = await Requests().post( + f"https://api.airtable.com/v0/meta/bases/{base_id}/tables/{table_id}/fields", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +async def update_field( + credentials: Credentials, + base_id: str, + table_id: str, + field_id: str, + name: str | None = None, + description: str | None = None, +) -> dict[str, str]: + + assert name or description, "At least one of name or description must be provided" + params: dict[str, str | dict[str, str]] = {} + if name: + params["name"] = name + if description: + params["description"] = description + + response = await Requests().patch( + f"https://api.airtable.com/v0/meta/bases/{base_id}/tables/{table_id}/fields/{field_id}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +################################################################# +# Record Management +################################################################# + + +async def list_records( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + # Query parameters + time_zone: AirtableTimeZones | None = None, + user_local: str | None = None, + page_size: int | None = None, + max_records: int | None = None, + offset: str | None = None, + view: str | None = None, + sort: list[dict[str, str]] | None = None, + filter_by_formula: str | None = None, + cell_format: dict[str, str] | None = None, + fields: list[str] | None = None, + return_fields_by_field_id: bool | None = None, + record_metadata: list[str] | None = None, +) -> dict[str, list[dict[str, dict[str, str]]]]: + + params: dict[str, str | dict[str, str] | list[dict[str, str]] | list[str]] = {} + if time_zone: + params["timeZone"] = time_zone + if user_local: + params["userLocal"] = user_local + if page_size: + params["pageSize"] = str(page_size) + if max_records: + params["maxRecords"] = str(max_records) + if offset: + params["offset"] = offset + if view: + params["view"] = view + if sort: + params["sort"] = sort + if filter_by_formula: + params["filterByFormula"] = filter_by_formula + if cell_format: + params["cellFormat"] = cell_format + if fields: + params["fields"] = fields + if return_fields_by_field_id: + params["returnFieldsByFieldId"] = str(return_fields_by_field_id) + if record_metadata: + params["recordMetadata"] = record_metadata + + response = await Requests().get( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +async def get_record( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + record_id: str, +) -> dict[str, dict[str, dict[str, str]]]: + + response = await Requests().get( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}/{record_id}", + headers={"Authorization": credentials.auth_header()}, + ) + return response.json() + + +async def update_multiple_records( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + records: list[dict[str, dict[str, str]]], + perform_upsert: dict[str, list[str]] | None = None, + return_fields_by_field_id: bool | None = None, + typecast: bool | None = None, +) -> dict[str, dict[str, dict[str, str]]]: + + params: dict[ + str, str | bool | dict[str, list[str]] | list[dict[str, dict[str, str]]] + ] = {} + if perform_upsert: + params["performUpsert"] = perform_upsert + if return_fields_by_field_id: + params["returnFieldsByFieldId"] = str(return_fields_by_field_id) + if typecast: + params["typecast"] = typecast + + params["records"] = [_convert_bools(record) for record in records] + + response = await Requests().patch( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +async def update_record( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + record_id: str, + return_fields_by_field_id: bool | None = None, + typecast: bool | None = None, + fields: dict[str, Any] | None = None, +) -> dict[str, dict[str, dict[str, str]]]: + params: dict[str, str | bool | dict[str, Any] | list[dict[str, dict[str, str]]]] = ( + {} + ) + if return_fields_by_field_id: + params["returnFieldsByFieldId"] = return_fields_by_field_id + if typecast: + params["typecast"] = typecast + if fields: + params["fields"] = fields + + response = await Requests().patch( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}/{record_id}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +async def create_record( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + fields: dict[str, Any] | None = None, + records: list[dict[str, Any]] | None = None, + return_fields_by_field_id: bool | None = None, + typecast: bool | None = None, +) -> dict[str, dict[str, dict[str, str]]]: + assert fields or records, "At least one of fields or records must be provided" + assert not (fields and records), "Only one of fields or records can be provided" + if records is not None: + assert ( + len(records) <= 10 + ), "Only up to 10 records can be provided when using records" + + params: dict[str, str | bool | dict[str, Any] | list[dict[str, Any]]] = {} + if fields: + params["fields"] = fields + if records: + params["records"] = records + if return_fields_by_field_id: + params["returnFieldsByFieldId"] = return_fields_by_field_id + if typecast: + params["typecast"] = typecast + + response = await Requests().post( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + + return response.json() + + +async def delete_multiple_records( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + records: list[str], +) -> dict[str, dict[str, dict[str, str]]]: + + query_string = "&".join([f"records[]={quote(record)}" for record in records]) + response = await Requests().delete( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}?{query_string}", + headers={"Authorization": credentials.auth_header()}, + ) + return response.json() + + +async def delete_record( + credentials: Credentials, + base_id: str, + table_id_or_name: str, + record_id: str, +) -> dict[str, dict[str, dict[str, str]]]: + + response = await Requests().delete( + f"https://api.airtable.com/v0/{base_id}/{table_id_or_name}/{record_id}", + headers={"Authorization": credentials.auth_header()}, + ) + return response.json() + + +async def create_webhook( + credentials: Credentials, + base_id: str, + webhook_specification: WebhookSpecification, + notification_url: str | None = None, +) -> Any: + + params: dict[str, Any] = { + "specification": { + "options": { + "filters": webhook_specification.filters.model_dump(exclude_unset=True), + } + }, + } + if webhook_specification.includes: + params["specification"]["options"]["includes"] = ( + webhook_specification.includes.model_dump(exclude_unset=True) + ) + if notification_url: + params["notificationUrl"] = notification_url + + response = await Requests().post( + f"https://api.airtable.com/v0/bases/{base_id}/webhooks", + headers={"Authorization": credentials.auth_header()}, + json=_convert_bools(params), + ) + return response.json() + + +async def delete_webhook( + credentials: Credentials, + base_id: str, + webhook_id: str, +) -> Any: + + response = await Requests().delete( + f"https://api.airtable.com/v0/bases/{base_id}/webhooks/{webhook_id}", + headers={"Authorization": credentials.auth_header()}, + ) + return response.json() + + +async def list_webhook_payloads( + credentials: Credentials, + base_id: str, + webhook_id: str, + cursor: str | None = None, + limit: int | None = None, +) -> ListWebhookPayloadsResponse: + + query_string = "" + if cursor: + query_string += f"cursor={cursor}" + if limit: + query_string += f"limit={limit}" + + if query_string: + query_string = f"?{query_string}" + + response = await Requests().get( + f"https://api.airtable.com/v0/bases/{base_id}/webhooks/{webhook_id}/payloads{query_string}", + headers={"Authorization": credentials.auth_header()}, + ) + try: + logger.info(f"Response: {response.json()}") + return ListWebhookPayloadsResponse( + payloads=response.json().get("payloads", []), + cursor=response.json().get("cursor"), + might_have_more=response.json().get("might_have_more") == "True", + payloadFormat=response.json().get("payloadFormat", "v0"), + ) + except Exception as e: + raise ValueError( + f"Failed to validate webhook payloads response: {e}\nResponse: {response.json()}" + ) + + +async def list_webhooks( + credentials: Credentials, + base_id: str, +) -> Any: + + response = await Requests().get( + f"https://api.airtable.com/v0/bases/{base_id}/webhooks", + headers={"Authorization": credentials.auth_header()}, + ) + return response.json() + + +class OAuthAuthorizeRequest(BaseModel): + """OAuth authorization request parameters for Airtable. + + Parameters: + client_id: An opaque string that identifies your integration with Airtable + redirect_uri: The URI for the authorize response redirect. Must exactly match a redirect URI + associated with your integration. HTTPS is required for any URI beside localhost. + response_type: The string "code" + scope: A space delimited list of unique scopes. All scopes must be valid Airtable defined scopes + that have been selected for your integration. At least one scope is required. + state: A cryptographically generated, opaque string for CSRF protection + code_challenge: The base64 url-encoding of the sha256 of the code_verifier. Protects against + man-in-the-middle grant code injection attacks. Part of the PKCE extension of OAuth. + code_challenge_method: The string "S256" + """ + + client_id: str + redirect_uri: str + response_type: str = "code" + scope: str + state: str + code_challenge: str + code_challenge_method: str = "S256" + + +class OAuthTokenRequest(BaseModel): + """OAuth token request parameters for Airtable. + + These parameters must be formatted via application/x-www-form-urlencoded encoding. + + Parameters: + code: The grant code generated during the authorization request. Can only be used once. + client_id: The client_id used in the authorization request that generated the code. + Optional if your integration has a client_secret. Used to prevent MITM attacks. + redirect_uri: The redirect_uri used in the authorization request that generated the code. + Used to prevent MITM attacks. + grant_type: The string "authorization_code". + code_verifier: A cryptographically generated, opaque string used to generate the + code_challenge parameter in the authorization request that generated the code. + """ + + code: str + client_id: str + redirect_uri: str + grant_type: str = "authorization_code" + code_verifier: str + + +class OAuthRefreshTokenRequest(BaseModel): + """OAuth token refresh request parameters for Airtable. + + These parameters must be formatted via application/x-www-form-urlencoded encoding. + + Parameters: + refresh_token: The saved refresh token from the previous token grant. + client_id: Required if your integration does not have a client_secret. + Used to prevent MITM attacks. + grant_type: The string "refresh_token". + scope: If specified, a subset of the token's existing scopes. Optional. + """ + + refresh_token: str + client_id: str | None = None + grant_type: str = "refresh_token" + scope: str | None = None + + +class OAuthTokenResponse(BaseModel): + """OAuth token response from Airtable. + + Successful response has HTTP status code 200 (OK). + + Parameters: + access_token: An opaque string. Can be used to make requests to the Airtable API on behalf + of the user, and cannot be recovered if lost. + refresh_token: An opaque string. Can be used to request a new access token after the current + one expires. + token_type: The string "Bearer " (space intentional) + scope: A string that is a space delimited list of scopes granted to this access token. Can be + recovered using the get userId and scopes endpoint. + expires_in: An integer. Time in seconds until the access token expires (expected value is 60 minutes). + refresh_expires_in: An integer. Time in seconds until the refresh token expires (expected value is 60 days). + """ + + access_token: str + refresh_token: str + token_type: str + scope: str + expires_in: int + refresh_expires_in: int + + +def make_oauth_authorize_url( + client_id: str, + redirect_uri: str, + scopes: list[str], + state: str, + code_challenge: str, +) -> str: + """ + Generate the OAuth authorization URL for Airtable. + + Args: + client_id: An opaque string that identifies your integration with Airtable + redirect_uri: The URI for the authorize response redirect + scope: A space delimited list of unique scopes + state: A cryptographically generated, opaque string for CSRF protection + code_challenge: The base64 url-encoding of the sha256 of the code_verifier + code_challenge_method: The string "S256" (default) + response_type: The string "code" (default) + + Returns: + The authorization URL that the user should visit + """ + # Validate the request parameters + request_params = OAuthAuthorizeRequest( + client_id=client_id, + redirect_uri=redirect_uri, + scope=" ".join(scopes), + state=state, + code_challenge=code_challenge, + ) + + # Build the authorization URL + base_url = "https://airtable.com/oauth2/v1/authorize" + query_string = urlencode(request_params.model_dump(exclude_none=True)) + + return f"{base_url}?{query_string}" + + +async def oauth_exchange_code_for_tokens( + client_id: str, + code_verifier: bytes, + code: str, + redirect_uri: str, + client_secret: str | None = None, +) -> OAuthTokenResponse: + """ + Exchange an authorization code for access and refresh tokens. + + Args: + client_id: The Airtable integration client ID. + code_verifier: The original code_verifier (required for PKCE). + code: The authorization code returned by Airtable. + redirect_uri: The redirect URI used during authorization. + client_secret: Integration client secret if available (optional). + + Returns: + Parsed JSON response containing the access token, refresh token, scope, etc. + """ + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + # Add Authorization header for confidential clients + if client_secret: + credentials_encoded = base64.urlsafe_b64encode( + f"{client_id}:{client_secret}".encode() + ).decode() + headers["Authorization"] = f"Basic {credentials_encoded}" + + data = OAuthTokenRequest( + code=code, + client_id=client_id, + redirect_uri=redirect_uri, + grant_type="authorization_code", + code_verifier=code_verifier.decode("utf-8"), + ).model_dump(exclude_none=True) + + response = await Requests().post( + "https://airtable.com/oauth2/v1/token", + headers=headers, + data=data, + ) + + if response.ok: + return OAuthTokenResponse.model_validate(response.json()) + raise ValueError( + f"Failed to exchange code for tokens: {response.status} {response.text}" + ) + + +# NEW helper for refreshing tokens +async def oauth_refresh_tokens( + client_id: str, + refresh_token: str, + client_secret: str | None = None, +) -> OAuthTokenResponse: + """ + Refresh an expired (or soon-to-expire) access token. + + Args: + client_id: The Airtable integration client ID. + refresh_token: The refresh token previously issued by Airtable. + client_secret: Integration client secret if available (optional). + + Returns: + Parsed JSON response containing the new tokens and metadata. + https://airtable.com/oauth2/v1/authorize?client_id=7642abbb-8fbc-494c-b6e0-58484364e28c&redirect_uri=https%3A%2F%2Fdev-builder.agpt.co%2Fauth%2Fintegrations%2Foauth_callback&response_type=code&scope=&state=OcmqX6Y5MTkhHLc6vkbR6uEtSiZHawzEUcxDscqkWRk&code_challenge=v2Ly1CcG8UkCXJ2n--TEKZc6HeKaN1wrZLgIr_qVnJ8&code_challenge_method=S256 + """ + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + if client_secret: + credentials_encoded = base64.urlsafe_b64encode( + f"{client_id}:{client_secret}".encode() + ).decode() + headers["Authorization"] = f"Basic {credentials_encoded}" + + data = OAuthRefreshTokenRequest( + refresh_token=refresh_token, + client_id=client_id, + grant_type="refresh_token", + ).model_dump(exclude_none=True) + + response = await Requests().post( + "https://airtable.com/oauth2/v1/token", + headers=headers, + data=data, + ) + + if response.ok: + return OAuthTokenResponse.model_validate(response.json()) + raise ValueError(f"Failed to refresh tokens: {response.status} {response.text}") + + +################################################################# +# Base Management +################################################################# + + +async def create_base( + credentials: Credentials, + workspace_id: str, + name: str, + tables: list[dict] = [ + { + "description": "Default table", + "name": "Default table", + "fields": [ + { + "name": "ID", + "type": "number", + "description": "Auto-incrementing ID field", + "options": {"precision": 0}, + } + ], + } + ], +) -> dict: + """ + Create a new base in Airtable. + + Args: + credentials: Airtable API credentials + workspace_id: The workspace ID where the base will be created + name: The name of the new base + tables: Optional list of table objects to create in the base + + Returns: + dict: Response containing the created base information + """ + params: dict[str, Any] = { + "name": name, + "workspaceId": workspace_id, + } + + if tables: + params["tables"] = tables + + print(params) + + response = await Requests().post( + "https://api.airtable.com/v0/meta/bases", + headers={ + "Authorization": credentials.auth_header(), + "Content-Type": "application/json", + }, + json=_convert_bools(params), + ) + + return response.json() + + +async def list_bases( + credentials: Credentials, + offset: str | None = None, +) -> dict: + """ + List all bases that the authenticated user has access to. + + Args: + credentials: Airtable API credentials + offset: Optional pagination offset + + Returns: + dict: Response containing the list of bases + """ + params = {} + if offset: + params["offset"] = offset + + response = await Requests().get( + "https://api.airtable.com/v0/meta/bases", + headers={"Authorization": credentials.auth_header()}, + params=params, + ) + + return response.json() diff --git a/autogpt_platform/backend/backend/blocks/airtable/_api_test.py b/autogpt_platform/backend/backend/blocks/airtable/_api_test.py new file mode 100644 index 000000000000..02f15a509fa1 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/_api_test.py @@ -0,0 +1,323 @@ +from os import getenv +from uuid import uuid4 + +import pytest + +from backend.sdk import APIKeyCredentials, SecretStr + +from ._api import ( + TableFieldType, + WebhookFilters, + WebhookSpecification, + create_base, + create_field, + create_record, + create_table, + create_webhook, + delete_multiple_records, + delete_record, + delete_webhook, + get_record, + list_bases, + list_records, + list_webhook_payloads, + update_field, + update_multiple_records, + update_record, + update_table, +) + + +@pytest.mark.asyncio +async def test_create_update_table(): + + key = getenv("AIRTABLE_API_KEY") + if not key: + return pytest.skip("AIRTABLE_API_KEY is not set") + + credentials = APIKeyCredentials( + provider="airtable", + api_key=SecretStr(key), + ) + postfix = uuid4().hex[:4] + workspace_id = "wsphuHmfllg7V3Brd" + response = await create_base(credentials, workspace_id, "API Testing Base") + assert response is not None, f"Checking create base response: {response}" + assert ( + response.get("id") is not None + ), f"Checking create base response id: {response}" + base_id = response.get("id") + assert base_id is not None, f"Checking create base response id: {base_id}" + + response = await list_bases(credentials) + assert response is not None, f"Checking list bases response: {response}" + assert "API Testing Base" in [ + base.get("name") for base in response.get("bases", []) + ], f"Checking list bases response bases: {response}" + + table_name = f"test_table_{postfix}" + table_fields = [{"name": "test_field", "type": "singleLineText"}] + table = await create_table(credentials, base_id, table_name, table_fields) + assert table.get("name") == table_name + + table_id = table.get("id") + + assert table_id is not None + + table_name = f"test_table_updated_{postfix}" + table_description = "test_description_updated" + table = await update_table( + credentials, + base_id, + table_id, + table_name=table_name, + table_description=table_description, + ) + assert table.get("name") == table_name + assert table.get("description") == table_description + + +@pytest.mark.asyncio +async def test_invalid_field_type(): + + key = getenv("AIRTABLE_API_KEY") + if not key: + return pytest.skip("AIRTABLE_API_KEY is not set") + + credentials = APIKeyCredentials( + provider="airtable", + api_key=SecretStr(key), + ) + postfix = uuid4().hex[:4] + base_id = "appZPxegHEU3kDc1S" + table_name = f"test_table_{postfix}" + table_fields = [{"name": "test_field", "type": "notValid"}] + with pytest.raises(AssertionError): + await create_table(credentials, base_id, table_name, table_fields) + + +@pytest.mark.asyncio +async def test_create_and_update_field(): + key = getenv("AIRTABLE_API_KEY") + if not key: + return pytest.skip("AIRTABLE_API_KEY is not set") + + credentials = APIKeyCredentials( + provider="airtable", + api_key=SecretStr(key), + ) + postfix = uuid4().hex[:4] + base_id = "appZPxegHEU3kDc1S" + table_name = f"test_table_{postfix}" + table_fields = [{"name": "test_field", "type": "singleLineText"}] + table = await create_table(credentials, base_id, table_name, table_fields) + assert table.get("name") == table_name + + table_id = table.get("id") + + assert table_id is not None + + field_name = f"test_field_{postfix}" + field_type = TableFieldType.SINGLE_LINE_TEXT + field = await create_field(credentials, base_id, table_id, field_type, field_name) + assert field.get("name") == field_name + + field_id = field.get("id") + + assert field_id is not None + assert isinstance(field_id, str) + + field_name = f"test_field_updated_{postfix}" + field = await update_field(credentials, base_id, table_id, field_id, field_name) + assert field.get("name") == field_name + + field_description = "test_description_updated" + field = await update_field( + credentials, base_id, table_id, field_id, description=field_description + ) + assert field.get("description") == field_description + + +@pytest.mark.asyncio +async def test_record_management(): + key = getenv("AIRTABLE_API_KEY") + if not key: + return pytest.skip("AIRTABLE_API_KEY is not set") + + credentials = APIKeyCredentials( + provider="airtable", + api_key=SecretStr(key), + ) + postfix = uuid4().hex[:4] + base_id = "appZPxegHEU3kDc1S" + table_name = f"test_table_{postfix}" + table_fields = [{"name": "test_field", "type": "singleLineText"}] + table = await create_table(credentials, base_id, table_name, table_fields) + assert table.get("name") == table_name + + table_id = table.get("id") + assert table_id is not None + + # Create a record + record_fields = {"test_field": "test_value"} + record = await create_record(credentials, base_id, table_id, fields=record_fields) + fields = record.get("fields") + assert fields is not None + assert isinstance(fields, dict) + assert fields.get("test_field") == "test_value" + + record_id = record.get("id") + + assert record_id is not None + assert isinstance(record_id, str) + + # Get a record + record = await get_record(credentials, base_id, table_id, record_id) + fields = record.get("fields") + assert fields is not None + assert isinstance(fields, dict) + assert fields.get("test_field") == "test_value" + + # Updata a record + record_fields = {"test_field": "test_value_updated"} + record = await update_record( + credentials, base_id, table_id, record_id, fields=record_fields + ) + fields = record.get("fields") + assert fields is not None + assert isinstance(fields, dict) + assert fields.get("test_field") == "test_value_updated" + + # Delete a record + record = await delete_record(credentials, base_id, table_id, record_id) + assert record is not None + assert record.get("id") == record_id + assert record.get("deleted") + + # Create 2 records + records = [ + {"fields": {"test_field": "test_value_1"}}, + {"fields": {"test_field": "test_value_2"}}, + ] + response = await create_record(credentials, base_id, table_id, records=records) + created_records = response.get("records") + assert created_records is not None + assert isinstance(created_records, list) + assert len(created_records) == 2, f"Created records: {created_records}" + first_record = created_records[0] # type: ignore + second_record = created_records[1] # type: ignore + first_record_id = first_record.get("id") + second_record_id = second_record.get("id") + assert first_record_id is not None + assert second_record_id is not None + assert first_record_id != second_record_id + first_fields = first_record.get("fields") + second_fields = second_record.get("fields") + assert first_fields is not None + assert second_fields is not None + assert first_fields.get("test_field") == "test_value_1" # type: ignore + assert second_fields.get("test_field") == "test_value_2" # type: ignore + + # List records + response = await list_records(credentials, base_id, table_id) + records = response.get("records") + assert records is not None + assert len(records) == 2, f"Records: {records}" + assert isinstance(records, list), f"Type of records: {type(records)}" + + # Update multiple records + records = [ + {"id": first_record_id, "fields": {"test_field": "test_value_1_updated"}}, + {"id": second_record_id, "fields": {"test_field": "test_value_2_updated"}}, + ] + response = await update_multiple_records( + credentials, base_id, table_id, records=records + ) + updated_records = response.get("records") + assert updated_records is not None + assert len(updated_records) == 2, f"Updated records: {updated_records}" + assert isinstance( + updated_records, list + ), f"Type of updated records: {type(updated_records)}" + first_updated = updated_records[0] # type: ignore + second_updated = updated_records[1] # type: ignore + first_updated_fields = first_updated.get("fields") + second_updated_fields = second_updated.get("fields") + assert first_updated_fields is not None + assert second_updated_fields is not None + assert first_updated_fields.get("test_field") == "test_value_1_updated" # type: ignore + assert second_updated_fields.get("test_field") == "test_value_2_updated" # type: ignore + + # Delete multiple records + assert isinstance(first_record_id, str) + assert isinstance(second_record_id, str) + response = await delete_multiple_records( + credentials, base_id, table_id, records=[first_record_id, second_record_id] + ) + deleted_records = response.get("records") + assert deleted_records is not None + assert len(deleted_records) == 2, f"Deleted records: {deleted_records}" + assert isinstance( + deleted_records, list + ), f"Type of deleted records: {type(deleted_records)}" + first_deleted = deleted_records[0] # type: ignore + second_deleted = deleted_records[1] # type: ignore + assert first_deleted.get("deleted") + assert second_deleted.get("deleted") + + +@pytest.mark.asyncio +async def test_webhook_management(): + key = getenv("AIRTABLE_API_KEY") + if not key: + return pytest.skip("AIRTABLE_API_KEY is not set") + + credentials = APIKeyCredentials( + provider="airtable", + api_key=SecretStr(key), + ) + postfix = uuid4().hex[:4] + base_id = "appZPxegHEU3kDc1S" + table_name = f"test_table_{postfix}" + table_fields = [{"name": "test_field", "type": "singleLineText"}] + table = await create_table(credentials, base_id, table_name, table_fields) + assert table.get("name") == table_name + + table_id = table.get("id") + assert table_id is not None + webhook_specification = WebhookSpecification( + filters=WebhookFilters( + dataTypes=["tableData", "tableFields", "tableMetadata"], + changeTypes=["add", "update", "remove"], + ) + ) + response = await create_webhook(credentials, base_id, webhook_specification) + assert response is not None, f"Checking create webhook response: {response}" + assert ( + response.get("id") is not None + ), f"Checking create webhook response id: {response}" + assert ( + response.get("macSecretBase64") is not None + ), f"Checking create webhook response macSecretBase64: {response}" + + webhook_id = response.get("id") + assert webhook_id is not None, f"Webhook ID: {webhook_id}" + assert isinstance(webhook_id, str) + + response = await create_record( + credentials, base_id, table_id, fields={"test_field": "test_value"} + ) + assert response is not None, f"Checking create record response: {response}" + assert ( + response.get("id") is not None + ), f"Checking create record response id: {response}" + fields = response.get("fields") + assert fields is not None, f"Checking create record response fields: {response}" + assert ( + fields.get("test_field") == "test_value" + ), f"Checking create record response fields test_field: {response}" + + response = await list_webhook_payloads(credentials, base_id, webhook_id) + assert response is not None, f"Checking list webhook payloads response: {response}" + + response = await delete_webhook(credentials, base_id, webhook_id) diff --git a/autogpt_platform/backend/backend/blocks/airtable/_config.py b/autogpt_platform/backend/backend/blocks/airtable/_config.py new file mode 100644 index 000000000000..ee168881d8f7 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/_config.py @@ -0,0 +1,32 @@ +""" +Shared configuration for all Airtable blocks using the SDK pattern. +""" + +from backend.sdk import BlockCostType, ProviderBuilder + +from ._oauth import AirtableOAuthHandler, AirtableScope +from ._webhook import AirtableWebhookManager + +# Configure the Airtable provider with API key authentication +airtable = ( + ProviderBuilder("airtable") + .with_api_key("AIRTABLE_API_KEY", "Airtable Personal Access Token") + .with_webhook_manager(AirtableWebhookManager) + .with_base_cost(1, BlockCostType.RUN) + .with_oauth( + AirtableOAuthHandler, + scopes=[ + v.value + for v in [ + AirtableScope.DATA_RECORDS_READ, + AirtableScope.DATA_RECORDS_WRITE, + AirtableScope.SCHEMA_BASES_READ, + AirtableScope.SCHEMA_BASES_WRITE, + AirtableScope.WEBHOOK_MANAGE, + ] + ], + client_id_env_var="AIRTABLE_CLIENT_ID", + client_secret_env_var="AIRTABLE_CLIENT_SECRET", + ) + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/airtable/_oauth.py b/autogpt_platform/backend/backend/blocks/airtable/_oauth.py new file mode 100644 index 000000000000..9cd69ec3defe --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/_oauth.py @@ -0,0 +1,185 @@ +""" +Airtable OAuth handler implementation. +""" + +import time +from enum import Enum +from logging import getLogger +from typing import Optional + +from backend.sdk import BaseOAuthHandler, OAuth2Credentials, ProviderName, SecretStr + +from ._api import ( + OAuthTokenResponse, + make_oauth_authorize_url, + oauth_exchange_code_for_tokens, + oauth_refresh_tokens, +) + +logger = getLogger(__name__) + + +class AirtableScope(str, Enum): + # Basic scopes + DATA_RECORDS_READ = "data.records:read" + DATA_RECORDS_WRITE = "data.records:write" + DATA_RECORD_COMMENTS_READ = "data.recordComments:read" + DATA_RECORD_COMMENTS_WRITE = "data.recordComments:write" + SCHEMA_BASES_READ = "schema.bases:read" + SCHEMA_BASES_WRITE = "schema.bases:write" + WEBHOOK_MANAGE = "webhook:manage" + BLOCK_MANAGE = "block:manage" + USER_EMAIL_READ = "user.email:read" + + # Enterprise member scopes + ENTERPRISE_GROUPS_READ = "enterprise.groups:read" + WORKSPACES_AND_BASES_READ = "workspacesAndBases:read" + WORKSPACES_AND_BASES_WRITE = "workspacesAndBases:write" + WORKSPACES_AND_BASES_SHARES_MANAGE = "workspacesAndBases.shares:manage" + + # Enterprise admin scopes + ENTERPRISE_SCIM_USERS_AND_GROUPS_MANAGE = "enterprise.scim.usersAndGroups:manage" + ENTERPRISE_AUDIT_LOGS_READ = "enterprise.auditLogs:read" + ENTERPRISE_CHANGE_EVENTS_READ = "enterprise.changeEvents:read" + ENTERPRISE_EXPORTS_MANAGE = "enterprise.exports:manage" + ENTERPRISE_ACCOUNT_READ = "enterprise.account:read" + ENTERPRISE_ACCOUNT_WRITE = "enterprise.account:write" + ENTERPRISE_USER_READ = "enterprise.user:read" + ENTERPRISE_USER_WRITE = "enterprise.user:write" + ENTERPRISE_GROUPS_MANAGE = "enterprise.groups:manage" + WORKSPACES_AND_BASES_MANAGE = "workspacesAndBases:manage" + HYPERDB_RECORDS_READ = "hyperDB.records:read" + HYPERDB_RECORDS_WRITE = "hyperDB.records:write" + + +class AirtableOAuthHandler(BaseOAuthHandler): + """ + OAuth2 handler for Airtable with PKCE support. + """ + + PROVIDER_NAME = ProviderName("airtable") + DEFAULT_SCOPES = [ + v.value + for v in [ + AirtableScope.DATA_RECORDS_READ, + AirtableScope.DATA_RECORDS_WRITE, + AirtableScope.SCHEMA_BASES_READ, + AirtableScope.SCHEMA_BASES_WRITE, + AirtableScope.WEBHOOK_MANAGE, + ] + ] + + def __init__(self, client_id: str, client_secret: Optional[str], redirect_uri: str): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.scopes = self.DEFAULT_SCOPES + self.auth_base_url = "https://airtable.com/oauth2/v1/authorize" + self.token_url = "https://airtable.com/oauth2/v1/token" + + def get_login_url( + self, scopes: list[str], state: str, code_challenge: Optional[str] + ) -> str: + logger.debug("Generating Airtable OAuth login URL") + # Generate code_challenge if not provided (PKCE is required) + if not scopes: + logger.debug("No scopes provided, using default scopes") + scopes = self.scopes + + logger.debug(f"Using scopes: {scopes}") + logger.debug(f"State: {state}") + logger.debug(f"Code challenge: {code_challenge}") + if not code_challenge: + logger.error("Code challenge is required but none was provided") + raise ValueError("No code challenge provided") + + try: + url = make_oauth_authorize_url( + self.client_id, self.redirect_uri, scopes, state, code_challenge + ) + logger.debug(f"Generated OAuth URL: {url}") + return url + except Exception as e: + logger.error(f"Failed to generate OAuth URL: {str(e)}") + raise + + async def exchange_code_for_tokens( + self, code: str, scopes: list[str], code_verifier: Optional[str] + ) -> OAuth2Credentials: + logger.debug("Exchanging authorization code for tokens") + logger.debug(f"Code: {code[:4]}...") # Log first 4 chars only for security + logger.debug(f"Scopes: {scopes}") + if not code_verifier: + logger.error("Code verifier is required but none was provided") + raise ValueError("No code verifier provided") + + try: + response: OAuthTokenResponse = await oauth_exchange_code_for_tokens( + client_id=self.client_id, + code=code, + code_verifier=code_verifier.encode("utf-8"), + redirect_uri=self.redirect_uri, + client_secret=self.client_secret, + ) + logger.info("Successfully exchanged code for tokens") + + credentials = OAuth2Credentials( + access_token=SecretStr(response.access_token), + refresh_token=SecretStr(response.refresh_token), + access_token_expires_at=int(time.time()) + response.expires_in, + refresh_token_expires_at=int(time.time()) + response.refresh_expires_in, + provider=self.PROVIDER_NAME, + scopes=scopes, + ) + logger.debug(f"Access token expires in {response.expires_in} seconds") + logger.debug( + f"Refresh token expires in {response.refresh_expires_in} seconds" + ) + return credentials + + except Exception as e: + logger.error(f"Failed to exchange code for tokens: {str(e)}") + raise + + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: + logger.debug("Attempting to refresh OAuth tokens") + + if credentials.refresh_token is None: + logger.error("Cannot refresh tokens - no refresh token available") + raise ValueError("No refresh token available") + + try: + response: OAuthTokenResponse = await oauth_refresh_tokens( + client_id=self.client_id, + refresh_token=credentials.refresh_token.get_secret_value(), + client_secret=self.client_secret, + ) + logger.info("Successfully refreshed tokens") + + new_credentials = OAuth2Credentials( + id=credentials.id, + access_token=SecretStr(response.access_token), + refresh_token=SecretStr(response.refresh_token), + access_token_expires_at=int(time.time()) + response.expires_in, + refresh_token_expires_at=int(time.time()) + response.refresh_expires_in, + provider=self.PROVIDER_NAME, + scopes=self.scopes, + ) + logger.debug(f"New access token expires in {response.expires_in} seconds") + logger.debug( + f"New refresh token expires in {response.refresh_expires_in} seconds" + ) + return new_credentials + + except Exception as e: + logger.error(f"Failed to refresh tokens: {str(e)}") + raise + + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + logger.debug("Token revocation requested") + logger.info( + "Airtable doesn't provide a token revocation endpoint - tokens will expire naturally after 60 minutes" + ) + return False diff --git a/autogpt_platform/backend/backend/blocks/airtable/_webhook.py b/autogpt_platform/backend/backend/blocks/airtable/_webhook.py new file mode 100644 index 000000000000..58e6f95d0c2a --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/_webhook.py @@ -0,0 +1,154 @@ +""" +Webhook management for Airtable blocks. +""" + +import hashlib +import hmac +import logging +from enum import Enum + +from backend.sdk import ( + BaseWebhooksManager, + Credentials, + ProviderName, + Webhook, + update_webhook, +) + +from ._api import ( + WebhookFilters, + WebhookSpecification, + create_webhook, + delete_webhook, + list_webhook_payloads, +) + +logger = logging.getLogger(__name__) + + +class AirtableWebhookEvent(str, Enum): + TABLE_DATA = "tableData" + TABLE_FIELDS = "tableFields" + TABLE_METADATA = "tableMetadata" + + +class AirtableWebhookManager(BaseWebhooksManager): + """Webhook manager for Airtable API.""" + + PROVIDER_NAME = ProviderName("airtable") + + @classmethod + async def validate_payload( + cls, webhook: Webhook, request, credentials: Credentials | None + ) -> tuple[dict, str]: + """Validate incoming webhook payload and signature.""" + + if not credentials: + raise ValueError("Missing credentials in webhook metadata") + + payload = await request.json() + + # Verify webhook signature using HMAC-SHA256 + if webhook.secret: + mac_secret = webhook.config.get("mac_secret") + if mac_secret: + # Get the raw body for signature verification + body = await request.body() + + # Calculate expected signature + mac_secret_decoded = mac_secret.encode() + hmac_obj = hmac.new(mac_secret_decoded, body, hashlib.sha256) + expected_mac = f"hmac-sha256={hmac_obj.hexdigest()}" + + # Get signature from headers + signature = request.headers.get("X-Airtable-Content-MAC") + + if signature and not hmac.compare_digest(signature, expected_mac): + raise ValueError("Invalid webhook signature") + + # Validate payload structure + required_fields = ["base", "webhook", "timestamp"] + if not all(field in payload for field in required_fields): + raise ValueError("Invalid webhook payload structure") + + if "id" not in payload["base"] or "id" not in payload["webhook"]: + raise ValueError("Missing required IDs in webhook payload") + base_id = payload["base"]["id"] + webhook_id = payload["webhook"]["id"] + + # get payload request parameters + cursor = webhook.config.get("cursor", 1) + + response = await list_webhook_payloads(credentials, base_id, webhook_id, cursor) + + # update webhook config + await update_webhook( + webhook.id, + config={"base_id": base_id, "cursor": response.cursor}, + ) + + event_type = "notification" + return response.model_dump(), event_type + + async def _register_webhook( + self, + credentials: Credentials, + webhook_type: str, + resource: str, + events: list[str], + ingress_url: str, + secret: str, + ) -> tuple[str, dict]: + """Register webhook with Airtable API.""" + + # Parse resource to get base_id and table_id/name + # Resource format: "{base_id}/{table_id_or_name}" + parts = resource.split("/", 1) + if len(parts) != 2: + raise ValueError("Resource must be in format: {base_id}/{table_id_or_name}") + + base_id, table_id_or_name = parts + + # Prepare webhook specification + webhook_specification = WebhookSpecification( + filters=WebhookFilters( + dataTypes=events, + ) + ) + + # Create webhook + webhook_data = await create_webhook( + credentials=credentials, + base_id=base_id, + webhook_specification=webhook_specification, + notification_url=ingress_url, + ) + + webhook_id = webhook_data["id"] + mac_secret = webhook_data.get("macSecretBase64") + + return webhook_id, { + "webhook_id": webhook_id, + "base_id": base_id, + "table_id_or_name": table_id_or_name, + "events": events, + "mac_secret": mac_secret, + "cursor": 1, + "expiration_time": webhook_data.get("expirationTime"), + } + + async def _deregister_webhook( + self, webhook: Webhook, credentials: Credentials + ) -> None: + """Deregister webhook from Airtable API.""" + + base_id = webhook.config.get("base_id") + webhook_id = webhook.config.get("webhook_id") + + if not base_id: + raise ValueError("Missing base_id in webhook metadata") + + if not webhook_id: + raise ValueError("Missing webhook_id in webhook metadata") + + await delete_webhook(credentials, base_id, webhook_id) diff --git a/autogpt_platform/backend/backend/blocks/airtable/bases.py b/autogpt_platform/backend/backend/blocks/airtable/bases.py new file mode 100644 index 000000000000..12910f9e6706 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/bases.py @@ -0,0 +1,122 @@ +""" +Airtable base operation blocks. +""" + +from typing import Optional + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._api import create_base, list_bases +from ._config import airtable + + +class AirtableCreateBaseBlock(Block): + """ + Creates a new base in an Airtable workspace. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + workspace_id: str = SchemaField( + description="The workspace ID where the base will be created" + ) + name: str = SchemaField(description="The name of the new base") + tables: list[dict] = SchemaField( + description="At least one table and field must be specified. Array of table objects to create in the base. Each table should have 'name' and 'fields' properties", + default=[ + { + "description": "Default table", + "name": "Default table", + "fields": [ + { + "name": "ID", + "type": "number", + "description": "Auto-incrementing ID field", + "options": {"precision": 0}, + } + ], + } + ], + ) + + class Output(BlockSchema): + base_id: str = SchemaField(description="The ID of the created base") + tables: list[dict] = SchemaField(description="Array of table objects") + table: dict = SchemaField(description="A single table object") + + def __init__(self): + super().__init__( + id="f59b88a8-54ce-4676-a508-fd614b4e8dce", + description="Create a new base in Airtable", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + data = await create_base( + credentials, + input_data.workspace_id, + input_data.name, + input_data.tables, + ) + + yield "base_id", data.get("id", None) + yield "tables", data.get("tables", []) + for table in data.get("tables", []): + yield "table", table + + +class AirtableListBasesBlock(Block): + """ + Lists all bases in an Airtable workspace that the user has access to. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + trigger: str = SchemaField( + description="Trigger the block to run - value is ignored", default="manual" + ) + offset: str = SchemaField( + description="Pagination offset from previous request", default="" + ) + + class Output(BlockSchema): + bases: list[dict] = SchemaField(description="Array of base objects") + offset: Optional[str] = SchemaField( + description="Offset for next page (null if no more bases)", default=None + ) + + def __init__(self): + super().__init__( + id="4bd8d466-ed5d-4e44-8083-97f25a8044e7", + description="List all bases in Airtable", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + data = await list_bases( + credentials, + offset=input_data.offset if input_data.offset else None, + ) + + yield "bases", data.get("bases", []) + yield "offset", data.get("offset", None) diff --git a/autogpt_platform/backend/backend/blocks/airtable/records.py b/autogpt_platform/backend/backend/blocks/airtable/records.py new file mode 100644 index 000000000000..82e5986e847f --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/records.py @@ -0,0 +1,283 @@ +""" +Airtable record operation blocks. +""" + +from typing import Optional + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._api import ( + create_record, + delete_multiple_records, + get_record, + list_records, + update_multiple_records, +) +from ._config import airtable + + +class AirtableListRecordsBlock(Block): + """ + Lists records from an Airtable table with optional filtering, sorting, and pagination. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id_or_name: str = SchemaField(description="Table ID or name") + filter_formula: str = SchemaField( + description="Airtable formula to filter records", default="" + ) + view: str = SchemaField(description="View ID or name to use", default="") + sort: list[dict] = SchemaField( + description="Sort configuration (array of {field, direction})", default=[] + ) + max_records: int = SchemaField( + description="Maximum number of records to return", default=100 + ) + page_size: int = SchemaField( + description="Number of records per page (max 100)", default=100 + ) + offset: str = SchemaField( + description="Pagination offset from previous request", default="" + ) + return_fields: list[str] = SchemaField( + description="Specific fields to return (comma-separated)", default=[] + ) + + class Output(BlockSchema): + records: list[dict] = SchemaField(description="Array of record objects") + offset: Optional[str] = SchemaField( + description="Offset for next page (null if no more records)", default=None + ) + + def __init__(self): + super().__init__( + id="588a9fde-5733-4da7-b03c-35f5671e960f", + description="List records from an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + data = await list_records( + credentials, + input_data.base_id, + input_data.table_id_or_name, + filter_by_formula=( + input_data.filter_formula if input_data.filter_formula else None + ), + view=input_data.view if input_data.view else None, + sort=input_data.sort if input_data.sort else None, + max_records=input_data.max_records if input_data.max_records else None, + page_size=min(input_data.page_size, 100) if input_data.page_size else None, + offset=input_data.offset if input_data.offset else None, + fields=input_data.return_fields if input_data.return_fields else None, + ) + + yield "records", data.get("records", []) + yield "offset", data.get("offset", None) + + +class AirtableGetRecordBlock(Block): + """ + Retrieves a single record from an Airtable table by its ID. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id_or_name: str = SchemaField(description="Table ID or name") + record_id: str = SchemaField(description="The record ID to retrieve") + + class Output(BlockSchema): + id: str = SchemaField(description="The record ID") + fields: dict = SchemaField(description="The record fields") + created_time: str = SchemaField(description="The record created time") + + def __init__(self): + super().__init__( + id="c29c5cbf-0aff-40f9-bbb5-f26061792d2b", + description="Get a single record from Airtable", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + record = await get_record( + credentials, + input_data.base_id, + input_data.table_id_or_name, + input_data.record_id, + ) + + yield "id", record.get("id", None) + yield "fields", record.get("fields", None) + yield "created_time", record.get("createdTime", None) + + +class AirtableCreateRecordsBlock(Block): + """ + Creates one or more records in an Airtable table. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id_or_name: str = SchemaField(description="Table ID or name") + records: list[dict] = SchemaField( + description="Array of records to create (each with 'fields' object)" + ) + typecast: bool = SchemaField( + description="Automatically convert string values to appropriate types", + default=False, + ) + return_fields_by_field_id: bool | None = SchemaField( + description="Return fields by field ID", + default=None, + ) + + class Output(BlockSchema): + records: list[dict] = SchemaField(description="Array of created record objects") + details: dict = SchemaField(description="Details of the created records") + + def __init__(self): + super().__init__( + id="42527e98-47b6-44ce-ac0e-86b4883721d3", + description="Create records in an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + # The create_record API expects records in a specific format + data = await create_record( + credentials, + input_data.base_id, + input_data.table_id_or_name, + records=[{"fields": record} for record in input_data.records], + typecast=input_data.typecast if input_data.typecast else None, + return_fields_by_field_id=input_data.return_fields_by_field_id, + ) + + yield "records", data.get("records", []) + details = data.get("details", None) + if details: + yield "details", details + + +class AirtableUpdateRecordsBlock(Block): + """ + Updates one or more existing records in an Airtable table. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id_or_name: str = SchemaField( + description="Table ID or name - It's better to use the table ID instead of the name" + ) + records: list[dict] = SchemaField( + description="Array of records to update (each with 'id' and 'fields')" + ) + typecast: bool | None = SchemaField( + description="Automatically convert string values to appropriate types", + default=None, + ) + + class Output(BlockSchema): + records: list[dict] = SchemaField(description="Array of updated record objects") + + def __init__(self): + super().__init__( + id="6e7d2590-ac2b-4b5d-b08c-fc039cd77e1f", + description="Update records in an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + # The update_multiple_records API expects records with id and fields + data = await update_multiple_records( + credentials, + input_data.base_id, + input_data.table_id_or_name, + records=input_data.records, + typecast=input_data.typecast if input_data.typecast else None, + return_fields_by_field_id=False, # Use field names, not IDs + ) + + yield "records", data.get("records", []) + + +class AirtableDeleteRecordsBlock(Block): + """ + Deletes one or more records from an Airtable table. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id_or_name: str = SchemaField( + description="Table ID or name - It's better to use the table ID instead of the name" + ) + record_ids: list[str] = SchemaField( + description="Array of upto 10 record IDs to delete" + ) + + class Output(BlockSchema): + records: list[dict] = SchemaField(description="Array of deletion results") + + def __init__(self): + super().__init__( + id="93e22b8b-3642-4477-aefb-1c0929a4a3a6", + description="Delete records from an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + if len(input_data.record_ids) > 10: + yield "error", "Only upto 10 record IDs can be deleted at a time" + else: + data = await delete_multiple_records( + credentials, + input_data.base_id, + input_data.table_id_or_name, + input_data.record_ids, + ) + + yield "records", data.get("records", []) diff --git a/autogpt_platform/backend/backend/blocks/airtable/schema.py b/autogpt_platform/backend/backend/blocks/airtable/schema.py new file mode 100644 index 000000000000..5d2006a2ffb0 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/schema.py @@ -0,0 +1,252 @@ +""" +Airtable schema and table management blocks. +""" + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + Requests, + SchemaField, +) + +from ._api import TableFieldType, create_field, create_table, update_field, update_table +from ._config import airtable + + +class AirtableListSchemaBlock(Block): + """ + Retrieves the complete schema of an Airtable base, including all tables, + fields, and views. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + + class Output(BlockSchema): + base_schema: dict = SchemaField( + description="Complete base schema with tables, fields, and views" + ) + tables: list[dict] = SchemaField(description="Array of table objects") + + def __init__(self): + super().__init__( + id="64291d3c-99b5-47b7-a976-6d94293cdb2d", + description="Get the complete schema of an Airtable base", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + api_key = credentials.api_key.get_secret_value() + + # Get base schema + response = await Requests().get( + f"https://api.airtable.com/v0/meta/bases/{input_data.base_id}/tables", + headers={"Authorization": f"Bearer {api_key}"}, + ) + + data = response.json() + + yield "base_schema", data + yield "tables", data.get("tables", []) + + +class AirtableCreateTableBlock(Block): + """ + Creates a new table in an Airtable base with specified fields and views. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_name: str = SchemaField(description="The name of the table to create") + table_fields: list[dict] = SchemaField( + description="Table fields with name, type, and options", + default=[{"name": "Name", "type": "singleLineText"}], + ) + + class Output(BlockSchema): + table: dict = SchemaField(description="Created table object") + table_id: str = SchemaField(description="ID of the created table") + + def __init__(self): + super().__init__( + id="fcc20ced-d817-42ea-9b40-c35e7bf34b4f", + description="Create a new table in an Airtable base", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + table_data = await create_table( + credentials, + input_data.base_id, + input_data.table_name, + input_data.table_fields, + ) + + yield "table", table_data + yield "table_id", table_data.get("id", "") + + +class AirtableUpdateTableBlock(Block): + """ + Updates an existing table's properties such as name or description. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id: str = SchemaField(description="The table ID to update") + table_name: str | None = SchemaField( + description="The name of the table to update", default=None + ) + table_description: str | None = SchemaField( + description="The description of the table to update", default=None + ) + date_dependency: dict | None = SchemaField( + description="The date dependency of the table to update", default=None + ) + + class Output(BlockSchema): + table: dict = SchemaField(description="Updated table object") + + def __init__(self): + super().__init__( + id="34077c5f-f962-49f2-9ec6-97c67077013a", + description="Update table properties", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + table_data = await update_table( + credentials, + input_data.base_id, + input_data.table_id, + input_data.table_name, + input_data.table_description, + input_data.date_dependency, + ) + + yield "table", table_data + + +class AirtableCreateFieldBlock(Block): + """ + Adds a new field (column) to an existing Airtable table. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id: str = SchemaField(description="The table ID to add field to") + field_type: TableFieldType = SchemaField( + description="The type of the field to create", + default=TableFieldType.SINGLE_LINE_TEXT, + advanced=False, + ) + name: str = SchemaField(description="The name of the field to create") + description: str | None = SchemaField( + description="The description of the field to create", default=None + ) + options: dict[str, str] | None = SchemaField( + description="The options of the field to create", default=None + ) + + class Output(BlockSchema): + field: dict = SchemaField(description="Created field object") + field_id: str = SchemaField(description="ID of the created field") + + def __init__(self): + super().__init__( + id="6c98a32f-dbf9-45d8-a2a8-5e97e8326351", + description="Add a new field to an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + field_data = await create_field( + credentials, + input_data.base_id, + input_data.table_id, + input_data.field_type, + input_data.name, + ) + + yield "field", field_data + yield "field_id", field_data.get("id", "") + + +class AirtableUpdateFieldBlock(Block): + """ + Updates an existing field's properties in an Airtable table. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="The Airtable base ID") + table_id: str = SchemaField(description="The table ID containing the field") + field_id: str = SchemaField(description="The field ID to update") + name: str | None = SchemaField( + description="The name of the field to update", default=None, advanced=False + ) + description: str | None = SchemaField( + description="The description of the field to update", + default=None, + advanced=False, + ) + + class Output(BlockSchema): + field: dict = SchemaField(description="Updated field object") + + def __init__(self): + super().__init__( + id="f46ac716-3b18-4da1-92e4-34ca9a464d48", + description="Update field properties in an Airtable table", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + field_data = await update_field( + credentials, + input_data.base_id, + input_data.table_id, + input_data.field_id, + input_data.name, + input_data.description, + ) + + yield "field", field_data diff --git a/autogpt_platform/backend/backend/blocks/airtable/triggers.py b/autogpt_platform/backend/backend/blocks/airtable/triggers.py new file mode 100644 index 000000000000..2cfc8178e304 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/airtable/triggers.py @@ -0,0 +1,113 @@ +from backend.sdk import ( + BaseModel, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + BlockWebhookConfig, + CredentialsMetaInput, + ProviderName, + SchemaField, +) + +from ._api import WebhookPayload +from ._config import airtable + + +class AirtableEventSelector(BaseModel): + """ + Selects the Airtable webhook event to trigger on. + """ + + tableData: bool = True + tableFields: bool = True + tableMetadata: bool = True + + +class AirtableWebhookTriggerBlock(Block): + """ + Starts a flow whenever Airtable emits a webhook event. + + Thin wrapper just forwards the payloads one at a time to the next block. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = airtable.credentials_field( + description="Airtable API credentials" + ) + base_id: str = SchemaField(description="Airtable base ID") + table_id_or_name: str = SchemaField(description="Airtable table ID or name") + payload: dict = SchemaField(hidden=True, default_factory=dict) + events: AirtableEventSelector = SchemaField( + description="Airtable webhook event filter" + ) + + class Output(BlockSchema): + payload: WebhookPayload = SchemaField(description="Airtable webhook payload") + + def __init__(self): + example_payload = { + "payloads": [ + { + "timestamp": "2022-02-01T21:25:05.663Z", + "baseTransactionNumber": 4, + "actionMetadata": { + "source": "client", + "sourceMetadata": { + "user": { + "id": "usr00000000000000", + "email": "foo@bar.com", + "permissionLevel": "create", + } + }, + }, + "payloadFormat": "v0", + } + ], + "cursor": 5, + "mightHaveMore": False, + } + + super().__init__( + # NOTE: This is disabled whilst the webhook system is finalised. + disabled=False, + id="d0180ce6-ccb9-48c7-8256-b39e93e62801", + description="Starts a flow whenever Airtable emits a webhook event", + categories={BlockCategory.INPUT, BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + block_type=BlockType.WEBHOOK, + webhook_config=BlockWebhookConfig( + provider=ProviderName("airtable"), + webhook_type="not-used", + event_filter_input="events", + event_format="{event}", + resource_format="{base_id}/{table_id_or_name}", + ), + test_input={ + "credentials": airtable.get_test_credentials().model_dump(), + "base_id": "app1234567890", + "table_id_or_name": "table1234567890", + "events": AirtableEventSelector( + tableData=True, + tableFields=True, + tableMetadata=False, + ).model_dump(), + "payload": example_payload, + }, + test_credentials=airtable.get_test_credentials(), + test_output=[ + ( + "payload", + WebhookPayload.model_validate(example_payload["payloads"][0]), + ), + ], + ) + + async def run(self, input_data: Input, **kwargs) -> BlockOutput: + if len(input_data.payload["payloads"]) > 0: + for item in input_data.payload["payloads"]: + yield "payload", WebhookPayload.model_validate(item) + else: + yield "error", "No valid payloads found in webhook payload" diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/__init__.py b/autogpt_platform/backend/backend/blocks/ayrshare/__init__.py new file mode 100644 index 000000000000..94a567109e60 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/__init__.py @@ -0,0 +1,15 @@ +AYRSHARE_BLOCK_IDS = [ + "cbd52c2a-06d2-43ed-9560-6576cc163283", # PostToBlueskyBlock + "3352f512-3524-49ed-a08f-003042da2fc1", # PostToFacebookBlock + "9e8f844e-b4a5-4b25-80f2-9e1dd7d67625", # PostToXBlock + "589af4e4-507f-42fd-b9ac-a67ecef25811", # PostToLinkedInBlock + "89b02b96-a7cb-46f4-9900-c48b32fe1552", # PostToInstagramBlock + "0082d712-ff1b-4c3d-8a8d-6c7721883b83", # PostToYouTubeBlock + "c7733580-3c72-483e-8e47-a8d58754d853", # PostToRedditBlock + "47bc74eb-4af2-452c-b933-af377c7287df", # PostToTelegramBlock + "2c38c783-c484-4503-9280-ef5d1d345a7e", # PostToGMBBlock + "3ca46e05-dbaa-4afb-9e95-5a429c4177e6", # PostToPinterestBlock + "7faf4b27-96b0-4f05-bf64-e0de54ae74e1", # PostToTikTokBlock + "f8c3b2e1-9d4a-4e5f-8c7b-6a9e8d2f1c3b", # PostToThreadsBlock + "a9d7f854-2c83-4e96-b3a1-7f2e9c5d4b8e", # PostToSnapchatBlock +] diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/_util.py b/autogpt_platform/backend/backend/blocks/ayrshare/_util.py new file mode 100644 index 000000000000..a647e933df58 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/_util.py @@ -0,0 +1,152 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + +from backend.data.block import BlockSchema +from backend.data.model import SchemaField, UserIntegrations +from backend.integrations.ayrshare import AyrshareClient +from backend.util.clients import get_database_manager_async_client +from backend.util.exceptions import MissingConfigError + + +async def get_profile_key(user_id: str): + user_integrations: UserIntegrations = ( + await get_database_manager_async_client().get_user_integrations(user_id) + ) + return user_integrations.managed_credentials.ayrshare_profile_key + + +class BaseAyrshareInput(BlockSchema): + """Base input model for Ayrshare social media posts with common fields.""" + + post: str = SchemaField( + description="The post text to be published", default="", advanced=False + ) + media_urls: list[str] = SchemaField( + description="Optional list of media URLs to include. Set is_video in advanced settings to true if you want to upload videos.", + default_factory=list, + advanced=False, + ) + is_video: bool = SchemaField( + description="Whether the media is a video", default=False, advanced=True + ) + schedule_date: Optional[datetime] = SchemaField( + description="UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ)", + default=None, + advanced=True, + ) + disable_comments: bool = SchemaField( + description="Whether to disable comments", default=False, advanced=True + ) + shorten_links: bool = SchemaField( + description="Whether to shorten links", default=False, advanced=True + ) + unsplash: Optional[str] = SchemaField( + description="Unsplash image configuration", default=None, advanced=True + ) + requires_approval: bool = SchemaField( + description="Whether to enable approval workflow", + default=False, + advanced=True, + ) + random_post: bool = SchemaField( + description="Whether to generate random post text", + default=False, + advanced=True, + ) + random_media_url: bool = SchemaField( + description="Whether to generate random media", default=False, advanced=True + ) + notes: Optional[str] = SchemaField( + description="Additional notes for the post", default=None, advanced=True + ) + + +class CarouselItem(BaseModel): + """Model for Facebook carousel items.""" + + name: str = Field(..., description="The name of the item") + link: str = Field(..., description="The link of the item") + picture: str = Field(..., description="The picture URL of the item") + + +class CallToAction(BaseModel): + """Model for Google My Business Call to Action.""" + + action_type: str = Field( + ..., description="Type of action (book, order, shop, learn_more, sign_up, call)" + ) + url: Optional[str] = Field( + description="URL for the action (not required for 'call' action)" + ) + + +class EventDetails(BaseModel): + """Model for Google My Business Event details.""" + + title: str = Field(..., description="Event title") + start_date: str = Field(..., description="Event start date (ISO format)") + end_date: str = Field(..., description="Event end date (ISO format)") + + +class OfferDetails(BaseModel): + """Model for Google My Business Offer details.""" + + title: str = Field(..., description="Offer title") + start_date: str = Field(..., description="Offer start date (ISO format)") + end_date: str = Field(..., description="Offer end date (ISO format)") + coupon_code: str = Field(..., description="Coupon code (max 58 characters)") + redeem_online_url: str = Field(..., description="URL to redeem the offer") + terms_conditions: str = Field(..., description="Terms and conditions") + + +class InstagramUserTag(BaseModel): + """Model for Instagram user tags.""" + + username: str = Field(..., description="Instagram username (without @)") + x: Optional[float] = Field(description="X coordinate (0.0-1.0) for image posts") + y: Optional[float] = Field(description="Y coordinate (0.0-1.0) for image posts") + + +class LinkedInTargeting(BaseModel): + """Model for LinkedIn audience targeting.""" + + countries: Optional[list[str]] = Field( + description="Country codes (e.g., ['US', 'IN', 'DE', 'GB'])" + ) + seniorities: Optional[list[str]] = Field( + description="Seniority levels (e.g., ['Senior', 'VP'])" + ) + degrees: Optional[list[str]] = Field(description="Education degrees") + fields_of_study: Optional[list[str]] = Field(description="Fields of study") + industries: Optional[list[str]] = Field(description="Industry categories") + job_functions: Optional[list[str]] = Field(description="Job function categories") + staff_count_ranges: Optional[list[str]] = Field(description="Company size ranges") + + +class PinterestCarouselOption(BaseModel): + """Model for Pinterest carousel image options.""" + + title: Optional[str] = Field(description="Image title") + link: Optional[str] = Field(description="External destination link for the image") + description: Optional[str] = Field(description="Image description") + + +class YouTubeTargeting(BaseModel): + """Model for YouTube country targeting.""" + + block: Optional[list[str]] = Field( + description="Country codes to block (e.g., ['US', 'CA'])" + ) + allow: Optional[list[str]] = Field( + description="Country codes to allow (e.g., ['GB', 'AU'])" + ) + + +def create_ayrshare_client(): + """Create an Ayrshare client instance.""" + try: + return AyrshareClient() + except MissingConfigError: + return None diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_bluesky.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_bluesky.py new file mode 100644 index 000000000000..0d6eeed0a1ac --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_bluesky.py @@ -0,0 +1,114 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToBlueskyBlock(Block): + """Block for posting to Bluesky with Bluesky-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Bluesky posts.""" + + # Override post field to include character limit information + post: str = SchemaField( + description="The post text to be published (max 300 characters for Bluesky)", + default="", + advanced=False, + ) + + # Override media_urls to include Bluesky-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs to include. Bluesky supports up to 4 images or 1 video.", + default_factory=list, + advanced=False, + ) + + # Bluesky-specific options + alt_text: list[str] = SchemaField( + description="Alt text for each media item (accessibility)", + default_factory=list, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="cbd52c2a-06d2-43ed-9560-6576cc163283", + description="Post to Bluesky using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToBlueskyBlock.Input, + output_schema=PostToBlueskyBlock.Output, + ) + + async def run( + self, + input_data: "PostToBlueskyBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Bluesky with Bluesky-specific options.""" + + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate character limit for Bluesky + if len(input_data.post) > 300: + yield "error", f"Post text exceeds Bluesky's 300 character limit ({len(input_data.post)} characters)" + return + + # Validate media constraints for Bluesky + if len(input_data.media_urls) > 4: + yield "error", "Bluesky supports a maximum of 4 images or 1 video" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Bluesky-specific options + bluesky_options = {} + if input_data.alt_text: + bluesky_options["altText"] = input_data.alt_text + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.BLUESKY], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + bluesky_options=bluesky_options if bluesky_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_facebook.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_facebook.py new file mode 100644 index 000000000000..dccc443ef90c --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_facebook.py @@ -0,0 +1,212 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import ( + BaseAyrshareInput, + CarouselItem, + create_ayrshare_client, + get_profile_key, +) + + +class PostToFacebookBlock(Block): + """Block for posting to Facebook with Facebook-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Facebook posts.""" + + # Facebook-specific options + is_carousel: bool = SchemaField( + description="Whether to post a carousel", default=False, advanced=True + ) + carousel_link: str = SchemaField( + description="The URL for the 'See More At' button in the carousel", + default="", + advanced=True, + ) + carousel_items: list[CarouselItem] = SchemaField( + description="List of carousel items with name, link and picture URLs. Min 2, max 10 items.", + default_factory=list, + advanced=True, + ) + is_reels: bool = SchemaField( + description="Whether to post to Facebook Reels", + default=False, + advanced=True, + ) + reels_title: str = SchemaField( + description="Title for the Reels video (max 255 chars)", + default="", + advanced=True, + ) + reels_thumbnail: str = SchemaField( + description="Thumbnail URL for Reels video (JPEG/PNG, <10MB)", + default="", + advanced=True, + ) + is_story: bool = SchemaField( + description="Whether to post as a Facebook Story", + default=False, + advanced=True, + ) + media_captions: list[str] = SchemaField( + description="Captions for each media item", + default_factory=list, + advanced=True, + ) + location_id: str = SchemaField( + description="Facebook Page ID or name for location tagging", + default="", + advanced=True, + ) + age_min: int = SchemaField( + description="Minimum age for audience targeting (13,15,18,21,25)", + default=0, + advanced=True, + ) + target_countries: list[str] = SchemaField( + description="List of country codes to target (max 25)", + default_factory=list, + advanced=True, + ) + alt_text: list[str] = SchemaField( + description="Alt text for each media item", + default_factory=list, + advanced=True, + ) + video_title: str = SchemaField( + description="Title for video post", default="", advanced=True + ) + video_thumbnail: str = SchemaField( + description="Thumbnail URL for video post", default="", advanced=True + ) + is_draft: bool = SchemaField( + description="Save as draft in Meta Business Suite", + default=False, + advanced=True, + ) + scheduled_publish_date: str = SchemaField( + description="Schedule publish time in Meta Business Suite (UTC)", + default="", + advanced=True, + ) + preview_link: str = SchemaField( + description="URL for custom link preview", default="", advanced=True + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="3352f512-3524-49ed-a08f-003042da2fc1", + description="Post to Facebook using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToFacebookBlock.Input, + output_schema=PostToFacebookBlock.Output, + ) + + async def run( + self, + input_data: "PostToFacebookBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Facebook with Facebook-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Facebook-specific options + facebook_options = {} + if input_data.is_carousel: + facebook_options["isCarousel"] = True + if input_data.carousel_link: + facebook_options["carouselLink"] = input_data.carousel_link + if input_data.carousel_items: + facebook_options["carouselItems"] = [ + item.dict() for item in input_data.carousel_items + ] + + if input_data.is_reels: + facebook_options["isReels"] = True + if input_data.reels_title: + facebook_options["reelsTitle"] = input_data.reels_title + if input_data.reels_thumbnail: + facebook_options["reelsThumbnail"] = input_data.reels_thumbnail + + if input_data.is_story: + facebook_options["isStory"] = True + + if input_data.media_captions: + facebook_options["mediaCaptions"] = input_data.media_captions + + if input_data.location_id: + facebook_options["locationId"] = input_data.location_id + + if input_data.age_min > 0: + facebook_options["ageMin"] = input_data.age_min + + if input_data.target_countries: + facebook_options["targetCountries"] = input_data.target_countries + + if input_data.alt_text: + facebook_options["altText"] = input_data.alt_text + + if input_data.video_title: + facebook_options["videoTitle"] = input_data.video_title + + if input_data.video_thumbnail: + facebook_options["videoThumbnail"] = input_data.video_thumbnail + + if input_data.is_draft: + facebook_options["isDraft"] = True + + if input_data.scheduled_publish_date: + facebook_options["scheduledPublishDate"] = input_data.scheduled_publish_date + + if input_data.preview_link: + facebook_options["previewLink"] = input_data.preview_link + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.FACEBOOK], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + facebook_options=facebook_options if facebook_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_gmb.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_gmb.py new file mode 100644 index 000000000000..5c510cccb317 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_gmb.py @@ -0,0 +1,210 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToGMBBlock(Block): + """Block for posting to Google My Business with GMB-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Google My Business posts.""" + + # Override media_urls to include GMB-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. GMB supports only one image or video per post.", + default_factory=list, + advanced=False, + ) + + # GMB-specific options + is_photo_video: bool = SchemaField( + description="Whether this is a photo/video post (appears in Photos section)", + default=False, + advanced=True, + ) + photo_category: str = SchemaField( + description="Category for photo/video: cover, profile, logo, exterior, interior, product, at_work, food_and_drink, menu, common_area, rooms, teams", + default="", + advanced=True, + ) + # Call to action options (flattened from CallToAction object) + call_to_action_type: str = SchemaField( + description="Type of action button: 'book', 'order', 'shop', 'learn_more', 'sign_up', or 'call'", + default="", + advanced=True, + ) + call_to_action_url: str = SchemaField( + description="URL for the action button (not required for 'call' action)", + default="", + advanced=True, + ) + # Event details options (flattened from EventDetails object) + event_title: str = SchemaField( + description="Event title for event posts", + default="", + advanced=True, + ) + event_start_date: str = SchemaField( + description="Event start date in ISO format (e.g., '2024-03-15T09:00:00Z')", + default="", + advanced=True, + ) + event_end_date: str = SchemaField( + description="Event end date in ISO format (e.g., '2024-03-15T17:00:00Z')", + default="", + advanced=True, + ) + # Offer details options (flattened from OfferDetails object) + offer_title: str = SchemaField( + description="Offer title for promotional posts", + default="", + advanced=True, + ) + offer_start_date: str = SchemaField( + description="Offer start date in ISO format (e.g., '2024-03-15T00:00:00Z')", + default="", + advanced=True, + ) + offer_end_date: str = SchemaField( + description="Offer end date in ISO format (e.g., '2024-04-15T23:59:59Z')", + default="", + advanced=True, + ) + offer_coupon_code: str = SchemaField( + description="Coupon code for the offer (max 58 characters)", + default="", + advanced=True, + ) + offer_redeem_online_url: str = SchemaField( + description="URL where customers can redeem the offer online", + default="", + advanced=True, + ) + offer_terms_conditions: str = SchemaField( + description="Terms and conditions for the offer", + default="", + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="2c38c783-c484-4503-9280-ef5d1d345a7e", + description="Post to Google My Business using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToGMBBlock.Input, + output_schema=PostToGMBBlock.Output, + ) + + async def run( + self, input_data: "PostToGMBBlock.Input", *, user_id: str, **kwargs + ) -> BlockOutput: + """Post to Google My Business with GMB-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate GMB constraints + if len(input_data.media_urls) > 1: + yield "error", "Google My Business supports only one image or video per post" + return + + # Validate offer coupon code length + if input_data.offer_coupon_code and len(input_data.offer_coupon_code) > 58: + yield "error", "GMB offer coupon code cannot exceed 58 characters" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build GMB-specific options + gmb_options = {} + + # Photo/Video post options + if input_data.is_photo_video: + gmb_options["isPhotoVideo"] = True + if input_data.photo_category: + gmb_options["category"] = input_data.photo_category + + # Call to Action (from flattened fields) + if input_data.call_to_action_type: + cta_dict = {"actionType": input_data.call_to_action_type} + # URL not required for 'call' action type + if ( + input_data.call_to_action_type != "call" + and input_data.call_to_action_url + ): + cta_dict["url"] = input_data.call_to_action_url + gmb_options["callToAction"] = cta_dict + + # Event details (from flattened fields) + if ( + input_data.event_title + and input_data.event_start_date + and input_data.event_end_date + ): + gmb_options["event"] = { + "title": input_data.event_title, + "startDate": input_data.event_start_date, + "endDate": input_data.event_end_date, + } + + # Offer details (from flattened fields) + if ( + input_data.offer_title + and input_data.offer_start_date + and input_data.offer_end_date + and input_data.offer_coupon_code + and input_data.offer_redeem_online_url + and input_data.offer_terms_conditions + ): + gmb_options["offer"] = { + "title": input_data.offer_title, + "startDate": input_data.offer_start_date, + "endDate": input_data.offer_end_date, + "couponCode": input_data.offer_coupon_code, + "redeemOnlineUrl": input_data.offer_redeem_online_url, + "termsConditions": input_data.offer_terms_conditions, + } + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.GOOGLE_MY_BUSINESS], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + gmb_options=gmb_options if gmb_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_instagram.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_instagram.py new file mode 100644 index 000000000000..1fc7c77df019 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_instagram.py @@ -0,0 +1,249 @@ +from typing import Any + +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import ( + BaseAyrshareInput, + InstagramUserTag, + create_ayrshare_client, + get_profile_key, +) + + +class PostToInstagramBlock(Block): + """Block for posting to Instagram with Instagram-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Instagram posts.""" + + # Override post field to include Instagram-specific information + post: str = SchemaField( + description="The post text (max 2,200 chars, up to 30 hashtags, 3 @mentions)", + default="", + advanced=False, + ) + + # Override media_urls to include Instagram-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. Instagram supports up to 10 images/videos in a carousel.", + default_factory=list, + advanced=False, + ) + + # Instagram-specific options + is_story: bool | None = SchemaField( + description="Whether to post as Instagram Story (24-hour expiration)", + default=None, + advanced=True, + ) + + # ------- REELS OPTIONS ------- + share_reels_feed: bool | None = SchemaField( + description="Whether Reel should appear in both Feed and Reels tabs", + default=None, + advanced=True, + ) + audio_name: str | None = SchemaField( + description="Audio name for Reels (e.g., 'The Weeknd - Blinding Lights')", + default=None, + advanced=True, + ) + thumbnail: str | None = SchemaField( + description="Thumbnail URL for Reel video", default=None, advanced=True + ) + thumbnail_offset: int | None = SchemaField( + description="Thumbnail frame offset in milliseconds (default: 0)", + default=0, + advanced=True, + ) + + # ------- POST OPTIONS ------- + + alt_text: list[str] = SchemaField( + description="Alt text for each media item (up to 1,000 chars each, accessibility feature), each item in the list corresponds to a media item in the media_urls list", + default_factory=list, + advanced=True, + ) + + location_id: str | None = SchemaField( + description="Facebook Page ID or name for location tagging (e.g., '7640348500' or '@guggenheimmuseum')", + default=None, + advanced=True, + ) + user_tags: list[dict[str, Any]] = SchemaField( + description="List of users to tag with coordinates for images", + default_factory=list, + advanced=True, + ) + collaborators: list[str] = SchemaField( + description="Instagram usernames to invite as collaborators (max 3, public accounts only)", + default_factory=list, + advanced=True, + ) + auto_resize: bool | None = SchemaField( + description="Auto-resize images to 1080x1080px for Instagram", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + id="89b02b96-a7cb-46f4-9900-c48b32fe1552", + description="Post to Instagram using Ayrshare. Requires a Business or Creator Instagram Account connected with a Facebook Page", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToInstagramBlock.Input, + output_schema=PostToInstagramBlock.Output, + ) + + async def run( + self, + input_data: "PostToInstagramBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Instagram with Instagram-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate Instagram constraints + if len(input_data.post) > 2200: + yield "error", f"Instagram post text exceeds 2,200 character limit ({len(input_data.post)} characters)" + return + + if len(input_data.media_urls) > 10: + yield "error", "Instagram supports a maximum of 10 images/videos in a carousel" + return + + if len(input_data.collaborators) > 3: + yield "error", "Instagram supports a maximum of 3 collaborators" + return + + # Validate that if any reel option is set, all required reel options are set + reel_options = [ + input_data.share_reels_feed, + input_data.audio_name, + input_data.thumbnail, + ] + + if any(reel_options) and not all(reel_options): + yield "error", "When posting a reel, all reel options must be set: share_reels_feed, audio_name, and either thumbnail or thumbnail_offset" + return + + # Count hashtags and mentions + hashtag_count = input_data.post.count("#") + mention_count = input_data.post.count("@") + + if hashtag_count > 30: + yield "error", f"Instagram allows maximum 30 hashtags ({hashtag_count} found)" + return + + if mention_count > 3: + yield "error", f"Instagram allows maximum 3 @mentions ({mention_count} found)" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Instagram-specific options + instagram_options = {} + + # Stories + if input_data.is_story: + instagram_options["stories"] = True + + # Reels options + if input_data.share_reels_feed is not None: + instagram_options["shareReelsFeed"] = input_data.share_reels_feed + + if input_data.audio_name: + instagram_options["audioName"] = input_data.audio_name + + if input_data.thumbnail: + instagram_options["thumbNail"] = input_data.thumbnail + elif input_data.thumbnail_offset and input_data.thumbnail_offset > 0: + instagram_options["thumbNailOffset"] = input_data.thumbnail_offset + + # Alt text + if input_data.alt_text: + # Validate alt text length + for i, alt in enumerate(input_data.alt_text): + if len(alt) > 1000: + yield "error", f"Alt text {i+1} exceeds 1,000 character limit ({len(alt)} characters)" + return + instagram_options["altText"] = input_data.alt_text + + # Location + if input_data.location_id: + instagram_options["locationId"] = input_data.location_id + + # User tags + if input_data.user_tags: + user_tags_list = [] + for tag in input_data.user_tags: + try: + tag_obj = InstagramUserTag(**tag) + except Exception as e: + yield "error", f"Invalid user tag: {e}, tages need to be a dictionary with a 3 items: username (str), x (float) and y (float)" + return + tag_dict: dict[str, float | str] = {"username": tag_obj.username} + if tag_obj.x is not None and tag_obj.y is not None: + # Validate coordinates + if not (0.0 <= tag_obj.x <= 1.0) or not (0.0 <= tag_obj.y <= 1.0): + yield "error", f"User tag coordinates must be between 0.0 and 1.0 (user: {tag_obj.username})" + return + tag_dict["x"] = tag_obj.x + tag_dict["y"] = tag_obj.y + user_tags_list.append(tag_dict) + instagram_options["userTags"] = user_tags_list + + # Collaborators + if input_data.collaborators: + instagram_options["collaborators"] = input_data.collaborators + + # Auto resize + if input_data.auto_resize: + instagram_options["autoResize"] = True + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.INSTAGRAM], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + instagram_options=instagram_options if instagram_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_linkedin.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_linkedin.py new file mode 100644 index 000000000000..7fad89e838bc --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_linkedin.py @@ -0,0 +1,222 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToLinkedInBlock(Block): + """Block for posting to LinkedIn with LinkedIn-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for LinkedIn posts.""" + + # Override post field to include LinkedIn-specific information + post: str = SchemaField( + description="The post text (max 3,000 chars, hashtags supported with #)", + default="", + advanced=False, + ) + + # Override media_urls to include LinkedIn-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. LinkedIn supports up to 9 images, videos, or documents (PPT, PPTX, DOC, DOCX, PDF <100MB, <300 pages).", + default_factory=list, + advanced=False, + ) + + # LinkedIn-specific options + visibility: str = SchemaField( + description="Post visibility: 'public' (default), 'connections' (personal only), 'loggedin'", + default="public", + advanced=True, + ) + alt_text: list[str] = SchemaField( + description="Alt text for each image (accessibility feature, not supported for videos/documents)", + default_factory=list, + advanced=True, + ) + titles: list[str] = SchemaField( + description="Title/caption for each image or video", + default_factory=list, + advanced=True, + ) + document_title: str = SchemaField( + description="Title for document posts (max 400 chars, uses filename if not specified)", + default="", + advanced=True, + ) + thumbnail: str = SchemaField( + description="Thumbnail URL for video (PNG/JPG, same dimensions as video, <10MB)", + default="", + advanced=True, + ) + # LinkedIn targeting options (flattened from LinkedInTargeting object) + targeting_countries: list[str] | None = SchemaField( + description="Country codes for targeting (e.g., ['US', 'IN', 'DE', 'GB']). Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_seniorities: list[str] | None = SchemaField( + description="Seniority levels for targeting (e.g., ['Senior', 'VP']). Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_degrees: list[str] | None = SchemaField( + description="Education degrees for targeting. Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_fields_of_study: list[str] | None = SchemaField( + description="Fields of study for targeting. Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_industries: list[str] | None = SchemaField( + description="Industry categories for targeting. Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_job_functions: list[str] | None = SchemaField( + description="Job function categories for targeting. Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + targeting_staff_count_ranges: list[str] | None = SchemaField( + description="Company size ranges for targeting. Requires 300+ followers in target audience.", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + id="589af4e4-507f-42fd-b9ac-a67ecef25811", + description="Post to LinkedIn using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToLinkedInBlock.Input, + output_schema=PostToLinkedInBlock.Output, + ) + + async def run( + self, + input_data: "PostToLinkedInBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to LinkedIn with LinkedIn-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate LinkedIn constraints + if len(input_data.post) > 3000: + yield "error", f"LinkedIn post text exceeds 3,000 character limit ({len(input_data.post)} characters)" + return + + if len(input_data.media_urls) > 9: + yield "error", "LinkedIn supports a maximum of 9 images/videos/documents" + return + + if input_data.document_title and len(input_data.document_title) > 400: + yield "error", f"LinkedIn document title exceeds 400 character limit ({len(input_data.document_title)} characters)" + return + + # Validate visibility option + valid_visibility = ["public", "connections", "loggedin"] + if input_data.visibility not in valid_visibility: + yield "error", f"LinkedIn visibility must be one of: {', '.join(valid_visibility)}" + return + + # Check for document extensions + document_extensions = [".ppt", ".pptx", ".doc", ".docx", ".pdf"] + has_documents = any( + any(url.lower().endswith(ext) for ext in document_extensions) + for url in input_data.media_urls + ) + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build LinkedIn-specific options + linkedin_options = {} + + # Visibility + if input_data.visibility != "public": + linkedin_options["visibility"] = input_data.visibility + + # Alt text (not supported for videos or documents) + if input_data.alt_text and not has_documents: + linkedin_options["altText"] = input_data.alt_text + + # Titles/captions + if input_data.titles: + linkedin_options["titles"] = input_data.titles + + # Document title + if input_data.document_title and has_documents: + linkedin_options["title"] = input_data.document_title + + # Video thumbnail + if input_data.thumbnail: + linkedin_options["thumbNail"] = input_data.thumbnail + + # Audience targeting (from flattened fields) + targeting_dict = {} + if input_data.targeting_countries: + targeting_dict["countries"] = input_data.targeting_countries + if input_data.targeting_seniorities: + targeting_dict["seniorities"] = input_data.targeting_seniorities + if input_data.targeting_degrees: + targeting_dict["degrees"] = input_data.targeting_degrees + if input_data.targeting_fields_of_study: + targeting_dict["fieldsOfStudy"] = input_data.targeting_fields_of_study + if input_data.targeting_industries: + targeting_dict["industries"] = input_data.targeting_industries + if input_data.targeting_job_functions: + targeting_dict["jobFunctions"] = input_data.targeting_job_functions + if input_data.targeting_staff_count_ranges: + targeting_dict["staffCountRanges"] = input_data.targeting_staff_count_ranges + + if targeting_dict: + linkedin_options["targeting"] = targeting_dict + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.LINKEDIN], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + linkedin_options=linkedin_options if linkedin_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_pinterest.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_pinterest.py new file mode 100644 index 000000000000..55bae8161890 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_pinterest.py @@ -0,0 +1,214 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import ( + BaseAyrshareInput, + PinterestCarouselOption, + create_ayrshare_client, + get_profile_key, +) + + +class PostToPinterestBlock(Block): + """Block for posting to Pinterest with Pinterest-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Pinterest posts.""" + + # Override post field to include Pinterest-specific information + post: str = SchemaField( + description="Pin description (max 500 chars, links not clickable - use link field instead)", + default="", + advanced=False, + ) + + # Override media_urls to include Pinterest-specific constraints + media_urls: list[str] = SchemaField( + description="Required image/video URLs. Pinterest requires at least one image. Videos need thumbnail. Up to 5 images for carousel.", + default_factory=list, + advanced=False, + ) + + # Pinterest-specific options + pin_title: str = SchemaField( + description="Pin title displayed in 'Add your title' section (max 100 chars)", + default="", + advanced=True, + ) + link: str = SchemaField( + description="Clickable destination URL when users click the pin (max 2048 chars)", + default="", + advanced=True, + ) + board_id: str = SchemaField( + description="Pinterest Board ID to post to (from /user/details endpoint, uses default board if not specified)", + default="", + advanced=True, + ) + note: str = SchemaField( + description="Private note for the pin (only visible to you and board collaborators)", + default="", + advanced=True, + ) + thumbnail: str = SchemaField( + description="Required thumbnail URL for video pins (must have valid image Content-Type)", + default="", + advanced=True, + ) + carousel_options: list[PinterestCarouselOption] = SchemaField( + description="Options for each image in carousel (title, link, description per image)", + default_factory=list, + advanced=True, + ) + alt_text: list[str] = SchemaField( + description="Alt text for each image/video (max 500 chars each, accessibility feature)", + default_factory=list, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="3ca46e05-dbaa-4afb-9e95-5a429c4177e6", + description="Post to Pinterest using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToPinterestBlock.Input, + output_schema=PostToPinterestBlock.Output, + ) + + async def run( + self, + input_data: "PostToPinterestBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Pinterest with Pinterest-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate Pinterest constraints + if len(input_data.post) > 500: + yield "error", f"Pinterest pin description exceeds 500 character limit ({len(input_data.post)} characters)" + return + + if len(input_data.pin_title) > 100: + yield "error", f"Pinterest pin title exceeds 100 character limit ({len(input_data.pin_title)} characters)" + return + + if len(input_data.link) > 2048: + yield "error", f"Pinterest link URL exceeds 2048 character limit ({len(input_data.link)} characters)" + return + + if len(input_data.media_urls) == 0: + yield "error", "Pinterest requires at least one image or video" + return + + if len(input_data.media_urls) > 5: + yield "error", "Pinterest supports a maximum of 5 images in a carousel" + return + + # Check if video is included and thumbnail is provided + video_extensions = [".mp4", ".mov", ".avi", ".mkv", ".wmv", ".flv", ".webm"] + has_video = any( + any(url.lower().endswith(ext) for ext in video_extensions) + for url in input_data.media_urls + ) + + if (has_video or input_data.is_video) and not input_data.thumbnail: + yield "error", "Pinterest video pins require a thumbnail URL" + return + + # Validate alt text length + for i, alt in enumerate(input_data.alt_text): + if len(alt) > 500: + yield "error", f"Pinterest alt text {i+1} exceeds 500 character limit ({len(alt)} characters)" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Pinterest-specific options + pinterest_options = {} + + # Pin title + if input_data.pin_title: + pinterest_options["title"] = input_data.pin_title + + # Clickable link + if input_data.link: + pinterest_options["link"] = input_data.link + + # Board ID + if input_data.board_id: + pinterest_options["boardId"] = input_data.board_id + + # Private note + if input_data.note: + pinterest_options["note"] = input_data.note + + # Video thumbnail + if input_data.thumbnail: + pinterest_options["thumbNail"] = input_data.thumbnail + + # Carousel options + if input_data.carousel_options: + carousel_list = [] + for option in input_data.carousel_options: + carousel_dict = {} + if option.title: + carousel_dict["title"] = option.title + if option.link: + carousel_dict["link"] = option.link + if option.description: + carousel_dict["description"] = option.description + if carousel_dict: # Only add if not empty + carousel_list.append(carousel_dict) + if carousel_list: + pinterest_options["carouselOptions"] = carousel_list + + # Alt text + if input_data.alt_text: + pinterest_options["altText"] = input_data.alt_text + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.PINTEREST], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + pinterest_options=pinterest_options if pinterest_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_reddit.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_reddit.py new file mode 100644 index 000000000000..c193f94d2ede --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_reddit.py @@ -0,0 +1,69 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToRedditBlock(Block): + """Block for posting to Reddit.""" + + class Input(BaseAyrshareInput): + """Input schema for Reddit posts.""" + + pass # Uses all base fields + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="c7733580-3c72-483e-8e47-a8d58754d853", + description="Post to Reddit using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToRedditBlock.Input, + output_schema=PostToRedditBlock.Output, + ) + + async def run( + self, input_data: "PostToRedditBlock.Input", *, user_id: str, **kwargs + ) -> BlockOutput: + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured." + return + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.REDDIT], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_snapchat.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_snapchat.py new file mode 100644 index 000000000000..8de728e56917 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_snapchat.py @@ -0,0 +1,129 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToSnapchatBlock(Block): + """Block for posting to Snapchat with Snapchat-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Snapchat posts.""" + + # Override post field to include Snapchat-specific information + post: str = SchemaField( + description="The post text (optional for video-only content)", + default="", + advanced=False, + ) + + # Override media_urls to include Snapchat-specific constraints + media_urls: list[str] = SchemaField( + description="Required video URL for Snapchat posts. Snapchat only supports video content.", + default_factory=list, + advanced=False, + ) + + # Snapchat-specific options + story_type: str = SchemaField( + description="Type of Snapchat content: 'story' (24-hour Stories), 'saved_story' (Saved Stories), or 'spotlight' (Spotlight posts)", + default="story", + advanced=True, + ) + video_thumbnail: str = SchemaField( + description="Thumbnail URL for video content (optional, auto-generated if not provided)", + default="", + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="a9d7f854-2c83-4e96-b3a1-7f2e9c5d4b8e", + description="Post to Snapchat using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToSnapchatBlock.Input, + output_schema=PostToSnapchatBlock.Output, + ) + + async def run( + self, + input_data: "PostToSnapchatBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Snapchat with Snapchat-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate Snapchat constraints + if not input_data.media_urls: + yield "error", "Snapchat requires at least one video URL" + return + + if len(input_data.media_urls) > 1: + yield "error", "Snapchat supports only one video per post" + return + + # Validate story type + valid_story_types = ["story", "saved_story", "spotlight"] + if input_data.story_type not in valid_story_types: + yield "error", f"Snapchat story type must be one of: {', '.join(valid_story_types)}" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Snapchat-specific options + snapchat_options = {} + + # Story type + if input_data.story_type != "story": + snapchat_options["storyType"] = input_data.story_type + + # Video thumbnail + if input_data.video_thumbnail: + snapchat_options["videoThumbnail"] = input_data.video_thumbnail + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.SNAPCHAT], + media_urls=input_data.media_urls, + is_video=True, # Snapchat only supports video + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + snapchat_options=snapchat_options if snapchat_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_telegram.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_telegram.py new file mode 100644 index 000000000000..a18d9a6cb11b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_telegram.py @@ -0,0 +1,116 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToTelegramBlock(Block): + """Block for posting to Telegram with Telegram-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Telegram posts.""" + + # Override post field to include Telegram-specific information + post: str = SchemaField( + description="The post text (empty string allowed). Use @handle to mention other Telegram users.", + default="", + advanced=False, + ) + + # Override media_urls to include Telegram-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. For animated GIFs, only one URL is allowed. Telegram will auto-preview links unless image/video is included.", + default_factory=list, + advanced=False, + ) + + # Override is_video to include GIF-specific information + is_video: bool = SchemaField( + description="Whether the media is a video. Set to true for animated GIFs that don't end in .gif/.GIF extension.", + default=False, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="47bc74eb-4af2-452c-b933-af377c7287df", + description="Post to Telegram using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToTelegramBlock.Input, + output_schema=PostToTelegramBlock.Output, + ) + + async def run( + self, + input_data: "PostToTelegramBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Telegram with Telegram-specific validation.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate Telegram constraints + # Check for animated GIFs - only one URL allowed + gif_extensions = [".gif", ".GIF"] + has_gif = any( + any(url.endswith(ext) for ext in gif_extensions) + for url in input_data.media_urls + ) + + if has_gif and len(input_data.media_urls) > 1: + yield "error", "Telegram animated GIFs support only one URL per post" + return + + # Auto-detect if we need to set is_video for GIFs without proper extension + detected_is_video = input_data.is_video + if input_data.media_urls and not has_gif and not input_data.is_video: + # Check if this might be a GIF without proper extension + # This is just informational - user needs to set is_video manually + pass + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.TELEGRAM], + media_urls=input_data.media_urls, + is_video=detected_is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_threads.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_threads.py new file mode 100644 index 000000000000..6fdf06f1a58e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_threads.py @@ -0,0 +1,111 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToThreadsBlock(Block): + """Block for posting to Threads with Threads-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for Threads posts.""" + + # Override post field to include Threads-specific information + post: str = SchemaField( + description="The post text (max 500 chars, empty string allowed). Only 1 hashtag allowed. Use @handle to mention users.", + default="", + advanced=False, + ) + + # Override media_urls to include Threads-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. Supports up to 20 images/videos in a carousel. Auto-preview links unless media is included.", + default_factory=list, + advanced=False, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + disabled=True, + id="f8c3b2e1-9d4a-4e5f-8c7b-6a9e8d2f1c3b", + description="Post to Threads using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToThreadsBlock.Input, + output_schema=PostToThreadsBlock.Output, + ) + + async def run( + self, + input_data: "PostToThreadsBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to Threads with Threads-specific validation.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate Threads constraints + if len(input_data.post) > 500: + yield "error", f"Threads post text exceeds 500 character limit ({len(input_data.post)} characters)" + return + + if len(input_data.media_urls) > 20: + yield "error", "Threads supports a maximum of 20 images/videos in a carousel" + return + + # Count hashtags (only 1 allowed) + hashtag_count = input_data.post.count("#") + if hashtag_count > 1: + yield "error", f"Threads allows only 1 hashtag per post ({hashtag_count} found)" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build Threads-specific options + threads_options = {} + # Note: Based on the documentation, Threads doesn't seem to have specific options + # beyond the standard ones. The main constraints are validation-based. + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.THREADS], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + threads_options=threads_options if threads_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_tiktok.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_tiktok.py new file mode 100644 index 000000000000..581e65b74ee5 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_tiktok.py @@ -0,0 +1,243 @@ +from enum import Enum + +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class TikTokVisibility(str, Enum): + PUBLIC = "public" + PRIVATE = "private" + FOLLOWERS = "followers" + + +class PostToTikTokBlock(Block): + """Block for posting to TikTok with TikTok-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for TikTok posts.""" + + # Override post field to include TikTok-specific information + post: str = SchemaField( + description="The post text (max 2,200 chars, empty string allowed). Use @handle to mention users. Line breaks will be ignored.", + advanced=False, + ) + + # Override media_urls to include TikTok-specific constraints + media_urls: list[str] = SchemaField( + description="Required media URLs. Either 1 video OR up to 35 images (JPG/JPEG/WEBP only). Cannot mix video and images.", + default_factory=list, + advanced=False, + ) + + # TikTok-specific options + auto_add_music: bool = SchemaField( + description="Whether to automatically add recommended music to the post. If you set this field to true, you can change the music later in the TikTok app.", + default=False, + advanced=True, + ) + disable_comments: bool = SchemaField( + description="Disable comments on the published post", + default=False, + advanced=True, + ) + disable_duet: bool = SchemaField( + description="Disable duets on published video (video only)", + default=False, + advanced=True, + ) + disable_stitch: bool = SchemaField( + description="Disable stitch on published video (video only)", + default=False, + advanced=True, + ) + is_ai_generated: bool = SchemaField( + description="If you enable the toggle, your video will be labeled as “Creator labeled as AI-generated” once posted and can’t be changed. The “Creator labeled as AI-generated” label indicates that the content was completely AI-generated or significantly edited with AI.", + default=False, + advanced=True, + ) + is_branded_content: bool = SchemaField( + description="Whether to enable the Branded Content toggle. If this field is set to true, the video will be labeled as Branded Content, indicating you are in a paid partnership with a brand. A “Paid partnership” label will be attached to the video.", + default=False, + advanced=True, + ) + is_brand_organic: bool = SchemaField( + description="Whether to enable the Brand Organic Content toggle. If this field is set to true, the video will be labeled as Brand Organic Content, indicating you are promoting yourself or your own business. A “Promotional content” label will be attached to the video.", + default=False, + advanced=True, + ) + image_cover_index: int = SchemaField( + description="Index of image to use as cover (0-based, image posts only)", + default=0, + advanced=True, + ) + title: str = SchemaField( + description="Title for image posts", default="", advanced=True + ) + thumbnail_offset: int = SchemaField( + description="Video thumbnail frame offset in milliseconds (video only)", + default=0, + advanced=True, + ) + visibility: TikTokVisibility = SchemaField( + description="Post visibility: 'public', 'private', 'followers', or 'friends'", + default=TikTokVisibility.PUBLIC, + advanced=True, + ) + draft: bool = SchemaField( + description="Create as draft post (video only)", + default=False, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + id="7faf4b27-96b0-4f05-bf64-e0de54ae74e1", + description="Post to TikTok using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToTikTokBlock.Input, + output_schema=PostToTikTokBlock.Output, + ) + + async def run( + self, input_data: "PostToTikTokBlock.Input", *, user_id: str, **kwargs + ) -> BlockOutput: + """Post to TikTok with TikTok-specific validation and options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate TikTok constraints + if len(input_data.post) > 2200: + yield "error", f"TikTok post text exceeds 2,200 character limit ({len(input_data.post)} characters)" + return + + if not input_data.media_urls: + yield "error", "TikTok requires at least one media URL (either 1 video or up to 35 images)" + return + + # Check for video vs image constraints + video_extensions = [".mp4", ".mov", ".avi", ".mkv", ".wmv", ".flv", ".webm"] + image_extensions = [".jpg", ".jpeg", ".webp"] + + has_video = input_data.is_video or any( + any(url.lower().endswith(ext) for ext in video_extensions) + for url in input_data.media_urls + ) + + has_images = any( + any(url.lower().endswith(ext) for ext in image_extensions) + for url in input_data.media_urls + ) + + if has_video and has_images: + yield "error", "TikTok does not support mixing video and images in the same post" + return + + if has_video and len(input_data.media_urls) > 1: + yield "error", "TikTok supports only 1 video per post" + return + + if has_images and len(input_data.media_urls) > 35: + yield "error", "TikTok supports a maximum of 35 images per post" + return + + # Validate image cover index + if has_images and input_data.image_cover_index >= len(input_data.media_urls): + yield "error", f"Image cover index {input_data.image_cover_index} is out of range (max: {len(input_data.media_urls) - 1})" + return + + # Check for PNG files (not supported) + has_png = any(url.lower().endswith(".png") for url in input_data.media_urls) + if has_png: + yield "error", "TikTok does not support PNG files. Please use JPG, JPEG, or WEBP for images." + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build TikTok-specific options + tiktok_options = {} + + # Common options + if input_data.auto_add_music and has_images: + tiktok_options["autoAddMusic"] = True + + if input_data.disable_comments: + tiktok_options["disableComments"] = True + + if input_data.is_branded_content: + tiktok_options["isBrandedContent"] = True + + if input_data.is_brand_organic: + tiktok_options["isBrandOrganic"] = True + + # Video-specific options + if has_video: + if input_data.disable_duet: + tiktok_options["disableDuet"] = True + + if input_data.disable_stitch: + tiktok_options["disableStitch"] = True + + if input_data.is_ai_generated: + tiktok_options["isAIGenerated"] = True + + if input_data.thumbnail_offset > 0: + tiktok_options["thumbNailOffset"] = input_data.thumbnail_offset + + if input_data.draft: + tiktok_options["draft"] = True + + # Image-specific options + if has_images: + if input_data.image_cover_index > 0: + tiktok_options["imageCoverIndex"] = input_data.image_cover_index + + if input_data.title: + tiktok_options["title"] = input_data.title + + if input_data.visibility != TikTokVisibility.PUBLIC: + tiktok_options["visibility"] = input_data.visibility.value + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.TIKTOK], + media_urls=input_data.media_urls, + is_video=has_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + tiktok_options=tiktok_options if tiktok_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_x.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_x.py new file mode 100644 index 000000000000..bc23ac2c78b8 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_x.py @@ -0,0 +1,241 @@ +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class PostToXBlock(Block): + """Block for posting to X / Twitter with Twitter-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for X / Twitter posts.""" + + # Override post field to include X-specific information + post: str = SchemaField( + description="The post text (max 280 chars, up to 25,000 for Premium users). Use @handle to mention users. Use \\n\\n for thread breaks.", + advanced=False, + ) + + # Override media_urls to include X-specific constraints + media_urls: list[str] = SchemaField( + description="Optional list of media URLs. X supports up to 4 images or videos per tweet. Auto-preview links unless media is included.", + default_factory=list, + advanced=False, + ) + + # X-specific options + reply_to_id: str | None = SchemaField( + description="ID of the tweet to reply to", + default=None, + advanced=True, + ) + quote_tweet_id: str | None = SchemaField( + description="ID of the tweet to quote (low-level Tweet ID)", + default=None, + advanced=True, + ) + poll_options: list[str] = SchemaField( + description="Poll options (2-4 choices)", + default_factory=list, + advanced=True, + ) + poll_duration: int = SchemaField( + description="Poll duration in minutes (1-10080)", + default=1440, + advanced=True, + ) + alt_text: list[str] = SchemaField( + description="Alt text for each image (max 1,000 chars each, not supported for videos)", + default_factory=list, + advanced=True, + ) + is_thread: bool = SchemaField( + description="Whether to automatically break post into thread based on line breaks", + default=False, + advanced=True, + ) + thread_number: bool = SchemaField( + description="Add thread numbers (1/n format) to each thread post", + default=False, + advanced=True, + ) + thread_media_urls: list[str] = SchemaField( + description="Media URLs for thread posts (one per thread, use 'null' to skip)", + default_factory=list, + advanced=True, + ) + long_post: bool = SchemaField( + description="Force long form post (requires Premium X account)", + default=False, + advanced=True, + ) + long_video: bool = SchemaField( + description="Enable long video upload (requires approval and Business/Enterprise plan)", + default=False, + advanced=True, + ) + subtitle_url: str = SchemaField( + description="URL to SRT subtitle file for videos (must be HTTPS and end in .srt)", + default="", + advanced=True, + ) + subtitle_language: str = SchemaField( + description="Language code for subtitles (default: 'en')", + default="en", + advanced=True, + ) + subtitle_name: str = SchemaField( + description="Name of caption track (max 150 chars, default: 'English')", + default="English", + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + id="9e8f844e-b4a5-4b25-80f2-9e1dd7d67625", + description="Post to X / Twitter using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToXBlock.Input, + output_schema=PostToXBlock.Output, + ) + + async def run( + self, + input_data: "PostToXBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to X / Twitter with enhanced X-specific options.""" + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate X constraints + if not input_data.long_post and len(input_data.post) > 280: + yield "error", f"X post text exceeds 280 character limit ({len(input_data.post)} characters). Enable 'long_post' for Premium accounts." + return + + if input_data.long_post and len(input_data.post) > 25000: + yield "error", f"X long post text exceeds 25,000 character limit ({len(input_data.post)} characters)" + return + + if len(input_data.media_urls) > 4: + yield "error", "X supports a maximum of 4 images or videos per tweet" + return + + # Validate poll options + if input_data.poll_options: + if len(input_data.poll_options) < 2 or len(input_data.poll_options) > 4: + yield "error", "X polls require 2-4 options" + return + + if input_data.poll_duration < 1 or input_data.poll_duration > 10080: + yield "error", "X poll duration must be between 1 and 10,080 minutes (7 days)" + return + + # Validate alt text + if input_data.alt_text: + for i, alt in enumerate(input_data.alt_text): + if len(alt) > 1000: + yield "error", f"X alt text {i+1} exceeds 1,000 character limit ({len(alt)} characters)" + return + + # Validate subtitle settings + if input_data.subtitle_url: + if not input_data.subtitle_url.startswith( + "https://" + ) or not input_data.subtitle_url.endswith(".srt"): + yield "error", "Subtitle URL must start with https:// and end with .srt" + return + + if len(input_data.subtitle_name) > 150: + yield "error", f"Subtitle name exceeds 150 character limit ({len(input_data.subtitle_name)} characters)" + return + + # Convert datetime to ISO format if provided + iso_date = ( + input_data.schedule_date.isoformat() if input_data.schedule_date else None + ) + + # Build X-specific options + twitter_options = {} + + # Basic options + if input_data.reply_to_id: + twitter_options["replyToId"] = input_data.reply_to_id + + if input_data.quote_tweet_id: + twitter_options["quoteTweetId"] = input_data.quote_tweet_id + + if input_data.long_post: + twitter_options["longPost"] = True + + if input_data.long_video: + twitter_options["longVideo"] = True + + # Poll options + if input_data.poll_options: + twitter_options["poll"] = { + "duration": input_data.poll_duration, + "options": input_data.poll_options, + } + + # Alt text for images + if input_data.alt_text: + twitter_options["altText"] = input_data.alt_text + + # Thread options + if input_data.is_thread: + twitter_options["thread"] = True + + if input_data.thread_number: + twitter_options["threadNumber"] = True + + if input_data.thread_media_urls: + twitter_options["mediaUrls"] = input_data.thread_media_urls + + # Video subtitle options + if input_data.subtitle_url: + twitter_options["subTitleUrl"] = input_data.subtitle_url + twitter_options["subTitleLanguage"] = input_data.subtitle_language + twitter_options["subTitleName"] = input_data.subtitle_name + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.TWITTER], + media_urls=input_data.media_urls, + is_video=input_data.is_video, + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + twitter_options=twitter_options if twitter_options else None, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/ayrshare/post_to_youtube.py b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_youtube.py new file mode 100644 index 000000000000..fbcc9afce678 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/ayrshare/post_to_youtube.py @@ -0,0 +1,310 @@ +from enum import Enum +from typing import Any + +from backend.integrations.ayrshare import PostIds, PostResponse, SocialPlatform +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + BlockType, + SchemaField, +) + +from ._util import BaseAyrshareInput, create_ayrshare_client, get_profile_key + + +class YouTubeVisibility(str, Enum): + PRIVATE = "private" + PUBLIC = "public" + UNLISTED = "unlisted" + + +class PostToYouTubeBlock(Block): + """Block for posting to YouTube with YouTube-specific options.""" + + class Input(BaseAyrshareInput): + """Input schema for YouTube posts.""" + + # Override post field to include YouTube-specific information + post: str = SchemaField( + description="Video description (max 5,000 chars, empty string allowed). Cannot contain < or > characters.", + advanced=False, + ) + + # Override media_urls to include YouTube-specific constraints + media_urls: list[str] = SchemaField( + description="Required video URL. YouTube only supports 1 video per post.", + default_factory=list, + advanced=False, + ) + + # YouTube-specific required options + title: str = SchemaField( + description="Video title (max 100 chars, required). Cannot contain < or > characters.", + advanced=False, + ) + + # YouTube-specific optional options + visibility: YouTubeVisibility = SchemaField( + description="Video visibility: 'private' (default), 'public' , or 'unlisted'", + default=YouTubeVisibility.PRIVATE, + advanced=False, + ) + thumbnail: str | None = SchemaField( + description="Thumbnail URL (JPEG/PNG under 2MB, must end in .png/.jpg/.jpeg). Requires phone verification.", + default=None, + advanced=True, + ) + playlist_id: str | None = SchemaField( + description="Playlist ID to add video (user must own playlist)", + default=None, + advanced=True, + ) + tags: list[str] | None = SchemaField( + description="Video tags (min 2 chars each, max 500 chars total)", + default=None, + advanced=True, + ) + made_for_kids: bool | None = SchemaField( + description="Self-declared kids content", default=None, advanced=True + ) + is_shorts: bool | None = SchemaField( + description="Post as YouTube Short (max 3 minutes, adds #shorts)", + default=None, + advanced=True, + ) + notify_subscribers: bool | None = SchemaField( + description="Send notification to subscribers", default=None, advanced=True + ) + category_id: int | None = SchemaField( + description="Video category ID (e.g., 24 = Entertainment)", + default=None, + advanced=True, + ) + contains_synthetic_media: bool | None = SchemaField( + description="Disclose realistic AI/synthetic content", + default=None, + advanced=True, + ) + publish_at: str | None = SchemaField( + description="UTC publish time (YouTube controlled, format: 2022-10-08T21:18:36Z)", + default=None, + advanced=True, + ) + # YouTube targeting options (flattened from YouTubeTargeting object) + targeting_block_countries: list[str] | None = SchemaField( + description="Country codes to block from viewing (e.g., ['US', 'CA'])", + default=None, + advanced=True, + ) + targeting_allow_countries: list[str] | None = SchemaField( + description="Country codes to allow viewing (e.g., ['GB', 'AU'])", + default=None, + advanced=True, + ) + subtitle_url: str | None = SchemaField( + description="URL to SRT or SBV subtitle file (must be HTTPS and end in .srt/.sbv, under 100MB)", + default=None, + advanced=True, + ) + subtitle_language: str | None = SchemaField( + description="Language code for subtitles (default: 'en')", + default=None, + advanced=True, + ) + subtitle_name: str | None = SchemaField( + description="Name of caption track (max 150 chars, default: 'English')", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + post_result: PostResponse = SchemaField(description="The result of the post") + post: PostIds = SchemaField(description="The result of the post") + + def __init__(self): + super().__init__( + id="0082d712-ff1b-4c3d-8a8d-6c7721883b83", + description="Post to YouTube using Ayrshare", + categories={BlockCategory.SOCIAL}, + block_type=BlockType.AYRSHARE, + input_schema=PostToYouTubeBlock.Input, + output_schema=PostToYouTubeBlock.Output, + ) + + async def run( + self, + input_data: "PostToYouTubeBlock.Input", + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + """Post to YouTube with YouTube-specific validation and options.""" + + profile_key = await get_profile_key(user_id) + if not profile_key: + yield "error", "Please link a social account via Ayrshare" + return + + client = create_ayrshare_client() + if not client: + yield "error", "Ayrshare integration is not configured. Please set up the AYRSHARE_API_KEY." + return + + # Validate YouTube constraints + if not input_data.title: + yield "error", "YouTube requires a video title" + return + + if len(input_data.title) > 100: + yield "error", f"YouTube title exceeds 100 character limit ({len(input_data.title)} characters)" + return + + if len(input_data.post) > 5000: + yield "error", f"YouTube description exceeds 5,000 character limit ({len(input_data.post)} characters)" + return + + # Check for forbidden characters + forbidden_chars = ["<", ">"] + for char in forbidden_chars: + if char in input_data.title: + yield "error", f"YouTube title cannot contain '{char}' character" + return + if char in input_data.post: + yield "error", f"YouTube description cannot contain '{char}' character" + return + + if not input_data.media_urls: + yield "error", "YouTube requires exactly one video URL" + return + + if len(input_data.media_urls) > 1: + yield "error", "YouTube supports only 1 video per post" + return + + # Validate visibility option + valid_visibility = ["private", "public", "unlisted"] + if input_data.visibility not in valid_visibility: + yield "error", f"YouTube visibility must be one of: {', '.join(valid_visibility)}" + return + + # Validate thumbnail URL format + if input_data.thumbnail: + valid_extensions = [".png", ".jpg", ".jpeg"] + if not any( + input_data.thumbnail.lower().endswith(ext) for ext in valid_extensions + ): + yield "error", "YouTube thumbnail must end in .png, .jpg, or .jpeg" + return + + # Validate tags + if input_data.tags: + total_tag_length = sum(len(tag) for tag in input_data.tags) + if total_tag_length > 500: + yield "error", f"YouTube tags total length exceeds 500 characters ({total_tag_length} characters)" + return + + for tag in input_data.tags: + if len(tag) < 2: + yield "error", f"YouTube tag '{tag}' is too short (minimum 2 characters)" + return + + # Validate subtitle URL + if input_data.subtitle_url: + if not input_data.subtitle_url.startswith("https://"): + yield "error", "YouTube subtitle URL must start with https://" + return + + valid_subtitle_extensions = [".srt", ".sbv"] + if not any( + input_data.subtitle_url.lower().endswith(ext) + for ext in valid_subtitle_extensions + ): + yield "error", "YouTube subtitle URL must end in .srt or .sbv" + return + + if input_data.subtitle_name and len(input_data.subtitle_name) > 150: + yield "error", f"YouTube subtitle name exceeds 150 character limit ({len(input_data.subtitle_name)} characters)" + return + + # Validate publish_at format if provided + if input_data.publish_at and input_data.schedule_date: + yield "error", "Cannot use both 'publish_at' and 'schedule_date'. Use 'publish_at' for YouTube-controlled publishing." + return + + # Convert datetime to ISO format if provided (only if not using publish_at) + iso_date = None + if not input_data.publish_at and input_data.schedule_date: + iso_date = input_data.schedule_date.isoformat() + + # Build YouTube-specific options + youtube_options: dict[str, Any] = {"title": input_data.title} + + # Basic options + if input_data.visibility != "private": + youtube_options["visibility"] = input_data.visibility + + if input_data.thumbnail: + youtube_options["thumbNail"] = input_data.thumbnail + + if input_data.playlist_id: + youtube_options["playListId"] = input_data.playlist_id + + if input_data.tags: + youtube_options["tags"] = input_data.tags + + if input_data.made_for_kids: + youtube_options["madeForKids"] = True + + if input_data.is_shorts: + youtube_options["shorts"] = True + + if not input_data.notify_subscribers: + youtube_options["notifySubscribers"] = False + + if input_data.category_id and input_data.category_id > 0: + youtube_options["categoryId"] = input_data.category_id + + if input_data.contains_synthetic_media: + youtube_options["containsSyntheticMedia"] = True + + if input_data.publish_at: + youtube_options["publishAt"] = input_data.publish_at + + # Country targeting (from flattened fields) + targeting_dict = {} + if input_data.targeting_block_countries: + targeting_dict["block"] = input_data.targeting_block_countries + if input_data.targeting_allow_countries: + targeting_dict["allow"] = input_data.targeting_allow_countries + + if targeting_dict: + youtube_options["targeting"] = targeting_dict + + # Subtitle options + if input_data.subtitle_url: + youtube_options["subTitleUrl"] = input_data.subtitle_url + youtube_options["subTitleLanguage"] = input_data.subtitle_language + youtube_options["subTitleName"] = input_data.subtitle_name + + response = await client.create_post( + post=input_data.post, + platforms=[SocialPlatform.YOUTUBE], + media_urls=input_data.media_urls, + is_video=True, # YouTube only supports videos + schedule_date=iso_date, + disable_comments=input_data.disable_comments, + shorten_links=input_data.shorten_links, + unsplash=input_data.unsplash, + requires_approval=input_data.requires_approval, + random_post=input_data.random_post, + random_media_url=input_data.random_media_url, + notes=input_data.notes, + youtube_options=youtube_options, + profile_key=profile_key.get_secret_value(), + ) + yield "post_result", response + if response.postIds: + for p in response.postIds: + yield "post", p diff --git a/autogpt_platform/backend/backend/blocks/baas/__init__.py b/autogpt_platform/backend/backend/blocks/baas/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/baas/_api.py b/autogpt_platform/backend/backend/blocks/baas/_api.py new file mode 100644 index 000000000000..6a53653aa3af --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/baas/_api.py @@ -0,0 +1,205 @@ +""" +Meeting BaaS API client module. +All API calls centralized for consistency and maintainability. +""" + +from typing import Any, Dict, List, Optional + +from backend.sdk import Requests + + +class MeetingBaasAPI: + """Client for Meeting BaaS API endpoints.""" + + BASE_URL = "https://api.meetingbaas.com" + + def __init__(self, api_key: str): + """Initialize API client with authentication key.""" + self.api_key = api_key + self.headers = {"x-meeting-baas-api-key": api_key} + self.requests = Requests() + + # Bot Management Endpoints + + async def join_meeting( + self, + bot_name: str, + meeting_url: str, + reserved: bool = False, + bot_image: Optional[str] = None, + entry_message: Optional[str] = None, + start_time: Optional[int] = None, + speech_to_text: Optional[Dict[str, Any]] = None, + webhook_url: Optional[str] = None, + automatic_leave: Optional[Dict[str, Any]] = None, + extra: Optional[Dict[str, Any]] = None, + recording_mode: str = "speaker_view", + streaming: Optional[Dict[str, Any]] = None, + deduplication_key: Optional[str] = None, + zoom_sdk_id: Optional[str] = None, + zoom_sdk_pwd: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Deploy a bot to join and record a meeting. + + POST /bots + """ + body = { + "bot_name": bot_name, + "meeting_url": meeting_url, + "reserved": reserved, + "recording_mode": recording_mode, + } + + # Add optional fields if provided + if bot_image is not None: + body["bot_image"] = bot_image + if entry_message is not None: + body["entry_message"] = entry_message + if start_time is not None: + body["start_time"] = start_time + if speech_to_text is not None: + body["speech_to_text"] = speech_to_text + if webhook_url is not None: + body["webhook_url"] = webhook_url + if automatic_leave is not None: + body["automatic_leave"] = automatic_leave + if extra is not None: + body["extra"] = extra + if streaming is not None: + body["streaming"] = streaming + if deduplication_key is not None: + body["deduplication_key"] = deduplication_key + if zoom_sdk_id is not None: + body["zoom_sdk_id"] = zoom_sdk_id + if zoom_sdk_pwd is not None: + body["zoom_sdk_pwd"] = zoom_sdk_pwd + + response = await self.requests.post( + f"{self.BASE_URL}/bots", + headers=self.headers, + json=body, + ) + return response.json() + + async def leave_meeting(self, bot_id: str) -> bool: + """ + Remove a bot from an ongoing meeting. + + DELETE /bots/{uuid} + """ + response = await self.requests.delete( + f"{self.BASE_URL}/bots/{bot_id}", + headers=self.headers, + ) + return response.status in [200, 204] + + async def retranscribe( + self, + bot_uuid: str, + speech_to_text: Optional[Dict[str, Any]] = None, + webhook_url: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Re-run transcription on a bot's audio. + + POST /bots/retranscribe + """ + body: Dict[str, Any] = {"bot_uuid": bot_uuid} + + if speech_to_text is not None: + body["speech_to_text"] = speech_to_text + if webhook_url is not None: + body["webhook_url"] = webhook_url + + response = await self.requests.post( + f"{self.BASE_URL}/bots/retranscribe", + headers=self.headers, + json=body, + ) + + if response.status == 202: + return {"accepted": True} + return response.json() + + # Data Retrieval Endpoints + + async def get_meeting_data( + self, bot_id: str, include_transcripts: bool = True + ) -> Dict[str, Any]: + """ + Retrieve meeting data including recording and transcripts. + + GET /bots/meeting_data + """ + params = { + "bot_id": bot_id, + "include_transcripts": str(include_transcripts).lower(), + } + + response = await self.requests.get( + f"{self.BASE_URL}/bots/meeting_data", + headers=self.headers, + params=params, + ) + return response.json() + + async def get_screenshots(self, bot_id: str) -> List[Dict[str, Any]]: + """ + Retrieve screenshots captured during a meeting. + + GET /bots/{uuid}/screenshots + """ + response = await self.requests.get( + f"{self.BASE_URL}/bots/{bot_id}/screenshots", + headers=self.headers, + ) + result = response.json() + # Ensure we return a list + if isinstance(result, list): + return result + return [] + + async def delete_data(self, bot_id: str) -> bool: + """ + Delete a bot's recorded data. + + POST /bots/{uuid}/delete_data + """ + response = await self.requests.post( + f"{self.BASE_URL}/bots/{bot_id}/delete_data", + headers=self.headers, + ) + return response.status == 200 + + async def list_bots_with_metadata( + self, + limit: Optional[int] = None, + offset: Optional[int] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = None, + filter_by: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """ + List bots with metadata including IDs, names, and meeting details. + + GET /bots/bots_with_metadata + """ + params = {} + if limit is not None: + params["limit"] = limit + if offset is not None: + params["offset"] = offset + if sort_by is not None: + params["sort_by"] = sort_by + if sort_order is not None: + params["sort_order"] = sort_order + if filter_by is not None: + params.update(filter_by) + + response = await self.requests.get( + f"{self.BASE_URL}/bots/bots_with_metadata", + headers=self.headers, + params=params, + ) + return response.json() diff --git a/autogpt_platform/backend/backend/blocks/baas/_config.py b/autogpt_platform/backend/backend/blocks/baas/_config.py new file mode 100644 index 000000000000..c6ae3e68f585 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/baas/_config.py @@ -0,0 +1,13 @@ +""" +Shared configuration for all Meeting BaaS blocks using the SDK pattern. +""" + +from backend.sdk import BlockCostType, ProviderBuilder + +# Configure the Meeting BaaS provider with API key authentication +baas = ( + ProviderBuilder("baas") + .with_api_key("MEETING_BAAS_API_KEY", "Meeting BaaS API Key") + .with_base_cost(5, BlockCostType.RUN) # Higher cost for meeting recording service + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/baas/bots.py b/autogpt_platform/backend/backend/blocks/baas/bots.py new file mode 100644 index 000000000000..da8400bb2c5e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/baas/bots.py @@ -0,0 +1,217 @@ +""" +Meeting BaaS bot (recording) blocks. +""" + +from typing import Optional + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._api import MeetingBaasAPI +from ._config import baas + + +class BaasBotJoinMeetingBlock(Block): + """ + Deploy a bot immediately or at a scheduled start_time to join and record a meeting. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = baas.credentials_field( + description="Meeting BaaS API credentials" + ) + meeting_url: str = SchemaField( + description="The URL of the meeting the bot should join" + ) + bot_name: str = SchemaField( + description="Display name for the bot in the meeting" + ) + bot_image: str = SchemaField( + description="URL to an image for the bot's avatar (16:9 ratio recommended)", + default="", + ) + entry_message: str = SchemaField( + description="Chat message the bot will post upon entry", default="" + ) + reserved: bool = SchemaField( + description="Use a reserved bot slot (joins 4 min before meeting)", + default=False, + ) + start_time: Optional[int] = SchemaField( + description="Unix timestamp (ms) when bot should join", default=None + ) + webhook_url: str | None = SchemaField( + description="URL to receive webhook events for this bot", default=None + ) + timeouts: dict = SchemaField( + description="Automatic leave timeouts configuration", default={} + ) + extra: dict = SchemaField( + description="Custom metadata to attach to the bot", default={} + ) + + class Output(BlockSchema): + bot_id: str = SchemaField(description="UUID of the deployed bot") + join_response: dict = SchemaField( + description="Full response from join operation" + ) + + def __init__(self): + super().__init__( + id="377d1a6a-a99b-46cf-9af3-1d1b12758e04", + description="Deploy a bot to join and record a meeting", + categories={BlockCategory.COMMUNICATION}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + api_key = credentials.api_key.get_secret_value() + api = MeetingBaasAPI(api_key) + + # Call API with all parameters + data = await api.join_meeting( + bot_name=input_data.bot_name, + meeting_url=input_data.meeting_url, + reserved=input_data.reserved, + bot_image=input_data.bot_image if input_data.bot_image else None, + entry_message=( + input_data.entry_message if input_data.entry_message else None + ), + start_time=input_data.start_time, + speech_to_text={"provider": "Default"}, + webhook_url=input_data.webhook_url if input_data.webhook_url else None, + automatic_leave=input_data.timeouts if input_data.timeouts else None, + extra=input_data.extra if input_data.extra else None, + ) + + yield "bot_id", data.get("bot_id", "") + yield "join_response", data + + +class BaasBotLeaveMeetingBlock(Block): + """ + Force the bot to exit the call. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = baas.credentials_field( + description="Meeting BaaS API credentials" + ) + bot_id: str = SchemaField(description="UUID of the bot to remove from meeting") + + class Output(BlockSchema): + left: bool = SchemaField(description="Whether the bot successfully left") + + def __init__(self): + super().__init__( + id="bf77d128-8b25-4280-b5c7-2d553ba7e482", + description="Remove a bot from an ongoing meeting", + categories={BlockCategory.COMMUNICATION}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + api_key = credentials.api_key.get_secret_value() + api = MeetingBaasAPI(api_key) + + # Leave meeting + left = await api.leave_meeting(input_data.bot_id) + + yield "left", left + + +class BaasBotFetchMeetingDataBlock(Block): + """ + Pull MP4 URL, transcript & metadata for a completed meeting. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = baas.credentials_field( + description="Meeting BaaS API credentials" + ) + bot_id: str = SchemaField(description="UUID of the bot whose data to fetch") + include_transcripts: bool = SchemaField( + description="Include transcript data in response", default=True + ) + + class Output(BlockSchema): + mp4_url: str = SchemaField( + description="URL to download the meeting recording (time-limited)" + ) + transcript: list = SchemaField(description="Meeting transcript data") + metadata: dict = SchemaField(description="Meeting metadata and bot information") + + def __init__(self): + super().__init__( + id="ea7c1309-303c-4da1-893f-89c0e9d64e78", + description="Retrieve recorded meeting data", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + api_key = credentials.api_key.get_secret_value() + api = MeetingBaasAPI(api_key) + + # Fetch meeting data + data = await api.get_meeting_data( + bot_id=input_data.bot_id, + include_transcripts=input_data.include_transcripts, + ) + + yield "mp4_url", data.get("mp4", "") + yield "transcript", data.get("bot_data", {}).get("transcripts", []) + yield "metadata", data.get("bot_data", {}).get("bot", {}) + + +class BaasBotDeleteRecordingBlock(Block): + """ + Purge MP4 + transcript data for privacy or storage management. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = baas.credentials_field( + description="Meeting BaaS API credentials" + ) + bot_id: str = SchemaField(description="UUID of the bot whose data to delete") + + class Output(BlockSchema): + deleted: bool = SchemaField( + description="Whether the data was successfully deleted" + ) + + def __init__(self): + super().__init__( + id="bf8d1aa6-42d8-4944-b6bd-6bac554c0d3b", + description="Permanently delete a meeting's recorded data", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + api_key = credentials.api_key.get_secret_value() + api = MeetingBaasAPI(api_key) + + # Delete recording data + deleted = await api.delete_data(input_data.bot_id) + + yield "deleted", deleted diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 1ac6abfc8caa..ef251489c742 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -188,3 +188,31 @@ async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "value", converted_value except Exception as e: yield "error", f"Failed to convert value: {str(e)}" + + +class ReverseListOrderBlock(Block): + """ + A block which takes in a list and returns it in the opposite order. + """ + + class Input(BlockSchema): + input_list: list[Any] = SchemaField(description="The list to reverse") + + class Output(BlockSchema): + reversed_list: list[Any] = SchemaField(description="The list in reversed order") + + def __init__(self): + super().__init__( + id="422cb708-3109-4277-bfe3-bc2ae5812777", + description="Reverses the order of elements in a list", + categories={BlockCategory.BASIC}, + input_schema=ReverseListOrderBlock.Input, + output_schema=ReverseListOrderBlock.Output, + test_input={"input_list": [1, 2, 3, 4, 5]}, + test_output=[("reversed_list", [5, 4, 3, 2, 1])], + ) + + async def run(self, input_data: Input, **kwargs) -> BlockOutput: + reversed_list = list(input_data.input_list) + reversed_list.reverse() + yield "reversed_list", reversed_list diff --git a/autogpt_platform/backend/backend/blocks/dataforseo/__init__.py b/autogpt_platform/backend/backend/blocks/dataforseo/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/dataforseo/_api.py b/autogpt_platform/backend/backend/blocks/dataforseo/_api.py new file mode 100644 index 000000000000..025b322e4810 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/dataforseo/_api.py @@ -0,0 +1,178 @@ +""" +DataForSEO API client with async support using the SDK patterns. +""" + +import base64 +from typing import Any, Dict, List, Optional + +from backend.sdk import Requests, UserPasswordCredentials + + +class DataForSeoClient: + """Client for the DataForSEO API using async requests.""" + + API_URL = "https://api.dataforseo.com" + + def __init__(self, credentials: UserPasswordCredentials): + self.credentials = credentials + self.requests = Requests( + trusted_origins=["https://api.dataforseo.com"], + raise_for_status=False, + ) + + def _get_headers(self) -> Dict[str, str]: + """Generate the authorization header using Basic Auth.""" + username = self.credentials.username.get_secret_value() + password = self.credentials.password.get_secret_value() + credentials_str = f"{username}:{password}" + encoded = base64.b64encode(credentials_str.encode("ascii")).decode("ascii") + return { + "Authorization": f"Basic {encoded}", + "Content-Type": "application/json", + } + + async def keyword_suggestions( + self, + keyword: str, + location_code: Optional[int] = None, + language_code: Optional[str] = None, + include_seed_keyword: bool = True, + include_serp_info: bool = False, + include_clickstream_data: bool = False, + limit: int = 100, + ) -> List[Dict[str, Any]]: + """ + Get keyword suggestions from DataForSEO Labs. + + Args: + keyword: Seed keyword + location_code: Location code for targeting + language_code: Language code (e.g., "en") + include_seed_keyword: Include seed keyword in results + include_serp_info: Include SERP data + include_clickstream_data: Include clickstream metrics + limit: Maximum number of results (up to 3000) + + Returns: + API response with keyword suggestions + """ + endpoint = f"{self.API_URL}/v3/dataforseo_labs/google/keyword_suggestions/live" + + # Build payload only with non-None values to avoid sending null fields + task_data: dict[str, Any] = { + "keyword": keyword, + } + + if location_code is not None: + task_data["location_code"] = location_code + if language_code is not None: + task_data["language_code"] = language_code + if include_seed_keyword is not None: + task_data["include_seed_keyword"] = include_seed_keyword + if include_serp_info is not None: + task_data["include_serp_info"] = include_serp_info + if include_clickstream_data is not None: + task_data["include_clickstream_data"] = include_clickstream_data + if limit is not None: + task_data["limit"] = limit + + payload = [task_data] + + response = await self.requests.post( + endpoint, + headers=self._get_headers(), + json=payload, + ) + + data = response.json() + + # Check for API errors + if response.status != 200: + error_message = data.get("status_message", "Unknown error") + raise Exception( + f"DataForSEO API error ({response.status}): {error_message}" + ) + + # Extract the results from the response + if data.get("tasks") and len(data["tasks"]) > 0: + task = data["tasks"][0] + if task.get("status_code") == 20000: # Success code + return task.get("result", []) + else: + error_msg = task.get("status_message", "Task failed") + raise Exception(f"DataForSEO task error: {error_msg}") + + return [] + + async def related_keywords( + self, + keyword: str, + location_code: Optional[int] = None, + language_code: Optional[str] = None, + include_seed_keyword: bool = True, + include_serp_info: bool = False, + include_clickstream_data: bool = False, + limit: int = 100, + ) -> List[Dict[str, Any]]: + """ + Get related keywords from DataForSEO Labs. + + Args: + keyword: Seed keyword + location_code: Location code for targeting + language_code: Language code (e.g., "en") + include_seed_keyword: Include seed keyword in results + include_serp_info: Include SERP data + include_clickstream_data: Include clickstream metrics + limit: Maximum number of results (up to 3000) + + Returns: + API response with related keywords + """ + endpoint = f"{self.API_URL}/v3/dataforseo_labs/google/related_keywords/live" + + # Build payload only with non-None values to avoid sending null fields + task_data: dict[str, Any] = { + "keyword": keyword, + } + + if location_code is not None: + task_data["location_code"] = location_code + if language_code is not None: + task_data["language_code"] = language_code + if include_seed_keyword is not None: + task_data["include_seed_keyword"] = include_seed_keyword + if include_serp_info is not None: + task_data["include_serp_info"] = include_serp_info + if include_clickstream_data is not None: + task_data["include_clickstream_data"] = include_clickstream_data + if limit is not None: + task_data["limit"] = limit + + payload = [task_data] + + response = await self.requests.post( + endpoint, + headers=self._get_headers(), + json=payload, + ) + + data = response.json() + + # Check for API errors + if response.status != 200: + error_message = data.get("status_message", "Unknown error") + raise Exception( + f"DataForSEO API error ({response.status}): {error_message}" + ) + + # Extract the results from the response + if data.get("tasks") and len(data["tasks"]) > 0: + task = data["tasks"][0] + if task.get("status_code") == 20000: # Success code + return task.get("result", []) + else: + error_msg = task.get("status_message", "Task failed") + raise Exception(f"DataForSEO task error: {error_msg}") + + return [] diff --git a/autogpt_platform/backend/backend/blocks/dataforseo/_config.py b/autogpt_platform/backend/backend/blocks/dataforseo/_config.py new file mode 100644 index 000000000000..10b2b91130c2 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/dataforseo/_config.py @@ -0,0 +1,17 @@ +""" +Configuration for all DataForSEO blocks using the new SDK pattern. +""" + +from backend.sdk import BlockCostType, ProviderBuilder + +# Build the DataForSEO provider with username/password authentication +dataforseo = ( + ProviderBuilder("dataforseo") + .with_user_password( + username_env_var="DATAFORSEO_USERNAME", + password_env_var="DATAFORSEO_PASSWORD", + title="DataForSEO Credentials", + ) + .with_base_cost(1, BlockCostType.RUN) + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/dataforseo/keyword_suggestions.py b/autogpt_platform/backend/backend/blocks/dataforseo/keyword_suggestions.py new file mode 100644 index 000000000000..ac36215bc72e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/dataforseo/keyword_suggestions.py @@ -0,0 +1,273 @@ +""" +DataForSEO Google Keyword Suggestions block. +""" + +from typing import Any, Dict, List, Optional + +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, + UserPasswordCredentials, +) + +from ._api import DataForSeoClient +from ._config import dataforseo + + +class KeywordSuggestion(BlockSchema): + """Schema for a keyword suggestion result.""" + + keyword: str = SchemaField(description="The keyword suggestion") + search_volume: Optional[int] = SchemaField( + description="Monthly search volume", default=None + ) + competition: Optional[float] = SchemaField( + description="Competition level (0-1)", default=None + ) + cpc: Optional[float] = SchemaField( + description="Cost per click in USD", default=None + ) + keyword_difficulty: Optional[int] = SchemaField( + description="Keyword difficulty score", default=None + ) + serp_info: Optional[Dict[str, Any]] = SchemaField( + description="data from SERP for each keyword", default=None + ) + clickstream_data: Optional[Dict[str, Any]] = SchemaField( + description="Clickstream data metrics", default=None + ) + + +class DataForSeoKeywordSuggestionsBlock(Block): + """Block for getting keyword suggestions from DataForSEO Labs.""" + + class Input(BlockSchema): + credentials: CredentialsMetaInput = dataforseo.credentials_field( + description="DataForSEO credentials (username and password)" + ) + keyword: str = SchemaField(description="Seed keyword to get suggestions for") + location_code: Optional[int] = SchemaField( + description="Location code for targeting (e.g., 2840 for USA)", + default=2840, # USA + ) + language_code: Optional[str] = SchemaField( + description="Language code (e.g., 'en' for English)", + default="en", + ) + include_seed_keyword: bool = SchemaField( + description="Include the seed keyword in results", + default=True, + ) + include_serp_info: bool = SchemaField( + description="Include SERP information", + default=False, + ) + include_clickstream_data: bool = SchemaField( + description="Include clickstream metrics", + default=False, + ) + limit: int = SchemaField( + description="Maximum number of results (up to 3000)", + default=100, + ge=1, + le=3000, + ) + + class Output(BlockSchema): + suggestions: List[KeywordSuggestion] = SchemaField( + description="List of keyword suggestions with metrics" + ) + suggestion: KeywordSuggestion = SchemaField( + description="A single keyword suggestion with metrics" + ) + total_count: int = SchemaField( + description="Total number of suggestions returned" + ) + seed_keyword: str = SchemaField( + description="The seed keyword used for the query" + ) + + def __init__(self): + super().__init__( + id="73c3e7c4-2b3f-4e9f-9e3e-8f7a5c3e2d45", + description="Get keyword suggestions from DataForSEO Labs Google API", + categories={BlockCategory.SEARCH, BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + test_input={ + "credentials": dataforseo.get_test_credentials().model_dump(), + "keyword": "digital marketing", + "location_code": 2840, + "language_code": "en", + "limit": 1, + }, + test_credentials=dataforseo.get_test_credentials(), + test_output=[ + ( + "suggestion", + lambda x: hasattr(x, "keyword") + and x.keyword == "digital marketing strategy", + ), + ("suggestions", lambda x: isinstance(x, list) and len(x) == 1), + ("total_count", 1), + ("seed_keyword", "digital marketing"), + ], + test_mock={ + "_fetch_keyword_suggestions": lambda *args, **kwargs: [ + { + "items": [ + { + "keyword": "digital marketing strategy", + "keyword_info": { + "search_volume": 10000, + "competition": 0.5, + "cpc": 2.5, + }, + "keyword_properties": { + "keyword_difficulty": 50, + }, + } + ] + } + ] + }, + ) + + async def _fetch_keyword_suggestions( + self, + client: DataForSeoClient, + input_data: Input, + ) -> Any: + """Private method to fetch keyword suggestions - can be mocked for testing.""" + return await client.keyword_suggestions( + keyword=input_data.keyword, + location_code=input_data.location_code, + language_code=input_data.language_code, + include_seed_keyword=input_data.include_seed_keyword, + include_serp_info=input_data.include_serp_info, + include_clickstream_data=input_data.include_clickstream_data, + limit=input_data.limit, + ) + + async def run( + self, + input_data: Input, + *, + credentials: UserPasswordCredentials, + **kwargs, + ) -> BlockOutput: + """Execute the keyword suggestions query.""" + client = DataForSeoClient(credentials) + + results = await self._fetch_keyword_suggestions(client, input_data) + + # Process and format the results + suggestions = [] + if results and len(results) > 0: + # results is a list, get the first element + first_result = results[0] if isinstance(results, list) else results + items = ( + first_result.get("items", []) if isinstance(first_result, dict) else [] + ) + for item in items: + # Create the KeywordSuggestion object + suggestion = KeywordSuggestion( + keyword=item.get("keyword", ""), + search_volume=item.get("keyword_info", {}).get("search_volume"), + competition=item.get("keyword_info", {}).get("competition"), + cpc=item.get("keyword_info", {}).get("cpc"), + keyword_difficulty=item.get("keyword_properties", {}).get( + "keyword_difficulty" + ), + serp_info=( + item.get("serp_info") if input_data.include_serp_info else None + ), + clickstream_data=( + item.get("clickstream_keyword_info") + if input_data.include_clickstream_data + else None + ), + ) + yield "suggestion", suggestion + suggestions.append(suggestion) + + yield "suggestions", suggestions + yield "total_count", len(suggestions) + yield "seed_keyword", input_data.keyword + + +class KeywordSuggestionExtractorBlock(Block): + """Extracts individual fields from a KeywordSuggestion object.""" + + class Input(BlockSchema): + suggestion: KeywordSuggestion = SchemaField( + description="The keyword suggestion object to extract fields from" + ) + + class Output(BlockSchema): + keyword: str = SchemaField(description="The keyword suggestion") + search_volume: Optional[int] = SchemaField( + description="Monthly search volume", default=None + ) + competition: Optional[float] = SchemaField( + description="Competition level (0-1)", default=None + ) + cpc: Optional[float] = SchemaField( + description="Cost per click in USD", default=None + ) + keyword_difficulty: Optional[int] = SchemaField( + description="Keyword difficulty score", default=None + ) + serp_info: Optional[Dict[str, Any]] = SchemaField( + description="data from SERP for each keyword", default=None + ) + clickstream_data: Optional[Dict[str, Any]] = SchemaField( + description="Clickstream data metrics", default=None + ) + + def __init__(self): + super().__init__( + id="4193cb94-677c-48b0-9eec-6ac72fffd0f2", + description="Extract individual fields from a KeywordSuggestion object", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + test_input={ + "suggestion": KeywordSuggestion( + keyword="test keyword", + search_volume=1000, + competition=0.5, + cpc=2.5, + keyword_difficulty=60, + ).model_dump() + }, + test_output=[ + ("keyword", "test keyword"), + ("search_volume", 1000), + ("competition", 0.5), + ("cpc", 2.5), + ("keyword_difficulty", 60), + ("serp_info", None), + ("clickstream_data", None), + ], + ) + + async def run( + self, + input_data: Input, + **kwargs, + ) -> BlockOutput: + """Extract fields from the KeywordSuggestion object.""" + suggestion = input_data.suggestion + + yield "keyword", suggestion.keyword + yield "search_volume", suggestion.search_volume + yield "competition", suggestion.competition + yield "cpc", suggestion.cpc + yield "keyword_difficulty", suggestion.keyword_difficulty + yield "serp_info", suggestion.serp_info + yield "clickstream_data", suggestion.clickstream_data diff --git a/autogpt_platform/backend/backend/blocks/dataforseo/related_keywords.py b/autogpt_platform/backend/backend/blocks/dataforseo/related_keywords.py new file mode 100644 index 000000000000..ae0ecf93e386 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/dataforseo/related_keywords.py @@ -0,0 +1,283 @@ +""" +DataForSEO Google Related Keywords block. +""" + +from typing import Any, Dict, List, Optional + +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, + UserPasswordCredentials, +) + +from ._api import DataForSeoClient +from ._config import dataforseo + + +class RelatedKeyword(BlockSchema): + """Schema for a related keyword result.""" + + keyword: str = SchemaField(description="The related keyword") + search_volume: Optional[int] = SchemaField( + description="Monthly search volume", default=None + ) + competition: Optional[float] = SchemaField( + description="Competition level (0-1)", default=None + ) + cpc: Optional[float] = SchemaField( + description="Cost per click in USD", default=None + ) + keyword_difficulty: Optional[int] = SchemaField( + description="Keyword difficulty score", default=None + ) + serp_info: Optional[Dict[str, Any]] = SchemaField( + description="SERP data for the keyword", default=None + ) + clickstream_data: Optional[Dict[str, Any]] = SchemaField( + description="Clickstream data metrics", default=None + ) + + +class DataForSeoRelatedKeywordsBlock(Block): + """Block for getting related keywords from DataForSEO Labs.""" + + class Input(BlockSchema): + credentials: CredentialsMetaInput = dataforseo.credentials_field( + description="DataForSEO credentials (username and password)" + ) + keyword: str = SchemaField( + description="Seed keyword to find related keywords for" + ) + location_code: Optional[int] = SchemaField( + description="Location code for targeting (e.g., 2840 for USA)", + default=2840, # USA + ) + language_code: Optional[str] = SchemaField( + description="Language code (e.g., 'en' for English)", + default="en", + ) + include_seed_keyword: bool = SchemaField( + description="Include the seed keyword in results", + default=True, + ) + include_serp_info: bool = SchemaField( + description="Include SERP information", + default=False, + ) + include_clickstream_data: bool = SchemaField( + description="Include clickstream metrics", + default=False, + ) + limit: int = SchemaField( + description="Maximum number of results (up to 3000)", + default=100, + ge=1, + le=3000, + ) + + class Output(BlockSchema): + related_keywords: List[RelatedKeyword] = SchemaField( + description="List of related keywords with metrics" + ) + related_keyword: RelatedKeyword = SchemaField( + description="A related keyword with metrics" + ) + total_count: int = SchemaField( + description="Total number of related keywords returned" + ) + seed_keyword: str = SchemaField( + description="The seed keyword used for the query" + ) + + def __init__(self): + super().__init__( + id="8f2e4d6a-1b3c-4a5e-9d7f-2c8e6a4b3f1d", + description="Get related keywords from DataForSEO Labs Google API", + categories={BlockCategory.SEARCH, BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + test_input={ + "credentials": dataforseo.get_test_credentials().model_dump(), + "keyword": "content marketing", + "location_code": 2840, + "language_code": "en", + "limit": 1, + }, + test_credentials=dataforseo.get_test_credentials(), + test_output=[ + ( + "related_keyword", + lambda x: hasattr(x, "keyword") and x.keyword == "content strategy", + ), + ("related_keywords", lambda x: isinstance(x, list) and len(x) == 1), + ("total_count", 1), + ("seed_keyword", "content marketing"), + ], + test_mock={ + "_fetch_related_keywords": lambda *args, **kwargs: [ + { + "items": [ + { + "keyword_data": { + "keyword": "content strategy", + "keyword_info": { + "search_volume": 8000, + "competition": 0.4, + "cpc": 3.0, + }, + "keyword_properties": { + "keyword_difficulty": 45, + }, + } + } + ] + } + ] + }, + ) + + async def _fetch_related_keywords( + self, + client: DataForSeoClient, + input_data: Input, + ) -> Any: + """Private method to fetch related keywords - can be mocked for testing.""" + return await client.related_keywords( + keyword=input_data.keyword, + location_code=input_data.location_code, + language_code=input_data.language_code, + include_seed_keyword=input_data.include_seed_keyword, + include_serp_info=input_data.include_serp_info, + include_clickstream_data=input_data.include_clickstream_data, + limit=input_data.limit, + ) + + async def run( + self, + input_data: Input, + *, + credentials: UserPasswordCredentials, + **kwargs, + ) -> BlockOutput: + """Execute the related keywords query.""" + client = DataForSeoClient(credentials) + + results = await self._fetch_related_keywords(client, input_data) + + # Process and format the results + related_keywords = [] + if results and len(results) > 0: + # results is a list, get the first element + first_result = results[0] if isinstance(results, list) else results + items = ( + first_result.get("items", []) if isinstance(first_result, dict) else [] + ) + for item in items: + # Extract keyword_data from the item + keyword_data = item.get("keyword_data", {}) + + # Create the RelatedKeyword object + keyword = RelatedKeyword( + keyword=keyword_data.get("keyword", ""), + search_volume=keyword_data.get("keyword_info", {}).get( + "search_volume" + ), + competition=keyword_data.get("keyword_info", {}).get("competition"), + cpc=keyword_data.get("keyword_info", {}).get("cpc"), + keyword_difficulty=keyword_data.get("keyword_properties", {}).get( + "keyword_difficulty" + ), + serp_info=( + keyword_data.get("serp_info") + if input_data.include_serp_info + else None + ), + clickstream_data=( + keyword_data.get("clickstream_keyword_info") + if input_data.include_clickstream_data + else None + ), + ) + yield "related_keyword", keyword + related_keywords.append(keyword) + + yield "related_keywords", related_keywords + yield "total_count", len(related_keywords) + yield "seed_keyword", input_data.keyword + + +class RelatedKeywordExtractorBlock(Block): + """Extracts individual fields from a RelatedKeyword object.""" + + class Input(BlockSchema): + related_keyword: RelatedKeyword = SchemaField( + description="The related keyword object to extract fields from" + ) + + class Output(BlockSchema): + keyword: str = SchemaField(description="The related keyword") + search_volume: Optional[int] = SchemaField( + description="Monthly search volume", default=None + ) + competition: Optional[float] = SchemaField( + description="Competition level (0-1)", default=None + ) + cpc: Optional[float] = SchemaField( + description="Cost per click in USD", default=None + ) + keyword_difficulty: Optional[int] = SchemaField( + description="Keyword difficulty score", default=None + ) + serp_info: Optional[Dict[str, Any]] = SchemaField( + description="SERP data for the keyword", default=None + ) + clickstream_data: Optional[Dict[str, Any]] = SchemaField( + description="Clickstream data metrics", default=None + ) + + def __init__(self): + super().__init__( + id="98342061-09d2-4952-bf77-0761fc8cc9a8", + description="Extract individual fields from a RelatedKeyword object", + categories={BlockCategory.DATA}, + input_schema=self.Input, + output_schema=self.Output, + test_input={ + "related_keyword": RelatedKeyword( + keyword="test related keyword", + search_volume=800, + competition=0.4, + cpc=3.0, + keyword_difficulty=55, + ).model_dump() + }, + test_output=[ + ("keyword", "test related keyword"), + ("search_volume", 800), + ("competition", 0.4), + ("cpc", 3.0), + ("keyword_difficulty", 55), + ("serp_info", None), + ("clickstream_data", None), + ], + ) + + async def run( + self, + input_data: Input, + **kwargs, + ) -> BlockOutput: + """Extract fields from the RelatedKeyword object.""" + related_keyword = input_data.related_keyword + + yield "keyword", related_keyword.keyword + yield "search_volume", related_keyword.search_volume + yield "competition", related_keyword.competition + yield "cpc", related_keyword.cpc + yield "keyword_difficulty", related_keyword.keyword_difficulty + yield "serp_info", related_keyword.serp_info + yield "clickstream_data", related_keyword.clickstream_data diff --git a/autogpt_platform/backend/backend/blocks/discord.py b/autogpt_platform/backend/backend/blocks/discord.py deleted file mode 100644 index 91aba5f4144e..000000000000 --- a/autogpt_platform/backend/backend/blocks/discord.py +++ /dev/null @@ -1,237 +0,0 @@ -from typing import Literal - -import aiohttp -import discord -from pydantic import SecretStr - -from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema -from backend.data.model import ( - APIKeyCredentials, - CredentialsField, - CredentialsMetaInput, - SchemaField, -) -from backend.integrations.providers import ProviderName - -DiscordCredentials = CredentialsMetaInput[ - Literal[ProviderName.DISCORD], Literal["api_key"] -] - - -def DiscordCredentialsField() -> DiscordCredentials: - return CredentialsField(description="Discord bot token") - - -TEST_CREDENTIALS = APIKeyCredentials( - id="01234567-89ab-cdef-0123-456789abcdef", - provider="discord", - api_key=SecretStr("test_api_key"), - title="Mock Discord API key", - expires_at=None, -) -TEST_CREDENTIALS_INPUT = { - "provider": TEST_CREDENTIALS.provider, - "id": TEST_CREDENTIALS.id, - "type": TEST_CREDENTIALS.type, - "title": TEST_CREDENTIALS.type, -} - - -class ReadDiscordMessagesBlock(Block): - class Input(BlockSchema): - credentials: DiscordCredentials = DiscordCredentialsField() - - class Output(BlockSchema): - message_content: str = SchemaField( - description="The content of the message received" - ) - channel_name: str = SchemaField( - description="The name of the channel the message was received from" - ) - username: str = SchemaField( - description="The username of the user who sent the message" - ) - - def __init__(self): - super().__init__( - id="df06086a-d5ac-4abb-9996-2ad0acb2eff7", - input_schema=ReadDiscordMessagesBlock.Input, # Assign input schema - output_schema=ReadDiscordMessagesBlock.Output, # Assign output schema - description="Reads messages from a Discord channel using a bot token.", - categories={BlockCategory.SOCIAL}, - test_input={ - "continuous_read": False, - "credentials": TEST_CREDENTIALS_INPUT, - }, - test_credentials=TEST_CREDENTIALS, - test_output=[ - ( - "message_content", - "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.", - ), - ("channel_name", "general"), - ("username", "test_user"), - ], - test_mock={ - "run_bot": lambda token: { - "output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.", - "channel_name": "general", - "username": "test_user", - } - }, - ) - - async def run_bot(self, token: SecretStr): - intents = discord.Intents.default() - intents.message_content = True - - client = discord.Client(intents=intents) - - self.output_data = None - self.channel_name = None - self.username = None - - @client.event - async def on_ready(): - print(f"Logged in as {client.user}") - - @client.event - async def on_message(message): - if message.author == client.user: - return - - self.output_data = message.content - self.channel_name = message.channel.name - self.username = message.author.name - - if message.attachments: - attachment = message.attachments[0] # Process the first attachment - if attachment.filename.endswith((".txt", ".py")): - async with aiohttp.ClientSession() as session: - async with session.get(attachment.url) as response: - file_content = response.text() - self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}" - - await client.close() - - await client.start(token.get_secret_value()) - - async def run( - self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs - ) -> BlockOutput: - async for output_name, output_value in self.__run(input_data, credentials): - yield output_name, output_value - - async def __run( - self, input_data: Input, credentials: APIKeyCredentials - ) -> BlockOutput: - try: - result = await self.run_bot(credentials.api_key) - - # For testing purposes, use the mocked result - if isinstance(result, dict): - self.output_data = result.get("output_data") - self.channel_name = result.get("channel_name") - self.username = result.get("username") - - if ( - self.output_data is None - or self.channel_name is None - or self.username is None - ): - raise ValueError("No message, channel name, or username received.") - - yield "message_content", self.output_data - yield "channel_name", self.channel_name - yield "username", self.username - - except discord.errors.LoginFailure as login_err: - raise ValueError(f"Login error occurred: {login_err}") - except Exception as e: - raise ValueError(f"An error occurred: {e}") - - -class SendDiscordMessageBlock(Block): - class Input(BlockSchema): - credentials: DiscordCredentials = DiscordCredentialsField() - message_content: str = SchemaField( - description="The content of the message received" - ) - channel_name: str = SchemaField( - description="The name of the channel the message was received from" - ) - - class Output(BlockSchema): - status: str = SchemaField( - description="The status of the operation (e.g., 'Message sent', 'Error')" - ) - - def __init__(self): - super().__init__( - id="d0822ab5-9f8a-44a3-8971-531dd0178b6b", - input_schema=SendDiscordMessageBlock.Input, # Assign input schema - output_schema=SendDiscordMessageBlock.Output, # Assign output schema - description="Sends a message to a Discord channel using a bot token.", - categories={BlockCategory.SOCIAL}, - test_input={ - "channel_name": "general", - "message_content": "Hello, Discord!", - "credentials": TEST_CREDENTIALS_INPUT, - }, - test_output=[("status", "Message sent")], - test_mock={ - "send_message": lambda token, channel_name, message_content: "Message sent" - }, - test_credentials=TEST_CREDENTIALS, - ) - - async def send_message(self, token: str, channel_name: str, message_content: str): - intents = discord.Intents.default() - intents.guilds = True # Required for fetching guild/channel information - client = discord.Client(intents=intents) - - @client.event - async def on_ready(): - print(f"Logged in as {client.user}") - for guild in client.guilds: - for channel in guild.text_channels: - if channel.name == channel_name: - # Split message into chunks if it exceeds 2000 characters - for chunk in self.chunk_message(message_content): - await channel.send(chunk) - self.output_data = "Message sent" - await client.close() - return - - self.output_data = "Channel not found" - await client.close() - - await client.start(token) - - def chunk_message(self, message: str, limit: int = 2000) -> list: - """Splits a message into chunks not exceeding the Discord limit.""" - return [message[i : i + limit] for i in range(0, len(message), limit)] - - async def run( - self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs - ) -> BlockOutput: - try: - result = await self.send_message( - credentials.api_key.get_secret_value(), - input_data.channel_name, - input_data.message_content, - ) - - # For testing purposes, use the mocked result - if isinstance(result, str): - self.output_data = result - - if self.output_data is None: - raise ValueError("No status message received.") - - yield "status", self.output_data - - except discord.errors.LoginFailure as login_err: - raise ValueError(f"Login error occurred: {login_err}") - except Exception as e: - raise ValueError(f"An error occurred: {e}") diff --git a/autogpt_platform/backend/backend/blocks/discord/_api.py b/autogpt_platform/backend/backend/blocks/discord/_api.py new file mode 100644 index 000000000000..3d7a52a39630 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/discord/_api.py @@ -0,0 +1,117 @@ +""" +Discord API helper functions for making authenticated requests. +""" + +import logging +from typing import Optional + +from pydantic import BaseModel + +from backend.data.model import OAuth2Credentials +from backend.util.request import Requests + +logger = logging.getLogger(__name__) + + +class DiscordAPIException(Exception): + """Exception raised for Discord API errors.""" + + def __init__(self, message: str, status_code: int): + super().__init__(message) + self.status_code = status_code + + +class DiscordOAuthUser(BaseModel): + """Model for Discord OAuth user response.""" + + user_id: str + username: str + avatar_url: str + banner: Optional[str] = None + accent_color: Optional[int] = None + + +def get_api(credentials: OAuth2Credentials) -> Requests: + """ + Create a Requests instance configured for Discord API calls with OAuth2 credentials. + + Args: + credentials: The OAuth2 credentials containing the access token. + + Returns: + A configured Requests instance for Discord API calls. + """ + return Requests( + trusted_origins=[], + extra_headers={ + "Authorization": f"Bearer {credentials.access_token.get_secret_value()}", + "Content-Type": "application/json", + }, + raise_for_status=False, + ) + + +async def get_current_user(credentials: OAuth2Credentials) -> DiscordOAuthUser: + """ + Fetch the current user's information using Discord OAuth2 API. + + Reference: https://discord.com/developers/docs/resources/user#get-current-user + + Args: + credentials: The OAuth2 credentials. + + Returns: + A model containing user data with avatar URL. + + Raises: + DiscordAPIException: If the API request fails. + """ + api = get_api(credentials) + response = await api.get("https://discord.com/api/oauth2/@me") + + if not response.ok: + error_text = response.text() + raise DiscordAPIException( + f"Failed to fetch user info: {response.status} - {error_text}", + response.status, + ) + + data = response.json() + logger.info(f"Discord OAuth2 API Response: {data}") + + # The /api/oauth2/@me endpoint returns a user object nested in the response + user_info = data.get("user", {}) + logger.info(f"User info extracted: {user_info}") + + # Build avatar URL + user_id = user_info.get("id") + avatar_hash = user_info.get("avatar") + if avatar_hash: + # Custom avatar + avatar_ext = "gif" if avatar_hash.startswith("a_") else "png" + avatar_url = ( + f"https://cdn.discordapp.com/avatars/{user_id}/{avatar_hash}.{avatar_ext}" + ) + else: + # Default avatar based on discriminator or user ID + discriminator = user_info.get("discriminator", "0") + if discriminator == "0": + # New username system - use user ID for default avatar + default_avatar_index = (int(user_id) >> 22) % 6 + else: + # Legacy discriminator system + default_avatar_index = int(discriminator) % 5 + avatar_url = ( + f"https://cdn.discordapp.com/embed/avatars/{default_avatar_index}.png" + ) + + result = DiscordOAuthUser( + user_id=user_id, + username=user_info.get("username", ""), + avatar_url=avatar_url, + banner=user_info.get("banner"), + accent_color=user_info.get("accent_color"), + ) + + logger.info(f"Returning user data: {result.model_dump()}") + return result diff --git a/autogpt_platform/backend/backend/blocks/discord/_auth.py b/autogpt_platform/backend/backend/blocks/discord/_auth.py new file mode 100644 index 000000000000..e6671a630afc --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/discord/_auth.py @@ -0,0 +1,74 @@ +from typing import Literal + +from pydantic import SecretStr + +from backend.data.model import ( + APIKeyCredentials, + CredentialsField, + CredentialsMetaInput, + OAuth2Credentials, +) +from backend.integrations.providers import ProviderName +from backend.util.settings import Secrets + +secrets = Secrets() +DISCORD_OAUTH_IS_CONFIGURED = bool( + secrets.discord_client_id and secrets.discord_client_secret +) + +# Bot token credentials (existing) +DiscordBotCredentials = APIKeyCredentials +DiscordBotCredentialsInput = CredentialsMetaInput[ + Literal[ProviderName.DISCORD], Literal["api_key"] +] + +# OAuth2 credentials (new) +DiscordOAuthCredentials = OAuth2Credentials +DiscordOAuthCredentialsInput = CredentialsMetaInput[ + Literal[ProviderName.DISCORD], Literal["oauth2"] +] + + +def DiscordBotCredentialsField() -> DiscordBotCredentialsInput: + """Creates a Discord bot token credentials field.""" + return CredentialsField(description="Discord bot token") + + +def DiscordOAuthCredentialsField(scopes: list[str]) -> DiscordOAuthCredentialsInput: + """Creates a Discord OAuth2 credentials field.""" + return CredentialsField( + description="Discord OAuth2 credentials", + required_scopes=set(scopes) | {"identify"}, # Basic user info scope + ) + + +# Test credentials for bot tokens +TEST_BOT_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="discord", + api_key=SecretStr("test_api_key"), + title="Mock Discord API key", + expires_at=None, +) +TEST_BOT_CREDENTIALS_INPUT = { + "provider": TEST_BOT_CREDENTIALS.provider, + "id": TEST_BOT_CREDENTIALS.id, + "type": TEST_BOT_CREDENTIALS.type, + "title": TEST_BOT_CREDENTIALS.type, +} + +# Test credentials for OAuth2 +TEST_OAUTH_CREDENTIALS = OAuth2Credentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="discord", + access_token=SecretStr("test_access_token"), + title="Mock Discord OAuth", + scopes=["identify"], + username="testuser", +) +TEST_OAUTH_CREDENTIALS_INPUT = { + "provider": TEST_OAUTH_CREDENTIALS.provider, + "id": TEST_OAUTH_CREDENTIALS.id, + "type": TEST_OAUTH_CREDENTIALS.type, + "title": TEST_OAUTH_CREDENTIALS.type, +} diff --git a/autogpt_platform/backend/backend/blocks/discord/bot_blocks.py b/autogpt_platform/backend/backend/blocks/discord/bot_blocks.py new file mode 100644 index 000000000000..22469de2d4ea --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/discord/bot_blocks.py @@ -0,0 +1,1140 @@ +import base64 +import io +import mimetypes +from pathlib import Path +from typing import Any + +import aiohttp +import discord +from pydantic import SecretStr + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import APIKeyCredentials, SchemaField +from backend.util.file import store_media_file +from backend.util.type import MediaFileType + +from ._auth import ( + TEST_BOT_CREDENTIALS, + TEST_BOT_CREDENTIALS_INPUT, + DiscordBotCredentialsField, + DiscordBotCredentialsInput, +) + +# Keep backward compatibility alias +DiscordCredentials = DiscordBotCredentialsInput +DiscordCredentialsField = DiscordBotCredentialsField +TEST_CREDENTIALS = TEST_BOT_CREDENTIALS +TEST_CREDENTIALS_INPUT = TEST_BOT_CREDENTIALS_INPUT + + +class ReadDiscordMessagesBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + + class Output(BlockSchema): + message_content: str = SchemaField( + description="The content of the message received" + ) + message_id: str = SchemaField(description="The ID of the message") + channel_id: str = SchemaField(description="The ID of the channel") + channel_name: str = SchemaField( + description="The name of the channel the message was received from" + ) + user_id: str = SchemaField( + description="The ID of the user who sent the message" + ) + username: str = SchemaField( + description="The username of the user who sent the message" + ) + + def __init__(self): + super().__init__( + id="df06086a-d5ac-4abb-9996-2ad0acb2eff7", + input_schema=ReadDiscordMessagesBlock.Input, # Assign input schema + output_schema=ReadDiscordMessagesBlock.Output, # Assign output schema + description="Reads messages from a Discord channel using a bot token.", + categories={BlockCategory.SOCIAL}, + test_input={ + "continuous_read": False, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "message_content", + "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.", + ), + ("message_id", "123456789012345678"), + ("channel_id", "987654321098765432"), + ("channel_name", "general"), + ("user_id", "111222333444555666"), + ("username", "test_user"), + ], + test_mock={ + "run_bot": lambda token: { + "output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.", + "message_id": "123456789012345678", + "channel_id": "987654321098765432", + "channel_name": "general", + "user_id": "111222333444555666", + "username": "test_user", + } + }, + ) + + async def run_bot(self, token: SecretStr): + intents = discord.Intents.default() + intents.message_content = True + + client = discord.Client(intents=intents) + + self.output_data = None + self.message_id = None + self.channel_id = None + self.channel_name = None + self.user_id = None + self.username = None + + @client.event + async def on_ready(): + print(f"Logged in as {client.user}") + + @client.event + async def on_message(message): + if message.author == client.user: + return + + self.output_data = message.content + self.message_id = str(message.id) + self.channel_id = str(message.channel.id) + self.channel_name = message.channel.name + self.user_id = str(message.author.id) + self.username = message.author.name + + if message.attachments: + attachment = message.attachments[0] # Process the first attachment + if attachment.filename.endswith((".txt", ".py")): + async with aiohttp.ClientSession() as session: + async with session.get(attachment.url) as response: + file_content = response.text() + self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}" + + await client.close() + + await client.start(token.get_secret_value()) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + async for output_name, output_value in self.__run(input_data, credentials): + yield output_name, output_value + + async def __run( + self, input_data: Input, credentials: APIKeyCredentials + ) -> BlockOutput: + try: + result = await self.run_bot(credentials.api_key) + + # For testing purposes, use the mocked result + if isinstance(result, dict): + self.output_data = result.get("output_data") + self.message_id = result.get("message_id") + self.channel_id = result.get("channel_id") + self.channel_name = result.get("channel_name") + self.user_id = result.get("user_id") + self.username = result.get("username") + + if ( + self.output_data is None + or self.channel_name is None + or self.username is None + ): + raise ValueError("No message, channel name, or username received.") + + yield "message_content", self.output_data + yield "message_id", self.message_id + yield "channel_id", self.channel_id + yield "channel_name", self.channel_name + yield "user_id", self.user_id + yield "username", self.username + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class SendDiscordMessageBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + message_content: str = SchemaField( + description="The content of the message to send" + ) + channel_name: str = SchemaField( + description="The name of the channel the message will be sent to" + ) + server_name: str = SchemaField( + description="The name of the server where the channel is located", + advanced=True, # Optional field for server name + default="", + ) + + class Output(BlockSchema): + status: str = SchemaField( + description="The status of the operation (e.g., 'Message sent', 'Error')" + ) + message_id: str = SchemaField(description="The ID of the sent message") + channel_id: str = SchemaField( + description="The ID of the channel where the message was sent" + ) + + def __init__(self): + super().__init__( + id="d0822ab5-9f8a-44a3-8971-531dd0178b6b", + input_schema=SendDiscordMessageBlock.Input, # Assign input schema + output_schema=SendDiscordMessageBlock.Output, # Assign output schema + description="Sends a message to a Discord channel using a bot token.", + categories={BlockCategory.SOCIAL}, + test_input={ + "channel_name": "general", + "message_content": "Hello, Discord!", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("status", "Message sent"), + ("message_id", "123456789012345678"), + ("channel_id", "987654321098765432"), + ], + test_mock={ + "send_message": lambda token, channel_name, server_name, message_content: { + "status": "Message sent", + "message_id": "123456789012345678", + "channel_id": "987654321098765432", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def send_message( + self, + token: str, + channel_name: str, + server_name: str | None, + message_content: str, + ) -> dict: + intents = discord.Intents.default() + intents.guilds = True # Required for fetching guild/channel information + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + print(f"Logged in as {client.user}") + for guild in client.guilds: + if server_name and guild.name != server_name: + continue + for channel in guild.text_channels: + if channel.name == channel_name: + # Split message into chunks if it exceeds 2000 characters + chunks = self.chunk_message(message_content) + last_message = None + for chunk in chunks: + last_message = await channel.send(chunk) + result["status"] = "Message sent" + result["message_id"] = ( + str(last_message.id) if last_message else "" + ) + result["channel_id"] = str(channel.id) + await client.close() + return + + result["status"] = "Channel not found" + await client.close() + + await client.start(token) + return result + + def chunk_message(self, message: str, limit: int = 2000) -> list: + """Splits a message into chunks not exceeding the Discord limit.""" + return [message[i : i + limit] for i in range(0, len(message), limit)] + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + result = await self.send_message( + token=credentials.api_key.get_secret_value(), + channel_name=input_data.channel_name, + server_name=input_data.server_name, + message_content=input_data.message_content, + ) + + # For testing purposes, use the mocked result + if isinstance(result, str): + result = {"status": result} + + yield "status", result.get("status", "Unknown error") + if "message_id" in result: + yield "message_id", result["message_id"] + if "channel_id" in result: + yield "channel_id", result["channel_id"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class SendDiscordDMBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + user_id: str = SchemaField( + description="The Discord user ID to send the DM to (e.g., '123456789012345678')" + ) + message_content: str = SchemaField( + description="The content of the direct message to send" + ) + + class Output(BlockSchema): + status: str = SchemaField(description="The status of the operation") + message_id: str = SchemaField(description="The ID of the sent message") + + def __init__(self): + super().__init__( + id="40d71a5a-e268-4060-9ee0-38ae6f225682", + input_schema=SendDiscordDMBlock.Input, + output_schema=SendDiscordDMBlock.Output, + description="Sends a direct message to a Discord user using their user ID.", + categories={BlockCategory.SOCIAL}, + test_input={ + "user_id": "123456789012345678", + "message_content": "Hello! This is a test DM.", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("status", "DM sent successfully"), + ("message_id", "987654321098765432"), + ], + test_mock={ + "send_dm": lambda token, user_id, message_content: { + "status": "DM sent successfully", + "message_id": "987654321098765432", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def send_dm(self, token: str, user_id: str, message_content: str) -> dict: + intents = discord.Intents.default() + intents.dm_messages = True + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + try: + user = await client.fetch_user(int(user_id)) + message = await user.send(message_content) + result["status"] = "DM sent successfully" + result["message_id"] = str(message.id) + except discord.errors.Forbidden: + result["status"] = ( + "Cannot send DM - user has DMs disabled or bot is blocked" + ) + except discord.errors.NotFound: + result["status"] = f"User with ID {user_id} not found" + except ValueError: + result["status"] = f"Invalid user ID format: {user_id}" + except Exception as e: + result["status"] = f"Error sending DM: {str(e)}" + finally: + await client.close() + + await client.start(token) + return result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + result = await self.send_dm( + token=credentials.api_key.get_secret_value(), + user_id=input_data.user_id, + message_content=input_data.message_content, + ) + + yield "status", result.get("status", "Unknown error") + if "message_id" in result: + yield "message_id", result["message_id"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class SendDiscordEmbedBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + channel_identifier: str = SchemaField( + description="Channel ID or channel name to send the embed to" + ) + server_name: str = SchemaField( + description="Server name (only needed if using channel name)", + advanced=True, + default="", + ) + title: str = SchemaField(description="The title of the embed", default="") + description: str = SchemaField( + description="The main content/description of the embed", default="" + ) + color: int = SchemaField( + description="Embed color as integer (e.g., 0x00ff00 for green)", + advanced=True, + default=0x5865F2, # Discord blurple + ) + thumbnail_url: str = SchemaField( + description="URL for the thumbnail image", advanced=True, default="" + ) + image_url: str = SchemaField( + description="URL for the main embed image", advanced=True, default="" + ) + author_name: str = SchemaField( + description="Author name to display", advanced=True, default="" + ) + footer_text: str = SchemaField( + description="Footer text", advanced=True, default="" + ) + fields: list[dict[str, Any]] = SchemaField( + description="List of field dictionaries with 'name', 'value', and optional 'inline' keys", + advanced=True, + default=[], + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Operation status") + message_id: str = SchemaField(description="ID of the sent embed message") + + def __init__(self): + super().__init__( + id="c76293f4-9ae8-454d-a029-0a3f8c5bc499", + input_schema=SendDiscordEmbedBlock.Input, + output_schema=SendDiscordEmbedBlock.Output, + description="Sends a rich embed message to a Discord channel.", + categories={BlockCategory.SOCIAL}, + test_input={ + "channel_identifier": "general", + "title": "Test Embed", + "description": "This is a test embed message", + "color": 0x00FF00, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("status", "Embed sent successfully"), + ("message_id", "123456789012345678"), + ], + test_mock={ + "send_embed": lambda *args, **kwargs: { + "status": "Embed sent successfully", + "message_id": "123456789012345678", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def send_embed( + self, + token: str, + channel_identifier: str, + server_name: str | None, + embed_data: dict, + ) -> dict: + intents = discord.Intents.default() + intents.guilds = True + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + channel = None + + # Try to parse as channel ID first + try: + channel_id = int(channel_identifier) + channel = client.get_channel(channel_id) + except ValueError: + # Not an ID, treat as channel name + for guild in client.guilds: + if server_name and guild.name != server_name: + continue + for ch in guild.text_channels: + if ch.name == channel_identifier: + channel = ch + break + if channel: + break + + if not channel: + result["status"] = f"Channel not found: {channel_identifier}" + await client.close() + return + + # Build the embed + embed = discord.Embed( + title=embed_data.get("title") or None, + description=embed_data.get("description") or None, + color=embed_data.get("color", 0x5865F2), + ) + + if embed_data.get("thumbnail_url"): + embed.set_thumbnail(url=embed_data["thumbnail_url"]) + + if embed_data.get("image_url"): + embed.set_image(url=embed_data["image_url"]) + + if embed_data.get("author_name"): + embed.set_author(name=embed_data["author_name"]) + + if embed_data.get("footer_text"): + embed.set_footer(text=embed_data["footer_text"]) + + # Add fields + for field in embed_data.get("fields", []): + if isinstance(field, dict) and "name" in field and "value" in field: + embed.add_field( + name=field["name"], + value=field["value"], + inline=field.get("inline", True), + ) + + try: + # Type check - ensure it's a text channel that can send messages + if not hasattr(channel, "send"): + result["status"] = ( + f"Channel {channel_identifier} cannot receive messages (not a text channel)" + ) + await client.close() + return + + message = await channel.send(embed=embed) # type: ignore + result["status"] = "Embed sent successfully" + result["message_id"] = str(message.id) + except Exception as e: + result["status"] = f"Error sending embed: {str(e)}" + finally: + await client.close() + + await client.start(token) + return result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + embed_data = { + "title": input_data.title, + "description": input_data.description, + "color": input_data.color, + "thumbnail_url": input_data.thumbnail_url, + "image_url": input_data.image_url, + "author_name": input_data.author_name, + "footer_text": input_data.footer_text, + "fields": input_data.fields, + } + + result = await self.send_embed( + token=credentials.api_key.get_secret_value(), + channel_identifier=input_data.channel_identifier, + server_name=input_data.server_name or None, + embed_data=embed_data, + ) + + yield "status", result.get("status", "Unknown error") + if "message_id" in result: + yield "message_id", result["message_id"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class SendDiscordFileBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + channel_identifier: str = SchemaField( + description="Channel ID or channel name to send the file to" + ) + server_name: str = SchemaField( + description="Server name (only needed if using channel name)", + advanced=True, + default="", + ) + file: MediaFileType = SchemaField( + description="The file to send (URL, data URI, or local path). Supports images, videos, documents, etc." + ) + filename: str = SchemaField( + description="Name of the file when sent (e.g., 'report.pdf', 'image.png')", + default="", + ) + message_content: str = SchemaField( + description="Optional message to send with the file", default="" + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Operation status") + message_id: str = SchemaField(description="ID of the sent message") + + def __init__(self): + super().__init__( + id="b1628cf2-4622-49bf-80cf-10e55826e247", + input_schema=SendDiscordFileBlock.Input, + output_schema=SendDiscordFileBlock.Output, + description="Sends a file attachment to a Discord channel.", + categories={BlockCategory.SOCIAL}, + test_input={ + "channel_identifier": "general", + "file": "data:text/plain;base64,VGVzdCBmaWxlIGNvbnRlbnQ=", + "filename": "test.txt", + "message_content": "Here's the file!", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("status", "File sent successfully"), + ("message_id", "123456789012345678"), + ], + test_mock={ + "send_file": lambda *args, **kwargs: { + "status": "File sent successfully", + "message_id": "123456789012345678", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def send_file( + self, + token: str, + channel_identifier: str, + server_name: str | None, + file: MediaFileType, + filename: str, + message_content: str, + graph_exec_id: str, + user_id: str, + ) -> dict: + intents = discord.Intents.default() + intents.guilds = True + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + channel = None + + # Try to parse as channel ID first + try: + channel_id = int(channel_identifier) + channel = client.get_channel(channel_id) + except ValueError: + # Not an ID, treat as channel name + for guild in client.guilds: + if server_name and guild.name != server_name: + continue + for ch in guild.text_channels: + if ch.name == channel_identifier: + channel = ch + break + if channel: + break + + if not channel: + result["status"] = f"Channel not found: {channel_identifier}" + await client.close() + return + + try: + # Handle MediaFileType - could be data URI, URL, or local path + file_bytes = None + detected_filename = filename + + if file.startswith("data:"): + # Data URI - extract the base64 content + header, encoded = file.split(",", 1) + file_bytes = base64.b64decode(encoded) + + # Try to get MIME type and suggest filename if not provided + if not filename and ";" in header: + mime_match = header.split(":")[1].split(";")[0] + ext = mimetypes.guess_extension(mime_match) or ".bin" + detected_filename = f"file{ext}" + + elif file.startswith(("http://", "https://")): + # URL - download the file + async with aiohttp.ClientSession() as session: + async with session.get(file) as response: + file_bytes = await response.read() + + # Try to get filename from URL if not provided + if not filename: + from urllib.parse import urlparse + + path = urlparse(file).path + detected_filename = Path(path).name or "download" + else: + # Local file path - read from stored media file + # This would be a path from a previous block's output + stored_file = await store_media_file( + graph_exec_id=graph_exec_id, + file=file, + user_id=user_id, + return_content=True, # Get as data URI + ) + # Now process as data URI + header, encoded = stored_file.split(",", 1) + file_bytes = base64.b64decode(encoded) + + if not filename: + detected_filename = Path(file).name or "file" + + if not file_bytes: + result["status"] = "Error: Could not read file content" + await client.close() + return + + # Create Discord file object + discord_file = discord.File( + io.BytesIO(file_bytes), filename=detected_filename or "file" + ) + + # Type check - ensure it's a text channel that can send messages + if not hasattr(channel, "send"): + result["status"] = ( + f"Channel {channel_identifier} cannot receive messages (not a text channel)" + ) + await client.close() + return + + # Send the file + message = await channel.send( # type: ignore + content=message_content if message_content else None, + file=discord_file, + ) + result["status"] = "File sent successfully" + result["message_id"] = str(message.id) + except Exception as e: + result["status"] = f"Error sending file: {str(e)}" + finally: + await client.close() + + await client.start(token) + return result + + async def run( + self, + input_data: Input, + *, + credentials: APIKeyCredentials, + graph_exec_id: str, + user_id: str, + **kwargs, + ) -> BlockOutput: + try: + result = await self.send_file( + token=credentials.api_key.get_secret_value(), + channel_identifier=input_data.channel_identifier, + server_name=input_data.server_name or None, + file=input_data.file, + filename=input_data.filename, + message_content=input_data.message_content, + graph_exec_id=graph_exec_id, + user_id=user_id, + ) + + yield "status", result.get("status", "Unknown error") + if "message_id" in result: + yield "message_id", result["message_id"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class ReplyToDiscordMessageBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + channel_id: str = SchemaField( + description="The channel ID where the message to reply to is located" + ) + message_id: str = SchemaField(description="The ID of the message to reply to") + reply_content: str = SchemaField(description="The content of the reply") + mention_author: bool = SchemaField( + description="Whether to mention the original message author", default=True + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Operation status") + reply_id: str = SchemaField(description="ID of the reply message") + + def __init__(self): + super().__init__( + id="7226cb99-6e7b-4672-b6b2-acec95336eec", + input_schema=ReplyToDiscordMessageBlock.Input, + output_schema=ReplyToDiscordMessageBlock.Output, + description="Replies to a specific Discord message.", + categories={BlockCategory.SOCIAL}, + test_input={ + "channel_id": "123456789012345678", + "message_id": "987654321098765432", + "reply_content": "This is a reply!", + "mention_author": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("status", "Reply sent successfully"), + ("reply_id", "111222333444555666"), + ], + test_mock={ + "send_reply": lambda *args, **kwargs: { + "status": "Reply sent successfully", + "reply_id": "111222333444555666", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def send_reply( + self, + token: str, + channel_id: str, + message_id: str, + reply_content: str, + mention_author: bool, + ) -> dict: + intents = discord.Intents.default() + intents.guilds = True + intents.message_content = True + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + try: + channel = client.get_channel(int(channel_id)) + if not channel: + channel = await client.fetch_channel(int(channel_id)) + + if not channel: + result["status"] = f"Channel with ID {channel_id} not found" + await client.close() + return + + # Type check - ensure it's a text channel that can fetch messages + if not hasattr(channel, "fetch_message"): + result["status"] = ( + f"Channel {channel_id} cannot fetch messages (not a text channel)" + ) + await client.close() + return + + # Fetch the message to reply to + try: + message = await channel.fetch_message(int(message_id)) # type: ignore + except discord.errors.NotFound: + result["status"] = f"Message with ID {message_id} not found" + await client.close() + return + + # Send the reply + reply = await message.reply( + content=reply_content, mention_author=mention_author + ) + result["status"] = "Reply sent successfully" + result["reply_id"] = str(reply.id) + + except ValueError as e: + result["status"] = f"Invalid ID format: {str(e)}" + except Exception as e: + result["status"] = f"Error sending reply: {str(e)}" + finally: + await client.close() + + await client.start(token) + return result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + result = await self.send_reply( + token=credentials.api_key.get_secret_value(), + channel_id=input_data.channel_id, + message_id=input_data.message_id, + reply_content=input_data.reply_content, + mention_author=input_data.mention_author, + ) + + yield "status", result.get("status", "Unknown error") + if "reply_id" in result: + yield "reply_id", result["reply_id"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class DiscordUserInfoBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + user_id: str = SchemaField( + description="The Discord user ID to get information about" + ) + + class Output(BlockSchema): + user_id: str = SchemaField( + description="The user's ID (passed through for chaining)" + ) + username: str = SchemaField(description="The user's username") + display_name: str = SchemaField(description="The user's display name") + discriminator: str = SchemaField( + description="The user's discriminator (if applicable)" + ) + avatar_url: str = SchemaField(description="URL to the user's avatar") + is_bot: bool = SchemaField(description="Whether the user is a bot") + created_at: str = SchemaField(description="When the account was created") + + def __init__(self): + super().__init__( + id="9aeed32a-6ebf-49b8-a0a3-e2e509d86120", + input_schema=DiscordUserInfoBlock.Input, + output_schema=DiscordUserInfoBlock.Output, + description="Gets information about a Discord user by their ID.", + categories={BlockCategory.SOCIAL}, + test_input={ + "user_id": "123456789012345678", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("user_id", "123456789012345678"), + ("username", "testuser"), + ("display_name", "Test User"), + ("discriminator", "0"), + ( + "avatar_url", + "https://cdn.discordapp.com/avatars/123456789012345678/avatar.png", + ), + ("is_bot", False), + ("created_at", "2020-01-01T00:00:00"), + ], + test_mock={ + "get_user_info": lambda token, user_id: { + "user_id": "123456789012345678", + "username": "testuser", + "display_name": "Test User", + "discriminator": "0", + "avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/avatar.png", + "is_bot": False, + "created_at": "2020-01-01T00:00:00", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def get_user_info(self, token: str, user_id: str) -> dict: + intents = discord.Intents.default() + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + try: + user = await client.fetch_user(int(user_id)) + + result["user_id"] = str(user.id) # Pass through the user ID + result["username"] = user.name + result["display_name"] = user.display_name or user.name + result["discriminator"] = user.discriminator + result["avatar_url"] = ( + str(user.avatar.url) + if user.avatar + else str(user.default_avatar.url) + ) + result["is_bot"] = user.bot + result["created_at"] = user.created_at.isoformat() + + except discord.errors.NotFound: + result["error"] = f"User with ID {user_id} not found" + except ValueError: + result["error"] = f"Invalid user ID format: {user_id}" + except Exception as e: + result["error"] = f"Error fetching user info: {str(e)}" + finally: + await client.close() + + await client.start(token) + return result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + result = await self.get_user_info( + token=credentials.api_key.get_secret_value(), user_id=input_data.user_id + ) + + if "error" in result: + raise ValueError(result["error"]) + + yield "user_id", result["user_id"] + yield "username", result["username"] + yield "display_name", result["display_name"] + yield "discriminator", result["discriminator"] + yield "avatar_url", result["avatar_url"] + yield "is_bot", result["is_bot"] + yield "created_at", result["created_at"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") + + +class DiscordChannelInfoBlock(Block): + class Input(BlockSchema): + credentials: DiscordCredentials = DiscordCredentialsField() + channel_identifier: str = SchemaField( + description="Channel name or channel ID to look up" + ) + server_name: str = SchemaField( + description="Server name (optional, helps narrow down search)", + advanced=True, + default="", + ) + + class Output(BlockSchema): + channel_id: str = SchemaField(description="The channel's ID") + channel_name: str = SchemaField(description="The channel's name") + server_id: str = SchemaField(description="The server's ID") + server_name: str = SchemaField(description="The server's name") + channel_type: str = SchemaField( + description="Type of channel (text, voice, etc)" + ) + + def __init__(self): + super().__init__( + id="592f815e-35c3-4fed-96cd-a69966b45c8f", + input_schema=DiscordChannelInfoBlock.Input, + output_schema=DiscordChannelInfoBlock.Output, + description="Resolves Discord channel names to IDs and vice versa.", + categories={BlockCategory.SOCIAL}, + test_input={ + "channel_identifier": "general", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ("channel_id", "123456789012345678"), + ("channel_name", "general"), + ("server_id", "987654321098765432"), + ("server_name", "Test Server"), + ("channel_type", "text"), + ], + test_mock={ + "get_channel_info": lambda token, channel_identifier, server_name: { + "channel_id": "123456789012345678", + "channel_name": "general", + "server_id": "987654321098765432", + "server_name": "Test Server", + "channel_type": "text", + } + }, + test_credentials=TEST_CREDENTIALS, + ) + + async def get_channel_info( + self, token: str, channel_identifier: str, server_name: str | None + ) -> dict: + intents = discord.Intents.default() + intents.guilds = True + client = discord.Client(intents=intents) + + result = {} + + @client.event + async def on_ready(): + # Try to parse as channel ID first + channel = None + try: + channel_id = int(channel_identifier) + channel = client.get_channel(channel_id) + if channel: + result["channel_id"] = str(channel.id) + # Private channels may not have a name attribute + result["channel_name"] = getattr(channel, "name", "Private Channel") + # Check if channel has guild (not private) + if hasattr(channel, "guild"): + guild = getattr(channel, "guild", None) + if guild: + result["server_id"] = str(guild.id) + result["server_name"] = guild.name + else: + result["server_id"] = "" + result["server_name"] = "Direct Message" + else: + result["server_id"] = "" + result["server_name"] = "Direct Message" + # Get channel type safely + result["channel_type"] = str(getattr(channel, "type", "unknown")) + await client.close() + return + except ValueError: + # Not an ID, treat as channel name + for guild in client.guilds: + if server_name and guild.name != server_name: + continue + for ch in guild.channels: + if ch.name == channel_identifier: + result["channel_id"] = str(ch.id) + result["channel_name"] = ch.name + result["server_id"] = str(guild.id) + result["server_name"] = guild.name + result["channel_type"] = str(ch.type) + await client.close() + return + + result["error"] = f"Channel not found: {channel_identifier}" + await client.close() + + await client.start(token) + return result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + try: + result = await self.get_channel_info( + token=credentials.api_key.get_secret_value(), + channel_identifier=input_data.channel_identifier, + server_name=input_data.server_name or None, + ) + + if "error" in result: + raise ValueError(result["error"]) + + yield "channel_id", result["channel_id"] + yield "channel_name", result["channel_name"] + yield "server_id", result["server_id"] + yield "server_name", result["server_name"] + yield "channel_type", result["channel_type"] + + except discord.errors.LoginFailure as login_err: + raise ValueError(f"Login error occurred: {login_err}") + except Exception as e: + raise ValueError(f"An error occurred: {e}") diff --git a/autogpt_platform/backend/backend/blocks/discord/oauth_blocks.py b/autogpt_platform/backend/backend/blocks/discord/oauth_blocks.py new file mode 100644 index 000000000000..31d2df65c26f --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/discord/oauth_blocks.py @@ -0,0 +1,99 @@ +""" +Discord OAuth-based blocks. +""" + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import OAuth2Credentials, SchemaField + +from ._api import DiscordOAuthUser, get_current_user +from ._auth import ( + DISCORD_OAUTH_IS_CONFIGURED, + TEST_OAUTH_CREDENTIALS, + TEST_OAUTH_CREDENTIALS_INPUT, + DiscordOAuthCredentialsField, + DiscordOAuthCredentialsInput, +) + + +class DiscordGetCurrentUserBlock(Block): + """ + Gets information about the currently authenticated Discord user using OAuth2. + This block requires Discord OAuth2 credentials (not bot tokens). + """ + + class Input(BlockSchema): + credentials: DiscordOAuthCredentialsInput = DiscordOAuthCredentialsField( + ["identify"] + ) + + class Output(BlockSchema): + user_id: str = SchemaField(description="The authenticated user's Discord ID") + username: str = SchemaField(description="The user's username") + avatar_url: str = SchemaField(description="URL to the user's avatar image") + banner_url: str = SchemaField( + description="URL to the user's banner image (if set)", default="" + ) + accent_color: int = SchemaField( + description="The user's accent color as an integer", default=0 + ) + + def __init__(self): + super().__init__( + id="8c7e39b8-4e9d-4f3a-b4e1-2a8c9d5f6e3b", + input_schema=DiscordGetCurrentUserBlock.Input, + output_schema=DiscordGetCurrentUserBlock.Output, + description="Gets information about the currently authenticated Discord user using OAuth2 credentials.", + categories={BlockCategory.SOCIAL}, + disabled=not DISCORD_OAUTH_IS_CONFIGURED, + test_input={ + "credentials": TEST_OAUTH_CREDENTIALS_INPUT, + }, + test_credentials=TEST_OAUTH_CREDENTIALS, + test_output=[ + ("user_id", "123456789012345678"), + ("username", "testuser"), + ( + "avatar_url", + "https://cdn.discordapp.com/avatars/123456789012345678/avatar.png", + ), + ("banner_url", ""), + ("accent_color", 0), + ], + test_mock={ + "get_user": lambda _: DiscordOAuthUser( + user_id="123456789012345678", + username="testuser", + avatar_url="https://cdn.discordapp.com/avatars/123456789012345678/avatar.png", + banner=None, + accent_color=0, + ) + }, + ) + + @staticmethod + async def get_user(credentials: OAuth2Credentials) -> DiscordOAuthUser: + user_info = await get_current_user(credentials) + return user_info + + async def run( + self, input_data: Input, *, credentials: OAuth2Credentials, **kwargs + ) -> BlockOutput: + try: + result = await self.get_user(credentials) + + # Yield each output field + yield "user_id", result.user_id + yield "username", result.username + yield "avatar_url", result.avatar_url + + # Handle banner URL if banner hash exists + if result.banner: + banner_url = f"https://cdn.discordapp.com/banners/{result.user_id}/{result.banner}.png" + yield "banner_url", banner_url + else: + yield "banner_url", "" + + yield "accent_color", result.accent_color or 0 + + except Exception as e: + raise ValueError(f"Failed to get Discord user info: {e}") diff --git a/autogpt_platform/backend/backend/blocks/enrichlayer/_api.py b/autogpt_platform/backend/backend/blocks/enrichlayer/_api.py new file mode 100644 index 000000000000..7d1f0e61f06c --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/enrichlayer/_api.py @@ -0,0 +1,408 @@ +""" +API module for Enrichlayer integration. + +This module provides a client for interacting with the Enrichlayer API, +which allows fetching LinkedIn profile data and related information. +""" + +import datetime +import enum +import logging +from json import JSONDecodeError +from typing import Any, Optional, TypeVar + +from pydantic import BaseModel, Field + +from backend.data.model import APIKeyCredentials +from backend.util.request import Requests + +logger = logging.getLogger(__name__) + +T = TypeVar("T") + + +class EnrichlayerAPIException(Exception): + """Exception raised for Enrichlayer API errors.""" + + def __init__(self, message: str, status_code: int): + super().__init__(message) + self.status_code = status_code + + +class FallbackToCache(enum.Enum): + ON_ERROR = "on-error" + NEVER = "never" + + +class UseCache(enum.Enum): + IF_PRESENT = "if-present" + NEVER = "never" + + +class SocialMediaProfiles(BaseModel): + """Social media profiles model.""" + + twitter: Optional[str] = None + facebook: Optional[str] = None + github: Optional[str] = None + + +class Experience(BaseModel): + """Experience model for LinkedIn profiles.""" + + company: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + location: Optional[str] = None + starts_at: Optional[dict[str, int]] = None + ends_at: Optional[dict[str, int]] = None + company_linkedin_profile_url: Optional[str] = None + + +class Education(BaseModel): + """Education model for LinkedIn profiles.""" + + school: Optional[str] = None + degree_name: Optional[str] = None + field_of_study: Optional[str] = None + starts_at: Optional[dict[str, int]] = None + ends_at: Optional[dict[str, int]] = None + school_linkedin_profile_url: Optional[str] = None + + +class PersonProfileResponse(BaseModel): + """Response model for LinkedIn person profile. + + This model represents the response from Enrichlayer's LinkedIn profile API. + The API returns comprehensive profile data including work experience, + education, skills, and contact information (when available). + + Example API Response: + { + "public_identifier": "johnsmith", + "full_name": "John Smith", + "occupation": "Software Engineer at Tech Corp", + "experiences": [ + { + "company": "Tech Corp", + "title": "Software Engineer", + "starts_at": {"year": 2020, "month": 1} + } + ], + "education": [...], + "skills": ["Python", "JavaScript", ...] + } + """ + + public_identifier: Optional[str] = None + profile_pic_url: Optional[str] = None + full_name: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + occupation: Optional[str] = None + headline: Optional[str] = None + summary: Optional[str] = None + country: Optional[str] = None + country_full_name: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + experiences: Optional[list[Experience]] = None + education: Optional[list[Education]] = None + languages: Optional[list[str]] = None + skills: Optional[list[str]] = None + inferred_salary: Optional[dict[str, Any]] = None + personal_email: Optional[str] = None + personal_contact_number: Optional[str] = None + social_media_profiles: Optional[SocialMediaProfiles] = None + extra: Optional[dict[str, Any]] = None + + +class SimilarProfile(BaseModel): + """Similar profile model for LinkedIn person lookup.""" + + similarity: float + linkedin_profile_url: str + + +class PersonLookupResponse(BaseModel): + """Response model for LinkedIn person lookup. + + This model represents the response from Enrichlayer's person lookup API. + The API returns a LinkedIn profile URL and similarity scores when + searching for a person by name and company. + + Example API Response: + { + "url": "https://www.linkedin.com/in/johnsmith/", + "name_similarity_score": 0.95, + "company_similarity_score": 0.88, + "title_similarity_score": 0.75, + "location_similarity_score": 0.60 + } + """ + + url: str | None = None + name_similarity_score: float | None + company_similarity_score: float | None + title_similarity_score: float | None + location_similarity_score: float | None + last_updated: datetime.datetime | None = None + profile: PersonProfileResponse | None = None + + +class RoleLookupResponse(BaseModel): + """Response model for LinkedIn role lookup. + + This model represents the response from Enrichlayer's role lookup API. + The API returns LinkedIn profile data for a specific role at a company. + + Example API Response: + { + "linkedin_profile_url": "https://www.linkedin.com/in/johnsmith/", + "profile_data": {...} // Full PersonProfileResponse data when enrich_profile=True + } + """ + + linkedin_profile_url: Optional[str] = None + profile_data: Optional[PersonProfileResponse] = None + + +class ProfilePictureResponse(BaseModel): + """Response model for LinkedIn profile picture. + + This model represents the response from Enrichlayer's profile picture API. + The API returns a URL to the person's LinkedIn profile picture. + + Example API Response: + { + "tmp_profile_pic_url": "https://media.licdn.com/dms/image/..." + } + """ + + tmp_profile_pic_url: str = Field( + ..., description="URL of the profile picture", alias="tmp_profile_pic_url" + ) + + @property + def profile_picture_url(self) -> str: + """Backward compatibility property for profile_picture_url.""" + return self.tmp_profile_pic_url + + +class EnrichlayerClient: + """Client for interacting with the Enrichlayer API.""" + + API_BASE_URL = "https://enrichlayer.com/api/v2" + + def __init__( + self, + credentials: Optional[APIKeyCredentials] = None, + custom_requests: Optional[Requests] = None, + ): + """ + Initialize the Enrichlayer client. + + Args: + credentials: The credentials to use for authentication. + custom_requests: Custom Requests instance for testing. + """ + if custom_requests: + self._requests = custom_requests + else: + headers: dict[str, str] = { + "Content-Type": "application/json", + } + if credentials: + headers["Authorization"] = ( + f"Bearer {credentials.api_key.get_secret_value()}" + ) + + self._requests = Requests( + extra_headers=headers, + raise_for_status=False, + ) + + async def _handle_response(self, response) -> Any: + """ + Handle API response and check for errors. + + Args: + response: The response object from the request. + + Returns: + The response data. + + Raises: + EnrichlayerAPIException: If the API request fails. + """ + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get("message", "") + except JSONDecodeError: + error_message = response.text + + raise EnrichlayerAPIException( + f"Enrichlayer API request failed ({response.status_code}): {error_message}", + response.status_code, + ) + + return response.json() + + async def fetch_profile( + self, + linkedin_url: str, + fallback_to_cache: FallbackToCache = FallbackToCache.ON_ERROR, + use_cache: UseCache = UseCache.IF_PRESENT, + include_skills: bool = False, + include_inferred_salary: bool = False, + include_personal_email: bool = False, + include_personal_contact_number: bool = False, + include_social_media: bool = False, + include_extra: bool = False, + ) -> PersonProfileResponse: + """ + Fetch a LinkedIn profile with optional parameters. + + Args: + linkedin_url: The LinkedIn profile URL to fetch. + fallback_to_cache: Cache usage if live fetch fails ('on-error' or 'never'). + use_cache: Cache utilization ('if-present' or 'never'). + include_skills: Whether to include skills data. + include_inferred_salary: Whether to include inferred salary data. + include_personal_email: Whether to include personal email. + include_personal_contact_number: Whether to include personal contact number. + include_social_media: Whether to include social media profiles. + include_extra: Whether to include additional data. + + Returns: + The LinkedIn profile data. + + Raises: + EnrichlayerAPIException: If the API request fails. + """ + params = { + "url": linkedin_url, + "fallback_to_cache": fallback_to_cache.value.lower(), + "use_cache": use_cache.value.lower(), + } + + if include_skills: + params["skills"] = "include" + if include_inferred_salary: + params["inferred_salary"] = "include" + if include_personal_email: + params["personal_email"] = "include" + if include_personal_contact_number: + params["personal_contact_number"] = "include" + if include_social_media: + params["twitter_profile_id"] = "include" + params["facebook_profile_id"] = "include" + params["github_profile_id"] = "include" + if include_extra: + params["extra"] = "include" + + response = await self._requests.get( + f"{self.API_BASE_URL}/profile", params=params + ) + return PersonProfileResponse(**await self._handle_response(response)) + + async def lookup_person( + self, + first_name: str, + company_domain: str, + last_name: str | None = None, + location: Optional[str] = None, + title: Optional[str] = None, + include_similarity_checks: bool = False, + enrich_profile: bool = False, + ) -> PersonLookupResponse: + """ + Look up a LinkedIn profile by person's information. + + Args: + first_name: The person's first name. + last_name: The person's last name. + company_domain: The domain of the company they work for. + location: The person's location. + title: The person's job title. + include_similarity_checks: Whether to include similarity checks. + enrich_profile: Whether to enrich the profile. + + Returns: + The LinkedIn profile lookup result. + + Raises: + EnrichlayerAPIException: If the API request fails. + """ + params = {"first_name": first_name, "company_domain": company_domain} + + if last_name: + params["last_name"] = last_name + if location: + params["location"] = location + if title: + params["title"] = title + if include_similarity_checks: + params["similarity_checks"] = "include" + if enrich_profile: + params["enrich_profile"] = "enrich" + + response = await self._requests.get( + f"{self.API_BASE_URL}/profile/resolve", params=params + ) + return PersonLookupResponse(**await self._handle_response(response)) + + async def lookup_role( + self, role: str, company_name: str, enrich_profile: bool = False + ) -> RoleLookupResponse: + """ + Look up a LinkedIn profile by role in a company. + + Args: + role: The role title (e.g., CEO, CTO). + company_name: The name of the company. + enrich_profile: Whether to enrich the profile. + + Returns: + The LinkedIn profile lookup result. + + Raises: + EnrichlayerAPIException: If the API request fails. + """ + params = { + "role": role, + "company_name": company_name, + } + + if enrich_profile: + params["enrich_profile"] = "enrich" + + response = await self._requests.get( + f"{self.API_BASE_URL}/find/company/role", params=params + ) + return RoleLookupResponse(**await self._handle_response(response)) + + async def get_profile_picture( + self, linkedin_profile_url: str + ) -> ProfilePictureResponse: + """ + Get a LinkedIn profile picture URL. + + Args: + linkedin_profile_url: The LinkedIn profile URL. + + Returns: + The profile picture URL. + + Raises: + EnrichlayerAPIException: If the API request fails. + """ + params = { + "linkedin_person_profile_url": linkedin_profile_url, + } + + response = await self._requests.get( + f"{self.API_BASE_URL}/person/profile-picture", params=params + ) + return ProfilePictureResponse(**await self._handle_response(response)) diff --git a/autogpt_platform/backend/backend/blocks/enrichlayer/_auth.py b/autogpt_platform/backend/backend/blocks/enrichlayer/_auth.py new file mode 100644 index 000000000000..70413adba4ad --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/enrichlayer/_auth.py @@ -0,0 +1,34 @@ +""" +Authentication module for Enrichlayer API integration. + +This module provides credential types and test credentials for the Enrichlayer API. +""" + +from typing import Literal + +from pydantic import SecretStr + +from backend.data.model import APIKeyCredentials, CredentialsMetaInput +from backend.integrations.providers import ProviderName + +# Define the type of credentials input expected for Enrichlayer API +EnrichlayerCredentialsInput = CredentialsMetaInput[ + Literal[ProviderName.ENRICHLAYER], Literal["api_key"] +] + +# Mock credentials for testing Enrichlayer API integration +TEST_CREDENTIALS = APIKeyCredentials( + id="1234a567-89bc-4def-ab12-3456cdef7890", + provider="enrichlayer", + api_key=SecretStr("mock-enrichlayer-api-key"), + title="Mock Enrichlayer API key", + expires_at=None, +) + +# Dictionary representation of test credentials for input fields +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.title, +} diff --git a/autogpt_platform/backend/backend/blocks/enrichlayer/linkedin.py b/autogpt_platform/backend/backend/blocks/enrichlayer/linkedin.py new file mode 100644 index 000000000000..52d593eb0ecc --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/enrichlayer/linkedin.py @@ -0,0 +1,527 @@ +""" +Block definitions for Enrichlayer API integration. + +This module implements blocks for interacting with the Enrichlayer API, +which provides access to LinkedIn profile data and related information. +""" + +import logging +from typing import Optional + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import APIKeyCredentials, CredentialsField, SchemaField +from backend.util.type import MediaFileType + +from ._api import ( + EnrichlayerClient, + Experience, + FallbackToCache, + PersonLookupResponse, + PersonProfileResponse, + RoleLookupResponse, + UseCache, +) +from ._auth import TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT, EnrichlayerCredentialsInput + +logger = logging.getLogger(__name__) + + +class GetLinkedinProfileBlock(Block): + """Block to fetch LinkedIn profile data using Enrichlayer API.""" + + class Input(BlockSchema): + """Input schema for GetLinkedinProfileBlock.""" + + linkedin_url: str = SchemaField( + description="LinkedIn profile URL to fetch data from", + placeholder="https://www.linkedin.com/in/username/", + ) + fallback_to_cache: FallbackToCache = SchemaField( + description="Cache usage if live fetch fails", + default=FallbackToCache.ON_ERROR, + advanced=True, + ) + use_cache: UseCache = SchemaField( + description="Cache utilization strategy", + default=UseCache.IF_PRESENT, + advanced=True, + ) + include_skills: bool = SchemaField( + description="Include skills data", + default=False, + advanced=True, + ) + include_inferred_salary: bool = SchemaField( + description="Include inferred salary data", + default=False, + advanced=True, + ) + include_personal_email: bool = SchemaField( + description="Include personal email", + default=False, + advanced=True, + ) + include_personal_contact_number: bool = SchemaField( + description="Include personal contact number", + default=False, + advanced=True, + ) + include_social_media: bool = SchemaField( + description="Include social media profiles", + default=False, + advanced=True, + ) + include_extra: bool = SchemaField( + description="Include additional data", + default=False, + advanced=True, + ) + credentials: EnrichlayerCredentialsInput = CredentialsField( + description="Enrichlayer API credentials" + ) + + class Output(BlockSchema): + """Output schema for GetLinkedinProfileBlock.""" + + profile: PersonProfileResponse = SchemaField( + description="LinkedIn profile data" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + """Initialize GetLinkedinProfileBlock.""" + super().__init__( + id="f6e0ac73-4f1d-4acb-b4b7-b67066c5984e", + description="Fetch LinkedIn profile data using Enrichlayer", + categories={BlockCategory.SOCIAL}, + input_schema=GetLinkedinProfileBlock.Input, + output_schema=GetLinkedinProfileBlock.Output, + test_input={ + "linkedin_url": "https://www.linkedin.com/in/williamhgates/", + "include_skills": True, + "include_social_media": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ( + "profile", + PersonProfileResponse( + public_identifier="williamhgates", + full_name="Bill Gates", + occupation="Co-chair at Bill & Melinda Gates Foundation", + experiences=[ + Experience( + company="Bill & Melinda Gates Foundation", + title="Co-chair", + starts_at={"year": 2000}, + ) + ], + ), + ) + ], + test_credentials=TEST_CREDENTIALS, + test_mock={ + "_fetch_profile": lambda *args, **kwargs: PersonProfileResponse( + public_identifier="williamhgates", + full_name="Bill Gates", + occupation="Co-chair at Bill & Melinda Gates Foundation", + experiences=[ + Experience( + company="Bill & Melinda Gates Foundation", + title="Co-chair", + starts_at={"year": 2000}, + ) + ], + ), + }, + ) + + @staticmethod + async def _fetch_profile( + credentials: APIKeyCredentials, + linkedin_url: str, + fallback_to_cache: FallbackToCache = FallbackToCache.ON_ERROR, + use_cache: UseCache = UseCache.IF_PRESENT, + include_skills: bool = False, + include_inferred_salary: bool = False, + include_personal_email: bool = False, + include_personal_contact_number: bool = False, + include_social_media: bool = False, + include_extra: bool = False, + ): + client = EnrichlayerClient(credentials) + profile = await client.fetch_profile( + linkedin_url=linkedin_url, + fallback_to_cache=fallback_to_cache, + use_cache=use_cache, + include_skills=include_skills, + include_inferred_salary=include_inferred_salary, + include_personal_email=include_personal_email, + include_personal_contact_number=include_personal_contact_number, + include_social_media=include_social_media, + include_extra=include_extra, + ) + return profile + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + """ + Run the block to fetch LinkedIn profile data. + + Args: + input_data: Input parameters for the block + credentials: API key credentials for Enrichlayer + **kwargs: Additional keyword arguments + + Yields: + Tuples of (output_name, output_value) + """ + try: + profile = await self._fetch_profile( + credentials=credentials, + linkedin_url=input_data.linkedin_url, + fallback_to_cache=input_data.fallback_to_cache, + use_cache=input_data.use_cache, + include_skills=input_data.include_skills, + include_inferred_salary=input_data.include_inferred_salary, + include_personal_email=input_data.include_personal_email, + include_personal_contact_number=input_data.include_personal_contact_number, + include_social_media=input_data.include_social_media, + include_extra=input_data.include_extra, + ) + yield "profile", profile + except Exception as e: + logger.error(f"Error fetching LinkedIn profile: {str(e)}") + yield "error", str(e) + + +class LinkedinPersonLookupBlock(Block): + """Block to look up LinkedIn profiles by person's information using Enrichlayer API.""" + + class Input(BlockSchema): + """Input schema for LinkedinPersonLookupBlock.""" + + first_name: str = SchemaField( + description="Person's first name", + placeholder="John", + advanced=False, + ) + last_name: str | None = SchemaField( + description="Person's last name", + placeholder="Doe", + default=None, + advanced=False, + ) + company_domain: str = SchemaField( + description="Domain of the company they work for (optional)", + placeholder="example.com", + advanced=False, + ) + location: Optional[str] = SchemaField( + description="Person's location (optional)", + placeholder="San Francisco", + default=None, + ) + title: Optional[str] = SchemaField( + description="Person's job title (optional)", + placeholder="CEO", + default=None, + ) + include_similarity_checks: bool = SchemaField( + description="Include similarity checks", + default=False, + advanced=True, + ) + enrich_profile: bool = SchemaField( + description="Enrich the profile with additional data", + default=False, + advanced=True, + ) + credentials: EnrichlayerCredentialsInput = CredentialsField( + description="Enrichlayer API credentials" + ) + + class Output(BlockSchema): + """Output schema for LinkedinPersonLookupBlock.""" + + lookup_result: PersonLookupResponse = SchemaField( + description="LinkedIn profile lookup result" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + """Initialize LinkedinPersonLookupBlock.""" + super().__init__( + id="d237a98a-5c4b-4a1c-b9e3-e6f9a6c81df7", + description="Look up LinkedIn profiles by person information using Enrichlayer", + categories={BlockCategory.SOCIAL}, + input_schema=LinkedinPersonLookupBlock.Input, + output_schema=LinkedinPersonLookupBlock.Output, + test_input={ + "first_name": "Bill", + "last_name": "Gates", + "company_domain": "gatesfoundation.org", + "include_similarity_checks": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ( + "lookup_result", + PersonLookupResponse( + url="https://www.linkedin.com/in/williamhgates/", + name_similarity_score=0.93, + company_similarity_score=0.83, + title_similarity_score=0.3, + location_similarity_score=0.20, + ), + ) + ], + test_credentials=TEST_CREDENTIALS, + test_mock={ + "_lookup_person": lambda *args, **kwargs: PersonLookupResponse( + url="https://www.linkedin.com/in/williamhgates/", + name_similarity_score=0.93, + company_similarity_score=0.83, + title_similarity_score=0.3, + location_similarity_score=0.20, + ) + }, + ) + + @staticmethod + async def _lookup_person( + credentials: APIKeyCredentials, + first_name: str, + company_domain: str, + last_name: str | None = None, + location: Optional[str] = None, + title: Optional[str] = None, + include_similarity_checks: bool = False, + enrich_profile: bool = False, + ): + client = EnrichlayerClient(credentials=credentials) + lookup_result = await client.lookup_person( + first_name=first_name, + last_name=last_name, + company_domain=company_domain, + location=location, + title=title, + include_similarity_checks=include_similarity_checks, + enrich_profile=enrich_profile, + ) + return lookup_result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + """ + Run the block to look up LinkedIn profiles. + + Args: + input_data: Input parameters for the block + credentials: API key credentials for Enrichlayer + **kwargs: Additional keyword arguments + + Yields: + Tuples of (output_name, output_value) + """ + try: + lookup_result = await self._lookup_person( + credentials=credentials, + first_name=input_data.first_name, + last_name=input_data.last_name, + company_domain=input_data.company_domain, + location=input_data.location, + title=input_data.title, + include_similarity_checks=input_data.include_similarity_checks, + enrich_profile=input_data.enrich_profile, + ) + yield "lookup_result", lookup_result + except Exception as e: + logger.error(f"Error looking up LinkedIn profile: {str(e)}") + yield "error", str(e) + + +class LinkedinRoleLookupBlock(Block): + """Block to look up LinkedIn profiles by role in a company using Enrichlayer API.""" + + class Input(BlockSchema): + """Input schema for LinkedinRoleLookupBlock.""" + + role: str = SchemaField( + description="Role title (e.g., CEO, CTO)", + placeholder="CEO", + ) + company_name: str = SchemaField( + description="Name of the company", + placeholder="Microsoft", + ) + enrich_profile: bool = SchemaField( + description="Enrich the profile with additional data", + default=False, + advanced=True, + ) + credentials: EnrichlayerCredentialsInput = CredentialsField( + description="Enrichlayer API credentials" + ) + + class Output(BlockSchema): + """Output schema for LinkedinRoleLookupBlock.""" + + role_lookup_result: RoleLookupResponse = SchemaField( + description="LinkedIn role lookup result" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + """Initialize LinkedinRoleLookupBlock.""" + super().__init__( + id="3b9fc742-06d4-49c7-b5ce-7e302dd7c8a7", + description="Look up LinkedIn profiles by role in a company using Enrichlayer", + categories={BlockCategory.SOCIAL}, + input_schema=LinkedinRoleLookupBlock.Input, + output_schema=LinkedinRoleLookupBlock.Output, + test_input={ + "role": "Co-chair", + "company_name": "Gates Foundation", + "enrich_profile": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ( + "role_lookup_result", + RoleLookupResponse( + linkedin_profile_url="https://www.linkedin.com/in/williamhgates/", + ), + ) + ], + test_credentials=TEST_CREDENTIALS, + test_mock={ + "_lookup_role": lambda *args, **kwargs: RoleLookupResponse( + linkedin_profile_url="https://www.linkedin.com/in/williamhgates/", + ), + }, + ) + + @staticmethod + async def _lookup_role( + credentials: APIKeyCredentials, + role: str, + company_name: str, + enrich_profile: bool = False, + ): + client = EnrichlayerClient(credentials=credentials) + role_lookup_result = await client.lookup_role( + role=role, + company_name=company_name, + enrich_profile=enrich_profile, + ) + return role_lookup_result + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + """ + Run the block to look up LinkedIn profiles by role. + + Args: + input_data: Input parameters for the block + credentials: API key credentials for Enrichlayer + **kwargs: Additional keyword arguments + + Yields: + Tuples of (output_name, output_value) + """ + try: + role_lookup_result = await self._lookup_role( + credentials=credentials, + role=input_data.role, + company_name=input_data.company_name, + enrich_profile=input_data.enrich_profile, + ) + yield "role_lookup_result", role_lookup_result + except Exception as e: + logger.error(f"Error looking up role in company: {str(e)}") + yield "error", str(e) + + +class GetLinkedinProfilePictureBlock(Block): + """Block to get LinkedIn profile pictures using Enrichlayer API.""" + + class Input(BlockSchema): + """Input schema for GetLinkedinProfilePictureBlock.""" + + linkedin_profile_url: str = SchemaField( + description="LinkedIn profile URL", + placeholder="https://www.linkedin.com/in/username/", + ) + credentials: EnrichlayerCredentialsInput = CredentialsField( + description="Enrichlayer API credentials" + ) + + class Output(BlockSchema): + """Output schema for GetLinkedinProfilePictureBlock.""" + + profile_picture_url: MediaFileType = SchemaField( + description="LinkedIn profile picture URL" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + """Initialize GetLinkedinProfilePictureBlock.""" + super().__init__( + id="68d5a942-9b3f-4e9a-b7c1-d96ea4321f0d", + description="Get LinkedIn profile pictures using Enrichlayer", + categories={BlockCategory.SOCIAL}, + input_schema=GetLinkedinProfilePictureBlock.Input, + output_schema=GetLinkedinProfilePictureBlock.Output, + test_input={ + "linkedin_profile_url": "https://www.linkedin.com/in/williamhgates/", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_output=[ + ( + "profile_picture_url", + "https://media.licdn.com/dms/image/C4D03AQFj-xjuXrLFSQ/profile-displayphoto-shrink_800_800/0/1576881858598?e=1686787200&v=beta&t=zrQC76QwsfQQIWthfOnrKRBMZ5D-qIAvzLXLmWgYvTk", + ) + ], + test_credentials=TEST_CREDENTIALS, + test_mock={ + "_get_profile_picture": lambda *args, **kwargs: "https://media.licdn.com/dms/image/C4D03AQFj-xjuXrLFSQ/profile-displayphoto-shrink_800_800/0/1576881858598?e=1686787200&v=beta&t=zrQC76QwsfQQIWthfOnrKRBMZ5D-qIAvzLXLmWgYvTk", + }, + ) + + @staticmethod + async def _get_profile_picture( + credentials: APIKeyCredentials, linkedin_profile_url: str + ): + client = EnrichlayerClient(credentials=credentials) + profile_picture_response = await client.get_profile_picture( + linkedin_profile_url=linkedin_profile_url, + ) + return profile_picture_response.profile_picture_url + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + """ + Run the block to get LinkedIn profile pictures. + + Args: + input_data: Input parameters for the block + credentials: API key credentials for Enrichlayer + **kwargs: Additional keyword arguments + + Yields: + Tuples of (output_name, output_value) + """ + try: + profile_picture = await self._get_profile_picture( + credentials=credentials, + linkedin_profile_url=input_data.linkedin_profile_url, + ) + yield "profile_picture_url", profile_picture + except Exception as e: + logger.error(f"Error getting profile picture: {str(e)}") + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/exa/_webhook.py b/autogpt_platform/backend/backend/blocks/exa/_webhook.py index a57efe23cb5b..725976c68c79 100644 --- a/autogpt_platform/backend/backend/blocks/exa/_webhook.py +++ b/autogpt_platform/backend/backend/blocks/exa/_webhook.py @@ -51,7 +51,9 @@ class WebhookType(str, Enum): WEBSET = "webset" @classmethod - async def validate_payload(cls, webhook: Webhook, request) -> tuple[dict, str]: + async def validate_payload( + cls, webhook: Webhook, request, credentials: Credentials | None + ) -> tuple[dict, str]: """Validate incoming webhook payload and signature.""" payload = await request.json() diff --git a/autogpt_platform/backend/backend/blocks/exa/answers.py b/autogpt_platform/backend/backend/blocks/exa/answers.py index 237edccd650f..fa3f6b403f44 100644 --- a/autogpt_platform/backend/backend/blocks/exa/answers.py +++ b/autogpt_platform/backend/backend/blocks/exa/answers.py @@ -119,6 +119,3 @@ async def run( except Exception as e: yield "error", str(e) - yield "answer", "" - yield "citations", [] - yield "cost_dollars", {} diff --git a/autogpt_platform/backend/backend/blocks/exa/model.py b/autogpt_platform/backend/backend/blocks/exa/model.py new file mode 100644 index 000000000000..69b223f46743 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/exa/model.py @@ -0,0 +1,247 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +# Enum definitions based on available options +class WebsetStatus(str, Enum): + IDLE = "idle" + PENDING = "pending" + RUNNING = "running" + PAUSED = "paused" + + +class WebsetSearchStatus(str, Enum): + CREATED = "created" + # Add more if known, based on example it's "created" + + +class ImportStatus(str, Enum): + PENDING = "pending" + # Add more if known + + +class ImportFormat(str, Enum): + CSV = "csv" + # Add more if known + + +class EnrichmentStatus(str, Enum): + PENDING = "pending" + # Add more if known + + +class EnrichmentFormat(str, Enum): + TEXT = "text" + # Add more if known + + +class MonitorStatus(str, Enum): + ENABLED = "enabled" + # Add more if known + + +class MonitorBehaviorType(str, Enum): + SEARCH = "search" + # Add more if known + + +class MonitorRunStatus(str, Enum): + CREATED = "created" + # Add more if known + + +class CanceledReason(str, Enum): + WEBSET_DELETED = "webset_deleted" + # Add more if known + + +class FailedReason(str, Enum): + INVALID_FORMAT = "invalid_format" + # Add more if known + + +class Confidence(str, Enum): + HIGH = "high" + # Add more if known + + +# Nested models + + +class Entity(BaseModel): + type: str + + +class Criterion(BaseModel): + description: str + successRate: Optional[int] = None + + +class ExcludeItem(BaseModel): + source: str = Field(default="import") + id: str + + +class Relationship(BaseModel): + definition: str + limit: Optional[float] = None + + +class ScopeItem(BaseModel): + source: str = Field(default="import") + id: str + relationship: Optional[Relationship] = None + + +class Progress(BaseModel): + found: int + analyzed: int + completion: int + timeLeft: int + + +class Bounds(BaseModel): + min: int + max: int + + +class Expected(BaseModel): + total: int + confidence: str = Field(default="high") # Use str or Confidence enum + bounds: Bounds + + +class Recall(BaseModel): + expected: Expected + reasoning: str + + +class WebsetSearch(BaseModel): + id: str + object: str = Field(default="webset_search") + status: str = Field(default="created") # Or use WebsetSearchStatus + websetId: str + query: str + entity: Entity + criteria: List[Criterion] + count: int + behavior: str = Field(default="override") + exclude: List[ExcludeItem] + scope: List[ScopeItem] + progress: Progress + recall: Recall + metadata: Dict[str, Any] = Field(default_factory=dict) + canceledAt: Optional[datetime] = None + canceledReason: Optional[str] = Field(default=None) # Or use CanceledReason + createdAt: datetime + updatedAt: datetime + + +class ImportEntity(BaseModel): + type: str + + +class Import(BaseModel): + id: str + object: str = Field(default="import") + status: str = Field(default="pending") # Or use ImportStatus + format: str = Field(default="csv") # Or use ImportFormat + entity: ImportEntity + title: str + count: int + metadata: Dict[str, Any] = Field(default_factory=dict) + failedReason: Optional[str] = Field(default=None) # Or use FailedReason + failedAt: Optional[datetime] = None + failedMessage: Optional[str] = None + createdAt: datetime + updatedAt: datetime + + +class Option(BaseModel): + label: str + + +class WebsetEnrichment(BaseModel): + id: str + object: str = Field(default="webset_enrichment") + status: str = Field(default="pending") # Or use EnrichmentStatus + websetId: str + title: str + description: str + format: str = Field(default="text") # Or use EnrichmentFormat + options: List[Option] + instructions: str + metadata: Dict[str, Any] = Field(default_factory=dict) + createdAt: datetime + updatedAt: datetime + + +class Cadence(BaseModel): + cron: str + timezone: str = Field(default="Etc/UTC") + + +class BehaviorConfig(BaseModel): + query: Optional[str] = None + criteria: Optional[List[Criterion]] = None + entity: Optional[Entity] = None + count: Optional[int] = None + behavior: Optional[str] = Field(default=None) + + +class Behavior(BaseModel): + type: str = Field(default="search") # Or use MonitorBehaviorType + config: BehaviorConfig + + +class MonitorRun(BaseModel): + id: str + object: str = Field(default="monitor_run") + status: str = Field(default="created") # Or use MonitorRunStatus + monitorId: str + type: str = Field(default="search") + completedAt: Optional[datetime] = None + failedAt: Optional[datetime] = None + failedReason: Optional[str] = None + canceledAt: Optional[datetime] = None + createdAt: datetime + updatedAt: datetime + + +class Monitor(BaseModel): + id: str + object: str = Field(default="monitor") + status: str = Field(default="enabled") # Or use MonitorStatus + websetId: str + cadence: Cadence + behavior: Behavior + lastRun: Optional[MonitorRun] = None + nextRunAt: Optional[datetime] = None + metadata: Dict[str, Any] = Field(default_factory=dict) + createdAt: datetime + updatedAt: datetime + + +class Webset(BaseModel): + id: str + object: str = Field(default="webset") + status: WebsetStatus + externalId: Optional[str] = None + title: Optional[str] = None + searches: List[WebsetSearch] + imports: List[Import] + enrichments: List[WebsetEnrichment] + monitors: List[Monitor] + streams: List[Any] + createdAt: datetime + updatedAt: datetime + metadata: Dict[str, Any] = Field(default_factory=dict) + + +class ListWebsets(BaseModel): + data: List[Webset] + hasMore: bool + nextCursor: Optional[str] = None diff --git a/autogpt_platform/backend/backend/blocks/exa/webhook_blocks.py b/autogpt_platform/backend/backend/blocks/exa/webhook_blocks.py index 2f909d8691ae..eb3854ed9c3f 100644 --- a/autogpt_platform/backend/backend/blocks/exa/webhook_blocks.py +++ b/autogpt_platform/backend/backend/blocks/exa/webhook_blocks.py @@ -114,6 +114,7 @@ class Output(BlockSchema): def __init__(self): super().__init__( + disabled=True, id="d0204ed8-8b81-408d-8b8d-ed087a546228", description="Receive webhook notifications for Exa webset events", categories={BlockCategory.INPUT}, diff --git a/autogpt_platform/backend/backend/blocks/exa/websets.py b/autogpt_platform/backend/backend/blocks/exa/websets.py index bff5f09c5fc1..bec57e01d91c 100644 --- a/autogpt_platform/backend/backend/blocks/exa/websets.py +++ b/autogpt_platform/backend/backend/blocks/exa/websets.py @@ -1,7 +1,33 @@ -from typing import Any, Optional +from datetime import datetime +from enum import Enum +from typing import Annotated, Any, Dict, List, Optional + +from exa_py import Exa +from exa_py.websets.types import ( + CreateCriterionParameters, + CreateEnrichmentParameters, + CreateWebsetParameters, + CreateWebsetParametersSearch, + ExcludeItem, + Format, + ImportItem, + ImportSource, + Option, + ScopeItem, + ScopeRelationship, + ScopeSourceType, + WebsetArticleEntity, + WebsetCompanyEntity, + WebsetCustomEntity, + WebsetPersonEntity, + WebsetResearchPaperEntity, + WebsetStatus, +) +from pydantic import Field from backend.sdk import ( APIKeyCredentials, + BaseModel, Block, BlockCategory, BlockOutput, @@ -12,7 +38,69 @@ ) from ._config import exa -from .helpers import WebsetEnrichmentConfig, WebsetSearchConfig + + +class SearchEntityType(str, Enum): + COMPANY = "company" + PERSON = "person" + ARTICLE = "article" + RESEARCH_PAPER = "research_paper" + CUSTOM = "custom" + AUTO = "auto" + + +class SearchType(str, Enum): + IMPORT = "import" + WEBSET = "webset" + + +class EnrichmentFormat(str, Enum): + TEXT = "text" + DATE = "date" + NUMBER = "number" + OPTIONS = "options" + EMAIL = "email" + PHONE = "phone" + + +class Webset(BaseModel): + id: str + status: WebsetStatus | None = Field(..., title="WebsetStatus") + """ + The status of the webset + """ + external_id: Annotated[Optional[str], Field(alias="externalId")] = None + """ + The external identifier for the webset + NOTE: Returning dict to avoid ui crashing due to nested objects + """ + searches: List[dict[str, Any]] | None = None + """ + The searches that have been performed on the webset. + NOTE: Returning dict to avoid ui crashing due to nested objects + """ + enrichments: List[dict[str, Any]] | None = None + """ + The Enrichments to apply to the Webset Items. + NOTE: Returning dict to avoid ui crashing due to nested objects + """ + monitors: List[dict[str, Any]] | None = None + """ + The Monitors for the Webset. + NOTE: Returning dict to avoid ui crashing due to nested objects + """ + metadata: Optional[Dict[str, Any]] = {} + """ + Set of key-value pairs you want to associate with this object. + """ + created_at: Annotated[datetime, Field(alias="createdAt")] | None = None + """ + The date and time the webset was created + """ + updated_at: Annotated[datetime, Field(alias="updatedAt")] | None = None + """ + The date and time the webset was last updated + """ class ExaCreateWebsetBlock(Block): @@ -20,40 +108,121 @@ class Input(BlockSchema): credentials: CredentialsMetaInput = exa.credentials_field( description="The Exa integration requires an API Key." ) - search: WebsetSearchConfig = SchemaField( - description="Initial search configuration for the Webset" + + # Search parameters (flattened) + search_query: str = SchemaField( + description="Your search query. Use this to describe what you are looking for. Any URL provided will be crawled and used as context for the search.", + placeholder="Marketing agencies based in the US, that focus on consumer products", + ) + search_count: Optional[int] = SchemaField( + default=10, + description="Number of items the search will attempt to find. The actual number of items found may be less than this number depending on the search complexity.", + ge=1, + le=1000, ) - enrichments: Optional[list[WebsetEnrichmentConfig]] = SchemaField( + search_entity_type: SearchEntityType = SchemaField( + default=SearchEntityType.AUTO, + description="Entity type: 'company', 'person', 'article', 'research_paper', or 'custom'. If not provided, we automatically detect the entity from the query.", + advanced=True, + ) + search_entity_description: Optional[str] = SchemaField( default=None, - description="Enrichments to apply to Webset items", + description="Description for custom entity type (required when search_entity_type is 'custom')", + advanced=True, + ) + + # Search criteria (flattened) + search_criteria: list[str] = SchemaField( + default_factory=list, + description="List of criteria descriptions that every item will be evaluated against. If not provided, we automatically detect the criteria from the query.", advanced=True, ) + + # Search exclude sources (flattened) + search_exclude_sources: list[str] = SchemaField( + default_factory=list, + description="List of source IDs (imports or websets) to exclude from search results", + advanced=True, + ) + search_exclude_types: list[SearchType] = SchemaField( + default_factory=list, + description="List of source types corresponding to exclude sources ('import' or 'webset')", + advanced=True, + ) + + # Search scope sources (flattened) + search_scope_sources: list[str] = SchemaField( + default_factory=list, + description="List of source IDs (imports or websets) to limit search scope to", + advanced=True, + ) + search_scope_types: list[SearchType] = SchemaField( + default_factory=list, + description="List of source types corresponding to scope sources ('import' or 'webset')", + advanced=True, + ) + search_scope_relationships: list[str] = SchemaField( + default_factory=list, + description="List of relationship definitions for hop searches (optional, one per scope source)", + advanced=True, + ) + search_scope_relationship_limits: list[int] = SchemaField( + default_factory=list, + description="List of limits on the number of related entities to find (optional, one per scope relationship)", + advanced=True, + ) + + # Import parameters (flattened) + import_sources: list[str] = SchemaField( + default_factory=list, + description="List of source IDs to import from", + advanced=True, + ) + import_types: list[SearchType] = SchemaField( + default_factory=list, + description="List of source types corresponding to import sources ('import' or 'webset')", + advanced=True, + ) + + # Enrichment parameters (flattened) + enrichment_descriptions: list[str] = SchemaField( + default_factory=list, + description="List of enrichment task descriptions to perform on each webset item", + advanced=True, + ) + enrichment_formats: list[EnrichmentFormat] = SchemaField( + default_factory=list, + description="List of formats for enrichment responses ('text', 'date', 'number', 'options', 'email', 'phone'). If not specified, we automatically select the best format.", + advanced=True, + ) + enrichment_options: list[list[str]] = SchemaField( + default_factory=list, + description="List of option lists for enrichments with 'options' format. Each inner list contains the option labels.", + advanced=True, + ) + enrichment_metadata: list[dict] = SchemaField( + default_factory=list, + description="List of metadata dictionaries for enrichments", + advanced=True, + ) + + # Webset metadata external_id: Optional[str] = SchemaField( default=None, - description="External identifier for the webset", + description="External identifier for the webset. You can use this to reference the webset by your own internal identifiers.", placeholder="my-webset-123", advanced=True, ) metadata: Optional[dict] = SchemaField( - default=None, + default_factory=dict, description="Key-value pairs to associate with this webset", advanced=True, ) class Output(BlockSchema): - webset_id: str = SchemaField( + webset: Webset = SchemaField( description="The unique identifier for the created webset" ) - status: str = SchemaField(description="The status of the webset") - external_id: Optional[str] = SchemaField( - description="The external identifier for the webset", default=None - ) - created_at: str = SchemaField( - description="The date and time the webset was created" - ) - error: str = SchemaField( - description="Error message if the request failed", default="" - ) def __init__(self): super().__init__( @@ -67,44 +236,171 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - url = "https://api.exa.ai/websets/v0/websets" - headers = { - "Content-Type": "application/json", - "x-api-key": credentials.api_key.get_secret_value(), - } - - # Build the payload - payload: dict[str, Any] = { - "search": input_data.search.model_dump(exclude_none=True), - } - - # Convert enrichments to API format - if input_data.enrichments: - enrichments_data = [] - for enrichment in input_data.enrichments: - enrichments_data.append(enrichment.model_dump(exclude_none=True)) - payload["enrichments"] = enrichments_data - - if input_data.external_id: - payload["externalId"] = input_data.external_id - - if input_data.metadata: - payload["metadata"] = input_data.metadata - try: - response = await Requests().post(url, headers=headers, json=payload) - data = response.json() - - yield "webset_id", data.get("id", "") - yield "status", data.get("status", "") - yield "external_id", data.get("externalId") - yield "created_at", data.get("createdAt", "") - - except Exception as e: - yield "error", str(e) - yield "webset_id", "" - yield "status", "" - yield "created_at", "" + exa = Exa(credentials.api_key.get_secret_value()) + + # ------------------------------------------------------------ + # Build entity (if explicitly provided) + # ------------------------------------------------------------ + entity = None + if input_data.search_entity_type == SearchEntityType.COMPANY: + entity = WebsetCompanyEntity(type="company") + elif input_data.search_entity_type == SearchEntityType.PERSON: + entity = WebsetPersonEntity(type="person") + elif input_data.search_entity_type == SearchEntityType.ARTICLE: + entity = WebsetArticleEntity(type="article") + elif input_data.search_entity_type == SearchEntityType.RESEARCH_PAPER: + entity = WebsetResearchPaperEntity(type="research_paper") + elif ( + input_data.search_entity_type == SearchEntityType.CUSTOM + and input_data.search_entity_description + ): + entity = WebsetCustomEntity( + type="custom", description=input_data.search_entity_description + ) + + # ------------------------------------------------------------ + # Build criteria list + # ------------------------------------------------------------ + criteria = None + if input_data.search_criteria: + criteria = [ + CreateCriterionParameters(description=item) + for item in input_data.search_criteria + ] + + # ------------------------------------------------------------ + # Build exclude sources list + # ------------------------------------------------------------ + exclude_items = None + if input_data.search_exclude_sources: + exclude_items = [] + for idx, src_id in enumerate(input_data.search_exclude_sources): + src_type = None + if input_data.search_exclude_types and idx < len( + input_data.search_exclude_types + ): + src_type = input_data.search_exclude_types[idx] + # Default to IMPORT if type missing + if src_type == SearchType.WEBSET: + source_enum = ImportSource.webset + else: + source_enum = ImportSource.import_ + exclude_items.append(ExcludeItem(source=source_enum, id=src_id)) + + # ------------------------------------------------------------ + # Build scope list + # ------------------------------------------------------------ + scope_items = None + if input_data.search_scope_sources: + scope_items = [] + for idx, src_id in enumerate(input_data.search_scope_sources): + src_type = None + if input_data.search_scope_types and idx < len( + input_data.search_scope_types + ): + src_type = input_data.search_scope_types[idx] + relationship = None + if input_data.search_scope_relationships and idx < len( + input_data.search_scope_relationships + ): + rel_def = input_data.search_scope_relationships[idx] + lim = None + if input_data.search_scope_relationship_limits and idx < len( + input_data.search_scope_relationship_limits + ): + lim = input_data.search_scope_relationship_limits[idx] + relationship = ScopeRelationship(definition=rel_def, limit=lim) + if src_type == SearchType.WEBSET: + src_enum = ScopeSourceType.webset + else: + src_enum = ScopeSourceType.import_ + scope_items.append( + ScopeItem(source=src_enum, id=src_id, relationship=relationship) + ) + + # ------------------------------------------------------------ + # Assemble search parameters (only if a query is provided) + # ------------------------------------------------------------ + search_params = None + if input_data.search_query: + search_params = CreateWebsetParametersSearch( + query=input_data.search_query, + count=input_data.search_count, + entity=entity, + criteria=criteria, + exclude=exclude_items, + scope=scope_items, + ) + + # ------------------------------------------------------------ + # Build imports list + # ------------------------------------------------------------ + imports_params = None + if input_data.import_sources: + imports_params = [] + for idx, src_id in enumerate(input_data.import_sources): + src_type = None + if input_data.import_types and idx < len(input_data.import_types): + src_type = input_data.import_types[idx] + if src_type == SearchType.WEBSET: + source_enum = ImportSource.webset + else: + source_enum = ImportSource.import_ + imports_params.append(ImportItem(source=source_enum, id=src_id)) + + # ------------------------------------------------------------ + # Build enrichment list + # ------------------------------------------------------------ + enrichments_params = None + if input_data.enrichment_descriptions: + enrichments_params = [] + for idx, desc in enumerate(input_data.enrichment_descriptions): + fmt = None + if input_data.enrichment_formats and idx < len( + input_data.enrichment_formats + ): + fmt_enum = input_data.enrichment_formats[idx] + if fmt_enum is not None: + fmt = Format( + fmt_enum.value if isinstance(fmt_enum, Enum) else fmt_enum + ) + options_list = None + if input_data.enrichment_options and idx < len( + input_data.enrichment_options + ): + raw_opts = input_data.enrichment_options[idx] + if raw_opts: + options_list = [Option(label=o) for o in raw_opts] + metadata_obj = None + if input_data.enrichment_metadata and idx < len( + input_data.enrichment_metadata + ): + metadata_obj = input_data.enrichment_metadata[idx] + enrichments_params.append( + CreateEnrichmentParameters( + description=desc, + format=fmt, + options=options_list, + metadata=metadata_obj, + ) + ) + + # ------------------------------------------------------------ + # Create the webset + # ------------------------------------------------------------ + webset = exa.websets.create( + params=CreateWebsetParameters( + search=search_params, + imports=imports_params, + enrichments=enrichments_params, + external_id=input_data.external_id, + metadata=input_data.metadata, + ) + ) + + # Use alias field names returned from Exa SDK so that nested models validate correctly + yield "webset", Webset.model_validate(webset.model_dump(by_alias=True)) class ExaUpdateWebsetBlock(Block): @@ -183,6 +479,11 @@ class Input(BlockSchema): credentials: CredentialsMetaInput = exa.credentials_field( description="The Exa integration requires an API Key." ) + trigger: Any | None = SchemaField( + default=None, + description="Trigger for the webset, value is ignored!", + advanced=False, + ) cursor: Optional[str] = SchemaField( default=None, description="Cursor for pagination through results", @@ -197,7 +498,9 @@ class Input(BlockSchema): ) class Output(BlockSchema): - websets: list = SchemaField(description="List of websets", default_factory=list) + websets: list[Webset] = SchemaField( + description="List of websets", default_factory=list + ) has_more: bool = SchemaField( description="Whether there are more results to paginate through", default=False, @@ -255,9 +558,6 @@ class Input(BlockSchema): description="The ID or external ID of the Webset to retrieve", placeholder="webset-id-or-external-id", ) - expand_items: bool = SchemaField( - default=False, description="Include items in the response", advanced=True - ) class Output(BlockSchema): webset_id: str = SchemaField(description="The unique identifier for the webset") @@ -309,12 +609,8 @@ async def run( "x-api-key": credentials.api_key.get_secret_value(), } - params = {} - if input_data.expand_items: - params["expand[]"] = "items" - try: - response = await Requests().get(url, headers=headers, params=params) + response = await Requests().get(url, headers=headers) data = response.json() yield "webset_id", data.get("id", "") diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/__init__.py b/autogpt_platform/backend/backend/blocks/firecrawl/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/_api.py b/autogpt_platform/backend/backend/blocks/firecrawl/_api.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/_config.py b/autogpt_platform/backend/backend/blocks/firecrawl/_config.py new file mode 100644 index 000000000000..cc176c4a86cb --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/_config.py @@ -0,0 +1,8 @@ +from backend.sdk import BlockCostType, ProviderBuilder + +firecrawl = ( + ProviderBuilder("firecrawl") + .with_api_key("FIRECRAWL_API_KEY", "Firecrawl API Key") + .with_base_cost(1, BlockCostType.RUN) + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/crawl.py b/autogpt_platform/backend/backend/blocks/firecrawl/crawl.py new file mode 100644 index 000000000000..6b8e1e0ad89b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/crawl.py @@ -0,0 +1,114 @@ +from enum import Enum +from typing import Any + +from firecrawl import FirecrawlApp, ScrapeOptions + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._config import firecrawl + + +class ScrapeFormat(Enum): + MARKDOWN = "markdown" + HTML = "html" + RAW_HTML = "rawHtml" + LINKS = "links" + SCREENSHOT = "screenshot" + SCREENSHOT_FULL_PAGE = "screenshot@fullPage" + JSON = "json" + CHANGE_TRACKING = "changeTracking" + + +class FirecrawlCrawlBlock(Block): + + class Input(BlockSchema): + credentials: CredentialsMetaInput = firecrawl.credentials_field() + url: str = SchemaField(description="The URL to crawl") + limit: int = SchemaField(description="The number of pages to crawl", default=10) + only_main_content: bool = SchemaField( + description="Only return the main content of the page excluding headers, navs, footers, etc.", + default=True, + ) + max_age: int = SchemaField( + description="The maximum age of the page in milliseconds - default is 1 hour", + default=3600000, + ) + wait_for: int = SchemaField( + description="Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load.", + default=0, + ) + formats: list[ScrapeFormat] = SchemaField( + description="The format of the crawl", default=[ScrapeFormat.MARKDOWN] + ) + + class Output(BlockSchema): + data: list[dict[str, Any]] = SchemaField(description="The result of the crawl") + markdown: str = SchemaField(description="The markdown of the crawl") + html: str = SchemaField(description="The html of the crawl") + raw_html: str = SchemaField(description="The raw html of the crawl") + links: list[str] = SchemaField(description="The links of the crawl") + screenshot: str = SchemaField(description="The screenshot of the crawl") + screenshot_full_page: str = SchemaField( + description="The screenshot full page of the crawl" + ) + json_data: dict[str, Any] = SchemaField( + description="The json data of the crawl" + ) + change_tracking: dict[str, Any] = SchemaField( + description="The change tracking of the crawl" + ) + + def __init__(self): + super().__init__( + id="bdbbaba0-03b7-4971-970e-699e2de6015e", + description="Firecrawl crawls websites to extract comprehensive data while bypassing blockers.", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + + app = FirecrawlApp(api_key=credentials.api_key.get_secret_value()) + + # Sync call + crawl_result = app.crawl_url( + input_data.url, + limit=input_data.limit, + scrape_options=ScrapeOptions( + formats=[format.value for format in input_data.formats], + onlyMainContent=input_data.only_main_content, + maxAge=input_data.max_age, + waitFor=input_data.wait_for, + ), + ) + yield "data", crawl_result.data + + for data in crawl_result.data: + for f in input_data.formats: + if f == ScrapeFormat.MARKDOWN: + yield "markdown", data.markdown + elif f == ScrapeFormat.HTML: + yield "html", data.html + elif f == ScrapeFormat.RAW_HTML: + yield "raw_html", data.rawHtml + elif f == ScrapeFormat.LINKS: + yield "links", data.links + elif f == ScrapeFormat.SCREENSHOT: + yield "screenshot", data.screenshot + elif f == ScrapeFormat.SCREENSHOT_FULL_PAGE: + yield "screenshot_full_page", data.screenshot + elif f == ScrapeFormat.CHANGE_TRACKING: + yield "change_tracking", data.changeTracking + elif f == ScrapeFormat.JSON: + yield "json", data.json diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/extract.py b/autogpt_platform/backend/backend/blocks/firecrawl/extract.py new file mode 100755 index 000000000000..8f3b507bec67 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/extract.py @@ -0,0 +1,66 @@ +from typing import Any + +from firecrawl import FirecrawlApp + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockCost, + BlockCostType, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, + cost, +) + +from ._config import firecrawl + + +@cost(BlockCost(2, BlockCostType.RUN)) +class FirecrawlExtractBlock(Block): + + class Input(BlockSchema): + credentials: CredentialsMetaInput = firecrawl.credentials_field() + urls: list[str] = SchemaField( + description="The URLs to crawl - at least one is required. Wildcards are supported. (/*)" + ) + prompt: str | None = SchemaField( + description="The prompt to use for the crawl", default=None, advanced=False + ) + output_schema: dict | None = SchemaField( + description="A Json Schema describing the output structure if more rigid structure is desired.", + default=None, + ) + enable_web_search: bool = SchemaField( + description="When true, extraction can follow links outside the specified domain.", + default=False, + ) + + class Output(BlockSchema): + data: dict[str, Any] = SchemaField(description="The result of the crawl") + + def __init__(self): + super().__init__( + id="d1774756-4d9e-40e6-bab1-47ec0ccd81b2", + description="Firecrawl crawls websites to extract comprehensive data while bypassing blockers.", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + + app = FirecrawlApp(api_key=credentials.api_key.get_secret_value()) + + extract_result = app.extract( + urls=input_data.urls, + prompt=input_data.prompt, + schema=input_data.output_schema, + enable_web_search=input_data.enable_web_search, + ) + + yield "data", extract_result.data diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/map.py b/autogpt_platform/backend/backend/blocks/firecrawl/map.py new file mode 100644 index 000000000000..7661377901b9 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/map.py @@ -0,0 +1,46 @@ +from firecrawl import FirecrawlApp + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._config import firecrawl + + +class FirecrawlMapWebsiteBlock(Block): + + class Input(BlockSchema): + credentials: CredentialsMetaInput = firecrawl.credentials_field() + + url: str = SchemaField(description="The website url to map") + + class Output(BlockSchema): + links: list[str] = SchemaField(description="The links of the website") + + def __init__(self): + super().__init__( + id="f0f43e2b-c943-48a0-a7f1-40136ca4d3b9", + description="Firecrawl maps a website to extract all the links.", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + + app = FirecrawlApp(api_key=credentials.api_key.get_secret_value()) + + # Sync call + map_result = app.map_url( + url=input_data.url, + ) + + yield "links", map_result.links diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/scrape.py b/autogpt_platform/backend/backend/blocks/firecrawl/scrape.py new file mode 100644 index 000000000000..65627ad9545b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/scrape.py @@ -0,0 +1,109 @@ +from enum import Enum +from typing import Any + +from firecrawl import FirecrawlApp + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._config import firecrawl + + +class ScrapeFormat(Enum): + MARKDOWN = "markdown" + HTML = "html" + RAW_HTML = "rawHtml" + LINKS = "links" + SCREENSHOT = "screenshot" + SCREENSHOT_FULL_PAGE = "screenshot@fullPage" + JSON = "json" + CHANGE_TRACKING = "changeTracking" + + +class FirecrawlScrapeBlock(Block): + + class Input(BlockSchema): + credentials: CredentialsMetaInput = firecrawl.credentials_field() + url: str = SchemaField(description="The URL to crawl") + limit: int = SchemaField(description="The number of pages to crawl", default=10) + only_main_content: bool = SchemaField( + description="Only return the main content of the page excluding headers, navs, footers, etc.", + default=True, + ) + max_age: int = SchemaField( + description="The maximum age of the page in milliseconds - default is 1 hour", + default=3600000, + ) + wait_for: int = SchemaField( + description="Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load.", + default=200, + ) + formats: list[ScrapeFormat] = SchemaField( + description="The format of the crawl", default=[ScrapeFormat.MARKDOWN] + ) + + class Output(BlockSchema): + data: dict[str, Any] = SchemaField(description="The result of the crawl") + markdown: str = SchemaField(description="The markdown of the crawl") + html: str = SchemaField(description="The html of the crawl") + raw_html: str = SchemaField(description="The raw html of the crawl") + links: list[str] = SchemaField(description="The links of the crawl") + screenshot: str = SchemaField(description="The screenshot of the crawl") + screenshot_full_page: str = SchemaField( + description="The screenshot full page of the crawl" + ) + json_data: dict[str, Any] = SchemaField( + description="The json data of the crawl" + ) + change_tracking: dict[str, Any] = SchemaField( + description="The change tracking of the crawl" + ) + + def __init__(self): + super().__init__( + id="ac444320-cf5e-4697-b586-2604c17a3e75", + description="Firecrawl scrapes a website to extract comprehensive data while bypassing blockers.", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + + app = FirecrawlApp(api_key=credentials.api_key.get_secret_value()) + + scrape_result = app.scrape_url( + input_data.url, + formats=[format.value for format in input_data.formats], + only_main_content=input_data.only_main_content, + max_age=input_data.max_age, + wait_for=input_data.wait_for, + ) + yield "data", scrape_result + + for f in input_data.formats: + if f == ScrapeFormat.MARKDOWN: + yield "markdown", scrape_result.markdown + elif f == ScrapeFormat.HTML: + yield "html", scrape_result.html + elif f == ScrapeFormat.RAW_HTML: + yield "raw_html", scrape_result.rawHtml + elif f == ScrapeFormat.LINKS: + yield "links", scrape_result.links + elif f == ScrapeFormat.SCREENSHOT: + yield "screenshot", scrape_result.screenshot + elif f == ScrapeFormat.SCREENSHOT_FULL_PAGE: + yield "screenshot_full_page", scrape_result.screenshot + elif f == ScrapeFormat.CHANGE_TRACKING: + yield "change_tracking", scrape_result.changeTracking + elif f == ScrapeFormat.JSON: + yield "json", scrape_result.json diff --git a/autogpt_platform/backend/backend/blocks/firecrawl/search.py b/autogpt_platform/backend/backend/blocks/firecrawl/search.py new file mode 100644 index 000000000000..521d0f7e35d5 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/firecrawl/search.py @@ -0,0 +1,79 @@ +from enum import Enum +from typing import Any + +from firecrawl import FirecrawlApp, ScrapeOptions + +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +from ._config import firecrawl + + +class ScrapeFormat(Enum): + MARKDOWN = "markdown" + HTML = "html" + RAW_HTML = "rawHtml" + LINKS = "links" + SCREENSHOT = "screenshot" + SCREENSHOT_FULL_PAGE = "screenshot@fullPage" + JSON = "json" + CHANGE_TRACKING = "changeTracking" + + +class FirecrawlSearchBlock(Block): + + class Input(BlockSchema): + credentials: CredentialsMetaInput = firecrawl.credentials_field() + query: str = SchemaField(description="The query to search for") + limit: int = SchemaField(description="The number of pages to crawl", default=10) + max_age: int = SchemaField( + description="The maximum age of the page in milliseconds - default is 1 hour", + default=3600000, + ) + wait_for: int = SchemaField( + description="Specify a delay in milliseconds before fetching the content, allowing the page sufficient time to load.", + default=200, + ) + formats: list[ScrapeFormat] = SchemaField( + description="Returns the content of the search if specified", default=[] + ) + + class Output(BlockSchema): + data: dict[str, Any] = SchemaField(description="The result of the search") + site: dict[str, Any] = SchemaField(description="The site of the search") + + def __init__(self): + super().__init__( + id="f8d2f28d-b3a1-405b-804e-418c087d288b", + description="Firecrawl searches the web for the given query.", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + + app = FirecrawlApp(api_key=credentials.api_key.get_secret_value()) + + # Sync call + scrape_result = app.search( + input_data.query, + limit=input_data.limit, + scrape_options=ScrapeOptions( + formats=[format.value for format in input_data.formats], + maxAge=input_data.max_age, + waitFor=input_data.wait_for, + ), + ) + yield "data", scrape_result + for site in scrape_result.data: + yield "site", site diff --git a/autogpt_platform/backend/backend/blocks/generic_webhook/_webhook.py b/autogpt_platform/backend/backend/blocks/generic_webhook/_webhook.py index 8a8b55a8eb67..9dd4b8c2c7ca 100644 --- a/autogpt_platform/backend/backend/blocks/generic_webhook/_webhook.py +++ b/autogpt_platform/backend/backend/blocks/generic_webhook/_webhook.py @@ -3,7 +3,7 @@ from fastapi import Request from strenum import StrEnum -from backend.sdk import ManualWebhookManagerBase, Webhook +from backend.sdk import Credentials, ManualWebhookManagerBase, Webhook logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ class GenericWebhooksManager(ManualWebhookManagerBase): @classmethod async def validate_payload( - cls, webhook: Webhook, request: Request + cls, webhook: Webhook, request: Request, credentials: Credentials | None = None ) -> tuple[dict, str]: payload = await request.json() event_type = GenericWebhookType.PLAIN diff --git a/autogpt_platform/backend/backend/blocks/github/ci.py b/autogpt_platform/backend/backend/blocks/github/ci.py new file mode 100644 index 000000000000..25adc04202d5 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/ci.py @@ -0,0 +1,388 @@ +import logging +import re +from enum import Enum +from typing import Optional + +from typing_extensions import TypedDict + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +from ._api import get_api +from ._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + GithubCredentials, + GithubCredentialsField, + GithubCredentialsInput, +) + +logger = logging.getLogger(__name__) + + +class CheckRunStatus(Enum): + QUEUED = "queued" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + + +class CheckRunConclusion(Enum): + SUCCESS = "success" + FAILURE = "failure" + NEUTRAL = "neutral" + CANCELLED = "cancelled" + SKIPPED = "skipped" + TIMED_OUT = "timed_out" + ACTION_REQUIRED = "action_required" + + +class GithubGetCIResultsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + target: str | int = SchemaField( + description="Commit SHA or PR number to get CI results for", + placeholder="abc123def or 123", + ) + search_pattern: Optional[str] = SchemaField( + description="Optional regex pattern to search for in CI logs (e.g., error messages, file names)", + placeholder=".*error.*|.*warning.*", + default=None, + advanced=True, + ) + check_name_filter: Optional[str] = SchemaField( + description="Optional filter for specific check names (supports wildcards)", + placeholder="*lint* or build-*", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + class CheckRunItem(TypedDict, total=False): + id: int + name: str + status: str + conclusion: Optional[str] + started_at: Optional[str] + completed_at: Optional[str] + html_url: str + details_url: Optional[str] + output_title: Optional[str] + output_summary: Optional[str] + output_text: Optional[str] + annotations: list[dict] + + class MatchedLine(TypedDict): + check_name: str + line_number: int + line: str + context: list[str] + + check_run: CheckRunItem = SchemaField( + title="Check Run", + description="Individual CI check run with details", + ) + check_runs: list[CheckRunItem] = SchemaField( + description="List of all CI check runs" + ) + matched_line: MatchedLine = SchemaField( + title="Matched Line", + description="Line matching the search pattern with context", + ) + matched_lines: list[MatchedLine] = SchemaField( + description="All lines matching the search pattern across all checks" + ) + overall_status: str = SchemaField( + description="Overall CI status (pending, success, failure)" + ) + overall_conclusion: str = SchemaField( + description="Overall CI conclusion if completed" + ) + total_checks: int = SchemaField(description="Total number of CI checks") + passed_checks: int = SchemaField(description="Number of passed checks") + failed_checks: int = SchemaField(description="Number of failed checks") + error: str = SchemaField(description="Error message if the operation failed") + + def __init__(self): + super().__init__( + id="8ad9e103-78f2-4fdb-ba12-3571f2c95e98", + description="This block gets CI results for a commit or PR, with optional search for specific errors/warnings in logs.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubGetCIResultsBlock.Input, + output_schema=GithubGetCIResultsBlock.Output, + test_input={ + "repo": "owner/repo", + "target": "abc123def456", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("overall_status", "completed"), + ("overall_conclusion", "success"), + ("total_checks", 1), + ("passed_checks", 1), + ("failed_checks", 0), + ( + "check_runs", + [ + { + "id": 123456, + "name": "build", + "status": "completed", + "conclusion": "success", + "started_at": "2024-01-01T00:00:00Z", + "completed_at": "2024-01-01T00:05:00Z", + "html_url": "https://github.com/owner/repo/runs/123456", + "details_url": None, + "output_title": "Build passed", + "output_summary": "All tests passed", + "output_text": "Build log output...", + "annotations": [], + } + ], + ), + ], + test_mock={ + "get_ci_results": lambda *args, **kwargs: { + "check_runs": [ + { + "id": 123456, + "name": "build", + "status": "completed", + "conclusion": "success", + "started_at": "2024-01-01T00:00:00Z", + "completed_at": "2024-01-01T00:05:00Z", + "html_url": "https://github.com/owner/repo/runs/123456", + "details_url": None, + "output_title": "Build passed", + "output_summary": "All tests passed", + "output_text": "Build log output...", + "annotations": [], + } + ], + "total_count": 1, + } + }, + ) + + @staticmethod + async def get_commit_sha(api, repo: str, target: str | int) -> str: + """Get commit SHA from either a commit SHA or PR URL.""" + # If it's already a SHA, return it + + if isinstance(target, str): + if re.match(r"^[0-9a-f]{6,40}$", target, re.IGNORECASE): + return target + + # If it's a PR URL, get the head SHA + if isinstance(target, int): + pr_url = f"https://api.github.com/repos/{repo}/pulls/{target}" + response = await api.get(pr_url) + pr_data = response.json() + return pr_data["head"]["sha"] + + raise ValueError("Target must be a commit SHA or PR URL") + + @staticmethod + async def search_in_logs( + check_runs: list, + pattern: str, + ) -> list[Output.MatchedLine]: + """Search for pattern in check run logs.""" + if not pattern: + return [] + + matched_lines = [] + regex = re.compile(pattern, re.IGNORECASE | re.MULTILINE) + + for check in check_runs: + output_text = check.get("output_text", "") or "" + if not output_text: + continue + + lines = output_text.split("\n") + for i, line in enumerate(lines): + if regex.search(line): + # Get context (2 lines before and after) + start = max(0, i - 2) + end = min(len(lines), i + 3) + context = lines[start:end] + + matched_lines.append( + { + "check_name": check["name"], + "line_number": i + 1, + "line": line, + "context": context, + } + ) + + return matched_lines + + @staticmethod + async def get_ci_results( + credentials: GithubCredentials, + repo: str, + target: str | int, + search_pattern: Optional[str] = None, + check_name_filter: Optional[str] = None, + ) -> dict: + api = get_api(credentials, convert_urls=False) + + # Get the commit SHA + commit_sha = await GithubGetCIResultsBlock.get_commit_sha(api, repo, target) + + # Get check runs for the commit + check_runs_url = ( + f"https://api.github.com/repos/{repo}/commits/{commit_sha}/check-runs" + ) + + # Get all pages of check runs + all_check_runs = [] + page = 1 + per_page = 100 + + while True: + response = await api.get( + check_runs_url, params={"per_page": per_page, "page": page} + ) + data = response.json() + + check_runs = data.get("check_runs", []) + all_check_runs.extend(check_runs) + + if len(check_runs) < per_page: + break + page += 1 + + # Filter by check name if specified + if check_name_filter: + import fnmatch + + filtered_runs = [] + for run in all_check_runs: + if fnmatch.fnmatch(run["name"].lower(), check_name_filter.lower()): + filtered_runs.append(run) + all_check_runs = filtered_runs + + # Get check run details with logs + detailed_runs = [] + for run in all_check_runs: + # Get detailed output including logs + if run.get("output", {}).get("text"): + # Already has output + detailed_run = { + "id": run["id"], + "name": run["name"], + "status": run["status"], + "conclusion": run.get("conclusion"), + "started_at": run.get("started_at"), + "completed_at": run.get("completed_at"), + "html_url": run["html_url"], + "details_url": run.get("details_url"), + "output_title": run.get("output", {}).get("title"), + "output_summary": run.get("output", {}).get("summary"), + "output_text": run.get("output", {}).get("text"), + "annotations": [], + } + else: + # Try to get logs from the check run + detailed_run = { + "id": run["id"], + "name": run["name"], + "status": run["status"], + "conclusion": run.get("conclusion"), + "started_at": run.get("started_at"), + "completed_at": run.get("completed_at"), + "html_url": run["html_url"], + "details_url": run.get("details_url"), + "output_title": run.get("output", {}).get("title"), + "output_summary": run.get("output", {}).get("summary"), + "output_text": None, + "annotations": [], + } + + # Get annotations if available + if run.get("output", {}).get("annotations_count", 0) > 0: + annotations_url = f"https://api.github.com/repos/{repo}/check-runs/{run['id']}/annotations" + try: + ann_response = await api.get(annotations_url) + detailed_run["annotations"] = ann_response.json() + except Exception: + pass + + detailed_runs.append(detailed_run) + + return { + "check_runs": detailed_runs, + "total_count": len(detailed_runs), + } + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + + try: + target = int(input_data.target) + except ValueError: + target = input_data.target + + result = await self.get_ci_results( + credentials, + input_data.repo, + target, + input_data.search_pattern, + input_data.check_name_filter, + ) + + check_runs = result["check_runs"] + + # Calculate overall status + if not check_runs: + yield "overall_status", "no_checks" + yield "overall_conclusion", "no_checks" + else: + all_completed = all(run["status"] == "completed" for run in check_runs) + if all_completed: + yield "overall_status", "completed" + # Determine overall conclusion + has_failure = any( + run["conclusion"] in ["failure", "timed_out", "action_required"] + for run in check_runs + ) + if has_failure: + yield "overall_conclusion", "failure" + else: + yield "overall_conclusion", "success" + else: + yield "overall_status", "pending" + yield "overall_conclusion", "pending" + + # Count checks + total = len(check_runs) + passed = sum(1 for run in check_runs if run.get("conclusion") == "success") + failed = sum( + 1 for run in check_runs if run.get("conclusion") in ["failure", "timed_out"] + ) + + yield "total_checks", total + yield "passed_checks", passed + yield "failed_checks", failed + + # Output check runs + yield "check_runs", check_runs + + # Search for patterns if specified + if input_data.search_pattern: + matched_lines = await self.search_in_logs( + check_runs, input_data.search_pattern + ) + if matched_lines: + yield "matched_lines", matched_lines diff --git a/autogpt_platform/backend/backend/blocks/github/reviews.py b/autogpt_platform/backend/backend/blocks/github/reviews.py new file mode 100644 index 000000000000..2b909da8ffd1 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/reviews.py @@ -0,0 +1,840 @@ +import logging +from enum import Enum +from typing import Any, List, Optional + +from typing_extensions import TypedDict + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +from ._api import get_api +from ._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + GithubCredentials, + GithubCredentialsField, + GithubCredentialsInput, +) + +logger = logging.getLogger(__name__) + + +class ReviewEvent(Enum): + COMMENT = "COMMENT" + APPROVE = "APPROVE" + REQUEST_CHANGES = "REQUEST_CHANGES" + + +class GithubCreatePRReviewBlock(Block): + class Input(BlockSchema): + class ReviewComment(TypedDict, total=False): + path: str + position: Optional[int] + body: str + line: Optional[int] # Will be used as position if position not provided + + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + pr_number: int = SchemaField( + description="Pull request number", + placeholder="123", + ) + body: str = SchemaField( + description="Body of the review comment", + placeholder="Enter your review comment", + ) + event: ReviewEvent = SchemaField( + description="The review action to perform", + default=ReviewEvent.COMMENT, + ) + create_as_draft: bool = SchemaField( + description="Create the review as a draft (pending) or post it immediately", + default=False, + advanced=False, + ) + comments: Optional[List[ReviewComment]] = SchemaField( + description="Optional inline comments to add to specific files/lines. Note: Only path, body, and position are supported. Position is line number in diff from first @@ hunk.", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + review_id: int = SchemaField(description="ID of the created review") + state: str = SchemaField( + description="State of the review (e.g., PENDING, COMMENTED, APPROVED, CHANGES_REQUESTED)" + ) + html_url: str = SchemaField(description="URL of the created review") + error: str = SchemaField( + description="Error message if the review creation failed" + ) + + def __init__(self): + super().__init__( + id="84754b30-97d2-4c37-a3b8-eb39f268275b", + description="This block creates a review on a GitHub pull request with optional inline comments. You can create it as a draft or post immediately. Note: For inline comments, 'position' should be the line number in the diff (starting from the first @@ hunk header).", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubCreatePRReviewBlock.Input, + output_schema=GithubCreatePRReviewBlock.Output, + test_input={ + "repo": "owner/repo", + "pr_number": 1, + "body": "This looks good to me!", + "event": "APPROVE", + "create_as_draft": False, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("review_id", 123456), + ("state", "APPROVED"), + ( + "html_url", + "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + ), + ], + test_mock={ + "create_review": lambda *args, **kwargs: ( + 123456, + "APPROVED", + "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + ) + }, + ) + + @staticmethod + async def create_review( + credentials: GithubCredentials, + repo: str, + pr_number: int, + body: str, + event: ReviewEvent, + create_as_draft: bool, + comments: Optional[List[Input.ReviewComment]] = None, + ) -> tuple[int, str, str]: + api = get_api(credentials, convert_urls=False) + + # GitHub API endpoint for creating reviews + reviews_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews" + + # Get commit_id if we have comments + commit_id = None + if comments: + # Get PR details to get the head commit for inline comments + pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" + pr_response = await api.get(pr_url) + pr_data = pr_response.json() + commit_id = pr_data["head"]["sha"] + + # Prepare the request data + # If create_as_draft is True, omit the event field (creates a PENDING review) + # Otherwise, use the actual event value which will auto-submit the review + data: dict[str, Any] = {"body": body} + + # Add commit_id if we have it + if commit_id: + data["commit_id"] = commit_id + + # Add comments if provided + if comments: + # Process comments to ensure they have the required fields + processed_comments = [] + for comment in comments: + comment_data: dict = { + "path": comment.get("path", ""), + "body": comment.get("body", ""), + } + # Add position or line + # Note: For review comments, only position is supported (not line/side) + if "position" in comment and comment.get("position") is not None: + comment_data["position"] = comment.get("position") + elif "line" in comment and comment.get("line") is not None: + # Note: Using line as position - may not work correctly + # Position should be calculated from the diff + comment_data["position"] = comment.get("line") + + # Note: side, start_line, and start_side are NOT supported for review comments + # They are only for standalone PR comments + + processed_comments.append(comment_data) + + data["comments"] = processed_comments + + if not create_as_draft: + # Only add event field if not creating a draft + data["event"] = event.value + + # Create the review + response = await api.post(reviews_url, json=data) + review_data = response.json() + + return review_data["id"], review_data["state"], review_data["html_url"] + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + review_id, state, html_url = await self.create_review( + credentials, + input_data.repo, + input_data.pr_number, + input_data.body, + input_data.event, + input_data.create_as_draft, + input_data.comments, + ) + yield "review_id", review_id + yield "state", state + yield "html_url", html_url + except Exception as e: + yield "error", str(e) + + +class GithubListPRReviewsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + pr_number: int = SchemaField( + description="Pull request number", + placeholder="123", + ) + + class Output(BlockSchema): + class ReviewItem(TypedDict): + id: int + user: str + state: str + body: str + html_url: str + + review: ReviewItem = SchemaField( + title="Review", + description="Individual review with details", + ) + reviews: list[ReviewItem] = SchemaField( + description="List of all reviews on the pull request" + ) + error: str = SchemaField(description="Error message if listing reviews failed") + + def __init__(self): + super().__init__( + id="f79bc6eb-33c0-4099-9c0f-d664ae1ba4d0", + description="This block lists all reviews for a specified GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListPRReviewsBlock.Input, + output_schema=GithubListPRReviewsBlock.Output, + test_input={ + "repo": "owner/repo", + "pr_number": 1, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "reviews", + [ + { + "id": 123456, + "user": "reviewer1", + "state": "APPROVED", + "body": "Looks good!", + "html_url": "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + } + ], + ), + ( + "review", + { + "id": 123456, + "user": "reviewer1", + "state": "APPROVED", + "body": "Looks good!", + "html_url": "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + }, + ), + ], + test_mock={ + "list_reviews": lambda *args, **kwargs: [ + { + "id": 123456, + "user": "reviewer1", + "state": "APPROVED", + "body": "Looks good!", + "html_url": "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + } + ] + }, + ) + + @staticmethod + async def list_reviews( + credentials: GithubCredentials, repo: str, pr_number: int + ) -> list[Output.ReviewItem]: + api = get_api(credentials, convert_urls=False) + + # GitHub API endpoint for listing reviews + reviews_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews" + + response = await api.get(reviews_url) + data = response.json() + + reviews: list[GithubListPRReviewsBlock.Output.ReviewItem] = [ + { + "id": review["id"], + "user": review["user"]["login"], + "state": review["state"], + "body": review.get("body", ""), + "html_url": review["html_url"], + } + for review in data + ] + return reviews + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + reviews = await self.list_reviews( + credentials, + input_data.repo, + input_data.pr_number, + ) + yield "reviews", reviews + for review in reviews: + yield "review", review + + +class GithubSubmitPendingReviewBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + pr_number: int = SchemaField( + description="Pull request number", + placeholder="123", + ) + review_id: int = SchemaField( + description="ID of the pending review to submit", + placeholder="123456", + ) + event: ReviewEvent = SchemaField( + description="The review action to perform when submitting", + default=ReviewEvent.COMMENT, + ) + + class Output(BlockSchema): + state: str = SchemaField(description="State of the submitted review") + html_url: str = SchemaField(description="URL of the submitted review") + error: str = SchemaField( + description="Error message if the review submission failed" + ) + + def __init__(self): + super().__init__( + id="2e468217-7ca0-4201-9553-36e93eb9357a", + description="This block submits a pending (draft) review on a GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubSubmitPendingReviewBlock.Input, + output_schema=GithubSubmitPendingReviewBlock.Output, + test_input={ + "repo": "owner/repo", + "pr_number": 1, + "review_id": 123456, + "event": "APPROVE", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("state", "APPROVED"), + ( + "html_url", + "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + ), + ], + test_mock={ + "submit_review": lambda *args, **kwargs: ( + "APPROVED", + "https://github.com/owner/repo/pull/1#pullrequestreview-123456", + ) + }, + ) + + @staticmethod + async def submit_review( + credentials: GithubCredentials, + repo: str, + pr_number: int, + review_id: int, + event: ReviewEvent, + ) -> tuple[str, str]: + api = get_api(credentials, convert_urls=False) + + # GitHub API endpoint for submitting a review + submit_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews/{review_id}/events" + + data = {"event": event.value} + + response = await api.post(submit_url, json=data) + review_data = response.json() + + return review_data["state"], review_data["html_url"] + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + state, html_url = await self.submit_review( + credentials, + input_data.repo, + input_data.pr_number, + input_data.review_id, + input_data.event, + ) + yield "state", state + yield "html_url", html_url + except Exception as e: + yield "error", str(e) + + +class GithubResolveReviewDiscussionBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + pr_number: int = SchemaField( + description="Pull request number", + placeholder="123", + ) + comment_id: int = SchemaField( + description="ID of the review comment to resolve/unresolve", + placeholder="123456", + ) + resolve: bool = SchemaField( + description="Whether to resolve (true) or unresolve (false) the discussion", + default=True, + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the operation was successful") + error: str = SchemaField(description="Error message if the operation failed") + + def __init__(self): + super().__init__( + id="b4b8a38c-95ae-4c91-9ef8-c2cffaf2b5d1", + description="This block resolves or unresolves a review discussion thread on a GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubResolveReviewDiscussionBlock.Input, + output_schema=GithubResolveReviewDiscussionBlock.Output, + test_input={ + "repo": "owner/repo", + "pr_number": 1, + "comment_id": 123456, + "resolve": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("success", True), + ], + test_mock={"resolve_discussion": lambda *args, **kwargs: True}, + ) + + @staticmethod + async def resolve_discussion( + credentials: GithubCredentials, + repo: str, + pr_number: int, + comment_id: int, + resolve: bool, + ) -> bool: + api = get_api(credentials, convert_urls=False) + + # Extract owner and repo name + parts = repo.split("/") + owner = parts[0] + repo_name = parts[1] + + # GitHub GraphQL API is needed for resolving/unresolving discussions + # First, we need to get the node ID of the comment + graphql_url = "https://api.github.com/graphql" + + # Query to get the review comment node ID + query = """ + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + reviewThreads(first: 100) { + nodes { + comments(first: 100) { + nodes { + databaseId + id + } + } + id + isResolved + } + } + } + } + } + """ + + variables = {"owner": owner, "repo": repo_name, "number": pr_number} + + response = await api.post( + graphql_url, json={"query": query, "variables": variables} + ) + data = response.json() + + # Find the thread containing our comment + thread_id = None + for thread in data["data"]["repository"]["pullRequest"]["reviewThreads"][ + "nodes" + ]: + for comment in thread["comments"]["nodes"]: + if comment["databaseId"] == comment_id: + thread_id = thread["id"] + break + if thread_id: + break + + if not thread_id: + raise ValueError(f"Comment {comment_id} not found in pull request") + + # Now resolve or unresolve the thread + # GitHub's GraphQL API has separate mutations for resolve and unresolve + if resolve: + mutation = """ + mutation($threadId: ID!) { + resolveReviewThread(input: {threadId: $threadId}) { + thread { + isResolved + } + } + } + """ + else: + mutation = """ + mutation($threadId: ID!) { + unresolveReviewThread(input: {threadId: $threadId}) { + thread { + isResolved + } + } + } + """ + + mutation_variables = {"threadId": thread_id} + + response = await api.post( + graphql_url, json={"query": mutation, "variables": mutation_variables} + ) + result = response.json() + + if "errors" in result: + raise Exception(f"GraphQL error: {result['errors']}") + + return True + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = await self.resolve_discussion( + credentials, + input_data.repo, + input_data.pr_number, + input_data.comment_id, + input_data.resolve, + ) + yield "success", success + except Exception as e: + yield "success", False + yield "error", str(e) + + +class GithubGetPRReviewCommentsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo: str = SchemaField( + description="GitHub repository", + placeholder="owner/repo", + ) + pr_number: int = SchemaField( + description="Pull request number", + placeholder="123", + ) + review_id: Optional[int] = SchemaField( + description="ID of a specific review to get comments from (optional)", + placeholder="123456", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + class CommentItem(TypedDict): + id: int + user: str + body: str + path: str + line: int + side: str + created_at: str + updated_at: str + in_reply_to_id: Optional[int] + html_url: str + + comment: CommentItem = SchemaField( + title="Comment", + description="Individual review comment with details", + ) + comments: list[CommentItem] = SchemaField( + description="List of all review comments on the pull request" + ) + error: str = SchemaField(description="Error message if getting comments failed") + + def __init__(self): + super().__init__( + id="1d34db7f-10c1-45c1-9d43-749f743c8bd4", + description="This block gets all review comments from a GitHub pull request or from a specific review.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubGetPRReviewCommentsBlock.Input, + output_schema=GithubGetPRReviewCommentsBlock.Output, + test_input={ + "repo": "owner/repo", + "pr_number": 1, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "comments", + [ + { + "id": 123456, + "user": "reviewer1", + "body": "This needs improvement", + "path": "src/main.py", + "line": 42, + "side": "RIGHT", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "in_reply_to_id": None, + "html_url": "https://github.com/owner/repo/pull/1#discussion_r123456", + } + ], + ), + ( + "comment", + { + "id": 123456, + "user": "reviewer1", + "body": "This needs improvement", + "path": "src/main.py", + "line": 42, + "side": "RIGHT", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "in_reply_to_id": None, + "html_url": "https://github.com/owner/repo/pull/1#discussion_r123456", + }, + ), + ], + test_mock={ + "get_comments": lambda *args, **kwargs: [ + { + "id": 123456, + "user": "reviewer1", + "body": "This needs improvement", + "path": "src/main.py", + "line": 42, + "side": "RIGHT", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "in_reply_to_id": None, + "html_url": "https://github.com/owner/repo/pull/1#discussion_r123456", + } + ] + }, + ) + + @staticmethod + async def get_comments( + credentials: GithubCredentials, + repo: str, + pr_number: int, + review_id: Optional[int] = None, + ) -> list[Output.CommentItem]: + api = get_api(credentials, convert_urls=False) + + # Determine the endpoint based on whether we want comments from a specific review + if review_id: + # Get comments from a specific review + comments_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews/{review_id}/comments" + else: + # Get all review comments on the PR + comments_url = ( + f"https://api.github.com/repos/{repo}/pulls/{pr_number}/comments" + ) + + response = await api.get(comments_url) + data = response.json() + + comments: list[GithubGetPRReviewCommentsBlock.Output.CommentItem] = [ + { + "id": comment["id"], + "user": comment["user"]["login"], + "body": comment["body"], + "path": comment.get("path", ""), + "line": comment.get("line", 0), + "side": comment.get("side", ""), + "created_at": comment["created_at"], + "updated_at": comment["updated_at"], + "in_reply_to_id": comment.get("in_reply_to_id"), + "html_url": comment["html_url"], + } + for comment in data + ] + return comments + + async def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + comments = await self.get_comments( + credentials, + input_data.repo, + input_data.pr_number, + input_data.review_id, + ) + yield "comments", comments + for comment in comments: + yield "comment", comment + except Exception as e: + yield "error", str(e) + + +class GithubCreateCommentObjectBlock(Block): + class Input(BlockSchema): + path: str = SchemaField( + description="The file path to comment on", + placeholder="src/main.py", + ) + body: str = SchemaField( + description="The comment text", + placeholder="Please fix this issue", + ) + position: Optional[int] = SchemaField( + description="Position in the diff (line number from first @@ hunk). Use this OR line.", + placeholder="6", + default=None, + advanced=True, + ) + line: Optional[int] = SchemaField( + description="Line number in the file (will be used as position if position not provided)", + placeholder="42", + default=None, + advanced=True, + ) + side: Optional[str] = SchemaField( + description="Side of the diff to comment on (NOTE: Only for standalone comments, not review comments)", + default="RIGHT", + advanced=True, + ) + start_line: Optional[int] = SchemaField( + description="Start line for multi-line comments (NOTE: Only for standalone comments, not review comments)", + default=None, + advanced=True, + ) + start_side: Optional[str] = SchemaField( + description="Side for the start of multi-line comments (NOTE: Only for standalone comments, not review comments)", + default=None, + advanced=True, + ) + + class Output(BlockSchema): + comment_object: dict = SchemaField( + description="The comment object formatted for GitHub API" + ) + + def __init__(self): + super().__init__( + id="b7d5e4f2-8c3a-4e6b-9f1d-7a8b9c5e4d3f", + description="Creates a comment object for use with GitHub blocks. Note: For review comments, only path, body, and position are used. Side fields are only for standalone PR comments.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubCreateCommentObjectBlock.Input, + output_schema=GithubCreateCommentObjectBlock.Output, + test_input={ + "path": "src/main.py", + "body": "Please fix this issue", + "position": 6, + }, + test_output=[ + ( + "comment_object", + { + "path": "src/main.py", + "body": "Please fix this issue", + "position": 6, + }, + ), + ], + ) + + async def run( + self, + input_data: Input, + **kwargs, + ) -> BlockOutput: + # Build the comment object + comment_obj: dict = { + "path": input_data.path, + "body": input_data.body, + } + + # Add position or line + if input_data.position is not None: + comment_obj["position"] = input_data.position + elif input_data.line is not None: + # Note: line will be used as position, which may not be accurate + # Position should be calculated from the diff + comment_obj["position"] = input_data.line + + # Add optional fields only if they differ from defaults or are explicitly provided + if input_data.side and input_data.side != "RIGHT": + comment_obj["side"] = input_data.side + if input_data.start_line is not None: + comment_obj["start_line"] = input_data.start_line + if input_data.start_side: + comment_obj["start_side"] = input_data.start_side + + yield "comment_object", comment_obj diff --git a/autogpt_platform/backend/backend/blocks/google/calendar.py b/autogpt_platform/backend/backend/blocks/google/calendar.py index 27cc9e595815..339daab43056 100644 --- a/autogpt_platform/backend/backend/blocks/google/calendar.py +++ b/autogpt_platform/backend/backend/blocks/google/calendar.py @@ -10,7 +10,7 @@ from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.settings import AppEnvironment, Settings +from backend.util.settings import Settings from ._auth import ( GOOGLE_OAUTH_IS_CONFIGURED, @@ -21,6 +21,8 @@ GoogleCredentialsInput, ) +settings = Settings() + class CalendarEvent(BaseModel): """Structured representation of a Google Calendar event.""" @@ -88,8 +90,6 @@ class Output(BlockSchema): ) def __init__(self): - settings = Settings() - # Create realistic test data for events test_now = datetime.now(tz=timezone.utc) test_tomorrow = test_now + timedelta(days=1) @@ -116,8 +116,7 @@ def __init__(self): categories={BlockCategory.PRODUCTIVITY, BlockCategory.DATA}, input_schema=GoogleCalendarReadEventsBlock.Input, output_schema=GoogleCalendarReadEventsBlock.Output, - disabled=not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, test_input={ "credentials": TEST_CREDENTIALS_INPUT, "calendar_id": "primary", @@ -224,8 +223,8 @@ def _build_service(credentials: GoogleCredentials, **kwargs): else None ), token_uri="https://oauth2.googleapis.com/token", - client_id=Settings().secrets.google_client_id, - client_secret=Settings().secrets.google_client_secret, + client_id=settings.secrets.google_client_id, + client_secret=settings.secrets.google_client_secret, scopes=credentials.scopes, ) return build("calendar", "v3", credentials=creds) @@ -442,16 +441,13 @@ class Output(BlockSchema): error: str = SchemaField(description="Error message if event creation failed") def __init__(self): - settings = Settings() - super().__init__( id="ed2ec950-fbff-4204-94c0-023fb1d625e0", description="This block creates a new event in Google Calendar with customizable parameters.", categories={BlockCategory.PRODUCTIVITY}, input_schema=GoogleCalendarCreateEventBlock.Input, output_schema=GoogleCalendarCreateEventBlock.Output, - disabled=not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, test_input={ "credentials": TEST_CREDENTIALS_INPUT, "event_title": "Team Meeting", @@ -575,8 +571,8 @@ def _build_service(credentials: GoogleCredentials, **kwargs): else None ), token_uri="https://oauth2.googleapis.com/token", - client_id=Settings().secrets.google_client_id, - client_secret=Settings().secrets.google_client_secret, + client_id=settings.secrets.google_client_id, + client_secret=settings.secrets.google_client_secret, scopes=credentials.scopes, ) return build("calendar", "v3", credentials=creds) diff --git a/autogpt_platform/backend/backend/blocks/google/gmail.py b/autogpt_platform/backend/backend/blocks/google/gmail.py index 0ae42d09a07f..1b1fb5eb74b4 100644 --- a/autogpt_platform/backend/backend/blocks/google/gmail.py +++ b/autogpt_platform/backend/backend/blocks/google/gmail.py @@ -1,12 +1,18 @@ import asyncio import base64 +from abc import ABC +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.policy import SMTP from email.utils import getaddresses, parseaddr from pathlib import Path -from typing import List +from typing import List, Literal, Optional from google.oauth2.credentials import Credentials from googleapiclient.discovery import build -from pydantic import BaseModel +from pydantic import BaseModel, Field from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField @@ -22,6 +28,107 @@ GoogleCredentialsInput, ) +settings = Settings() + +# No-wrap policy for plain text emails to prevent 78-char hard-wrap +NO_WRAP_POLICY = SMTP.clone(max_line_length=0) + + +def serialize_email_recipients(recipients: list[str]) -> str: + """Serialize recipients list to comma-separated string.""" + return ", ".join(recipients) + + +def _make_mime_text( + body: str, + content_type: Optional[Literal["auto", "plain", "html"]] = None, +) -> MIMEText: + """Create a MIMEText object with proper content type and no hard-wrap for plain text. + + This function addresses the common Gmail issue where plain text emails are + hard-wrapped at 78 characters, creating awkward narrow columns in modern + email clients. It also ensures HTML emails are properly identified and sent + with the correct MIME type. + + Args: + body: The email body content (plain text or HTML) + content_type: The content type - "auto" (default), "plain", or "html" + - "auto" or None: Auto-detects based on presence of HTML tags + - "plain": Forces plain text format without line wrapping + - "html": Forces HTML format with standard wrapping + + Returns: + MIMEText object configured with: + - Appropriate content subtype (plain or html) + - UTF-8 charset for proper Unicode support + - No-wrap policy for plain text (max_line_length=0) + - Standard wrapping for HTML content + + Examples: + >>> # Plain text email without wrapping + >>> mime = _make_mime_text("Long paragraph...", "plain") + >>> # HTML email with auto-detection + >>> mime = _make_mime_text("

Hello

", "auto") + """ + # Auto-detect content type if not specified or "auto" + if content_type is None or content_type == "auto": + # Simple heuristic: check for HTML tags in first 500 chars + looks_html = "<" in body[:500] and ">" in body[:500] + actual_type = "html" if looks_html else "plain" + else: + actual_type = content_type + + # Create MIMEText with appropriate settings + if actual_type == "html": + # HTML content - normal wrapping is OK + return MIMEText(body, _subtype="html", _charset="utf-8") + else: + # Plain text - use no-wrap policy to prevent 78-char hard-wrap + return MIMEText(body, _subtype="plain", _charset="utf-8", policy=NO_WRAP_POLICY) + + +async def create_mime_message( + input_data, + graph_exec_id: str, + user_id: str, +) -> str: + """Create a MIME message with attachments and return base64-encoded raw message.""" + + message = MIMEMultipart() + message["to"] = serialize_email_recipients(input_data.to) + message["subject"] = input_data.subject + + if input_data.cc: + message["cc"] = ", ".join(input_data.cc) + if input_data.bcc: + message["bcc"] = ", ".join(input_data.bcc) + + # Use the new helper function with content_type if available + content_type = getattr(input_data, "content_type", None) + message.attach(_make_mime_text(input_data.body, content_type)) + + # Handle attachments if any + if input_data.attachments: + for attach in input_data.attachments: + local_path = await store_media_file( + user_id=user_id, + graph_exec_id=graph_exec_id, + file=attach, + return_content=False, + ) + abs_path = get_exec_file_path(graph_exec_id, local_path) + part = MIMEBase("application", "octet-stream") + with open(abs_path, "rb") as f: + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", + f"attachment; filename={Path(abs_path).name}", + ) + message.attach(part) + + return base64.urlsafe_b64encode(message.as_bytes()).decode("utf-8") + class Attachment(BaseModel): filename: str @@ -32,11 +139,16 @@ class Attachment(BaseModel): class Email(BaseModel): threadId: str + labelIds: list[str] id: str subject: str snippet: str from_: str - to: str + to: list[str] # List of recipient email addresses + cc: list[str] = Field(default_factory=list) # CC recipients + bcc: list[str] = Field( + default_factory=list + ) # BCC recipients (rarely available in received emails) date: str body: str = "" # Default to an empty string sizeEstimate: int @@ -49,7 +161,165 @@ class Thread(BaseModel): historyId: str -class GmailReadBlock(Block): +class GmailSendResult(BaseModel): + id: str + status: str + + +class GmailDraftResult(BaseModel): + id: str + message_id: str + status: str + + +class GmailLabelResult(BaseModel): + label_id: str + status: str + + +class Profile(BaseModel): + emailAddress: str + messagesTotal: int + threadsTotal: int + historyId: str + + +class GmailBase(Block, ABC): + """Base class for Gmail blocks with common functionality.""" + + def _build_service(self, credentials: GoogleCredentials, **kwargs): + creds = Credentials( + token=( + credentials.access_token.get_secret_value() + if credentials.access_token + else None + ), + refresh_token=( + credentials.refresh_token.get_secret_value() + if credentials.refresh_token + else None + ), + token_uri="https://oauth2.googleapis.com/token", + client_id=settings.secrets.google_client_id, + client_secret=settings.secrets.google_client_secret, + scopes=credentials.scopes, + ) + return build("gmail", "v1", credentials=creds) + + async def _get_email_body(self, msg, service): + """Extract email body content with support for multipart messages and HTML conversion.""" + text = await self._walk_for_body(msg["payload"], msg["id"], service) + return text or "This email does not contain a readable body." + + async def _walk_for_body(self, part, msg_id, service, depth=0): + """Recursively walk through email parts to find readable body content.""" + # Prevent infinite recursion by limiting depth + if depth > 10: + return None + + mime_type = part.get("mimeType", "") + body = part.get("body", {}) + + # Handle text/plain content + if mime_type == "text/plain" and body.get("data"): + return self._decode_base64(body["data"]) + + # Handle text/html content (convert to plain text) + if mime_type == "text/html" and body.get("data"): + html_content = self._decode_base64(body["data"]) + if html_content: + try: + import html2text + + h = html2text.HTML2Text() + h.ignore_links = False + h.ignore_images = True + return h.handle(html_content) + except ImportError: + # Fallback: return raw HTML if html2text is not available + return html_content + + # Handle content stored as attachment + if body.get("attachmentId"): + attachment_data = await self._download_attachment_body( + body["attachmentId"], msg_id, service + ) + if attachment_data: + return self._decode_base64(attachment_data) + + # Recursively search in parts + for sub_part in part.get("parts", []): + text = await self._walk_for_body(sub_part, msg_id, service, depth + 1) + if text: + return text + + return None + + def _decode_base64(self, data): + """Safely decode base64 URL-safe data with proper padding.""" + if not data: + return None + try: + # Add padding if necessary + missing_padding = len(data) % 4 + if missing_padding: + data += "=" * (4 - missing_padding) + return base64.urlsafe_b64decode(data).decode("utf-8") + except Exception: + return None + + async def _download_attachment_body(self, attachment_id, msg_id, service): + """Download attachment content when email body is stored as attachment.""" + try: + attachment = await asyncio.to_thread( + lambda: service.users() + .messages() + .attachments() + .get(userId="me", messageId=msg_id, id=attachment_id) + .execute() + ) + return attachment.get("data") + except Exception: + return None + + async def _get_attachments(self, service, message): + attachments = [] + if "parts" in message["payload"]: + for part in message["payload"]["parts"]: + if part.get("filename"): + attachment = Attachment( + filename=part["filename"], + content_type=part["mimeType"], + size=int(part["body"].get("size", 0)), + attachment_id=part["body"]["attachmentId"], + ) + attachments.append(attachment) + return attachments + + async def download_attachment(self, service, message_id: str, attachment_id: str): + attachment = await asyncio.to_thread( + lambda: service.users() + .messages() + .attachments() + .get(userId="me", messageId=message_id, id=attachment_id) + .execute() + ) + file_data = base64.urlsafe_b64decode(attachment["data"].encode("UTF-8")) + return file_data + + async def _get_label_id(self, service, label_name: str) -> str | None: + """Get label ID by name from Gmail.""" + results = await asyncio.to_thread( + lambda: service.users().labels().list(userId="me").execute() + ) + labels = results.get("labels", []) + for label in labels: + if label["name"] == label_name: + return label["id"] + return None + + +class GmailReadBlock(GmailBase): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( ["https://www.googleapis.com/auth/gmail.readonly"] @@ -93,11 +363,14 @@ def __init__(self): "email", { "threadId": "t1", + "labelIds": ["INBOX"], "id": "1", "subject": "Test Email", "snippet": "This is a test email", "from_": "test@example.com", - "to": "recipient@example.com", + "to": ["recipient@example.com"], + "cc": [], + "bcc": [], "date": "2024-01-01", "body": "This is a test email", "sizeEstimate": 100, @@ -109,11 +382,14 @@ def __init__(self): [ { "threadId": "t1", + "labelIds": ["INBOX"], "id": "1", "subject": "Test Email", "snippet": "This is a test email", "from_": "test@example.com", - "to": "recipient@example.com", + "to": ["recipient@example.com"], + "cc": [], + "bcc": [], "date": "2024-01-01", "body": "This is a test email", "sizeEstimate": 100, @@ -126,11 +402,14 @@ def __init__(self): "_read_emails": lambda *args, **kwargs: [ { "threadId": "t1", + "labelIds": ["INBOX"], "id": "1", "subject": "Test Email", "snippet": "This is a test email", "from_": "test@example.com", - "to": "recipient@example.com", + "to": ["recipient@example.com"], + "cc": [], + "bcc": [], "date": "2024-01-01", "body": "This is a test email", "sizeEstimate": 100, @@ -144,9 +423,8 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - messages = await asyncio.to_thread( - self._read_emails, + service = self._build_service(credentials, **kwargs) + messages = await self._read_emails( service, input_data.query, input_data.max_results, @@ -156,27 +434,7 @@ async def run( yield "email", email yield "emails", messages - @staticmethod - def _build_service(credentials: GoogleCredentials, **kwargs): - creds = Credentials( - token=( - credentials.access_token.get_secret_value() - if credentials.access_token - else None - ), - refresh_token=( - credentials.refresh_token.get_secret_value() - if credentials.refresh_token - else None - ), - token_uri="https://oauth2.googleapis.com/token", - client_id=Settings().secrets.google_client_id, - client_secret=Settings().secrets.google_client_secret, - scopes=credentials.scopes, - ) - return build("gmail", "v1", credentials=creds) - - def _read_emails( + async def _read_emails( self, service, query: str | None, @@ -188,7 +446,10 @@ def _read_emails( if query and "https://www.googleapis.com/auth/gmail.metadata" not in scopes: list_kwargs["q"] = query - results = service.users().messages().list(**list_kwargs).execute() + results = await asyncio.to_thread( + lambda: service.users().messages().list(**list_kwargs).execute() + ) + messages = results.get("messages", []) email_data = [] @@ -198,8 +459,8 @@ def _read_emails( if "https://www.googleapis.com/auth/gmail.metadata" in scopes else "full" ) - msg = ( - service.users() + msg = await asyncio.to_thread( + lambda: service.users() .messages() .get(userId="me", id=message["id"], format=format_type) .execute() @@ -210,145 +471,180 @@ def _read_emails( for header in msg["payload"]["headers"] } - attachments = self._get_attachments(service, msg) + attachments = await self._get_attachments(service, msg) + + # Parse all recipients + to_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("to", "")]) + ] + cc_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("cc", "")]) + ] + bcc_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("bcc", "")]) + ] email = Email( - threadId=msg["threadId"], + threadId=msg.get("threadId", None), + labelIds=msg.get("labelIds", []), id=msg["id"], subject=headers.get("subject", "No Subject"), - snippet=msg["snippet"], + snippet=msg.get("snippet", ""), from_=parseaddr(headers.get("from", ""))[1], - to=parseaddr(headers.get("to", ""))[1], + to=to_recipients if to_recipients else [], + cc=cc_recipients, + bcc=bcc_recipients, date=headers.get("date", ""), - body=self._get_email_body(msg, service), - sizeEstimate=msg["sizeEstimate"], + body=await self._get_email_body(msg, service), + sizeEstimate=msg.get("sizeEstimate", 0), attachments=attachments, ) email_data.append(email) return email_data - def _get_email_body(self, msg, service): - """Extract email body content with support for multipart messages and HTML conversion.""" - text = self._walk_for_body(msg["payload"], msg["id"], service) - return text or "This email does not contain a readable body." - def _walk_for_body(self, part, msg_id, service, depth=0): - """Recursively walk through email parts to find readable body content.""" - # Prevent infinite recursion by limiting depth - if depth > 10: - return None +class GmailSendBlock(GmailBase): + """ + Sends emails through Gmail with intelligent content type detection. - mime_type = part.get("mimeType", "") - body = part.get("body", {}) + Features: + - Automatic HTML detection: Emails containing HTML tags are sent as text/html + - No hard-wrap for plain text: Plain text emails preserve natural line flow + - Manual content type override: Use content_type parameter to force specific format + - Full Unicode/emoji support with UTF-8 encoding + - Attachment support for multiple files + """ - # Handle text/plain content - if mime_type == "text/plain" and body.get("data"): - return self._decode_base64(body["data"]) - - # Handle text/html content (convert to plain text) - if mime_type == "text/html" and body.get("data"): - html_content = self._decode_base64(body["data"]) - if html_content: - try: - import html2text - - h = html2text.HTML2Text() - h.ignore_links = False - h.ignore_images = True - return h.handle(html_content) - except ImportError: - # Fallback: return raw HTML if html2text is not available - return html_content - - # Handle content stored as attachment - if body.get("attachmentId"): - attachment_data = self._download_attachment_body( - body["attachmentId"], msg_id, service - ) - if attachment_data: - return self._decode_base64(attachment_data) - - # Recursively search in parts - for sub_part in part.get("parts", []): - text = self._walk_for_body(sub_part, msg_id, service, depth + 1) - if text: - return text - - return None - - def _decode_base64(self, data): - """Safely decode base64 URL-safe data with proper padding.""" - if not data: - return None - try: - # Add padding if necessary - missing_padding = len(data) % 4 - if missing_padding: - data += "=" * (4 - missing_padding) - return base64.urlsafe_b64decode(data).decode("utf-8") - except Exception: - return None + class Input(BlockSchema): + credentials: GoogleCredentialsInput = GoogleCredentialsField( + ["https://www.googleapis.com/auth/gmail.send"] + ) + to: list[str] = SchemaField( + description="Recipient email addresses", + ) + subject: str = SchemaField( + description="Email subject", + ) + body: str = SchemaField( + description="Email body (plain text or HTML)", + ) + cc: list[str] = SchemaField(description="CC recipients", default_factory=list) + bcc: list[str] = SchemaField(description="BCC recipients", default_factory=list) + content_type: Optional[Literal["auto", "plain", "html"]] = SchemaField( + description="Content type: 'auto' (default - detects HTML), 'plain', or 'html'", + default=None, + advanced=True, + ) + attachments: list[MediaFileType] = SchemaField( + description="Files to attach", default_factory=list, advanced=True + ) - def _download_attachment_body(self, attachment_id, msg_id, service): - """Download attachment content when email body is stored as attachment.""" - try: - attachment = ( - service.users() - .messages() - .attachments() - .get(userId="me", messageId=msg_id, id=attachment_id) - .execute() - ) - return attachment.get("data") - except Exception: - return None + class Output(BlockSchema): + result: GmailSendResult = SchemaField( + description="Send confirmation", + ) + error: str = SchemaField( + description="Error message if any", + ) - def _get_attachments(self, service, message): - attachments = [] - if "parts" in message["payload"]: - for part in message["payload"]["parts"]: - if part["filename"]: - attachment = Attachment( - filename=part["filename"], - content_type=part["mimeType"], - size=int(part["body"].get("size", 0)), - attachment_id=part["body"]["attachmentId"], - ) - attachments.append(attachment) - return attachments + def __init__(self): + super().__init__( + id="6c27abc2-e51d-499e-a85f-5a0041ba94f0", + description="Send emails via Gmail with automatic HTML detection and proper text formatting. Plain text emails are sent without 78-character line wrapping, preserving natural paragraph flow. HTML emails are automatically detected and sent with correct MIME type.", + categories={BlockCategory.COMMUNICATION}, + input_schema=GmailSendBlock.Input, + output_schema=GmailSendBlock.Output, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, + test_input={ + "to": ["recipient@example.com"], + "subject": "Test Email", + "body": "This is a test email sent from GmailSendBlock.", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("result", {"id": "1", "status": "sent"}), + ], + test_mock={ + "_send_email": lambda *args, **kwargs: {"id": "1", "status": "sent"}, + }, + ) + + async def run( + self, + input_data: Input, + *, + credentials: GoogleCredentials, + graph_exec_id: str, + user_id: str, + **kwargs, + ) -> BlockOutput: + service = self._build_service(credentials, **kwargs) + result = await self._send_email( + service, + input_data, + graph_exec_id, + user_id, + ) + yield "result", result - # Add a new method to download attachment content - def download_attachment(self, service, message_id: str, attachment_id: str): - attachment = ( - service.users() + async def _send_email( + self, service, input_data: Input, graph_exec_id: str, user_id: str + ) -> dict: + if not input_data.to or not input_data.subject or not input_data.body: + raise ValueError( + "At least one recipient, subject, and body are required for sending an email" + ) + raw_message = await create_mime_message(input_data, graph_exec_id, user_id) + sent_message = await asyncio.to_thread( + lambda: service.users() .messages() - .attachments() - .get(userId="me", messageId=message_id, id=attachment_id) + .send(userId="me", body={"raw": raw_message}) .execute() ) - file_data = base64.urlsafe_b64decode(attachment["data"].encode("UTF-8")) - return file_data + return {"id": sent_message["id"], "status": "sent"} + + +class GmailCreateDraftBlock(GmailBase): + """ + Creates draft emails in Gmail with intelligent content type detection. + Features: + - Automatic HTML detection: Drafts containing HTML tags are formatted as text/html + - No hard-wrap for plain text: Plain text drafts preserve natural line flow + - Manual content type override: Use content_type parameter to force specific format + - Full Unicode/emoji support with UTF-8 encoding + - Attachment support for multiple files + """ -class GmailSendBlock(Block): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( - ["https://www.googleapis.com/auth/gmail.send"] + ["https://www.googleapis.com/auth/gmail.modify"] ) - to: str = SchemaField( - description="Recipient email address", + to: list[str] = SchemaField( + description="Recipient email addresses", ) subject: str = SchemaField( description="Email subject", ) body: str = SchemaField( - description="Email body", + description="Email body (plain text or HTML)", + ) + cc: list[str] = SchemaField(description="CC recipients", default_factory=list) + bcc: list[str] = SchemaField(description="BCC recipients", default_factory=list) + content_type: Optional[Literal["auto", "plain", "html"]] = SchemaField( + description="Content type: 'auto' (default - detects HTML), 'plain', or 'html'", + default=None, + advanced=True, + ) + attachments: list[MediaFileType] = SchemaField( + description="Files to attach", default_factory=list, advanced=True ) class Output(BlockSchema): - result: dict = SchemaField( - description="Send confirmation", + result: GmailDraftResult = SchemaField( + description="Draft creation result", ) error: str = SchemaField( description="Error message if any", @@ -356,60 +652,75 @@ class Output(BlockSchema): def __init__(self): super().__init__( - id="6c27abc2-e51d-499e-a85f-5a0041ba94f0", - description="This block sends an email using Gmail.", + id="e1eeead4-46cb-491e-8281-17b6b9c44a55", + description="Create draft emails in Gmail with automatic HTML detection and proper text formatting. Plain text drafts preserve natural paragraph flow without 78-character line wrapping. HTML content is automatically detected and formatted correctly.", categories={BlockCategory.COMMUNICATION}, - input_schema=GmailSendBlock.Input, - output_schema=GmailSendBlock.Output, + input_schema=GmailCreateDraftBlock.Input, + output_schema=GmailCreateDraftBlock.Output, disabled=not GOOGLE_OAUTH_IS_CONFIGURED, test_input={ - "to": "recipient@example.com", - "subject": "Test Email", - "body": "This is a test email sent from GmailSendBlock.", + "to": ["recipient@example.com"], + "subject": "Draft Test Email", + "body": "This is a test draft email.", "credentials": TEST_CREDENTIALS_INPUT, }, test_credentials=TEST_CREDENTIALS, test_output=[ - ("result", {"id": "1", "status": "sent"}), + ( + "result", + GmailDraftResult( + id="draft1", message_id="msg1", status="draft_created" + ), + ), ], test_mock={ - "_send_email": lambda *args, **kwargs: {"id": "1", "status": "sent"}, + "_create_draft": lambda *args, **kwargs: { + "id": "draft1", + "message": {"id": "msg1"}, + }, }, ) async def run( - self, input_data: Input, *, credentials: GoogleCredentials, **kwargs + self, + input_data: Input, + *, + credentials: GoogleCredentials, + graph_exec_id: str, + user_id: str, + **kwargs, ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - result = await asyncio.to_thread( - self._send_email, + service = self._build_service(credentials, **kwargs) + result = await self._create_draft( service, - input_data.to, - input_data.subject, - input_data.body, + input_data, + graph_exec_id, + user_id, ) - yield "result", result - - def _send_email(self, service, to: str, subject: str, body: str) -> dict: - if not to or not subject or not body: - raise ValueError("To, subject, and body are required for sending an email") - message = self._create_message(to, subject, body) - sent_message = ( - service.users().messages().send(userId="me", body=message).execute() + yield "result", GmailDraftResult( + id=result["id"], message_id=result["message"]["id"], status="draft_created" ) - return {"id": sent_message["id"], "status": "sent"} - def _create_message(self, to: str, subject: str, body: str) -> dict: - from email.mime.text import MIMEText + async def _create_draft( + self, service, input_data: Input, graph_exec_id: str, user_id: str + ) -> dict: + if not input_data.to or not input_data.subject: + raise ValueError( + "At least one recipient and subject are required for creating a draft" + ) + + raw_message = await create_mime_message(input_data, graph_exec_id, user_id) + draft = await asyncio.to_thread( + lambda: service.users() + .drafts() + .create(userId="me", body={"message": {"raw": raw_message}}) + .execute() + ) - message = MIMEText(body) - message["to"] = to - message["subject"] = subject - raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode("utf-8") - return {"raw": raw_message} + return draft -class GmailListLabelsBlock(Block): +class GmailListLabelsBlock(GmailBase): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( ["https://www.googleapis.com/auth/gmail.labels"] @@ -455,17 +766,19 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - result = await asyncio.to_thread(self._list_labels, service) + service = self._build_service(credentials, **kwargs) + result = await self._list_labels(service) yield "result", result - def _list_labels(self, service) -> list[dict]: - results = service.users().labels().list(userId="me").execute() + async def _list_labels(self, service) -> list[dict]: + results = await asyncio.to_thread( + lambda: service.users().labels().list(userId="me").execute() + ) labels = results.get("labels", []) return [{"id": label["id"], "name": label["name"]} for label in labels] -class GmailAddLabelBlock(Block): +class GmailAddLabelBlock(GmailBase): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( ["https://www.googleapis.com/auth/gmail.modify"] @@ -478,7 +791,7 @@ class Input(BlockSchema): ) class Output(BlockSchema): - result: dict = SchemaField( + result: GmailLabelResult = SchemaField( description="Label addition result", ) error: str = SchemaField( @@ -516,24 +829,33 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - result = await asyncio.to_thread( - self._add_label, service, input_data.message_id, input_data.label_name + service = self._build_service(credentials, **kwargs) + result = await self._add_label( + service, input_data.message_id, input_data.label_name ) yield "result", result - def _add_label(self, service, message_id: str, label_name: str) -> dict: - label_id = self._get_or_create_label(service, label_name) - service.users().messages().modify( - userId="me", id=message_id, body={"addLabelIds": [label_id]} - ).execute() + async def _add_label(self, service, message_id: str, label_name: str) -> dict: + label_id = await self._get_or_create_label(service, label_name) + result = await asyncio.to_thread( + lambda: service.users() + .messages() + .modify(userId="me", id=message_id, body={"addLabelIds": [label_id]}) + .execute() + ) + if not result.get("labelIds"): + return { + "status": "Label already applied or not found", + "label_id": label_id, + } + return {"status": "Label added successfully", "label_id": label_id} - def _get_or_create_label(self, service, label_name: str) -> str: - label_id = self._get_label_id(service, label_name) + async def _get_or_create_label(self, service, label_name: str) -> str: + label_id = await self._get_label_id(service, label_name) if not label_id: - label = ( - service.users() + label = await asyncio.to_thread( + lambda: service.users() .labels() .create(userId="me", body={"name": label_name}) .execute() @@ -541,16 +863,8 @@ def _get_or_create_label(self, service, label_name: str) -> str: label_id = label["id"] return label_id - def _get_label_id(self, service, label_name: str) -> str | None: - results = service.users().labels().list(userId="me").execute() - labels = results.get("labels", []) - for label in labels: - if label["name"] == label_name: - return label["id"] - return None - -class GmailRemoveLabelBlock(Block): +class GmailRemoveLabelBlock(GmailBase): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( ["https://www.googleapis.com/auth/gmail.modify"] @@ -563,7 +877,7 @@ class Input(BlockSchema): ) class Output(BlockSchema): - result: dict = SchemaField( + result: GmailLabelResult = SchemaField( description="Label removal result", ) error: str = SchemaField( @@ -601,32 +915,32 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - result = await asyncio.to_thread( - self._remove_label, service, input_data.message_id, input_data.label_name + service = self._build_service(credentials, **kwargs) + result = await self._remove_label( + service, input_data.message_id, input_data.label_name ) yield "result", result - def _remove_label(self, service, message_id: str, label_name: str) -> dict: - label_id = self._get_label_id(service, label_name) + async def _remove_label(self, service, message_id: str, label_name: str) -> dict: + label_id = await self._get_label_id(service, label_name) if label_id: - service.users().messages().modify( - userId="me", id=message_id, body={"removeLabelIds": [label_id]} - ).execute() + result = await asyncio.to_thread( + lambda: service.users() + .messages() + .modify(userId="me", id=message_id, body={"removeLabelIds": [label_id]}) + .execute() + ) + if not result.get("labelIds"): + return { + "status": "Label already removed or not applied", + "label_id": label_id, + } return {"status": "Label removed successfully", "label_id": label_id} else: return {"status": "Label not found", "label_name": label_name} - def _get_label_id(self, service, label_name: str) -> str | None: - results = service.users().labels().list(userId="me").execute() - labels = results.get("labels", []) - for label in labels: - if label["name"] == label_name: - return label["id"] - return None - -class GmailGetThreadBlock(Block): +class GmailGetThreadBlock(GmailBase): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( ["https://www.googleapis.com/auth/gmail.readonly"] @@ -657,13 +971,16 @@ def __init__(self): "messages": [ { "id": "188199feff9dc907", - "to": "nick@example.co", + "to": ["nick@example.co"], + "cc": [], + "bcc": [], "body": "This email does not contain a text body.", "date": "Thu, 17 Jul 2025 19:22:36 +0100", "from_": "bent@example.co", "snippet": "have a funny looking car -- Bently, Community Administrator For AutoGPT", "subject": "car", "threadId": "188199feff9dc907", + "labelIds": ["INBOX"], "attachments": [ { "size": 5694, @@ -685,13 +1002,16 @@ def __init__(self): "messages": [ { "id": "188199feff9dc907", - "to": "nick@example.co", + "to": ["nick@example.co"], + "cc": [], + "bcc": [], "body": "This email does not contain a text body.", "date": "Thu, 17 Jul 2025 19:22:36 +0100", "from_": "bent@example.co", "snippet": "have a funny looking car -- Bently, Community Administrator For AutoGPT", "subject": "car", "threadId": "188199feff9dc907", + "labelIds": ["INBOX"], "attachments": [ { "size": 5694, @@ -711,19 +1031,23 @@ def __init__(self): async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) - thread = self._get_thread(service, input_data.threadId, credentials.scopes) + service = self._build_service(credentials, **kwargs) + thread = await self._get_thread( + service, input_data.threadId, credentials.scopes + ) yield "thread", thread - def _get_thread(self, service, thread_id: str, scopes: list[str] | None) -> Thread: + async def _get_thread( + self, service, thread_id: str, scopes: list[str] | None + ) -> Thread: scopes = [s.lower() for s in (scopes or [])] format_type = ( "metadata" if "https://www.googleapis.com/auth/gmail.metadata" in scopes else "full" ) - thread = ( - service.users() + thread = await asyncio.to_thread( + lambda: service.users() .threads() .get(userId="me", id=thread_id, format=format_type) .execute() @@ -735,15 +1059,30 @@ def _get_thread(self, service, thread_id: str, scopes: list[str] | None) -> Thre h["name"].lower(): h["value"] for h in msg.get("payload", {}).get("headers", []) } - body = self._get_email_body(msg) - attachments = self._get_attachments(service, msg) + body = await self._get_email_body(msg, service) + attachments = await self._get_attachments(service, msg) + + # Parse all recipients + to_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("to", "")]) + ] + cc_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("cc", "")]) + ] + bcc_recipients = [ + addr.strip() for _, addr in getaddresses([headers.get("bcc", "")]) + ] + email = Email( threadId=msg.get("threadId", thread_id), - id=msg["id"], + labelIds=msg.get("labelIds", []), + id=msg.get("id"), subject=headers.get("subject", "No Subject"), snippet=msg.get("snippet", ""), from_=parseaddr(headers.get("from", ""))[1], - to=parseaddr(headers.get("to", ""))[1], + to=to_recipients if to_recipients else [], + cc=cc_recipients, + bcc=bcc_recipients, date=headers.get("date", ""), body=body, sizeEstimate=msg.get("sizeEstimate", 0), @@ -754,44 +1093,26 @@ def _get_thread(self, service, thread_id: str, scopes: list[str] | None) -> Thre thread["messages"] = parsed_messages return thread - def _get_email_body(self, msg): - payload = msg.get("payload") - if not payload: - return "This email does not contain a text body." - - if "parts" in payload: - for part in payload["parts"]: - if part.get("mimeType") == "text/plain" and "data" in part.get( - "body", {} - ): - return base64.urlsafe_b64decode(part["body"]["data"]).decode( - "utf-8" - ) - elif payload.get("mimeType") == "text/plain" and "data" in payload.get( - "body", {} - ): - return base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8") - return "This email does not contain a text body." - def _get_attachments(self, service, message): - attachments = [] - if "parts" in message["payload"]: - for part in message["payload"]["parts"]: - if part.get("filename"): - attachment = Attachment( - filename=part["filename"], - content_type=part["mimeType"], - size=int(part["body"].get("size", 0)), - attachment_id=part["body"]["attachmentId"], - ) - attachments.append(attachment) - return attachments +class GmailReplyBlock(GmailBase): + """ + Replies to Gmail threads with intelligent content type detection. + Features: + - Automatic HTML detection: Replies containing HTML tags are sent as text/html + - No hard-wrap for plain text: Plain text replies preserve natural line flow + - Manual content type override: Use content_type parameter to force specific format + - Reply-all functionality: Option to reply to all original recipients + - Thread preservation: Maintains proper email threading with headers + - Full Unicode/emoji support with UTF-8 encoding + """ -class GmailReplyBlock(Block): class Input(BlockSchema): credentials: GoogleCredentialsInput = GoogleCredentialsField( - ["https://www.googleapis.com/auth/gmail.send"] + [ + "https://www.googleapis.com/auth/gmail.send", + "https://www.googleapis.com/auth/gmail.readonly", + ] ) threadId: str = SchemaField(description="Thread ID to reply in") parentMessageId: str = SchemaField( @@ -804,7 +1125,12 @@ class Input(BlockSchema): description="Reply to all original recipients", default=False ) subject: str = SchemaField(description="Email subject", default="") - body: str = SchemaField(description="Email body") + body: str = SchemaField(description="Email body (plain text or HTML)") + content_type: Optional[Literal["auto", "plain", "html"]] = SchemaField( + description="Content type: 'auto' (default - detects HTML), 'plain', or 'html'", + default=None, + advanced=True, + ) attachments: list[MediaFileType] = SchemaField( description="Files to attach", default_factory=list, advanced=True ) @@ -813,12 +1139,15 @@ class Output(BlockSchema): messageId: str = SchemaField(description="Sent message ID") threadId: str = SchemaField(description="Thread ID") message: dict = SchemaField(description="Raw Gmail message object") + email: Email = SchemaField( + description="Parsed email object with decoded body and attachments" + ) error: str = SchemaField(description="Error message if any") def __init__(self): super().__init__( id="12bf5a24-9b90-4f40-9090-4e86e6995e60", - description="Reply to a Gmail thread", + description="Reply to Gmail threads with automatic HTML detection and proper text formatting. Plain text replies maintain natural paragraph flow without 78-character line wrapping. HTML content is automatically detected and sent with correct MIME type.", categories={BlockCategory.COMMUNICATION}, input_schema=GmailReplyBlock.Input, output_schema=GmailReplyBlock.Output, @@ -835,6 +1164,24 @@ def __init__(self): ("messageId", "m2"), ("threadId", "t1"), ("message", {"id": "m2", "threadId": "t1"}), + ( + "email", + Email( + threadId="t1", + labelIds=[], + id="m2", + subject="", + snippet="", + from_="", + to=[], + cc=[], + bcc=[], + date="", + body="Thanks", + sizeEstimate=0, + attachments=[], + ), + ), ], test_mock={ "_reply": lambda *args, **kwargs: { @@ -853,7 +1200,7 @@ async def run( user_id: str, **kwargs, ) -> BlockOutput: - service = GmailReadBlock._build_service(credentials, **kwargs) + service = self._build_service(credentials, **kwargs) message = await self._reply( service, input_data, @@ -863,12 +1210,28 @@ async def run( yield "messageId", message["id"] yield "threadId", message.get("threadId", input_data.threadId) yield "message", message + email = Email( + threadId=message.get("threadId", input_data.threadId), + labelIds=message.get("labelIds", []), + id=message["id"], + subject=input_data.subject or "", + snippet=message.get("snippet", ""), + from_="", # From address would need to be retrieved from the message headers + to=input_data.to if input_data.to else [], + cc=input_data.cc if input_data.cc else [], + bcc=input_data.bcc if input_data.bcc else [], + date="", # Date would need to be retrieved from the message headers + body=input_data.body, + sizeEstimate=message.get("sizeEstimate", 0), + attachments=[], # Attachments info not available from send response + ) + yield "email", email async def _reply( self, service, input_data: Input, graph_exec_id: str, user_id: str ) -> dict: - parent = ( - service.users() + parent = await asyncio.to_thread( + lambda: service.users() .messages() .get( userId="me", @@ -886,6 +1249,7 @@ async def _reply( ) .execute() ) + headers = { h["name"].lower(): h["value"] for h in parent.get("payload", {}).get("headers", []) @@ -912,11 +1276,6 @@ async def _reply( if headers.get("message-id"): references.append(headers["message-id"]) - from email import encoders - from email.mime.base import MIMEBase - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText - msg = MIMEMultipart() if input_data.to: msg["To"] = ", ".join(input_data.to) @@ -929,9 +1288,8 @@ async def _reply( msg["In-Reply-To"] = headers["message-id"] if references: msg["References"] = " ".join(references) - msg.attach( - MIMEText(input_data.body, "html" if "<" in input_data.body else "plain") - ) + # Use the new helper function for consistent content type handling + msg.attach(_make_mime_text(input_data.body, input_data.content_type)) for attach in input_data.attachments: local_path = await store_media_file( @@ -951,9 +1309,279 @@ async def _reply( msg.attach(part) raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8") - return ( - service.users() + return await asyncio.to_thread( + lambda: service.users() .messages() .send(userId="me", body={"threadId": input_data.threadId, "raw": raw}) .execute() ) + + +class GmailGetProfileBlock(GmailBase): + class Input(BlockSchema): + credentials: GoogleCredentialsInput = GoogleCredentialsField( + ["https://www.googleapis.com/auth/gmail.readonly"] + ) + + class Output(BlockSchema): + profile: Profile = SchemaField(description="Gmail user profile information") + error: str = SchemaField(description="Error message if any") + + def __init__(self): + super().__init__( + id="04b0d996-0908-4a4b-89dd-b9697ff253d3", + description="Get the authenticated user's Gmail profile details including email address and message statistics.", + categories={BlockCategory.COMMUNICATION}, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, + input_schema=GmailGetProfileBlock.Input, + output_schema=GmailGetProfileBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "profile", + { + "emailAddress": "test@example.com", + "messagesTotal": 1000, + "threadsTotal": 500, + "historyId": "12345", + }, + ), + ], + test_mock={ + "_get_profile": lambda *args, **kwargs: { + "emailAddress": "test@example.com", + "messagesTotal": 1000, + "threadsTotal": 500, + "historyId": "12345", + }, + }, + ) + + async def run( + self, input_data: Input, *, credentials: GoogleCredentials, **kwargs + ) -> BlockOutput: + service = self._build_service(credentials, **kwargs) + profile = await self._get_profile(service) + yield "profile", profile + + async def _get_profile(self, service) -> Profile: + result = await asyncio.to_thread( + lambda: service.users().getProfile(userId="me").execute() + ) + return Profile( + emailAddress=result.get("emailAddress", ""), + messagesTotal=result.get("messagesTotal", 0), + threadsTotal=result.get("threadsTotal", 0), + historyId=result.get("historyId", ""), + ) + + +class GmailForwardBlock(GmailBase): + """ + Forwards Gmail messages with intelligent content type detection. + + Features: + - Preserves original message headers and threading + - Automatic HTML detection for forwarded content + - Optional forward message customization + - Full attachment support from original message + - Manual content type override option + """ + + class Input(BlockSchema): + credentials: GoogleCredentialsInput = GoogleCredentialsField( + [ + "https://www.googleapis.com/auth/gmail.send", + "https://www.googleapis.com/auth/gmail.readonly", + ] + ) + messageId: str = SchemaField(description="ID of the message to forward") + to: list[str] = SchemaField(description="Recipients to forward the message to") + cc: list[str] = SchemaField(description="CC recipients", default_factory=list) + bcc: list[str] = SchemaField(description="BCC recipients", default_factory=list) + subject: str = SchemaField( + description="Optional custom subject (defaults to 'Fwd: [original subject]')", + default="", + ) + forwardMessage: str = SchemaField( + description="Optional message to include before the forwarded content", + default="", + ) + includeAttachments: bool = SchemaField( + description="Include attachments from the original message", + default=True, + ) + content_type: Optional[Literal["auto", "plain", "html"]] = SchemaField( + description="Content type: 'auto' (default - detects HTML), 'plain', or 'html'", + default=None, + advanced=True, + ) + additionalAttachments: list[MediaFileType] = SchemaField( + description="Additional files to attach", + default_factory=list, + advanced=True, + ) + + class Output(BlockSchema): + messageId: str = SchemaField(description="Forwarded message ID") + threadId: str = SchemaField(description="Thread ID") + status: str = SchemaField(description="Forward status") + error: str = SchemaField(description="Error message if any") + + def __init__(self): + super().__init__( + id="64d2301c-b3f5-4174-8ac0-111ca1e1a7c0", + description="Forward Gmail messages to other recipients with automatic HTML detection and proper formatting. Preserves original message threading and attachments.", + categories={BlockCategory.COMMUNICATION}, + input_schema=GmailForwardBlock.Input, + output_schema=GmailForwardBlock.Output, + disabled=not GOOGLE_OAUTH_IS_CONFIGURED, + test_input={ + "messageId": "m1", + "to": ["recipient@example.com"], + "forwardMessage": "FYI - forwarding this to you.", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("messageId", "m2"), + ("threadId", "t1"), + ("status", "forwarded"), + ], + test_mock={ + "_forward_message": lambda *args, **kwargs: { + "id": "m2", + "threadId": "t1", + }, + }, + ) + + async def run( + self, + input_data: Input, + *, + credentials: GoogleCredentials, + graph_exec_id: str, + user_id: str, + **kwargs, + ) -> BlockOutput: + service = self._build_service(credentials, **kwargs) + result = await self._forward_message( + service, + input_data, + graph_exec_id, + user_id, + ) + yield "messageId", result["id"] + yield "threadId", result.get("threadId", "") + yield "status", "forwarded" + + async def _forward_message( + self, service, input_data: Input, graph_exec_id: str, user_id: str + ) -> dict: + if not input_data.to: + raise ValueError("At least one recipient is required for forwarding") + + # Get the original message + original = await asyncio.to_thread( + lambda: service.users() + .messages() + .get(userId="me", id=input_data.messageId, format="full") + .execute() + ) + + headers = { + h["name"].lower(): h["value"] + for h in original.get("payload", {}).get("headers", []) + } + + # Create subject with Fwd: prefix if not already present + original_subject = headers.get("subject", "No Subject") + if input_data.subject: + subject = input_data.subject + elif not original_subject.lower().startswith("fwd:"): + subject = f"Fwd: {original_subject}" + else: + subject = original_subject + + # Build forwarded message body + original_from = headers.get("from", "Unknown") + original_date = headers.get("date", "Unknown") + original_to = headers.get("to", "Unknown") + + # Get the original body + original_body = await self._get_email_body(original, service) + + # Construct the forward header + forward_header = f""" +---------- Forwarded message --------- +From: {original_from} +Date: {original_date} +Subject: {original_subject} +To: {original_to} +""" + + # Combine optional forward message with original content + if input_data.forwardMessage: + body = f"{input_data.forwardMessage}\n\n{forward_header}\n\n{original_body}" + else: + body = f"{forward_header}\n\n{original_body}" + + # Create MIME message + msg = MIMEMultipart() + msg["To"] = ", ".join(input_data.to) + if input_data.cc: + msg["Cc"] = ", ".join(input_data.cc) + if input_data.bcc: + msg["Bcc"] = ", ".join(input_data.bcc) + msg["Subject"] = subject + + # Add body with proper content type + msg.attach(_make_mime_text(body, input_data.content_type)) + + # Include original attachments if requested + if input_data.includeAttachments: + attachments = await self._get_attachments(service, original) + for attachment in attachments: + # Download and attach each original attachment + attachment_data = await self.download_attachment( + service, input_data.messageId, attachment.attachment_id + ) + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment_data) + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", + f"attachment; filename={attachment.filename}", + ) + msg.attach(part) + + # Add any additional attachments + for attach in input_data.additionalAttachments: + local_path = await store_media_file( + user_id=user_id, + graph_exec_id=graph_exec_id, + file=attach, + return_content=False, + ) + abs_path = get_exec_file_path(graph_exec_id, local_path) + part = MIMEBase("application", "octet-stream") + with open(abs_path, "rb") as f: + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", f"attachment; filename={Path(abs_path).name}" + ) + msg.attach(part) + + # Send the forwarded message + raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8") + return await asyncio.to_thread( + lambda: service.users() + .messages() + .send(userId="me", body={"raw": raw}) + .execute() + ) diff --git a/autogpt_platform/backend/backend/blocks/google/sheets.py b/autogpt_platform/backend/backend/blocks/google/sheets.py index 6119fd15279a..6e63958c828c 100644 --- a/autogpt_platform/backend/backend/blocks/google/sheets.py +++ b/autogpt_platform/backend/backend/blocks/google/sheets.py @@ -7,7 +7,7 @@ from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.settings import AppEnvironment, Settings +from backend.util.settings import Settings from ._auth import ( GOOGLE_OAUTH_IS_CONFIGURED, @@ -19,10 +19,7 @@ ) settings = Settings() -GOOGLE_SHEETS_DISABLED = ( - not GOOGLE_OAUTH_IS_CONFIGURED - or settings.config.app_env == AppEnvironment.PRODUCTION -) +GOOGLE_SHEETS_DISABLED = not GOOGLE_OAUTH_IS_CONFIGURED def parse_a1_notation(a1: str) -> tuple[str | None, str]: diff --git a/autogpt_platform/backend/backend/blocks/ideogram.py b/autogpt_platform/backend/backend/blocks/ideogram.py index 468f8f1d1ef6..ef5aca248921 100644 --- a/autogpt_platform/backend/backend/blocks/ideogram.py +++ b/autogpt_platform/backend/backend/blocks/ideogram.py @@ -30,6 +30,7 @@ class IdeogramModelName(str, Enum): + V3 = "V_3" V2 = "V_2" V1 = "V_1" V1_TURBO = "V_1_TURBO" @@ -95,8 +96,8 @@ class Input(BlockSchema): title="Prompt", ) ideogram_model_name: IdeogramModelName = SchemaField( - description="The name of the Image Generation Model, e.g., V_2", - default=IdeogramModelName.V2, + description="The name of the Image Generation Model, e.g., V_3", + default=IdeogramModelName.V3, title="Image Generation Model", advanced=False, ) @@ -236,6 +237,111 @@ async def run_model( negative_prompt: Optional[str], color_palette_name: str, custom_colors: Optional[list[str]], + ): + # Use V3 endpoint for V3 model, legacy endpoint for others + if model_name == "V_3": + return await self._run_model_v3( + api_key, + prompt, + seed, + aspect_ratio, + magic_prompt_option, + style_type, + negative_prompt, + color_palette_name, + custom_colors, + ) + else: + return await self._run_model_legacy( + api_key, + model_name, + prompt, + seed, + aspect_ratio, + magic_prompt_option, + style_type, + negative_prompt, + color_palette_name, + custom_colors, + ) + + async def _run_model_v3( + self, + api_key: SecretStr, + prompt: str, + seed: Optional[int], + aspect_ratio: str, + magic_prompt_option: str, + style_type: str, + negative_prompt: Optional[str], + color_palette_name: str, + custom_colors: Optional[list[str]], + ): + url = "https://api.ideogram.ai/v1/ideogram-v3/generate" + headers = { + "Api-Key": api_key.get_secret_value(), + "Content-Type": "application/json", + } + + # Map legacy aspect ratio values to V3 format + aspect_ratio_map = { + "ASPECT_10_16": "10x16", + "ASPECT_16_10": "16x10", + "ASPECT_9_16": "9x16", + "ASPECT_16_9": "16x9", + "ASPECT_3_2": "3x2", + "ASPECT_2_3": "2x3", + "ASPECT_4_3": "4x3", + "ASPECT_3_4": "3x4", + "ASPECT_1_1": "1x1", + "ASPECT_1_3": "1x3", + "ASPECT_3_1": "3x1", + # Additional V3 supported ratios + "ASPECT_1_2": "1x2", + "ASPECT_2_1": "2x1", + "ASPECT_4_5": "4x5", + "ASPECT_5_4": "5x4", + } + + v3_aspect_ratio = aspect_ratio_map.get( + aspect_ratio, "1x1" + ) # Default to 1x1 if not found + + # Use JSON for V3 endpoint (simpler than multipart/form-data) + data: Dict[str, Any] = { + "prompt": prompt, + "aspect_ratio": v3_aspect_ratio, + "magic_prompt": magic_prompt_option, + "style_type": style_type, + } + + if seed is not None: + data["seed"] = seed + + if negative_prompt: + data["negative_prompt"] = negative_prompt + + # Note: V3 endpoint may have different color palette support + # For now, we'll omit color palettes for V3 to avoid errors + + try: + response = await Requests().post(url, headers=headers, json=data) + return response.json()["data"][0]["url"] + except RequestException as e: + raise Exception(f"Failed to fetch image with V3 endpoint: {str(e)}") + + async def _run_model_legacy( + self, + api_key: SecretStr, + model_name: str, + prompt: str, + seed: Optional[int], + aspect_ratio: str, + magic_prompt_option: str, + style_type: str, + negative_prompt: Optional[str], + color_palette_name: str, + custom_colors: Optional[list[str]], ): url = "https://api.ideogram.ai/generate" headers = { @@ -249,28 +355,33 @@ async def run_model( "model": model_name, "aspect_ratio": aspect_ratio, "magic_prompt_option": magic_prompt_option, - "style_type": style_type, } } + # Only add style_type for V2, V2_TURBO, and V3 models (V1 models don't support it) + if model_name in ["V_2", "V_2_TURBO", "V_3"]: + data["image_request"]["style_type"] = style_type + if seed is not None: data["image_request"]["seed"] = seed if negative_prompt: data["image_request"]["negative_prompt"] = negative_prompt - if color_palette_name != "NONE": - data["color_palette"] = {"name": color_palette_name} - elif custom_colors: - data["color_palette"] = { - "members": [{"color_hex": color} for color in custom_colors] - } + # Only add color palette for V2 and V2_TURBO models (V1 models don't support it) + if model_name in ["V_2", "V_2_TURBO"]: + if color_palette_name != "NONE": + data["color_palette"] = {"name": color_palette_name} + elif custom_colors: + data["color_palette"] = { + "members": [{"color_hex": color} for color in custom_colors] + } try: response = await Requests().post(url, headers=headers, json=data) return response.json()["data"][0]["url"] except RequestException as e: - raise Exception(f"Failed to fetch image: {str(e)}") + raise Exception(f"Failed to fetch image with legacy endpoint: {str(e)}") async def upscale_image(self, api_key: SecretStr, image_url: str): url = "https://api.ideogram.ai/upscale" diff --git a/autogpt_platform/backend/backend/blocks/linear/_config.py b/autogpt_platform/backend/backend/blocks/linear/_config.py index fdaa94dc1f99..c5337c481ccc 100644 --- a/autogpt_platform/backend/backend/blocks/linear/_config.py +++ b/autogpt_platform/backend/backend/blocks/linear/_config.py @@ -2,7 +2,6 @@ Shared configuration for all Linear blocks using the new SDK pattern. """ -import os from enum import Enum from backend.sdk import ( @@ -38,21 +37,11 @@ class LinearScope(str, Enum): ADMIN = "admin" -# Check if Linear OAuth is configured -client_id = os.getenv("LINEAR_CLIENT_ID") -client_secret = os.getenv("LINEAR_CLIENT_SECRET") -LINEAR_OAUTH_IS_CONFIGURED = bool(client_id and client_secret) - -# Build the Linear provider -builder = ( +linear = ( ProviderBuilder("linear") .with_api_key(env_var_name="LINEAR_API_KEY", title="Linear API Key") .with_base_cost(1, BlockCostType.RUN) -) - -# Linear only supports OAuth authentication -if LINEAR_OAUTH_IS_CONFIGURED: - builder = builder.with_oauth( + .with_oauth( LinearOAuthHandler, scopes=[ LinearScope.READ, @@ -63,9 +52,8 @@ class LinearScope(str, Enum): client_id_env_var="LINEAR_CLIENT_ID", client_secret_env_var="LINEAR_CLIENT_SECRET", ) - -# Build the provider -linear = builder.build() + .build() +) TEST_CREDENTIALS_OAUTH = OAuth2Credentials( diff --git a/autogpt_platform/backend/backend/blocks/linear/comment.py b/autogpt_platform/backend/backend/blocks/linear/comment.py index 3af28ac35cf6..17cd54c212e0 100644 --- a/autogpt_platform/backend/backend/blocks/linear/comment.py +++ b/autogpt_platform/backend/backend/blocks/linear/comment.py @@ -11,7 +11,6 @@ from ._api import LinearAPIException, LinearClient from ._config import ( - LINEAR_OAUTH_IS_CONFIGURED, TEST_CREDENTIALS_INPUT_OAUTH, TEST_CREDENTIALS_OAUTH, LinearScope, @@ -50,7 +49,6 @@ def __init__(self): "comment": "Test comment", "credentials": TEST_CREDENTIALS_INPUT_OAUTH, }, - disabled=not LINEAR_OAUTH_IS_CONFIGURED, test_credentials=TEST_CREDENTIALS_OAUTH, test_output=[("comment_id", "abc123"), ("comment_body", "Test comment")], test_mock={ diff --git a/autogpt_platform/backend/backend/blocks/linear/issues.py b/autogpt_platform/backend/backend/blocks/linear/issues.py index 35452f4a81b7..cd0fa0e98abf 100644 --- a/autogpt_platform/backend/backend/blocks/linear/issues.py +++ b/autogpt_platform/backend/backend/blocks/linear/issues.py @@ -11,7 +11,6 @@ from ._api import LinearAPIException, LinearClient from ._config import ( - LINEAR_OAUTH_IS_CONFIGURED, TEST_CREDENTIALS_INPUT_OAUTH, TEST_CREDENTIALS_OAUTH, LinearScope, @@ -53,7 +52,6 @@ def __init__(self): super().__init__( id="f9c68f55-dcca-40a8-8771-abf9601680aa", description="Creates a new issue on Linear", - disabled=not LINEAR_OAUTH_IS_CONFIGURED, input_schema=self.Input, output_schema=self.Output, categories={BlockCategory.PRODUCTIVITY, BlockCategory.ISSUE_TRACKING}, @@ -147,7 +145,6 @@ def __init__(self): description="Searches for issues on Linear", input_schema=self.Input, output_schema=self.Output, - disabled=not LINEAR_OAUTH_IS_CONFIGURED, test_input={ "term": "Test issue", "credentials": TEST_CREDENTIALS_INPUT_OAUTH, diff --git a/autogpt_platform/backend/backend/blocks/linear/projects.py b/autogpt_platform/backend/backend/blocks/linear/projects.py index 57c795030071..4eeb1ed99d30 100644 --- a/autogpt_platform/backend/backend/blocks/linear/projects.py +++ b/autogpt_platform/backend/backend/blocks/linear/projects.py @@ -11,7 +11,6 @@ from ._api import LinearAPIException, LinearClient from ._config import ( - LINEAR_OAUTH_IS_CONFIGURED, TEST_CREDENTIALS_INPUT_OAUTH, TEST_CREDENTIALS_OAUTH, LinearScope, @@ -45,7 +44,6 @@ def __init__(self): "term": "Test project", "credentials": TEST_CREDENTIALS_INPUT_OAUTH, }, - disabled=not LINEAR_OAUTH_IS_CONFIGURED, test_credentials=TEST_CREDENTIALS_OAUTH, test_output=[ ( diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index 6aff06903deb..b20df4052ef4 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -37,6 +37,7 @@ ProviderName.OPENAI, ProviderName.OPEN_ROUTER, ProviderName.LLAMA_API, + ProviderName.V0, ] AICredentials = CredentialsMetaInput[LLMProviderName, Literal["api_key"]] @@ -80,14 +81,20 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta): O3_MINI = "o3-mini" O3 = "o3-2025-04-16" O1 = "o1" - O1_PREVIEW = "o1-preview" O1_MINI = "o1-mini" + # GPT-5 models + GPT5 = "gpt-5-2025-08-07" + GPT5_MINI = "gpt-5-mini-2025-08-07" + GPT5_NANO = "gpt-5-nano-2025-08-07" + GPT5_CHAT = "gpt-5-chat-latest" GPT41 = "gpt-4.1-2025-04-14" + GPT41_MINI = "gpt-4.1-mini-2025-04-14" GPT4O_MINI = "gpt-4o-mini" GPT4O = "gpt-4o" GPT4_TURBO = "gpt-4-turbo" GPT3_5_TURBO = "gpt-3.5-turbo" # Anthropic models + CLAUDE_4_1_OPUS = "claude-opus-4-1-20250805" CLAUDE_4_OPUS = "claude-opus-4-20250514" CLAUDE_4_SONNET = "claude-sonnet-4-20250514" CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219" @@ -106,7 +113,6 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta): LLAMA3_1_8B = "llama-3.1-8b-instant" LLAMA3_70B = "llama3-70b-8192" LLAMA3_8B = "llama3-8b-8192" - MIXTRAL_8X7B = "mixtral-8x7b-32768" # Groq preview models DEEPSEEK_LLAMA_70B = "deepseek-r1-distill-llama-70b" # Ollama models @@ -116,21 +122,22 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta): OLLAMA_LLAMA3_405B = "llama3.1:405b" OLLAMA_DOLPHIN = "dolphin-mistral:latest" # OpenRouter models + OPENAI_GPT_OSS_120B = "openai/gpt-oss-120b" + OPENAI_GPT_OSS_20B = "openai/gpt-oss-20b" GEMINI_FLASH_1_5 = "google/gemini-flash-1.5" GEMINI_2_5_PRO = "google/gemini-2.5-pro-preview-03-25" - GROK_BETA = "x-ai/grok-beta" + GEMINI_2_5_FLASH = "google/gemini-2.5-flash" + GEMINI_2_0_FLASH = "google/gemini-2.0-flash-001" + GEMINI_2_5_FLASH_LITE_PREVIEW = "google/gemini-2.5-flash-lite-preview-06-17" + GEMINI_2_0_FLASH_LITE = "google/gemini-2.0-flash-lite-001" MISTRAL_NEMO = "mistralai/mistral-nemo" COHERE_COMMAND_R_08_2024 = "cohere/command-r-08-2024" COHERE_COMMAND_R_PLUS_08_2024 = "cohere/command-r-plus-08-2024" - EVA_QWEN_2_5_32B = "eva-unit-01/eva-qwen-2.5-32b" DEEPSEEK_CHAT = "deepseek/deepseek-chat" # Actually: DeepSeek V3 - PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = ( - "perplexity/llama-3.1-sonar-large-128k-online" - ) + DEEPSEEK_R1_0528 = "deepseek/deepseek-r1-0528" PERPLEXITY_SONAR = "perplexity/sonar" PERPLEXITY_SONAR_PRO = "perplexity/sonar-pro" PERPLEXITY_SONAR_DEEP_RESEARCH = "perplexity/sonar-deep-research" - QWEN_QWQ_32B_PREVIEW = "qwen/qwq-32b-preview" NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B = "nousresearch/hermes-3-llama-3.1-405b" NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B = "nousresearch/hermes-3-llama-3.1-70b" AMAZON_NOVA_LITE_V1 = "amazon/nova-lite-v1" @@ -140,11 +147,19 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta): GRYPHE_MYTHOMAX_L2_13B = "gryphe/mythomax-l2-13b" META_LLAMA_4_SCOUT = "meta-llama/llama-4-scout" META_LLAMA_4_MAVERICK = "meta-llama/llama-4-maverick" + GROK_4 = "x-ai/grok-4" + KIMI_K2 = "moonshotai/kimi-k2" + QWEN3_235B_A22B_THINKING = "qwen/qwen3-235b-a22b-thinking-2507" + QWEN3_CODER = "qwen/qwen3-coder" # Llama API models LLAMA_API_LLAMA_4_SCOUT = "Llama-4-Scout-17B-16E-Instruct-FP8" LLAMA_API_LLAMA4_MAVERICK = "Llama-4-Maverick-17B-128E-Instruct-FP8" LLAMA_API_LLAMA3_3_8B = "Llama-3.3-8B-Instruct" LLAMA_API_LLAMA3_3_70B = "Llama-3.3-70B-Instruct" + # v0 by Vercel models + V0_1_5_MD = "v0-1.5-md" + V0_1_5_LG = "v0-1.5-lg" + V0_1_0_MD = "v0-1.0-md" @property def metadata(self) -> ModelMetadata: @@ -168,11 +183,14 @@ def max_output_tokens(self) -> int | None: LlmModel.O3: ModelMetadata("openai", 200000, 100000), LlmModel.O3_MINI: ModelMetadata("openai", 200000, 100000), # o3-mini-2025-01-31 LlmModel.O1: ModelMetadata("openai", 200000, 100000), # o1-2024-12-17 - LlmModel.O1_PREVIEW: ModelMetadata( - "openai", 128000, 32768 - ), # o1-preview-2024-09-12 LlmModel.O1_MINI: ModelMetadata("openai", 128000, 65536), # o1-mini-2024-09-12 + # GPT-5 models + LlmModel.GPT5: ModelMetadata("openai", 400000, 128000), + LlmModel.GPT5_MINI: ModelMetadata("openai", 400000, 128000), + LlmModel.GPT5_NANO: ModelMetadata("openai", 400000, 128000), + LlmModel.GPT5_CHAT: ModelMetadata("openai", 400000, 16384), LlmModel.GPT41: ModelMetadata("openai", 1047576, 32768), + LlmModel.GPT41_MINI: ModelMetadata("openai", 1047576, 32768), LlmModel.GPT4O_MINI: ModelMetadata( "openai", 128000, 16384 ), # gpt-4o-mini-2024-07-18 @@ -182,6 +200,9 @@ def max_output_tokens(self) -> int | None: ), # gpt-4-turbo-2024-04-09 LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, 4096), # gpt-3.5-turbo-0125 # https://docs.anthropic.com/en/docs/about-claude/models + LlmModel.CLAUDE_4_1_OPUS: ModelMetadata( + "anthropic", 200000, 32000 + ), # claude-opus-4-1-20250805 LlmModel.CLAUDE_4_OPUS: ModelMetadata( "anthropic", 200000, 8192 ), # claude-4-opus-20250514 @@ -212,7 +233,6 @@ def max_output_tokens(self) -> int | None: LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 128000, 8192), LlmModel.LLAMA3_70B: ModelMetadata("groq", 8192, None), LlmModel.LLAMA3_8B: ModelMetadata("groq", 8192, None), - LlmModel.MIXTRAL_8X7B: ModelMetadata("groq", 32768, None), LlmModel.DEEPSEEK_LLAMA_70B: ModelMetadata("groq", 128000, None), # https://ollama.com/library LlmModel.OLLAMA_LLAMA3_3: ModelMetadata("ollama", 8192, None), @@ -223,15 +243,17 @@ def max_output_tokens(self) -> int | None: # https://openrouter.ai/models LlmModel.GEMINI_FLASH_1_5: ModelMetadata("open_router", 1000000, 8192), LlmModel.GEMINI_2_5_PRO: ModelMetadata("open_router", 1050000, 8192), - LlmModel.GROK_BETA: ModelMetadata("open_router", 131072, 131072), + LlmModel.GEMINI_2_5_FLASH: ModelMetadata("open_router", 1048576, 65535), + LlmModel.GEMINI_2_0_FLASH: ModelMetadata("open_router", 1048576, 8192), + LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelMetadata( + "open_router", 1048576, 65535 + ), + LlmModel.GEMINI_2_0_FLASH_LITE: ModelMetadata("open_router", 1048576, 8192), LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 128000, 4096), LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 128000, 4096), LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 128000, 4096), - LlmModel.EVA_QWEN_2_5_32B: ModelMetadata("open_router", 16384, 4096), LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 64000, 2048), - LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata( - "open_router", 127072, 127072 - ), + LlmModel.DEEPSEEK_R1_0528: ModelMetadata("open_router", 163840, 163840), LlmModel.PERPLEXITY_SONAR: ModelMetadata("open_router", 127000, 127000), LlmModel.PERPLEXITY_SONAR_PRO: ModelMetadata("open_router", 200000, 8000), LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: ModelMetadata( @@ -239,13 +261,14 @@ def max_output_tokens(self) -> int | None: 128000, 128000, ), - LlmModel.QWEN_QWQ_32B_PREVIEW: ModelMetadata("open_router", 32768, 32768), LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata( "open_router", 131000, 4096 ), LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata( "open_router", 12288, 12288 ), + LlmModel.OPENAI_GPT_OSS_120B: ModelMetadata("open_router", 131072, 131072), + LlmModel.OPENAI_GPT_OSS_20B: ModelMetadata("open_router", 131072, 32768), LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 300000, 5120), LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 128000, 5120), LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 300000, 5120), @@ -253,11 +276,19 @@ def max_output_tokens(self) -> int | None: LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4096, 4096), LlmModel.META_LLAMA_4_SCOUT: ModelMetadata("open_router", 131072, 131072), LlmModel.META_LLAMA_4_MAVERICK: ModelMetadata("open_router", 1048576, 1000000), + LlmModel.GROK_4: ModelMetadata("open_router", 256000, 256000), + LlmModel.KIMI_K2: ModelMetadata("open_router", 131000, 131000), + LlmModel.QWEN3_235B_A22B_THINKING: ModelMetadata("open_router", 262144, 262144), + LlmModel.QWEN3_CODER: ModelMetadata("open_router", 262144, 262144), # Llama API models LlmModel.LLAMA_API_LLAMA_4_SCOUT: ModelMetadata("llama_api", 128000, 4028), LlmModel.LLAMA_API_LLAMA4_MAVERICK: ModelMetadata("llama_api", 128000, 4028), LlmModel.LLAMA_API_LLAMA3_3_8B: ModelMetadata("llama_api", 128000, 4028), LlmModel.LLAMA_API_LLAMA3_3_70B: ModelMetadata("llama_api", 128000, 4028), + # v0 by Vercel models + LlmModel.V0_1_5_MD: ModelMetadata("v0", 128000, 64000), + LlmModel.V0_1_5_LG: ModelMetadata("v0", 512000, 64000), + LlmModel.V0_1_0_MD: ModelMetadata("v0", 128000, 64000), } for model in LlmModel: @@ -471,6 +502,7 @@ async def llm_call( messages=messages, max_tokens=max_tokens, tools=an_tools, + timeout=600, ) if not resp.content: @@ -653,7 +685,11 @@ async def llm_call( client = openai.OpenAI( base_url="https://api.aimlapi.com/v2", api_key=credentials.api_key.get_secret_value(), - default_headers={"X-Project": "AutoGPT"}, + default_headers={ + "X-Project": "AutoGPT", + "X-Title": "AutoGPT", + "HTTP-Referer": "https://github.com/Significant-Gravitas/AutoGPT", + }, ) completion = client.chat.completions.create( @@ -673,6 +709,42 @@ async def llm_call( ), reasoning=None, ) + elif provider == "v0": + tools_param = tools if tools else openai.NOT_GIVEN + client = openai.AsyncOpenAI( + base_url="https://api.v0.dev/v1", + api_key=credentials.api_key.get_secret_value(), + ) + + response_format = None + if json_format: + response_format = {"type": "json_object"} + + parallel_tool_calls_param = get_parallel_tool_calls_param( + llm_model, parallel_tool_calls + ) + + response = await client.chat.completions.create( + model=llm_model.value, + messages=prompt, # type: ignore + response_format=response_format, # type: ignore + max_tokens=max_tokens, + tools=tools_param, # type: ignore + parallel_tool_calls=parallel_tool_calls_param, + ) + + tool_calls = extract_openai_tool_calls(response) + reasoning = extract_openai_reasoning(response) + + return LLMResponse( + raw_response=response.choices[0].message, + prompt=prompt, + response=response.choices[0].message.content or "", + tool_calls=tool_calls, + prompt_tokens=response.usage.prompt_tokens if response.usage else 0, + completion_tokens=response.usage.completion_tokens if response.usage else 0, + reasoning=reasoning, + ) else: raise ValueError(f"Unsupported LLM provider: {provider}") @@ -920,10 +992,22 @@ def validate_response(parsed: object) -> str | None: ) if not response_error: + self.merge_stats( + NodeExecutionStats( + llm_call_count=retry_count + 1, + llm_retry_count=retry_count, + ) + ) yield "response", response_obj yield "prompt", self.prompt return else: + self.merge_stats( + NodeExecutionStats( + llm_call_count=retry_count + 1, + llm_retry_count=retry_count, + ) + ) yield "response", {"response": response_text} yield "prompt", self.prompt return @@ -955,13 +1039,6 @@ def validate_response(parsed: object) -> str | None: f"Reducing max_tokens to {input_data.max_tokens} for next attempt" ) retry_prompt = f"Error calling LLM: {e}" - finally: - self.merge_stats( - NodeExecutionStats( - llm_call_count=retry_count + 1, - llm_retry_count=retry_count, - ) - ) raise RuntimeError(retry_prompt) diff --git a/autogpt_platform/backend/backend/blocks/persistence.py b/autogpt_platform/backend/backend/blocks/persistence.py index 42634fdc9aec..8b165569b560 100644 --- a/autogpt_platform/backend/backend/blocks/persistence.py +++ b/autogpt_platform/backend/backend/blocks/persistence.py @@ -1,22 +1,13 @@ import logging from typing import Any, Literal -from autogpt_libs.utils.cache import thread_cached - from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField +from backend.util.clients import get_database_manager_async_client logger = logging.getLogger(__name__) -@thread_cached -def get_database_manager_client(): - from backend.executor import DatabaseManagerAsyncClient - from backend.util.service import get_service_client - - return get_service_client(DatabaseManagerAsyncClient, health_check=False) - - StorageScope = Literal["within_agent", "across_agents"] @@ -88,7 +79,7 @@ async def run( async def _store_data( self, user_id: str, node_exec_id: str, key: str, data: Any ) -> Any | None: - return await get_database_manager_client().set_execution_kv_data( + return await get_database_manager_async_client().set_execution_kv_data( user_id=user_id, node_exec_id=node_exec_id, key=key, @@ -149,7 +140,7 @@ async def run( yield "value", input_data.default_value async def _retrieve_data(self, user_id: str, key: str) -> Any | None: - return await get_database_manager_client().get_execution_kv_data( + return await get_database_manager_async_client().get_execution_kv_data( user_id=user_id, key=key, ) diff --git a/autogpt_platform/backend/backend/blocks/slant3d/order.py b/autogpt_platform/backend/backend/blocks/slant3d/order.py index f1cab18d273c..43a58024687d 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/order.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/order.py @@ -3,8 +3,7 @@ from backend.data.block import BlockOutput, BlockSchema from backend.data.model import APIKeyCredentials, SchemaField -from backend.util import settings -from backend.util.settings import BehaveAs +from backend.util.settings import BehaveAs, Settings from ._api import ( TEST_CREDENTIALS, @@ -16,6 +15,8 @@ ) from .base import Slant3DBlockBase +settings = Settings() + class Slant3DCreateOrderBlock(Slant3DBlockBase): """Block for creating new orders""" @@ -280,7 +281,7 @@ def __init__(self): input_schema=self.Input, output_schema=self.Output, # This block is disabled for cloud hosted because it allows access to all orders for the account - disabled=settings.Settings().config.behave_as == BehaveAs.CLOUD, + disabled=settings.config.behave_as == BehaveAs.CLOUD, test_input={"credentials": TEST_CREDENTIALS_INPUT}, test_credentials=TEST_CREDENTIALS, test_output=[ diff --git a/autogpt_platform/backend/backend/blocks/slant3d/webhook.py b/autogpt_platform/backend/backend/blocks/slant3d/webhook.py index 8a690cf1ad99..22f87b468d86 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/webhook.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/webhook.py @@ -9,8 +9,7 @@ ) from backend.data.model import SchemaField from backend.integrations.providers import ProviderName -from backend.util import settings -from backend.util.settings import AppEnvironment, BehaveAs +from backend.util.settings import AppEnvironment, BehaveAs, Settings from ._api import ( TEST_CREDENTIALS, @@ -19,6 +18,8 @@ Slant3DCredentialsInput, ) +settings = Settings() + class Slant3DTriggerBase: """Base class for Slant3D webhook triggers""" @@ -76,8 +77,8 @@ def __init__(self): ), # All webhooks are currently subscribed to for all orders. This works for self hosted, but not for cloud hosted prod disabled=( - settings.Settings().config.behave_as == BehaveAs.CLOUD - and settings.Settings().config.app_env != AppEnvironment.LOCAL + settings.config.behave_as == BehaveAs.CLOUD + and settings.config.app_env != AppEnvironment.LOCAL ), categories={BlockCategory.DEVELOPER_TOOLS}, input_schema=self.Input, diff --git a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py index 5227d1f7601a..9ae41d9c9382 100644 --- a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py +++ b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py @@ -3,8 +3,6 @@ from collections import Counter from typing import TYPE_CHECKING, Any -from autogpt_libs.utils.cache import thread_cached - import backend.blocks.llm as llm from backend.blocks.agent import AgentExecutorBlock from backend.data.block import ( @@ -15,8 +13,9 @@ BlockSchema, BlockType, ) -from backend.data.model import SchemaField +from backend.data.model import NodeExecutionStats, SchemaField from backend.util import json +from backend.util.clients import get_database_manager_async_client if TYPE_CHECKING: from backend.data.graph import Link, Node @@ -24,14 +23,6 @@ logger = logging.getLogger(__name__) -@thread_cached -def get_database_manager_client(): - from backend.executor import DatabaseManagerAsyncClient - from backend.util.service import get_service_client - - return get_service_client(DatabaseManagerAsyncClient, health_check=False) - - def _get_tool_requests(entry: dict[str, Any]) -> list[str]: """ Return a list of tool_call_ids if the entry is a tool request. @@ -300,9 +291,32 @@ async def _create_block_function_signature( for link in links: sink_name = SmartDecisionMakerBlock.cleanup(link.sink_name) - properties[sink_name] = sink_block_input_schema.get_field_schema( - link.sink_name - ) + + # Handle dynamic fields (e.g., values_#_*, items_$_*, etc.) + # These are fields that get merged by the executor into their base field + if ( + "_#_" in link.sink_name + or "_$_" in link.sink_name + or "_@_" in link.sink_name + ): + # For dynamic fields, provide a generic string schema + # The executor will handle merging these into the appropriate structure + properties[sink_name] = { + "type": "string", + "description": f"Dynamic value for {link.sink_name}", + } + else: + # For regular fields, use the block's schema + try: + properties[sink_name] = sink_block_input_schema.get_field_schema( + link.sink_name + ) + except (KeyError, AttributeError): + # If the field doesn't exist in the schema, provide a generic schema + properties[sink_name] = { + "type": "string", + "description": f"Value for {link.sink_name}", + } tool_function["parameters"] = { **block.input_schema.jsonschema(), @@ -333,7 +347,7 @@ async def _create_agent_function_signature( if not graph_id or not graph_version: raise ValueError("Graph ID or Graph Version not found in sink node.") - db_client = get_database_manager_client() + db_client = get_database_manager_async_client() sink_graph_meta = await db_client.get_graph_metadata(graph_id, graph_version) if not sink_graph_meta: raise ValueError( @@ -393,7 +407,7 @@ async def _create_function_signature(node_id: str) -> list[dict[str, Any]]: ValueError: If no tool links are found for the specified node_id, or if a sink node or its metadata cannot be found. """ - db_client = get_database_manager_client() + db_client = get_database_manager_async_client() tools = [ (link, node) for link, node in await db_client.get_connected_output_nodes(node_id) @@ -487,10 +501,6 @@ async def run( } ) prompt.extend(tool_output) - if input_data.multiple_tool_calls: - input_data.sys_prompt += "\nYou can call a tool (different tools) multiple times in a single response." - else: - input_data.sys_prompt += "\nOnly provide EXACTLY one function call, multiple tool calls is strictly prohibited." values = input_data.prompt_values if values: @@ -520,6 +530,15 @@ async def run( parallel_tool_calls=input_data.multiple_tool_calls, ) + # Track LLM usage stats + self.merge_stats( + NodeExecutionStats( + input_token_count=response.prompt_tokens, + output_token_count=response.completion_tokens, + llm_call_count=1, + ) + ) + if not response.tool_calls: yield "finished", response.response return diff --git a/autogpt_platform/backend/backend/blocks/stagehand/__init__.py b/autogpt_platform/backend/backend/blocks/stagehand/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/stagehand/_config.py b/autogpt_platform/backend/backend/blocks/stagehand/_config.py new file mode 100644 index 000000000000..43ec6cd5ac05 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/stagehand/_config.py @@ -0,0 +1,8 @@ +from backend.sdk import BlockCostType, ProviderBuilder + +stagehand = ( + ProviderBuilder("stagehand") + .with_api_key("STAGEHAND_API_KEY", "Stagehand API Key") + .with_base_cost(1, BlockCostType.RUN) + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/stagehand/blocks.py b/autogpt_platform/backend/backend/blocks/stagehand/blocks.py new file mode 100644 index 000000000000..50ec368dd20b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/stagehand/blocks.py @@ -0,0 +1,393 @@ +import logging +import signal +import threading +from contextlib import contextmanager +from enum import Enum + +# Monkey patch Stagehands to prevent signal handling in worker threads +import stagehand.main +from stagehand import Stagehand + +from backend.blocks.llm import ( + MODEL_METADATA, + AICredentials, + AICredentialsField, + LlmModel, + ModelMetadata, +) +from backend.blocks.stagehand._config import stagehand as stagehand_provider +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + SchemaField, +) + +# Store the original method +original_register_signal_handlers = stagehand.main.Stagehand._register_signal_handlers + + +def safe_register_signal_handlers(self): + """Only register signal handlers in the main thread""" + if threading.current_thread() is threading.main_thread(): + original_register_signal_handlers(self) + else: + # Skip signal handling in worker threads + pass + + +# Replace the method +stagehand.main.Stagehand._register_signal_handlers = safe_register_signal_handlers + + +@contextmanager +def disable_signal_handling(): + """Context manager to temporarily disable signal handling""" + if threading.current_thread() is not threading.main_thread(): + # In worker threads, temporarily replace signal.signal with a no-op + original_signal = signal.signal + + def noop_signal(*args, **kwargs): + pass + + signal.signal = noop_signal + try: + yield + finally: + signal.signal = original_signal + else: + # In main thread, don't modify anything + yield + + +logger = logging.getLogger(__name__) + + +class StagehandRecommendedLlmModel(str, Enum): + """ + This is subset of LLModel from autogpt_platform/backend/backend/blocks/llm.py + + It contains only the models recommended by Stagehand + """ + + # OpenAI + GPT41 = "gpt-4.1-2025-04-14" + GPT41_MINI = "gpt-4.1-mini-2025-04-14" + + # Anthropic + CLAUDE_3_7_SONNET = "claude-3-7-sonnet-20250219" + + @property + def provider_name(self) -> str: + """ + Returns the provider name for the model in the required format for Stagehand: + provider/model_name + """ + model_metadata = MODEL_METADATA[LlmModel(self.value)] + model_name = self.value + + if len(model_name.split("/")) == 1 and not self.value.startswith( + model_metadata.provider + ): + assert ( + model_metadata.provider != "open_router" + ), "Logic failed and open_router provider attempted to be prepended to model name! in stagehand/_config.py" + model_name = f"{model_metadata.provider}/{model_name}" + + logger.error(f"Model name: {model_name}") + return model_name + + @property + def provider(self) -> str: + return MODEL_METADATA[LlmModel(self.value)].provider + + @property + def metadata(self) -> ModelMetadata: + return MODEL_METADATA[LlmModel(self.value)] + + @property + def context_window(self) -> int: + return MODEL_METADATA[LlmModel(self.value)].context_window + + @property + def max_output_tokens(self) -> int | None: + return MODEL_METADATA[LlmModel(self.value)].max_output_tokens + + +class StagehandObserveBlock(Block): + class Input(BlockSchema): + # Browserbase credentials (Stagehand provider) or raw API key + stagehand_credentials: CredentialsMetaInput = ( + stagehand_provider.credentials_field( + description="Stagehand/Browserbase API key" + ) + ) + browserbase_project_id: str = SchemaField( + description="Browserbase project ID (required if using Browserbase)", + ) + # Model selection and credentials (provider-discriminated like llm.py) + model: StagehandRecommendedLlmModel = SchemaField( + title="LLM Model", + description="LLM to use for Stagehand (provider is inferred)", + default=StagehandRecommendedLlmModel.CLAUDE_3_7_SONNET, + advanced=False, + ) + model_credentials: AICredentials = AICredentialsField() + url: str = SchemaField( + description="URL to navigate to.", + ) + instruction: str = SchemaField( + description="Natural language description of elements or actions to discover.", + ) + iframes: bool = SchemaField( + description="Whether to search within iframes. If True, Stagehand will search for actions within iframes.", + default=True, + ) + domSettleTimeoutMs: int = SchemaField( + description="Timeout in milliseconds for DOM settlement.Wait longer for dynamic content", + default=45000, + ) + + class Output(BlockSchema): + selector: str = SchemaField(description="XPath selector to locate element.") + description: str = SchemaField(description="Human-readable description") + method: str | None = SchemaField(description="Suggested action method") + arguments: list[str] | None = SchemaField( + description="Additional action parameters" + ) + + def __init__(self): + super().__init__( + id="d3863944-0eaf-45c4-a0c9-63e0fe1ee8b9", + description="Find suggested actions for your workflows", + categories={BlockCategory.AI, BlockCategory.DEVELOPER_TOOLS}, + input_schema=StagehandObserveBlock.Input, + output_schema=StagehandObserveBlock.Output, + ) + + async def run( + self, + input_data: Input, + *, + stagehand_credentials: APIKeyCredentials, + model_credentials: APIKeyCredentials, + **kwargs, + ) -> BlockOutput: + + logger.info(f"OBSERVE: Stagehand credentials: {stagehand_credentials}") + logger.info( + f"OBSERVE: Model credentials: {model_credentials} for provider {model_credentials.provider} secret: {model_credentials.api_key.get_secret_value()}" + ) + + with disable_signal_handling(): + stagehand = Stagehand( + api_key=stagehand_credentials.api_key.get_secret_value(), + project_id=input_data.browserbase_project_id, + model_name=input_data.model.provider_name, + model_api_key=model_credentials.api_key.get_secret_value(), + ) + + await stagehand.init() + + page = stagehand.page + + assert page is not None, "Stagehand page is not initialized" + + await page.goto(input_data.url) + + observe_results = await page.observe( + input_data.instruction, + iframes=input_data.iframes, + domSettleTimeoutMs=input_data.domSettleTimeoutMs, + ) + for result in observe_results: + yield "selector", result.selector + yield "description", result.description + yield "method", result.method + yield "arguments", result.arguments + + +class StagehandActBlock(Block): + class Input(BlockSchema): + # Browserbase credentials (Stagehand provider) or raw API key + stagehand_credentials: CredentialsMetaInput = ( + stagehand_provider.credentials_field( + description="Stagehand/Browserbase API key" + ) + ) + browserbase_project_id: str = SchemaField( + description="Browserbase project ID (required if using Browserbase)", + ) + # Model selection and credentials (provider-discriminated like llm.py) + model: StagehandRecommendedLlmModel = SchemaField( + title="LLM Model", + description="LLM to use for Stagehand (provider is inferred)", + default=StagehandRecommendedLlmModel.CLAUDE_3_7_SONNET, + advanced=False, + ) + model_credentials: AICredentials = AICredentialsField() + url: str = SchemaField( + description="URL to navigate to.", + ) + action: list[str] = SchemaField( + description="Action to perform. Suggested actions are: click, fill, type, press, scroll, select from dropdown. For multi-step actions, add an entry for each step.", + ) + variables: dict[str, str] = SchemaField( + description="Variables to use in the action. Variables contains data you want the action to use.", + default_factory=dict, + ) + iframes: bool = SchemaField( + description="Whether to search within iframes. If True, Stagehand will search for actions within iframes.", + default=True, + ) + domSettleTimeoutMs: int = SchemaField( + description="Timeout in milliseconds for DOM settlement.Wait longer for dynamic content", + default=45000, + ) + timeoutMs: int = SchemaField( + description="Timeout in milliseconds for DOM ready. Extended timeout for slow-loading forms", + default=60000, + ) + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether the action was completed successfully" + ) + message: str = SchemaField(description="Details about the action’s execution.") + action: str = SchemaField(description="Action performed") + + def __init__(self): + super().__init__( + id="86eba68b-9549-4c0b-a0db-47d85a56cc27", + description="Interact with a web page by performing actions on a web page. Use it to build self-healing and deterministic automations that adapt to website chang.", + categories={BlockCategory.AI, BlockCategory.DEVELOPER_TOOLS}, + input_schema=StagehandActBlock.Input, + output_schema=StagehandActBlock.Output, + ) + + async def run( + self, + input_data: Input, + *, + stagehand_credentials: APIKeyCredentials, + model_credentials: APIKeyCredentials, + **kwargs, + ) -> BlockOutput: + + logger.info(f"ACT: Stagehand credentials: {stagehand_credentials}") + logger.info( + f"ACT: Model credentials: {model_credentials} for provider {model_credentials.provider} secret: {model_credentials.api_key.get_secret_value()}" + ) + + with disable_signal_handling(): + stagehand = Stagehand( + api_key=stagehand_credentials.api_key.get_secret_value(), + project_id=input_data.browserbase_project_id, + model_name=input_data.model.provider_name, + model_api_key=model_credentials.api_key.get_secret_value(), + ) + + await stagehand.init() + + page = stagehand.page + + assert page is not None, "Stagehand page is not initialized" + + await page.goto(input_data.url) + for action in input_data.action: + action_results = await page.act( + action, + variables=input_data.variables, + iframes=input_data.iframes, + domSettleTimeoutMs=input_data.domSettleTimeoutMs, + timeoutMs=input_data.timeoutMs, + ) + yield "success", action_results.success + yield "message", action_results.message + yield "action", action_results.action + + +class StagehandExtractBlock(Block): + class Input(BlockSchema): + # Browserbase credentials (Stagehand provider) or raw API key + stagehand_credentials: CredentialsMetaInput = ( + stagehand_provider.credentials_field( + description="Stagehand/Browserbase API key" + ) + ) + browserbase_project_id: str = SchemaField( + description="Browserbase project ID (required if using Browserbase)", + ) + # Model selection and credentials (provider-discriminated like llm.py) + model: StagehandRecommendedLlmModel = SchemaField( + title="LLM Model", + description="LLM to use for Stagehand (provider is inferred)", + default=StagehandRecommendedLlmModel.CLAUDE_3_7_SONNET, + advanced=False, + ) + model_credentials: AICredentials = AICredentialsField() + url: str = SchemaField( + description="URL to navigate to.", + ) + instruction: str = SchemaField( + description="Natural language description of elements or actions to discover.", + ) + iframes: bool = SchemaField( + description="Whether to search within iframes. If True, Stagehand will search for actions within iframes.", + default=True, + ) + domSettleTimeoutMs: int = SchemaField( + description="Timeout in milliseconds for DOM settlement.Wait longer for dynamic content", + default=45000, + ) + + class Output(BlockSchema): + extraction: str = SchemaField(description="Extracted data from the page.") + + def __init__(self): + super().__init__( + id="fd3c0b18-2ba6-46ae-9339-fcb40537ad98", + description="Extract structured data from a webpage.", + categories={BlockCategory.AI, BlockCategory.DEVELOPER_TOOLS}, + input_schema=StagehandExtractBlock.Input, + output_schema=StagehandExtractBlock.Output, + ) + + async def run( + self, + input_data: Input, + *, + stagehand_credentials: APIKeyCredentials, + model_credentials: APIKeyCredentials, + **kwargs, + ) -> BlockOutput: + + logger.info(f"EXTRACT: Stagehand credentials: {stagehand_credentials}") + logger.info( + f"EXTRACT: Model credentials: {model_credentials} for provider {model_credentials.provider} secret: {model_credentials.api_key.get_secret_value()}" + ) + + with disable_signal_handling(): + stagehand = Stagehand( + api_key=stagehand_credentials.api_key.get_secret_value(), + project_id=input_data.browserbase_project_id, + model_name=input_data.model.provider_name, + model_api_key=model_credentials.api_key.get_secret_value(), + ) + + await stagehand.init() + + page = stagehand.page + + assert page is not None, "Stagehand page is not initialized" + + await page.goto(input_data.url) + extraction = await page.extract( + input_data.instruction, + iframes=input_data.iframes, + domSettleTimeoutMs=input_data.domSettleTimeoutMs, + ) + yield "extraction", str(extraction.model_dump()["extraction"]) diff --git a/autogpt_platform/backend/backend/blocks/system/__init__.py b/autogpt_platform/backend/backend/blocks/system/__init__.py new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/system/__init__.py @@ -0,0 +1 @@ + diff --git a/autogpt_platform/backend/backend/blocks/system/library_operations.py b/autogpt_platform/backend/backend/blocks/system/library_operations.py new file mode 100644 index 000000000000..2cf1aec0db26 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/system/library_operations.py @@ -0,0 +1,283 @@ +import logging +from typing import Any + +from pydantic import BaseModel + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField +from backend.util.clients import get_database_manager_async_client + +logger = logging.getLogger(__name__) + + +# Duplicate pydantic models for store data so we don't accidently change the data shape in the blocks unintentionally when editing the backend +class LibraryAgent(BaseModel): + """Model representing an agent in the user's library.""" + + library_agent_id: str = "" + agent_id: str = "" + agent_version: int = 0 + agent_name: str = "" + description: str = "" + creator: str = "" + is_archived: bool = False + categories: list[str] = [] + + +class AddToLibraryFromStoreBlock(Block): + """ + Block that adds an agent from the store to the user's library. + This enables users to easily import agents from the marketplace into their personal collection. + """ + + class Input(BlockSchema): + store_listing_version_id: str = SchemaField( + description="The ID of the store listing version to add to library" + ) + agent_name: str | None = SchemaField( + description="Optional custom name for the agent in your library", + default=None, + ) + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether the agent was successfully added to library" + ) + library_agent_id: str = SchemaField( + description="The ID of the library agent entry" + ) + agent_id: str = SchemaField(description="The ID of the agent graph") + agent_version: int = SchemaField( + description="The version number of the agent graph" + ) + agent_name: str = SchemaField(description="The name of the agent") + message: str = SchemaField(description="Success or error message") + + def __init__(self): + super().__init__( + id="2602a7b1-3f4d-4e5f-9c8b-1a2b3c4d5e6f", + description="Add an agent from the store to your personal library", + categories={BlockCategory.BASIC}, + input_schema=AddToLibraryFromStoreBlock.Input, + output_schema=AddToLibraryFromStoreBlock.Output, + test_input={ + "store_listing_version_id": "test-listing-id", + "agent_name": "My Custom Agent", + }, + test_output=[ + ("success", True), + ("library_agent_id", "test-library-id"), + ("agent_id", "test-agent-id"), + ("agent_version", 1), + ("agent_name", "Test Agent"), + ("message", "Agent successfully added to library"), + ], + test_mock={ + "_add_to_library": lambda *_, **__: LibraryAgent( + library_agent_id="test-library-id", + agent_id="test-agent-id", + agent_version=1, + agent_name="Test Agent", + ) + }, + ) + + async def run( + self, + input_data: Input, + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + library_agent = await self._add_to_library( + user_id=user_id, + store_listing_version_id=input_data.store_listing_version_id, + custom_name=input_data.agent_name, + ) + + yield "success", True + yield "library_agent_id", library_agent.library_agent_id + yield "agent_id", library_agent.agent_id + yield "agent_version", library_agent.agent_version + yield "agent_name", library_agent.agent_name + yield "message", "Agent successfully added to library" + + async def _add_to_library( + self, + user_id: str, + store_listing_version_id: str, + custom_name: str | None = None, + ) -> LibraryAgent: + """ + Add a store agent to the user's library using the existing library database function. + """ + library_agent = ( + await get_database_manager_async_client().add_store_agent_to_library( + store_listing_version_id=store_listing_version_id, user_id=user_id + ) + ) + + # If custom name is provided, we could update the library agent name here + # For now, we'll just return the agent info + agent_name = custom_name if custom_name else library_agent.name + + return LibraryAgent( + library_agent_id=library_agent.id, + agent_id=library_agent.graph_id, + agent_version=library_agent.graph_version, + agent_name=agent_name, + ) + + +class ListLibraryAgentsBlock(Block): + """ + Block that lists all agents in the user's library. + """ + + class Input(BlockSchema): + search_query: str | None = SchemaField( + description="Optional search query to filter agents", default=None + ) + limit: int = SchemaField( + description="Maximum number of agents to return", default=50, ge=1, le=100 + ) + page: int = SchemaField( + description="Page number for pagination", default=1, ge=1 + ) + + class Output(BlockSchema): + agents: list[LibraryAgent] = SchemaField( + description="List of agents in the library", + default_factory=list, + ) + agent: LibraryAgent = SchemaField( + description="Individual library agent (yielded for each agent)" + ) + total_count: int = SchemaField( + description="Total number of agents in library", default=0 + ) + page: int = SchemaField(description="Current page number", default=1) + total_pages: int = SchemaField(description="Total number of pages", default=1) + + def __init__(self): + super().__init__( + id="082602d3-a74d-4600-9e9c-15b3af7eae98", + description="List all agents in your personal library", + categories={BlockCategory.BASIC, BlockCategory.DATA}, + input_schema=ListLibraryAgentsBlock.Input, + output_schema=ListLibraryAgentsBlock.Output, + test_input={ + "search_query": None, + "limit": 10, + "page": 1, + }, + test_output=[ + ( + "agents", + [ + LibraryAgent( + library_agent_id="test-lib-id", + agent_id="test-agent-id", + agent_version=1, + agent_name="Test Library Agent", + description="A test agent in library", + creator="Test User", + ), + ], + ), + ("total_count", 1), + ("page", 1), + ("total_pages", 1), + ( + "agent", + LibraryAgent( + library_agent_id="test-lib-id", + agent_id="test-agent-id", + agent_version=1, + agent_name="Test Library Agent", + description="A test agent in library", + creator="Test User", + ), + ), + ], + test_mock={ + "_list_library_agents": lambda *_, **__: { + "agents": [ + LibraryAgent( + library_agent_id="test-lib-id", + agent_id="test-agent-id", + agent_version=1, + agent_name="Test Library Agent", + description="A test agent in library", + creator="Test User", + ) + ], + "total": 1, + "page": 1, + "total_pages": 1, + } + }, + ) + + async def run( + self, + input_data: Input, + *, + user_id: str, + **kwargs, + ) -> BlockOutput: + result = await self._list_library_agents( + user_id=user_id, + search_query=input_data.search_query, + limit=input_data.limit, + page=input_data.page, + ) + + agents = result["agents"] + + yield "agents", agents + yield "total_count", result["total"] + yield "page", result["page"] + yield "total_pages", result["total_pages"] + + # Yield each agent individually for better graph connectivity + for agent in agents: + yield "agent", agent + + async def _list_library_agents( + self, + user_id: str, + search_query: str | None = None, + limit: int = 50, + page: int = 1, + ) -> dict[str, Any]: + """ + List agents in the user's library using the database client. + """ + result = await get_database_manager_async_client().list_library_agents( + user_id=user_id, + search_term=search_query, + page=page, + page_size=limit, + ) + + agents = [ + LibraryAgent( + library_agent_id=agent.id, + agent_id=agent.graph_id, + agent_version=agent.graph_version, + agent_name=agent.name, + description=getattr(agent, "description", ""), + creator=getattr(agent, "creator", ""), + is_archived=getattr(agent, "is_archived", False), + categories=getattr(agent, "categories", []), + ) + for agent in result.agents + ] + + return { + "agents": agents, + "total": result.pagination.total_items, + "page": result.pagination.current_page, + "total_pages": result.pagination.total_pages, + } diff --git a/autogpt_platform/backend/backend/blocks/system/store_operations.py b/autogpt_platform/backend/backend/blocks/system/store_operations.py new file mode 100644 index 000000000000..6f5763bc93a0 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/system/store_operations.py @@ -0,0 +1,311 @@ +import logging +from typing import Literal + +from pydantic import BaseModel + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField +from backend.util.clients import get_database_manager_async_client + +logger = logging.getLogger(__name__) + + +# Duplicate pydantic models for store data so we don't accidently change the data shape in the blocks unintentionally when editing the backend +class StoreAgent(BaseModel): + """Model representing a store agent.""" + + slug: str = "" + name: str = "" + description: str = "" + creator: str = "" + rating: float = 0.0 + runs: int = 0 + categories: list[str] = [] + + +class StoreAgentDict(BaseModel): + """Dictionary representation of a store agent.""" + + slug: str + name: str + description: str + creator: str + rating: float + runs: int + + +class SearchAgentsResponse(BaseModel): + """Response from searching store agents.""" + + agents: list[StoreAgentDict] + total_count: int + + +class StoreAgentDetails(BaseModel): + """Detailed information about a store agent.""" + + found: bool + store_listing_version_id: str = "" + agent_name: str = "" + description: str = "" + creator: str = "" + categories: list[str] = [] + runs: int = 0 + rating: float = 0.0 + + +class GetStoreAgentDetailsBlock(Block): + """ + Block that retrieves detailed information about an agent from the store. + """ + + class Input(BlockSchema): + creator: str = SchemaField(description="The username of the agent creator") + slug: str = SchemaField(description="The name of the agent") + + class Output(BlockSchema): + found: bool = SchemaField( + description="Whether the agent was found in the store" + ) + store_listing_version_id: str = SchemaField( + description="The store listing version ID" + ) + agent_name: str = SchemaField(description="Name of the agent") + description: str = SchemaField(description="Description of the agent") + creator: str = SchemaField(description="Creator of the agent") + categories: list[str] = SchemaField( + description="Categories the agent belongs to", default_factory=list + ) + runs: int = SchemaField( + description="Number of times the agent has been run", default=0 + ) + rating: float = SchemaField( + description="Average rating of the agent", default=0.0 + ) + + def __init__(self): + super().__init__( + id="b604f0ec-6e0d-40a7-bf55-9fd09997cced", + description="Get detailed information about an agent from the store", + categories={BlockCategory.BASIC, BlockCategory.DATA}, + input_schema=GetStoreAgentDetailsBlock.Input, + output_schema=GetStoreAgentDetailsBlock.Output, + test_input={"creator": "test-creator", "slug": "test-agent-slug"}, + test_output=[ + ("found", True), + ("store_listing_version_id", "test-listing-id"), + ("agent_name", "Test Agent"), + ("description", "A test agent"), + ("creator", "Test Creator"), + ("categories", ["productivity", "automation"]), + ("runs", 100), + ("rating", 4.5), + ], + test_mock={ + "_get_agent_details": lambda *_, **__: StoreAgentDetails( + found=True, + store_listing_version_id="test-listing-id", + agent_name="Test Agent", + description="A test agent", + creator="Test Creator", + categories=["productivity", "automation"], + runs=100, + rating=4.5, + ) + }, + static_output=True, + ) + + async def run( + self, + input_data: Input, + **kwargs, + ) -> BlockOutput: + details = await self._get_agent_details( + creator=input_data.creator, slug=input_data.slug + ) + yield "found", details.found + yield "store_listing_version_id", details.store_listing_version_id + yield "agent_name", details.agent_name + yield "description", details.description + yield "creator", details.creator + yield "categories", details.categories + yield "runs", details.runs + yield "rating", details.rating + + async def _get_agent_details(self, creator: str, slug: str) -> StoreAgentDetails: + """ + Retrieve detailed information about a store agent. + """ + # Get by specific version ID + agent_details = ( + await get_database_manager_async_client().get_store_agent_details( + username=creator, agent_name=slug + ) + ) + + return StoreAgentDetails( + found=True, + store_listing_version_id=agent_details.store_listing_version_id, + agent_name=agent_details.agent_name, + description=agent_details.description, + creator=agent_details.creator, + categories=( + agent_details.categories if hasattr(agent_details, "categories") else [] + ), + runs=agent_details.runs, + rating=agent_details.rating, + ) + + +class SearchStoreAgentsBlock(Block): + """ + Block that searches for agents in the store based on various criteria. + """ + + class Input(BlockSchema): + query: str | None = SchemaField( + description="Search query to find agents", default=None + ) + category: str | None = SchemaField( + description="Filter by category", default=None + ) + sort_by: Literal["rating", "runs", "name", "recent"] = SchemaField( + description="How to sort the results", default="rating" + ) + limit: int = SchemaField( + description="Maximum number of results to return", default=10, ge=1, le=100 + ) + + class Output(BlockSchema): + agents: list[StoreAgent] = SchemaField( + description="List of agents matching the search criteria", + default_factory=list, + ) + agent: StoreAgent = SchemaField(description="Basic information of the agent") + total_count: int = SchemaField( + description="Total number of agents found", default=0 + ) + + def __init__(self): + super().__init__( + id="39524701-026c-4328-87cc-1b88c8e2cb4c", + description="Search for agents in the store", + categories={BlockCategory.BASIC, BlockCategory.DATA}, + input_schema=SearchStoreAgentsBlock.Input, + output_schema=SearchStoreAgentsBlock.Output, + test_input={ + "query": "productivity", + "category": None, + "sort_by": "rating", + "limit": 10, + }, + test_output=[ + ( + "agents", + [ + { + "slug": "test-agent", + "name": "Test Agent", + "description": "A test agent", + "creator": "Test Creator", + "rating": 4.5, + "runs": 100, + } + ], + ), + ("total_count", 1), + ( + "agent", + { + "slug": "test-agent", + "name": "Test Agent", + "description": "A test agent", + "creator": "Test Creator", + "rating": 4.5, + "runs": 100, + }, + ), + ], + test_mock={ + "_search_agents": lambda *_, **__: SearchAgentsResponse( + agents=[ + StoreAgentDict( + slug="test-agent", + name="Test Agent", + description="A test agent", + creator="Test Creator", + rating=4.5, + runs=100, + ) + ], + total_count=1, + ) + }, + ) + + async def run( + self, + input_data: Input, + **kwargs, + ) -> BlockOutput: + result = await self._search_agents( + query=input_data.query, + category=input_data.category, + sort_by=input_data.sort_by, + limit=input_data.limit, + ) + + agents = result.agents + total_count = result.total_count + + # Convert to dict for output + agents_as_dicts = [agent.model_dump() for agent in agents] + + yield "agents", agents_as_dicts + yield "total_count", total_count + + for agent_dict in agents_as_dicts: + yield "agent", agent_dict + + async def _search_agents( + self, + query: str | None = None, + category: str | None = None, + sort_by: str = "rating", + limit: int = 10, + ) -> SearchAgentsResponse: + """ + Search for agents in the store using the existing store database function. + """ + # Map our sort_by to the store's sorted_by parameter + sorted_by_map = { + "rating": "most_popular", + "runs": "most_runs", + "name": "alphabetical", + "recent": "recently_updated", + } + + result = await get_database_manager_async_client().get_store_agents( + featured=False, + creators=None, + sorted_by=sorted_by_map.get(sort_by, "most_popular"), + search_query=query, + category=category, + page=1, + page_size=limit, + ) + + agents = [ + StoreAgentDict( + slug=agent.slug, + name=agent.agent_name, + description=agent.description, + creator=agent.creator, + rating=agent.rating, + runs=agent.runs, + ) + for agent in result.agents + ] + + return SearchAgentsResponse(agents=agents, total_count=len(agents)) diff --git a/autogpt_platform/backend/backend/blocks/test/test_llm.py b/autogpt_platform/backend/backend/blocks/test/test_llm.py new file mode 100644 index 000000000000..f77251d754fb --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/test/test_llm.py @@ -0,0 +1,492 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from backend.data.model import NodeExecutionStats + + +class TestLLMStatsTracking: + """Test that LLM blocks correctly track token usage statistics.""" + + @pytest.mark.asyncio + async def test_llm_call_returns_token_counts(self): + """Test that llm_call returns proper token counts in LLMResponse.""" + import backend.blocks.llm as llm + + # Mock the OpenAI client + mock_response = MagicMock() + mock_response.choices = [ + MagicMock(message=MagicMock(content="Test response", tool_calls=None)) + ] + mock_response.usage = MagicMock(prompt_tokens=10, completion_tokens=20) + + # Test with mocked OpenAI response + with patch("openai.AsyncOpenAI") as mock_openai: + mock_client = AsyncMock() + mock_openai.return_value = mock_client + mock_client.chat.completions.create = AsyncMock(return_value=mock_response) + + response = await llm.llm_call( + credentials=llm.TEST_CREDENTIALS, + llm_model=llm.LlmModel.GPT4O, + prompt=[{"role": "user", "content": "Hello"}], + json_format=False, + max_tokens=100, + ) + + assert isinstance(response, llm.LLMResponse) + assert response.prompt_tokens == 10 + assert response.completion_tokens == 20 + assert response.response == "Test response" + + @pytest.mark.asyncio + async def test_ai_structured_response_block_tracks_stats(self): + """Test that AIStructuredResponseGeneratorBlock correctly tracks stats.""" + import backend.blocks.llm as llm + + block = llm.AIStructuredResponseGeneratorBlock() + + # Mock the llm_call method + async def mock_llm_call(*args, **kwargs): + return llm.LLMResponse( + raw_response="", + prompt=[], + response='{"key1": "value1", "key2": "value2"}', + tool_calls=None, + prompt_tokens=15, + completion_tokens=25, + reasoning=None, + ) + + block.llm_call = mock_llm_call # type: ignore + + # Run the block + input_data = llm.AIStructuredResponseGeneratorBlock.Input( + prompt="Test prompt", + expected_format={"key1": "desc1", "key2": "desc2"}, + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore # type: ignore + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Check stats + assert block.execution_stats.input_token_count == 15 + assert block.execution_stats.output_token_count == 25 + assert block.execution_stats.llm_call_count == 1 + assert block.execution_stats.llm_retry_count == 0 + + # Check output + assert "response" in outputs + assert outputs["response"] == {"key1": "value1", "key2": "value2"} + + @pytest.mark.asyncio + async def test_ai_text_generator_block_tracks_stats(self): + """Test that AITextGeneratorBlock correctly tracks stats through delegation.""" + import backend.blocks.llm as llm + + block = llm.AITextGeneratorBlock() + + # Mock the underlying structured response block + async def mock_llm_call(input_data, credentials): + # Simulate the structured block setting stats + block.execution_stats = NodeExecutionStats( + input_token_count=30, + output_token_count=40, + llm_call_count=1, + ) + return "Generated text" # AITextGeneratorBlock.llm_call returns a string + + block.llm_call = mock_llm_call # type: ignore + + # Run the block + input_data = llm.AITextGeneratorBlock.Input( + prompt="Generate text", + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Check stats + assert block.execution_stats.input_token_count == 30 + assert block.execution_stats.output_token_count == 40 + assert block.execution_stats.llm_call_count == 1 + + # Check output - AITextGeneratorBlock returns the response directly, not in a dict + assert outputs["response"] == "Generated text" + + @pytest.mark.asyncio + async def test_stats_accumulation_with_retries(self): + """Test that stats correctly accumulate across retries.""" + import backend.blocks.llm as llm + + block = llm.AIStructuredResponseGeneratorBlock() + + # Counter to track calls + call_count = 0 + + async def mock_llm_call(*args, **kwargs): + nonlocal call_count + call_count += 1 + + # First call returns invalid format + if call_count == 1: + return llm.LLMResponse( + raw_response="", + prompt=[], + response='{"wrong": "format"}', + tool_calls=None, + prompt_tokens=10, + completion_tokens=15, + reasoning=None, + ) + # Second call returns correct format + else: + return llm.LLMResponse( + raw_response="", + prompt=[], + response='{"key1": "value1", "key2": "value2"}', + tool_calls=None, + prompt_tokens=20, + completion_tokens=25, + reasoning=None, + ) + + block.llm_call = mock_llm_call # type: ignore + + # Run the block with retry + input_data = llm.AIStructuredResponseGeneratorBlock.Input( + prompt="Test prompt", + expected_format={"key1": "desc1", "key2": "desc2"}, + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + retry=2, + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Check stats - should accumulate both calls + # For 2 attempts: attempt 1 (failed) + attempt 2 (success) = 2 total + # but llm_call_count is only set on success, so it shows 1 for the final successful attempt + assert block.execution_stats.input_token_count == 30 # 10 + 20 + assert block.execution_stats.output_token_count == 40 # 15 + 25 + assert block.execution_stats.llm_call_count == 2 # retry_count + 1 = 1 + 1 = 2 + assert block.execution_stats.llm_retry_count == 1 + + @pytest.mark.asyncio + async def test_ai_text_summarizer_multiple_chunks(self): + """Test that AITextSummarizerBlock correctly accumulates stats across multiple chunks.""" + import backend.blocks.llm as llm + + block = llm.AITextSummarizerBlock() + + # Track calls to simulate multiple chunks + call_count = 0 + + async def mock_llm_call(input_data, credentials): + nonlocal call_count + call_count += 1 + + # Create a mock block with stats to merge from + mock_structured_block = llm.AIStructuredResponseGeneratorBlock() + mock_structured_block.execution_stats = NodeExecutionStats( + input_token_count=25, + output_token_count=15, + llm_call_count=1, + ) + + # Simulate merge_llm_stats behavior + block.merge_llm_stats(mock_structured_block) + + if "final_summary" in input_data.expected_format: + return {"final_summary": "Final combined summary"} + else: + return {"summary": f"Summary of chunk {call_count}"} + + block.llm_call = mock_llm_call # type: ignore + + # Create long text that will be split into chunks + long_text = " ".join(["word"] * 1000) # Moderate size to force ~2-3 chunks + + input_data = llm.AITextSummarizerBlock.Input( + text=long_text, + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + max_tokens=100, # Small chunks + chunk_overlap=10, + ) + + # Run the block + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Block finished - now grab and assert stats + assert block.execution_stats is not None + assert call_count > 1 # Should have made multiple calls + assert block.execution_stats.llm_call_count > 0 + assert block.execution_stats.input_token_count > 0 + assert block.execution_stats.output_token_count > 0 + + # Check output + assert "summary" in outputs + assert outputs["summary"] == "Final combined summary" + + @pytest.mark.asyncio + async def test_ai_text_summarizer_real_llm_call_stats(self): + """Test AITextSummarizer with real LLM call mocking to verify llm_call_count.""" + from unittest.mock import AsyncMock, MagicMock, patch + + import backend.blocks.llm as llm + + block = llm.AITextSummarizerBlock() + + # Mock the actual LLM call instead of the llm_call method + call_count = 0 + + async def mock_create(*args, **kwargs): + nonlocal call_count + call_count += 1 + + mock_response = MagicMock() + # Return different responses for chunk summary vs final summary + if call_count == 1: + mock_response.choices = [ + MagicMock( + message=MagicMock( + content='{"summary": "Test chunk summary"}', tool_calls=None + ) + ) + ] + else: + mock_response.choices = [ + MagicMock( + message=MagicMock( + content='{"final_summary": "Test final summary"}', + tool_calls=None, + ) + ) + ] + mock_response.usage = MagicMock(prompt_tokens=50, completion_tokens=30) + return mock_response + + with patch("openai.AsyncOpenAI") as mock_openai: + mock_client = AsyncMock() + mock_openai.return_value = mock_client + mock_client.chat.completions.create = mock_create + + # Test with very short text (should only need 1 chunk + 1 final summary) + input_data = llm.AITextSummarizerBlock.Input( + text="This is a short text.", + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + max_tokens=1000, # Large enough to avoid chunking + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + print(f"Actual calls made: {call_count}") + print(f"Block stats: {block.execution_stats}") + print(f"LLM call count: {block.execution_stats.llm_call_count}") + + # Should have made 2 calls: 1 for chunk summary + 1 for final summary + assert block.execution_stats.llm_call_count >= 1 + assert block.execution_stats.input_token_count > 0 + assert block.execution_stats.output_token_count > 0 + + @pytest.mark.asyncio + async def test_ai_conversation_block_tracks_stats(self): + """Test that AIConversationBlock correctly tracks stats.""" + import backend.blocks.llm as llm + + block = llm.AIConversationBlock() + + # Mock the llm_call method + async def mock_llm_call(input_data, credentials): + block.execution_stats = NodeExecutionStats( + input_token_count=100, + output_token_count=50, + llm_call_count=1, + ) + return {"response": "AI response to conversation"} + + block.llm_call = mock_llm_call # type: ignore + + # Run the block + input_data = llm.AIConversationBlock.Input( + messages=[ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + {"role": "user", "content": "How are you?"}, + ], + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Check stats + assert block.execution_stats.input_token_count == 100 + assert block.execution_stats.output_token_count == 50 + assert block.execution_stats.llm_call_count == 1 + + # Check output + assert outputs["response"] == {"response": "AI response to conversation"} + + @pytest.mark.asyncio + async def test_ai_list_generator_with_retries(self): + """Test that AIListGeneratorBlock correctly tracks stats with retries.""" + import backend.blocks.llm as llm + + block = llm.AIListGeneratorBlock() + + # Counter to track calls + call_count = 0 + + async def mock_llm_call(input_data, credentials): + nonlocal call_count + call_count += 1 + + # Update stats + if hasattr(block, "execution_stats") and block.execution_stats: + block.execution_stats.input_token_count += 40 + block.execution_stats.output_token_count += 20 + block.execution_stats.llm_call_count += 1 + else: + block.execution_stats = NodeExecutionStats( + input_token_count=40, + output_token_count=20, + llm_call_count=1, + ) + + if call_count == 1: + # First call returns invalid format + return {"response": "not a valid list"} + else: + # Second call returns valid list + return {"response": "['item1', 'item2', 'item3']"} + + block.llm_call = mock_llm_call # type: ignore + + # Run the block + input_data = llm.AIListGeneratorBlock.Input( + focus="test items", + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + max_retries=3, + ) + + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Check stats - should have 2 calls + assert call_count == 2 + assert block.execution_stats.input_token_count == 80 # 40 * 2 + assert block.execution_stats.output_token_count == 40 # 20 * 2 + assert block.execution_stats.llm_call_count == 2 + + # Check output + assert outputs["generated_list"] == ["item1", "item2", "item3"] + + @pytest.mark.asyncio + async def test_merge_llm_stats(self): + """Test the merge_llm_stats method correctly merges stats from another block.""" + import backend.blocks.llm as llm + + block1 = llm.AITextGeneratorBlock() + block2 = llm.AIStructuredResponseGeneratorBlock() + + # Set stats on block2 + block2.execution_stats = NodeExecutionStats( + input_token_count=100, + output_token_count=50, + llm_call_count=2, + llm_retry_count=1, + ) + block2.prompt = [{"role": "user", "content": "Test"}] + + # Merge stats from block2 into block1 + block1.merge_llm_stats(block2) + + # Check that stats were merged + assert block1.execution_stats.input_token_count == 100 + assert block1.execution_stats.output_token_count == 50 + assert block1.execution_stats.llm_call_count == 2 + assert block1.execution_stats.llm_retry_count == 1 + assert block1.prompt == [{"role": "user", "content": "Test"}] + + @pytest.mark.asyncio + async def test_stats_initialization(self): + """Test that blocks properly initialize stats when not present.""" + import backend.blocks.llm as llm + + block = llm.AIStructuredResponseGeneratorBlock() + + # Initially stats should be initialized with zeros + assert hasattr(block, "execution_stats") + assert block.execution_stats.llm_call_count == 0 + + # Mock llm_call + async def mock_llm_call(*args, **kwargs): + return llm.LLMResponse( + raw_response="", + prompt=[], + response='{"result": "test"}', + tool_calls=None, + prompt_tokens=10, + completion_tokens=20, + reasoning=None, + ) + + block.llm_call = mock_llm_call # type: ignore + + # Run the block + input_data = llm.AIStructuredResponseGeneratorBlock.Input( + prompt="Test", + expected_format={"result": "desc"}, + model=llm.LlmModel.GPT4O, + credentials=llm.TEST_CREDENTIALS_INPUT, # type: ignore + ) + + # Run the block + outputs = {} + async for output_name, output_data in block.run( + input_data, credentials=llm.TEST_CREDENTIALS + ): + outputs[output_name] = output_data + + # Block finished - now grab and assert stats + assert block.execution_stats is not None + assert block.execution_stats.input_token_count == 10 + assert block.execution_stats.output_token_count == 20 + assert block.execution_stats.llm_call_count == 1 # Should have exactly 1 call + + # Check output + assert "response" in outputs + assert outputs["response"] == {"result": "test"} diff --git a/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker.py b/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker.py index 96711d1e50f9..c09eac09e165 100644 --- a/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker.py +++ b/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker.py @@ -1,14 +1,8 @@ import logging import pytest -from prisma.models import User - -import backend.blocks.llm as llm -from backend.blocks.agent import AgentExecutorBlock -from backend.blocks.basic import StoreValueBlock -from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock -from backend.data import graph -from backend.data.model import ProviderName + +from backend.data.model import ProviderName, User from backend.server.model import CreateGraph from backend.server.rest_api import AgentServer from backend.usecases.sample import create_test_graph, create_test_user @@ -17,12 +11,14 @@ logger = logging.getLogger(__name__) -async def create_graph(s: SpinTestServer, g: graph.Graph, u: User) -> graph.Graph: +async def create_graph(s: SpinTestServer, g, u: User): logger.info("Creating graph for user %s", u.id) return await s.agent_server.test_create_graph(CreateGraph(graph=g), u.id) async def create_credentials(s: SpinTestServer, u: User): + import backend.blocks.llm as llm + provider = ProviderName.OPENAI credentials = llm.TEST_CREDENTIALS return await s.agent_server.test_create_credentials(u.id, provider, credentials) @@ -30,7 +26,7 @@ async def create_credentials(s: SpinTestServer, u: User): async def execute_graph( agent_server: AgentServer, - test_graph: graph.Graph, + test_graph, test_user: User, input_data: dict, num_execs: int = 4, @@ -57,6 +53,10 @@ async def execute_graph( @pytest.mark.asyncio(loop_scope="session") async def test_graph_validation_with_tool_nodes_correct(server: SpinTestServer): + from backend.blocks.agent import AgentExecutorBlock + from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock + from backend.data import graph + test_user = await create_test_user() test_tool_graph = await create_graph(server, create_test_graph(), test_user) creds = await create_credentials(server, test_user) @@ -106,6 +106,11 @@ async def test_graph_validation_with_tool_nodes_correct(server: SpinTestServer): @pytest.mark.asyncio(loop_scope="session") async def test_smart_decision_maker_function_signature(server: SpinTestServer): + from backend.blocks.agent import AgentExecutorBlock + from backend.blocks.basic import StoreValueBlock + from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock + from backend.data import graph + test_user = await create_test_user() test_tool_graph = await create_graph(server, create_test_graph(), test_user) creds = await create_credentials(server, test_user) @@ -187,3 +192,61 @@ async def test_smart_decision_maker_function_signature(server: SpinTestServer): ] == "Trigger the block to produce the output. The value is only used when `data` is None." ) + + +@pytest.mark.asyncio +async def test_smart_decision_maker_tracks_llm_stats(): + """Test that SmartDecisionMakerBlock correctly tracks LLM usage stats.""" + from unittest.mock import MagicMock, patch + + import backend.blocks.llm as llm_module + from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock + + block = SmartDecisionMakerBlock() + + # Mock the llm.llm_call function to return controlled data + mock_response = MagicMock() + mock_response.response = "I need to think about this." + mock_response.tool_calls = None # No tool calls for simplicity + mock_response.prompt_tokens = 50 + mock_response.completion_tokens = 25 + mock_response.reasoning = None + mock_response.raw_response = { + "role": "assistant", + "content": "I need to think about this.", + } + + # Mock the _create_function_signature method to avoid database calls + with patch("backend.blocks.llm.llm_call", return_value=mock_response), patch.object( + SmartDecisionMakerBlock, "_create_function_signature", return_value=[] + ): + + # Create test input + input_data = SmartDecisionMakerBlock.Input( + prompt="Should I continue with this task?", + model=llm_module.LlmModel.GPT4O, + credentials=llm_module.TEST_CREDENTIALS_INPUT, # type: ignore + ) + + # Execute the block + outputs = {} + async for output_name, output_data in block.run( + input_data, + credentials=llm_module.TEST_CREDENTIALS, + graph_id="test-graph-id", + node_id="test-node-id", + graph_exec_id="test-exec-id", + node_exec_id="test-node-exec-id", + user_id="test-user-id", + ): + outputs[output_name] = output_data + + # Verify stats tracking + assert block.execution_stats is not None + assert block.execution_stats.input_token_count == 50 + assert block.execution_stats.output_token_count == 25 + assert block.execution_stats.llm_call_count == 1 + + # Verify outputs + assert "finished" in outputs # Should have finished since no tool calls + assert outputs["finished"] == "I need to think about this." diff --git a/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker_dict.py b/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker_dict.py new file mode 100644 index 000000000000..0b405b3fcd4e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/test/test_smart_decision_maker_dict.py @@ -0,0 +1,130 @@ +from unittest.mock import Mock + +import pytest + +from backend.blocks.data_manipulation import AddToListBlock, CreateDictionaryBlock +from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock + + +@pytest.mark.asyncio +async def test_smart_decision_maker_handles_dynamic_dict_fields(): + """Test Smart Decision Maker can handle dynamic dictionary fields (_#_) for any block""" + + # Create a mock node for CreateDictionaryBlock + mock_node = Mock() + mock_node.block = CreateDictionaryBlock() + mock_node.block_id = CreateDictionaryBlock().id + mock_node.input_default = {} + + # Create mock links with dynamic dictionary fields + mock_links = [ + Mock( + source_name="tools_^_create_dict_~_name", + sink_name="values_#_name", # Dynamic dict field + sink_id="dict_node_id", + source_id="smart_decision_node_id", + ), + Mock( + source_name="tools_^_create_dict_~_age", + sink_name="values_#_age", # Dynamic dict field + sink_id="dict_node_id", + source_id="smart_decision_node_id", + ), + Mock( + source_name="tools_^_create_dict_~_city", + sink_name="values_#_city", # Dynamic dict field + sink_id="dict_node_id", + source_id="smart_decision_node_id", + ), + ] + + # Generate function signature + signature = await SmartDecisionMakerBlock._create_block_function_signature( + mock_node, mock_links # type: ignore + ) + + # Verify the signature was created successfully + assert signature["type"] == "function" + assert "parameters" in signature["function"] + assert "properties" in signature["function"]["parameters"] + + # Check that dynamic fields are handled + properties = signature["function"]["parameters"]["properties"] + assert len(properties) == 3 # Should have all three fields + + # Each dynamic field should have proper schema + for prop_value in properties.values(): + assert "type" in prop_value + assert prop_value["type"] == "string" # Dynamic fields get string type + assert "description" in prop_value + assert "Dynamic value for" in prop_value["description"] + + +@pytest.mark.asyncio +async def test_smart_decision_maker_handles_dynamic_list_fields(): + """Test Smart Decision Maker can handle dynamic list fields (_$_) for any block""" + + # Create a mock node for AddToListBlock + mock_node = Mock() + mock_node.block = AddToListBlock() + mock_node.block_id = AddToListBlock().id + mock_node.input_default = {} + + # Create mock links with dynamic list fields + mock_links = [ + Mock( + source_name="tools_^_add_to_list_~_0", + sink_name="entries_$_0", # Dynamic list field + sink_id="list_node_id", + source_id="smart_decision_node_id", + ), + Mock( + source_name="tools_^_add_to_list_~_1", + sink_name="entries_$_1", # Dynamic list field + sink_id="list_node_id", + source_id="smart_decision_node_id", + ), + ] + + # Generate function signature + signature = await SmartDecisionMakerBlock._create_block_function_signature( + mock_node, mock_links # type: ignore + ) + + # Verify dynamic list fields are handled properly + assert signature["type"] == "function" + properties = signature["function"]["parameters"]["properties"] + assert len(properties) == 2 # Should have both list items + + # Each dynamic field should have proper schema + for prop_value in properties.values(): + assert prop_value["type"] == "string" + assert "Dynamic value for" in prop_value["description"] + + +@pytest.mark.asyncio +async def test_create_dict_block_with_dynamic_values(): + """Test CreateDictionaryBlock processes dynamic values correctly""" + + block = CreateDictionaryBlock() + + # Simulate what happens when executor merges dynamic fields + # The executor merges values_#_* fields into the values dict + input_data = block.input_schema( + values={ + "existing": "value", + "name": "Alice", # This would come from values_#_name + "age": 25, # This would come from values_#_age + } + ) + + # Run the block + result = {} + async for output_name, output_value in block.run(input_data): + result[output_name] = output_value + + # Check the result + assert "dictionary" in result + assert result["dictionary"]["existing"] == "value" + assert result["dictionary"]["name"] == "Alice" + assert result["dictionary"]["age"] == 25 diff --git a/autogpt_platform/backend/backend/blocks/test/test_store_operations.py b/autogpt_platform/backend/backend/blocks/test/test_store_operations.py new file mode 100644 index 000000000000..088d0d60e582 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/test/test_store_operations.py @@ -0,0 +1,155 @@ +from unittest.mock import MagicMock + +import pytest + +from backend.blocks.system.library_operations import ( + AddToLibraryFromStoreBlock, + LibraryAgent, +) +from backend.blocks.system.store_operations import ( + GetStoreAgentDetailsBlock, + SearchAgentsResponse, + SearchStoreAgentsBlock, + StoreAgentDetails, + StoreAgentDict, +) + + +@pytest.mark.asyncio +async def test_add_to_library_from_store_block_success(mocker): + """Test successful addition of agent from store to library.""" + block = AddToLibraryFromStoreBlock() + + # Mock the library agent response + mock_library_agent = MagicMock() + mock_library_agent.id = "lib-agent-123" + mock_library_agent.graph_id = "graph-456" + mock_library_agent.graph_version = 1 + mock_library_agent.name = "Test Agent" + + mocker.patch.object( + block, + "_add_to_library", + return_value=LibraryAgent( + library_agent_id="lib-agent-123", + agent_id="graph-456", + agent_version=1, + agent_name="Test Agent", + ), + ) + + input_data = block.Input( + store_listing_version_id="store-listing-v1", agent_name="Custom Agent Name" + ) + + outputs = {} + async for name, value in block.run(input_data, user_id="test-user"): + outputs[name] = value + + assert outputs["success"] is True + assert outputs["library_agent_id"] == "lib-agent-123" + assert outputs["agent_id"] == "graph-456" + assert outputs["agent_version"] == 1 + assert outputs["agent_name"] == "Test Agent" + assert outputs["message"] == "Agent successfully added to library" + + +@pytest.mark.asyncio +async def test_get_store_agent_details_block_success(mocker): + """Test successful retrieval of store agent details.""" + block = GetStoreAgentDetailsBlock() + + mocker.patch.object( + block, + "_get_agent_details", + return_value=StoreAgentDetails( + found=True, + store_listing_version_id="version-123", + agent_name="Test Agent", + description="A test agent for testing", + creator="Test Creator", + categories=["productivity", "automation"], + runs=100, + rating=4.5, + ), + ) + + input_data = block.Input(creator="Test Creator", slug="test-slug") + outputs = {} + async for name, value in block.run(input_data): + outputs[name] = value + + assert outputs["found"] is True + assert outputs["store_listing_version_id"] == "version-123" + assert outputs["agent_name"] == "Test Agent" + assert outputs["description"] == "A test agent for testing" + assert outputs["creator"] == "Test Creator" + assert outputs["categories"] == ["productivity", "automation"] + assert outputs["runs"] == 100 + assert outputs["rating"] == 4.5 + + +@pytest.mark.asyncio +async def test_search_store_agents_block(mocker): + """Test searching for store agents.""" + block = SearchStoreAgentsBlock() + + mocker.patch.object( + block, + "_search_agents", + return_value=SearchAgentsResponse( + agents=[ + StoreAgentDict( + slug="creator1/agent1", + name="Agent One", + description="First test agent", + creator="Creator 1", + rating=4.8, + runs=500, + ), + StoreAgentDict( + slug="creator2/agent2", + name="Agent Two", + description="Second test agent", + creator="Creator 2", + rating=4.2, + runs=200, + ), + ], + total_count=2, + ), + ) + + input_data = block.Input( + query="test", category="productivity", sort_by="rating", limit=10 + ) + + outputs = {} + async for name, value in block.run(input_data): + outputs[name] = value + + assert len(outputs["agents"]) == 2 + assert outputs["total_count"] == 2 + assert outputs["agents"][0]["name"] == "Agent One" + assert outputs["agents"][0]["rating"] == 4.8 + + +@pytest.mark.asyncio +async def test_search_store_agents_block_empty_results(mocker): + """Test searching with no results.""" + block = SearchStoreAgentsBlock() + + mocker.patch.object( + block, + "_search_agents", + return_value=SearchAgentsResponse(agents=[], total_count=0), + ) + + input_data = block.Input(query="nonexistent", limit=10) + + outputs = {} + async for name, value in block.run(input_data): + outputs[name] = value + + assert outputs["agents"] == [] + assert outputs["total_count"] == 0 diff --git a/autogpt_platform/backend/backend/blocks/time_blocks.py b/autogpt_platform/backend/backend/blocks/time_blocks.py index 05d8e3699fe3..df5c34af5c80 100644 --- a/autogpt_platform/backend/backend/blocks/time_blocks.py +++ b/autogpt_platform/backend/backend/blocks/time_blocks.py @@ -1,19 +1,144 @@ import asyncio +import logging import time from datetime import datetime, timedelta -from typing import Any, Union +from typing import Any, Literal, Union +from zoneinfo import ZoneInfo + +from pydantic import BaseModel from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.execution import UserContext from backend.data.model import SchemaField +# Shared timezone literal type for all time/date blocks +TimezoneLiteral = Literal[ + "UTC", # UTC±00:00 + "Pacific/Honolulu", # UTC-10:00 + "America/Anchorage", # UTC-09:00 (Alaska) + "America/Los_Angeles", # UTC-08:00 (Pacific) + "America/Denver", # UTC-07:00 (Mountain) + "America/Chicago", # UTC-06:00 (Central) + "America/New_York", # UTC-05:00 (Eastern) + "America/Caracas", # UTC-04:00 + "America/Sao_Paulo", # UTC-03:00 + "America/St_Johns", # UTC-02:30 (Newfoundland) + "Atlantic/South_Georgia", # UTC-02:00 + "Atlantic/Azores", # UTC-01:00 + "Europe/London", # UTC+00:00 (GMT/BST) + "Europe/Paris", # UTC+01:00 (CET) + "Europe/Athens", # UTC+02:00 (EET) + "Europe/Moscow", # UTC+03:00 + "Asia/Tehran", # UTC+03:30 (Iran) + "Asia/Dubai", # UTC+04:00 + "Asia/Kabul", # UTC+04:30 (Afghanistan) + "Asia/Karachi", # UTC+05:00 (Pakistan) + "Asia/Kolkata", # UTC+05:30 (India) + "Asia/Kathmandu", # UTC+05:45 (Nepal) + "Asia/Dhaka", # UTC+06:00 (Bangladesh) + "Asia/Yangon", # UTC+06:30 (Myanmar) + "Asia/Bangkok", # UTC+07:00 + "Asia/Shanghai", # UTC+08:00 (China) + "Australia/Eucla", # UTC+08:45 + "Asia/Tokyo", # UTC+09:00 (Japan) + "Australia/Adelaide", # UTC+09:30 + "Australia/Sydney", # UTC+10:00 + "Australia/Lord_Howe", # UTC+10:30 + "Pacific/Noumea", # UTC+11:00 + "Pacific/Auckland", # UTC+12:00 (New Zealand) + "Pacific/Chatham", # UTC+12:45 + "Pacific/Tongatapu", # UTC+13:00 + "Pacific/Kiritimati", # UTC+14:00 + "Etc/GMT-12", # UTC+12:00 + "Etc/GMT+12", # UTC-12:00 +] + +logger = logging.getLogger(__name__) + + +def _get_timezone( + format_type: Any, # Any format type with timezone and use_user_timezone attributes + user_timezone: str | None, +) -> ZoneInfo: + """ + Determine which timezone to use based on format settings and user context. + + Args: + format_type: The format configuration containing timezone settings + user_timezone: The user's timezone from context + + Returns: + ZoneInfo object for the determined timezone + """ + if format_type.use_user_timezone and user_timezone: + tz = ZoneInfo(user_timezone) + logger.debug(f"Using user timezone: {user_timezone}") + else: + tz = ZoneInfo(format_type.timezone) + logger.debug(f"Using specified timezone: {format_type.timezone}") + return tz + + +def _format_datetime_iso8601(dt: datetime, include_microseconds: bool = False) -> str: + """ + Format a datetime object to ISO8601 string. + + Args: + dt: The datetime object to format + include_microseconds: Whether to include microseconds in the output + + Returns: + ISO8601 formatted string + """ + if include_microseconds: + return dt.isoformat() + else: + return dt.isoformat(timespec="seconds") + + +# BACKWARDS COMPATIBILITY NOTE: +# The timezone field is kept at the format level (not block level) for backwards compatibility. +# Existing graphs have timezone saved within format_type, moving it would break them. +# +# The use_user_timezone flag was added to allow using the user's profile timezone. +# Default is False to maintain backwards compatibility - existing graphs will continue +# using their specified timezone. +# +# KNOWN ISSUE: If a user switches between format types (strftime <-> iso8601), +# the timezone setting doesn't carry over. This is a UX issue but fixing it would +# require either: +# 1. Moving timezone to block level (breaking change, needs migration) +# 2. Complex state management to sync timezone across format types +# +# Future migration path: When we do a major version bump, consider moving timezone +# to the block Input level for better UX. + + +class TimeStrftimeFormat(BaseModel): + discriminator: Literal["strftime"] + format: str = "%H:%M:%S" + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + + +class TimeISO8601Format(BaseModel): + discriminator: Literal["iso8601"] + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + include_microseconds: bool = False + class GetCurrentTimeBlock(Block): class Input(BlockSchema): trigger: str = SchemaField( description="Trigger any data to output the current time" ) - format: str = SchemaField( - description="Format of the time to output", default="%H:%M:%S" + format_type: Union[TimeStrftimeFormat, TimeISO8601Format] = SchemaField( + discriminator="discriminator", + description="Format type for time output (strftime with custom format or ISO 8601)", + default=TimeStrftimeFormat(discriminator="strftime"), ) class Output(BlockSchema): @@ -30,19 +155,71 @@ def __init__(self): output_schema=GetCurrentTimeBlock.Output, test_input=[ {"trigger": "Hello"}, - {"trigger": "Hello", "format": "%H:%M"}, + { + "trigger": "Hello", + "format_type": { + "discriminator": "strftime", + "format": "%H:%M", + }, + }, + { + "trigger": "Hello", + "format_type": { + "discriminator": "iso8601", + "timezone": "UTC", + "include_microseconds": False, + }, + }, ], test_output=[ ("time", lambda _: time.strftime("%H:%M:%S")), ("time", lambda _: time.strftime("%H:%M")), + ( + "time", + lambda t: "T" in t and ("+" in t or "Z" in t), + ), # Check for ISO format with timezone ], ) - async def run(self, input_data: Input, **kwargs) -> BlockOutput: - current_time = time.strftime(input_data.format) + async def run( + self, input_data: Input, *, user_context: UserContext, **kwargs + ) -> BlockOutput: + # Extract timezone from user_context (always present) + effective_timezone = user_context.timezone + + # Get the appropriate timezone + tz = _get_timezone(input_data.format_type, effective_timezone) + dt = datetime.now(tz=tz) + + if isinstance(input_data.format_type, TimeISO8601Format): + # Get the full ISO format and extract just the time portion with timezone + full_iso = _format_datetime_iso8601( + dt, input_data.format_type.include_microseconds + ) + # Extract time portion (everything after 'T') + current_time = full_iso.split("T")[1] if "T" in full_iso else full_iso + current_time = f"T{current_time}" # Add T prefix for ISO 8601 time format + else: # TimeStrftimeFormat + current_time = dt.strftime(input_data.format_type.format) + yield "time", current_time +class DateStrftimeFormat(BaseModel): + discriminator: Literal["strftime"] + format: str = "%Y-%m-%d" + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + + +class DateISO8601Format(BaseModel): + discriminator: Literal["iso8601"] + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + + class GetCurrentDateBlock(Block): class Input(BlockSchema): trigger: str = SchemaField( @@ -53,8 +230,10 @@ class Input(BlockSchema): description="Offset in days from the current date", default=0, ) - format: str = SchemaField( - description="Format of the date to output", default="%Y-%m-%d" + format_type: Union[DateStrftimeFormat, DateISO8601Format] = SchemaField( + discriminator="discriminator", + description="Format type for date output (strftime with custom format or ISO 8601)", + default=DateStrftimeFormat(discriminator="strftime"), ) class Output(BlockSchema): @@ -71,7 +250,22 @@ def __init__(self): output_schema=GetCurrentDateBlock.Output, test_input=[ {"trigger": "Hello", "offset": "7"}, - {"trigger": "Hello", "offset": "7", "format": "%m/%d/%Y"}, + { + "trigger": "Hello", + "offset": "7", + "format_type": { + "discriminator": "strftime", + "format": "%m/%d/%Y", + }, + }, + { + "trigger": "Hello", + "offset": "0", + "format_type": { + "discriminator": "iso8601", + "timezone": "UTC", + }, + }, ], test_output=[ ( @@ -85,16 +279,52 @@ def __init__(self): < timedelta(days=8), # 7 days difference + 1 day error margin. ), + ( + "date", + lambda t: len(t) == 10 + and t[4] == "-" + and t[7] == "-", # ISO date format YYYY-MM-DD + ), ], ) async def run(self, input_data: Input, **kwargs) -> BlockOutput: + # Extract timezone from user_context (required keyword argument) + user_context: UserContext = kwargs["user_context"] + effective_timezone = user_context.timezone + try: offset = int(input_data.offset) except ValueError: offset = 0 - current_date = datetime.now() - timedelta(days=offset) - yield "date", current_date.strftime(input_data.format) + + # Get the appropriate timezone + tz = _get_timezone(input_data.format_type, effective_timezone) + current_date = datetime.now(tz=tz) - timedelta(days=offset) + + if isinstance(input_data.format_type, DateISO8601Format): + # ISO 8601 date format is YYYY-MM-DD + date_str = current_date.date().isoformat() + else: # DateStrftimeFormat + date_str = current_date.strftime(input_data.format_type.format) + + yield "date", date_str + + +class StrftimeFormat(BaseModel): + discriminator: Literal["strftime"] + format: str = "%Y-%m-%d %H:%M:%S" + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + + +class ISO8601Format(BaseModel): + discriminator: Literal["iso8601"] + timezone: TimezoneLiteral = "UTC" + # When True, overrides timezone with user's profile timezone + use_user_timezone: bool = False + include_microseconds: bool = False class GetCurrentDateAndTimeBlock(Block): @@ -102,9 +332,10 @@ class Input(BlockSchema): trigger: str = SchemaField( description="Trigger any data to output the current date and time" ) - format: str = SchemaField( - description="Format of the date and time to output", - default="%Y-%m-%d %H:%M:%S", + format_type: Union[StrftimeFormat, ISO8601Format] = SchemaField( + discriminator="discriminator", + description="Format type for date and time output (strftime with custom format or ISO 8601/RFC 3339)", + default=StrftimeFormat(discriminator="strftime"), ) class Output(BlockSchema): @@ -121,20 +352,65 @@ def __init__(self): output_schema=GetCurrentDateAndTimeBlock.Output, test_input=[ {"trigger": "Hello"}, + { + "trigger": "Hello", + "format_type": { + "discriminator": "strftime", + "format": "%Y/%m/%d", + }, + }, + { + "trigger": "Hello", + "format_type": { + "discriminator": "iso8601", + "timezone": "UTC", + "include_microseconds": False, + }, + }, ], test_output=[ ( "date_time", lambda t: abs( - datetime.now() - datetime.strptime(t, "%Y-%m-%d %H:%M:%S") + datetime.now(tz=ZoneInfo("UTC")) + - datetime.strptime(t + "+00:00", "%Y-%m-%d %H:%M:%S%z") ) < timedelta(seconds=10), # 10 seconds error margin. ), + ( + "date_time", + lambda t: abs( + datetime.now().date() - datetime.strptime(t, "%Y/%m/%d").date() + ) + < timedelta(days=1), # Date format only, no time component + ), + ( + "date_time", + lambda t: abs( + datetime.now(tz=ZoneInfo("UTC")) - datetime.fromisoformat(t) + ) + < timedelta(seconds=10), # 10 seconds error margin for ISO format. + ), ], ) async def run(self, input_data: Input, **kwargs) -> BlockOutput: - current_date_time = time.strftime(input_data.format) + # Extract timezone from user_context (required keyword argument) + user_context: UserContext = kwargs["user_context"] + effective_timezone = user_context.timezone + + # Get the appropriate timezone + tz = _get_timezone(input_data.format_type, effective_timezone) + dt = datetime.now(tz=tz) + + if isinstance(input_data.format_type, ISO8601Format): + # ISO 8601 format with specified timezone (also RFC3339-compliant) + current_date_time = _format_datetime_iso8601( + dt, input_data.format_type.include_microseconds + ) + else: # StrftimeFormat + current_date_time = dt.strftime(input_data.format_type.format) + yield "date_time", current_date_time diff --git a/autogpt_platform/backend/backend/blocks/wolfram/__init__.py b/autogpt_platform/backend/backend/blocks/wolfram/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/blocks/wolfram/_api.py b/autogpt_platform/backend/backend/blocks/wolfram/_api.py new file mode 100644 index 000000000000..8c76542b1f1e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wolfram/_api.py @@ -0,0 +1,14 @@ +from backend.sdk import APIKeyCredentials, Requests + + +async def llm_api_call(credentials: APIKeyCredentials, question: str) -> str: + params = {"appid": credentials.api_key.get_secret_value(), "input": question} + response = await Requests().get( + "https://www.wolframalpha.com/api/v1/llm-api", params=params + ) + if not response.ok: + raise ValueError(f"API request failed: {response.status} {response.text()}") + + answer = response.text() if response.text() else "" + + return answer diff --git a/autogpt_platform/backend/backend/blocks/wolfram/llm_api.py b/autogpt_platform/backend/backend/blocks/wolfram/llm_api.py new file mode 100644 index 000000000000..5586e0d8ef29 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wolfram/llm_api.py @@ -0,0 +1,50 @@ +from backend.sdk import ( + APIKeyCredentials, + Block, + BlockCategory, + BlockCostType, + BlockOutput, + BlockSchema, + CredentialsMetaInput, + ProviderBuilder, + SchemaField, +) + +from ._api import llm_api_call + +wolfram = ( + ProviderBuilder("wolfram") + .with_api_key("WOLFRAM_APP_ID", "Wolfram Alpha App ID") + .with_base_cost(1, BlockCostType.RUN) + .build() +) + + +class AskWolframBlock(Block): + """ + Ask Wolfram Alpha a question. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = wolfram.credentials_field( + description="Wolfram Alpha API credentials" + ) + question: str = SchemaField(description="The question to ask") + + class Output(BlockSchema): + answer: str = SchemaField(description="The answer to the question") + + def __init__(self): + super().__init__( + id="b7710ce4-68ef-4e82-9a2f-f0b874ef9c7d", + description="Ask Wolfram Alpha a question", + categories={BlockCategory.SEARCH}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs + ) -> BlockOutput: + answer = await llm_api_call(credentials, input_data.question) + yield "answer", answer diff --git a/autogpt_platform/backend/backend/blocks/wordpress/__init__.py b/autogpt_platform/backend/backend/blocks/wordpress/__init__.py new file mode 100644 index 000000000000..c7b1e26eeaa8 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wordpress/__init__.py @@ -0,0 +1,3 @@ +from .blog import WordPressCreatePostBlock + +__all__ = ["WordPressCreatePostBlock"] diff --git a/autogpt_platform/backend/backend/blocks/wordpress/_api.py b/autogpt_platform/backend/backend/blocks/wordpress/_api.py new file mode 100644 index 000000000000..070dbd898b8c --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wordpress/_api.py @@ -0,0 +1,498 @@ +from datetime import datetime +from enum import Enum +from logging import getLogger +from typing import Any, Dict, List, Union +from urllib.parse import urlencode + +from backend.sdk import BaseModel, Credentials, Requests + +logger = getLogger(__name__) + +WORDPRESS_BASE_URL = "https://public-api.wordpress.com/" + + +class OAuthAuthorizeRequest(BaseModel): + """OAuth authorization request parameters for WordPress. + + Parameters: + client_id: Your application's client ID from WordPress.com + redirect_uri: The URI for the authorize response redirect. Must exactly match a redirect URI + associated with your application. + response_type: Can be "code" or "token". "code" should be used for server side applications. + scope: A space delimited list of scopes. Optional, defaults to single blog access. + blog: Optional blog parameter with the URL or blog ID for a WordPress.com blog or Jetpack site. + """ + + client_id: str + redirect_uri: str + response_type: str = "code" + scope: str | None = None + blog: str | None = None + + +class OAuthTokenRequest(BaseModel): + """OAuth token request parameters for WordPress. + + These parameters must be formatted via application/x-www-form-urlencoded encoding. + + Parameters: + code: The grant code returned in the redirect. Can only be used once. + client_id: Your application's client ID. + redirect_uri: The redirect_uri used in the authorization request. + client_secret: Your application's client secret. + grant_type: The string "authorization_code". + """ + + code: str + client_id: str + redirect_uri: str + client_secret: str + grant_type: str = "authorization_code" + + +class OAuthRefreshTokenRequest(BaseModel): + """OAuth token refresh request parameters for WordPress. + + Note: WordPress OAuth2 tokens do not expire when using the "code" response type, + so refresh tokens are typically not needed for server-side applications. + + Parameters: + refresh_token: The saved refresh token from the previous token grant. + client_id: Your application's client ID. + client_secret: Your application's client secret. + grant_type: The string "refresh_token". + """ + + refresh_token: str + client_id: str + client_secret: str + grant_type: str = "refresh_token" + + +class OAuthTokenResponse(BaseModel): + """OAuth token response from WordPress. + + Successful response has HTTP status code 200 (OK). + + Parameters: + access_token: An opaque string. Can be used to make requests to the WordPress API on behalf + of the user. + blog_id: The ID of the authorized blog. + blog_url: The URL of the authorized blog. + token_type: The string "bearer". + scope: Optional field for global tokens containing the granted scopes. + refresh_token: Optional refresh token (typically not provided for server-side apps). + expires_in: Optional expiration time (tokens from code flow don't expire). + """ + + access_token: str + blog_id: str | None = None + blog_url: str | None = None + token_type: str = "bearer" + scope: str | None = None + refresh_token: str | None = None + expires_in: int | None = None + + +def make_oauth_authorize_url( + client_id: str, + redirect_uri: str, + scopes: list[str] | None = None, +) -> str: + """ + Generate the OAuth authorization URL for WordPress. + + Args: + client_id: Your application's client ID from WordPress.com + redirect_uri: The URI for the authorize response redirect + scopes: Optional list of scopes. Defaults to single blog access if not provided. + blog: Optional blog URL or ID for a WordPress.com blog or Jetpack site. + + Returns: + The authorization URL that the user should visit + """ + # Build request parameters + params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + } + + if scopes: + params["scope"] = " ".join(scopes) + + # Build the authorization URL + base_url = f"{WORDPRESS_BASE_URL}oauth2/authorize" + query_string = urlencode(params) + + return f"{base_url}?{query_string}" + + +async def oauth_exchange_code_for_tokens( + client_id: str, + client_secret: str, + code: str, + redirect_uri: str, +) -> OAuthTokenResponse: + """ + Exchange an authorization code for access token. + + Args: + client_id: Your application's client ID. + client_secret: Your application's client secret. + code: The authorization code returned by WordPress. + redirect_uri: The redirect URI used during authorization. + + Returns: + Parsed JSON response containing the access token, blog info, etc. + """ + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + data = OAuthTokenRequest( + code=code, + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + grant_type="authorization_code", + ).model_dump(exclude_none=True) + + response = await Requests().post( + f"{WORDPRESS_BASE_URL}oauth2/token", + headers=headers, + data=data, + ) + + if response.ok: + return OAuthTokenResponse.model_validate(response.json()) + raise ValueError( + f"Failed to exchange code for tokens: {response.status} {response.text}" + ) + + +async def oauth_refresh_tokens( + client_id: str, + client_secret: str, + refresh_token: str, +) -> OAuthTokenResponse: + """ + Refresh an expired access token (for implicit/client-side tokens only). + + Note: Tokens obtained via the "code" flow for server-side applications do not expire. + This is primarily used for client-side applications using implicit OAuth. + + Args: + client_id: Your application's client ID. + client_secret: Your application's client secret. + refresh_token: The refresh token previously issued by WordPress. + + Returns: + Parsed JSON response containing the new access token. + """ + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + data = OAuthRefreshTokenRequest( + refresh_token=refresh_token, + client_id=client_id, + client_secret=client_secret, + grant_type="refresh_token", + ).model_dump(exclude_none=True) + + response = await Requests().post( + f"{WORDPRESS_BASE_URL}oauth2/token", + headers=headers, + data=data, + ) + + if response.ok: + return OAuthTokenResponse.model_validate(response.json()) + raise ValueError(f"Failed to refresh tokens: {response.status} {response.text}") + + +class TokenInfoResponse(BaseModel): + """Token validation response from WordPress. + + Parameters: + client_id: Your application's client ID. + user_id: The WordPress.com user ID. + blog_id: The blog ID associated with the token. + scope: The scope of the token. + """ + + client_id: str + user_id: str + blog_id: str | None = None + scope: str | None = None + + +async def validate_token( + client_id: str, + token: str, +) -> TokenInfoResponse: + """ + Validate an access token and get associated metadata. + + Args: + client_id: Your application's client ID. + token: The access token to validate. + + Returns: + Token info including user ID, blog ID, and scope. + """ + + params = { + "client_id": client_id, + "token": token, + } + + response = await Requests().get( + f"{WORDPRESS_BASE_URL}oauth2/token-info", + params=params, + ) + + if response.ok: + return TokenInfoResponse.model_validate(response.json()) + raise ValueError(f"Invalid token: {response.status} {response.text}") + + +async def make_api_request( + endpoint: str, + access_token: str, + method: str = "GET", + data: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, +) -> dict[str, Any]: + """ + Make an authenticated API request to WordPress. + + Args: + endpoint: The API endpoint (e.g., "/rest/v1/me/", "/rest/v1/sites/{site_id}/posts/new") + access_token: The OAuth access token + method: HTTP method (GET, POST, etc.) + data: Request body data for POST/PUT requests + params: Query parameters + + Returns: + JSON response from the API + """ + + headers = { + "Authorization": f"Bearer {access_token}", + } + + if data and method in ["POST", "PUT", "PATCH"]: + headers["Content-Type"] = "application/json" + + # Ensure endpoint starts with / + if not endpoint.startswith("/"): + endpoint = f"/{endpoint}" + + url = f"{WORDPRESS_BASE_URL.rstrip('/')}{endpoint}" + + request_method = getattr(Requests(), method.lower()) + response = await request_method( + url, + headers=headers, + json=data if method in ["POST", "PUT", "PATCH"] else None, + params=params, + ) + + if response.ok: + return response.json() + raise ValueError(f"API request failed: {response.status} {response.text}") + + +# Post-related models and functions + + +class PostStatus(str, Enum): + """WordPress post status options.""" + + PUBLISH = "publish" + PRIVATE = "private" + DRAFT = "draft" + PENDING = "pending" + FUTURE = "future" + AUTO_DRAFT = "auto-draft" + + +class PostFormat(str, Enum): + """WordPress post format options.""" + + STANDARD = "standard" + ASIDE = "aside" + CHAT = "chat" + GALLERY = "gallery" + LINK = "link" + IMAGE = "image" + QUOTE = "quote" + STATUS = "status" + VIDEO = "video" + AUDIO = "audio" + + +class CreatePostRequest(BaseModel): + """Request model for creating a WordPress post. + + All fields are optional except those you want to set. + """ + + # Basic content + title: str | None = None + content: str | None = None + excerpt: str | None = None + + # Post metadata + date: datetime | None = None + slug: str | None = None + author: str | None = None + status: PostStatus | None = PostStatus.PUBLISH + password: str | None = None + sticky: bool | None = False + + # Organization + parent: int | None = None + type: str | None = "post" + categories: List[str] | None = None + tags: List[str] | None = None + format: PostFormat | None = None + + # Media + featured_image: str | None = None + media_urls: List[str] | None = None + + # Sharing + publicize: bool | None = True + publicize_message: str | None = None + + # Engagement + likes_enabled: bool | None = None + sharing_enabled: bool | None = True + discussion: Dict[str, bool] | None = None + + # Page-specific + menu_order: int | None = None + page_template: str | None = None + + # Advanced + metadata: List[Dict[str, Any]] | None = None + + class Config: + json_encoders = {datetime: lambda v: v.isoformat()} + + +class PostAuthor(BaseModel): + """Author information in post response.""" + + ID: int + login: str + email: Union[str, bool, None] = None + name: str + nice_name: str + URL: str | None = None + avatar_URL: str | None = None + + +class PostResponse(BaseModel): + """Response model for a WordPress post.""" + + ID: int + site_ID: int + author: PostAuthor + date: datetime + modified: datetime + title: str + URL: str + short_URL: str + content: str + excerpt: str + slug: str + guid: str + status: str + sticky: bool + password: str | None = "" + parent: Union[Dict[str, Any], bool, None] = None + type: str + discussion: Dict[str, Union[str, bool, int]] + likes_enabled: bool + sharing_enabled: bool + like_count: int + i_like: bool + is_reblogged: bool + is_following: bool + global_ID: str + featured_image: str | None = None + post_thumbnail: Dict[str, Any] | None = None + format: str + geo: Union[Dict[str, Any], bool, None] = None + menu_order: int | None = None + page_template: str | None = None + publicize_URLs: List[str] + terms: Dict[str, Dict[str, Any]] + tags: Dict[str, Dict[str, Any]] + categories: Dict[str, Dict[str, Any]] + attachments: Dict[str, Dict[str, Any]] + attachment_count: int + metadata: List[Dict[str, Any]] + meta: Dict[str, Any] + capabilities: Dict[str, bool] + revisions: List[int] | None = None + other_URLs: Dict[str, Any] | None = None + + +async def create_post( + credentials: Credentials, + site: str, + post_data: CreatePostRequest, +) -> PostResponse: + """ + Create a new post on a WordPress site. + + Args: + site: Site ID or domain (e.g., "myblog.wordpress.com" or "123456789") + access_token: OAuth access token + post_data: Post data using CreatePostRequest model + + Returns: + PostResponse with the created post details + """ + + # Convert the post data to a dictionary, excluding None values + data = post_data.model_dump(exclude_none=True) + + # Handle special fields that need conversion + if "categories" in data and isinstance(data["categories"], list): + data["categories"] = ",".join(str(c) for c in data["categories"]) + + if "tags" in data and isinstance(data["tags"], list): + data["tags"] = ",".join(str(t) for t in data["tags"]) + + # Make the API request + endpoint = f"/rest/v1.1/sites/{site}/posts/new" + + headers = { + "Authorization": credentials.auth_header(), + "Content-Type": "application/x-www-form-urlencoded", + } + + response = await Requests().post( + f"{WORDPRESS_BASE_URL.rstrip('/')}{endpoint}", + headers=headers, + data=data, + ) + + if response.ok: + return PostResponse.model_validate(response.json()) + + error_data = ( + response.json() + if response.headers.get("content-type", "").startswith("application/json") + else {} + ) + error_message = error_data.get("message", response.text) + raise ValueError(f"Failed to create post: {response.status} - {error_message}") diff --git a/autogpt_platform/backend/backend/blocks/wordpress/_config.py b/autogpt_platform/backend/backend/blocks/wordpress/_config.py new file mode 100644 index 000000000000..64cd8bdb758c --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wordpress/_config.py @@ -0,0 +1,20 @@ +from backend.sdk import BlockCostType, ProviderBuilder + +from ._oauth import WordPressOAuthHandler, WordPressScope + +wordpress = ( + ProviderBuilder("wordpress") + .with_base_cost(1, BlockCostType.RUN) + .with_oauth( + WordPressOAuthHandler, + scopes=[ + v.value + for v in [ + WordPressScope.POSTS, + ] + ], + client_id_env_var="WORDPRESS_CLIENT_ID", + client_secret_env_var="WORDPRESS_CLIENT_SECRET", + ) + .build() +) diff --git a/autogpt_platform/backend/backend/blocks/wordpress/_oauth.py b/autogpt_platform/backend/backend/blocks/wordpress/_oauth.py new file mode 100644 index 000000000000..7dfb74107a51 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wordpress/_oauth.py @@ -0,0 +1,214 @@ +import time +from enum import Enum +from logging import getLogger +from typing import Optional +from urllib.parse import quote + +from backend.sdk import BaseOAuthHandler, OAuth2Credentials, ProviderName, SecretStr + +from ._api import ( + OAuthTokenResponse, + TokenInfoResponse, + make_oauth_authorize_url, + oauth_exchange_code_for_tokens, + oauth_refresh_tokens, + validate_token, +) + +logger = getLogger(__name__) + + +class WordPressScope(str, Enum): + """WordPress OAuth2 scopes. + + Note: If no scope is specified, the token will grant full access to a single blog. + Special scopes: + - auth: Access to /me endpoints only, primarily for WordPress.com Connect + - global: Full access to all blogs the user has on WordPress.com + """ + + # Common endpoint-specific scopes + POSTS = "posts" + COMMENTS = "comments" + LIKES = "likes" + FOLLOW = "follow" + STATS = "stats" + USERS = "users" + SITES = "sites" + MEDIA = "media" + + # Special scopes + AUTH = "auth" # Access to /me endpoints only + GLOBAL = "global" # Full access to all user's blogs + + +class WordPressOAuthHandler(BaseOAuthHandler): + """ + OAuth2 handler for WordPress.com and Jetpack sites. + + Supports both single blog and global access tokens. + Server-side tokens (using 'code' response type) do not expire. + """ + + PROVIDER_NAME = ProviderName("wordpress") + # Default to no scopes for single blog access + DEFAULT_SCOPES = [] + + def __init__(self, client_id: str, client_secret: Optional[str], redirect_uri: str): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.scopes = self.DEFAULT_SCOPES + + def get_login_url( + self, scopes: list[str], state: str, code_challenge: Optional[str] = None + ) -> str: + logger.debug("Generating WordPress OAuth login URL") + # WordPress doesn't require PKCE, so code_challenge is not used + if not scopes: + logger.debug("No scopes provided, will default to single blog access") + scopes = self.scopes + + logger.debug(f"Using scopes: {scopes}") + logger.debug(f"State: {state}") + + try: + base_url = make_oauth_authorize_url( + self.client_id, self.redirect_uri, scopes if scopes else None + ) + + separator = "&" if "?" in base_url else "?" + url = f"{base_url}{separator}state={quote(state)}" + logger.debug(f"Generated OAuth URL: {url}") + return url + except Exception as e: + logger.error(f"Failed to generate OAuth URL: {str(e)}") + raise + + async def exchange_code_for_tokens( + self, code: str, scopes: list[str], code_verifier: Optional[str] = None + ) -> OAuth2Credentials: + logger.debug("Exchanging authorization code for tokens") + logger.debug(f"Code: {code[:4]}...") + logger.debug(f"Scopes: {scopes}") + + # WordPress doesn't use PKCE, so code_verifier is not needed + + try: + response: OAuthTokenResponse = await oauth_exchange_code_for_tokens( + client_id=self.client_id, + client_secret=self.client_secret if self.client_secret else "", + code=code, + redirect_uri=self.redirect_uri, + ) + logger.info("Successfully exchanged code for tokens") + + # Store blog info in metadata + metadata = {} + if response.blog_id: + metadata["blog_id"] = response.blog_id + if response.blog_url: + metadata["blog_url"] = response.blog_url + + # WordPress tokens from code flow don't expire + credentials = OAuth2Credentials( + access_token=SecretStr(response.access_token), + refresh_token=( + SecretStr(response.refresh_token) + if response.refresh_token + else None + ), + access_token_expires_at=None, + refresh_token_expires_at=None, + provider=self.PROVIDER_NAME, + scopes=scopes if scopes else [], + metadata=metadata, + ) + + if response.expires_in: + logger.debug( + f"Token expires in {response.expires_in} seconds (client-side token)" + ) + else: + logger.debug("Token does not expire (server-side token)") + + return credentials + + except Exception as e: + logger.error(f"Failed to exchange code for tokens: {str(e)}") + raise + + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: + """ + Added for completeness, as WordPress tokens don't expire + """ + + logger.debug("Attempting to refresh OAuth tokens") + + # Server-side tokens don't expire + if credentials.access_token_expires_at is None: + logger.info("Token does not expire (server-side token), no refresh needed") + return credentials + + if credentials.refresh_token is None: + logger.error("Cannot refresh tokens - no refresh token available") + raise ValueError("No refresh token available") + + try: + response: OAuthTokenResponse = await oauth_refresh_tokens( + client_id=self.client_id, + client_secret=self.client_secret if self.client_secret else "", + refresh_token=credentials.refresh_token.get_secret_value(), + ) + logger.info("Successfully refreshed tokens") + + # Preserve blog info from original credentials + metadata = credentials.metadata or {} + if response.blog_id: + metadata["blog_id"] = response.blog_id + if response.blog_url: + metadata["blog_url"] = response.blog_url + + new_credentials = OAuth2Credentials( + access_token=SecretStr(response.access_token), + refresh_token=( + SecretStr(response.refresh_token) + if response.refresh_token + else credentials.refresh_token + ), + access_token_expires_at=( + int(time.time()) + response.expires_in + if response.expires_in + else None + ), + refresh_token_expires_at=None, + provider=self.PROVIDER_NAME, + scopes=credentials.scopes, + metadata=metadata, + ) + + if response.expires_in: + logger.debug( + f"New access token expires in {response.expires_in} seconds" + ) + else: + logger.debug("New token does not expire") + + return new_credentials + + except Exception as e: + logger.error(f"Failed to refresh tokens: {str(e)}") + raise + + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + logger.debug("Token revocation requested") + logger.info( + "WordPress doesn't provide a token revocation endpoint - server-side tokens don't expire" + ) + return False + + async def validate_access_token(self, token: str) -> TokenInfoResponse: + """Validate an access token and get associated metadata.""" + return await validate_token(self.client_id, token) diff --git a/autogpt_platform/backend/backend/blocks/wordpress/blog.py b/autogpt_platform/backend/backend/blocks/wordpress/blog.py new file mode 100644 index 000000000000..5474b7afda28 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/wordpress/blog.py @@ -0,0 +1,92 @@ +from backend.sdk import ( + Block, + BlockCategory, + BlockOutput, + BlockSchema, + Credentials, + CredentialsMetaInput, + SchemaField, +) + +from ._api import CreatePostRequest, PostResponse, PostStatus, create_post +from ._config import wordpress + + +class WordPressCreatePostBlock(Block): + """ + Creates a new post on a WordPress.com site or Jetpack-enabled site and publishes it. + """ + + class Input(BlockSchema): + credentials: CredentialsMetaInput = wordpress.credentials_field() + site: str = SchemaField( + description="Site ID or domain (e.g., 'myblog.wordpress.com' or '123456789')" + ) + title: str = SchemaField(description="The post title") + content: str = SchemaField(description="The post content (HTML supported)") + excerpt: str | None = SchemaField( + description="An optional post excerpt/summary", default=None + ) + slug: str | None = SchemaField( + description="The URL slug for the post (auto-generated if not provided)", + default=None, + ) + author: str | None = SchemaField( + description="Username or ID of the author (defaults to authenticated user)", + default=None, + ) + categories: list[str] = SchemaField( + description="List of category names or IDs", default=[] + ) + tags: list[str] = SchemaField( + description="List of tag names or IDs", default=[] + ) + featured_image: str | None = SchemaField( + description="Post ID of an existing attachment to set as featured image", + default=None, + ) + media_urls: list[str] = SchemaField( + description="URLs of images to sideload and attach to the post", default=[] + ) + + class Output(BlockSchema): + post_id: int = SchemaField(description="The ID of the created post") + post_url: str = SchemaField(description="The full URL of the created post") + short_url: str = SchemaField(description="The shortened wp.me URL") + post_data: dict = SchemaField(description="Complete post data returned by API") + + def __init__(self): + super().__init__( + id="ee4fe08c-18f9-442f-a985-235379b932e1", + description="Create a new post on WordPress.com or Jetpack sites", + categories={BlockCategory.SOCIAL}, + input_schema=self.Input, + output_schema=self.Output, + ) + + async def run( + self, input_data: Input, *, credentials: Credentials, **kwargs + ) -> BlockOutput: + post_request = CreatePostRequest( + title=input_data.title, + content=input_data.content, + excerpt=input_data.excerpt, + slug=input_data.slug, + author=input_data.author, + categories=input_data.categories, + tags=input_data.tags, + featured_image=input_data.featured_image, + media_urls=input_data.media_urls, + status=PostStatus.PUBLISH, + ) + + post_response: PostResponse = await create_post( + credentials=credentials, + site=input_data.site, + post_data=post_request, + ) + + yield "post_id", post_response.ID + yield "post_url", post_response.URL + yield "short_url", post_response.short_URL + yield "post_data", post_response.model_dump() diff --git a/autogpt_platform/backend/backend/data/analytics.py b/autogpt_platform/backend/backend/data/analytics.py index 9ee028522107..fde2d3fd6e50 100644 --- a/autogpt_platform/backend/backend/data/analytics.py +++ b/autogpt_platform/backend/backend/data/analytics.py @@ -2,6 +2,8 @@ import prisma.types +from backend.util.json import SafeJson + logger = logging.getLogger(__name__) @@ -15,7 +17,7 @@ async def log_raw_analytics( data=prisma.types.AnalyticsDetailsCreateInput( userId=user_id, type=type, - data=prisma.Json(data), + data=SafeJson(data), dataIndex=data_index, ) ) diff --git a/autogpt_platform/backend/backend/data/block.py b/autogpt_platform/backend/backend/data/block.py index 5f28fefc879d..0b47ae9362d7 100644 --- a/autogpt_platform/backend/backend/data/block.py +++ b/autogpt_platform/backend/backend/data/block.py @@ -1,5 +1,7 @@ import functools import inspect +import logging +import os from abc import ABC, abstractmethod from collections.abc import AsyncGenerator as AsyncGen from enum import Enum @@ -35,6 +37,8 @@ is_credentials_field_name, ) +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from .graph import Link @@ -55,6 +59,7 @@ class BlockType(Enum): WEBHOOK_MANUAL = "Webhook (manual)" AGENT = "Agent" AI = "AI" + AYRSHARE = "Ayrshare" class BlockCategory(Enum): @@ -491,6 +496,125 @@ def get_blocks() -> dict[str, Type[Block]]: return load_all_blocks() +def is_block_auth_configured( + block_cls: type["Block[BlockSchema, BlockSchema]"], +) -> bool: + """ + Check if a block has a valid authentication method configured at runtime. + + For example if a block is an OAuth-only block and there env vars are not set, + do not show it in the UI. + + """ + from backend.sdk.registry import AutoRegistry + + # Create an instance to access input_schema + try: + block = block_cls() + except Exception as e: + # If we can't create a block instance, assume it's not OAuth-only + logger.error(f"Error creating block instance for {block_cls.__name__}: {e}") + return True + logger.debug( + f"Checking if block {block_cls.__name__} has a valid provider configured" + ) + + # Get all credential inputs from input schema + credential_inputs = block.input_schema.get_credentials_fields_info() + required_inputs = block.input_schema.get_required_fields() + if not credential_inputs: + logger.debug( + f"Block {block_cls.__name__} has no credential inputs - Treating as valid" + ) + return True + + # Check credential inputs + if len(required_inputs.intersection(credential_inputs.keys())) == 0: + logger.debug( + f"Block {block_cls.__name__} has only optional credential inputs" + " - will work without credentials configured" + ) + if len(credential_inputs) > 1: + logger.warning( + f"Block {block_cls.__name__} has multiple credential inputs: " + f"{', '.join(credential_inputs.keys())}" + ) + + # Check if the credential inputs for this block are correctly configured + for field_name, field_info in credential_inputs.items(): + provider_names = field_info.provider + if not provider_names: + logger.warning( + f"Block {block_cls.__name__} " + f"has credential input '{field_name}' with no provider options" + " - Disabling" + ) + return False + + # If a field has multiple possible providers, each one needs to be usable to + # prevent breaking the UX + for _provider_name in provider_names: + provider_name = _provider_name.value + if provider_name in ProviderName.__members__.values(): + logger.debug( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"provider '{provider_name}' is part of the legacy provider system" + " - Treating as valid" + ) + break + + provider = AutoRegistry.get_provider(provider_name) + if not provider: + logger.warning( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"refers to unknown provider '{provider_name}' - Disabling" + ) + return False + + # Check the provider's supported auth types + if field_info.supported_types != provider.supported_auth_types: + logger.warning( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"has mismatched supported auth types (field <> Provider): " + f"{field_info.supported_types} != {provider.supported_auth_types}" + ) + + if not (supported_auth_types := provider.supported_auth_types): + # No auth methods are been configured for this provider + logger.warning( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"provider '{provider_name}' " + "has no authentication methods configured - Disabling" + ) + return False + + # Check if provider supports OAuth + if "oauth2" in supported_auth_types: + # Check if OAuth environment variables are set + if (oauth_config := provider.oauth_config) and bool( + os.getenv(oauth_config.client_id_env_var) + and os.getenv(oauth_config.client_secret_env_var) + ): + logger.debug( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"provider '{provider_name}' is configured for OAuth" + ) + else: + logger.error( + f"Block {block_cls.__name__} credential input '{field_name}' " + f"provider '{provider_name}' " + "is missing OAuth client ID or secret - Disabling" + ) + return False + + logger.debug( + f"Block {block_cls.__name__} credential input '{field_name}' is valid; " + f"supported credential types: {', '.join(field_info.supported_types)}" + ) + + return True + + async def initialize_blocks() -> None: # First, sync all provider costs to blocks # Imported here to avoid circular import diff --git a/autogpt_platform/backend/backend/data/block_cost_config.py b/autogpt_platform/backend/backend/data/block_cost_config.py index 1344be814548..64ad5222b32f 100644 --- a/autogpt_platform/backend/backend/data/block_cost_config.py +++ b/autogpt_platform/backend/backend/data/block_cost_config.py @@ -5,6 +5,12 @@ from backend.blocks.apollo.organization import SearchOrganizationsBlock from backend.blocks.apollo.people import SearchPeopleBlock from backend.blocks.apollo.person import GetPersonDetailBlock +from backend.blocks.enrichlayer.linkedin import ( + GetLinkedinProfileBlock, + GetLinkedinProfilePictureBlock, + LinkedinPersonLookupBlock, + LinkedinRoleLookupBlock, +) from backend.blocks.flux_kontext import AIImageEditorBlock, FluxKontextModelName from backend.blocks.ideogram import IdeogramModelBlock from backend.blocks.jina.embeddings import JinaEmbeddingBlock @@ -30,6 +36,7 @@ anthropic_credentials, apollo_credentials, did_credentials, + enrichlayer_credentials, groq_credentials, ideogram_credentials, jina_credentials, @@ -39,6 +46,7 @@ replicate_credentials, revid_credentials, unreal_credentials, + v0_credentials, ) # =============== Configure the cost for each LLM Model call =============== # @@ -47,13 +55,19 @@ LlmModel.O3: 4, LlmModel.O3_MINI: 2, # $1.10 / $4.40 LlmModel.O1: 16, # $15 / $60 - LlmModel.O1_PREVIEW: 16, LlmModel.O1_MINI: 4, + # GPT-5 models + LlmModel.GPT5: 2, + LlmModel.GPT5_MINI: 1, + LlmModel.GPT5_NANO: 1, + LlmModel.GPT5_CHAT: 2, LlmModel.GPT41: 2, + LlmModel.GPT41_MINI: 1, LlmModel.GPT4O_MINI: 1, LlmModel.GPT4O: 3, LlmModel.GPT4_TURBO: 10, LlmModel.GPT3_5_TURBO: 1, + LlmModel.CLAUDE_4_1_OPUS: 21, LlmModel.CLAUDE_4_OPUS: 21, LlmModel.CLAUDE_4_SONNET: 5, LlmModel.CLAUDE_3_7_SONNET: 5, @@ -67,7 +81,6 @@ LlmModel.AIML_API_LLAMA_3_2_3B: 1, LlmModel.LLAMA3_8B: 1, LlmModel.LLAMA3_70B: 1, - LlmModel.MIXTRAL_8X7B: 1, LlmModel.GEMMA2_9B: 1, LlmModel.LLAMA3_3_70B: 1, # $0.59 / $0.79 LlmModel.LLAMA3_1_8B: 1, @@ -77,19 +90,17 @@ LlmModel.OLLAMA_LLAMA3_405B: 1, LlmModel.DEEPSEEK_LLAMA_70B: 1, # ? / ? LlmModel.OLLAMA_DOLPHIN: 1, + LlmModel.OPENAI_GPT_OSS_120B: 1, + LlmModel.OPENAI_GPT_OSS_20B: 1, LlmModel.GEMINI_FLASH_1_5: 1, LlmModel.GEMINI_2_5_PRO: 4, - LlmModel.GROK_BETA: 5, LlmModel.MISTRAL_NEMO: 1, LlmModel.COHERE_COMMAND_R_08_2024: 1, LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3, - LlmModel.EVA_QWEN_2_5_32B: 1, LlmModel.DEEPSEEK_CHAT: 2, - LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: 1, LlmModel.PERPLEXITY_SONAR: 1, LlmModel.PERPLEXITY_SONAR_PRO: 5, LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: 10, - LlmModel.QWEN_QWQ_32B_PREVIEW: 2, LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: 1, LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: 1, LlmModel.AMAZON_NOVA_LITE_V1: 1, @@ -103,6 +114,19 @@ LlmModel.LLAMA_API_LLAMA4_MAVERICK: 1, LlmModel.LLAMA_API_LLAMA3_3_8B: 1, LlmModel.LLAMA_API_LLAMA3_3_70B: 1, + LlmModel.GROK_4: 9, + LlmModel.KIMI_K2: 1, + LlmModel.QWEN3_235B_A22B_THINKING: 1, + LlmModel.QWEN3_CODER: 9, + LlmModel.GEMINI_2_5_FLASH: 1, + LlmModel.GEMINI_2_0_FLASH: 1, + LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: 1, + LlmModel.GEMINI_2_0_FLASH_LITE: 1, + LlmModel.DEEPSEEK_R1_0528: 1, + # v0 by Vercel models + LlmModel.V0_1_5_MD: 1, + LlmModel.V0_1_5_LG: 2, + LlmModel.V0_1_0_MD: 1, } for model in LlmModel: @@ -192,6 +216,23 @@ for model, cost in MODEL_COST.items() if MODEL_METADATA[model].provider == "llama_api" ] + # v0 by Vercel Models + + [ + BlockCost( + cost_type=BlockCostType.RUN, + cost_filter={ + "model": model, + "credentials": { + "id": v0_credentials.id, + "provider": v0_credentials.provider, + "type": v0_credentials.type, + }, + }, + cost_amount=cost, + ) + for model, cost in MODEL_COST.items() + if MODEL_METADATA[model].provider == "v0" + ] # AI/ML Api Models + [ BlockCost( @@ -266,7 +307,18 @@ "type": ideogram_credentials.type, } }, - ) + ), + BlockCost( + cost_amount=18, + cost_filter={ + "ideogram_model_name": "V_3", + "credentials": { + "id": ideogram_credentials.id, + "provider": ideogram_credentials.provider, + "type": ideogram_credentials.type, + }, + }, + ), ], AIShortformVideoCreatorBlock: [ BlockCost( @@ -364,6 +416,54 @@ }, ) ], + GetLinkedinProfileBlock: [ + BlockCost( + cost_amount=1, + cost_filter={ + "credentials": { + "id": enrichlayer_credentials.id, + "provider": enrichlayer_credentials.provider, + "type": enrichlayer_credentials.type, + } + }, + ) + ], + LinkedinPersonLookupBlock: [ + BlockCost( + cost_amount=2, + cost_filter={ + "credentials": { + "id": enrichlayer_credentials.id, + "provider": enrichlayer_credentials.provider, + "type": enrichlayer_credentials.type, + } + }, + ) + ], + LinkedinRoleLookupBlock: [ + BlockCost( + cost_amount=3, + cost_filter={ + "credentials": { + "id": enrichlayer_credentials.id, + "provider": enrichlayer_credentials.provider, + "type": enrichlayer_credentials.type, + } + }, + ) + ], + GetLinkedinProfilePictureBlock: [ + BlockCost( + cost_amount=3, + cost_filter={ + "credentials": { + "id": enrichlayer_credentials.id, + "provider": enrichlayer_credentials.provider, + "type": enrichlayer_credentials.type, + } + }, + ) + ], SmartDecisionMakerBlock: LLM_COST, SearchOrganizationsBlock: [ BlockCost( diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index c8d29eb62fa2..b83d6fbb564b 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -34,9 +34,10 @@ from backend.data.notifications import NotificationEventModel, RefundRequestData from backend.data.user import get_user_by_id, get_user_email_by_id from backend.notifications.notifications import queue_notification_async -from backend.server.model import Pagination from backend.server.v2.admin.model import UserHistoryResponse from backend.util.exceptions import InsufficientBalanceError +from backend.util.json import SafeJson +from backend.util.models import Pagination from backend.util.retry import func_retry from backend.util.settings import Settings @@ -285,11 +286,17 @@ async def _enable_transaction( transaction = await CreditTransaction.prisma().find_first_or_raise( where={"transactionKey": transaction_key, "userId": user_id} ) - if transaction.isActive: return async with db.locked_transaction(f"usr_trx_{user_id}"): + + transaction = await CreditTransaction.prisma().find_first_or_raise( + where={"transactionKey": transaction_key, "userId": user_id} + ) + if transaction.isActive: + return + user_balance, _ = await self._get_credits(user_id) await CreditTransaction.prisma().update( where={ @@ -316,7 +323,7 @@ async def _add_transaction( transaction_key: str | None = None, ceiling_balance: int | None = None, fail_insufficient_credits: bool = True, - metadata: Json = Json({}), + metadata: Json = SafeJson({}), ) -> tuple[int, str]: """ Add a new transaction for the user. @@ -356,15 +363,15 @@ async def _add_transaction( amount = min(-user_balance, 0) # Create the transaction - transaction_data = CreditTransactionCreateInput( - userId=user_id, - amount=amount, - runningBalance=user_balance + amount, - type=transaction_type, - metadata=metadata, - isActive=is_active, - createdAt=self.time_now(), - ) + transaction_data: CreditTransactionCreateInput = { + "userId": user_id, + "amount": amount, + "runningBalance": user_balance + amount, + "type": transaction_type, + "metadata": metadata, + "isActive": is_active, + "createdAt": self.time_now(), + } if transaction_key: transaction_data["transactionKey"] = transaction_key tx = await CreditTransaction.prisma().create(data=transaction_data) @@ -399,7 +406,7 @@ async def spend_credits( user_id=user_id, amount=-cost, transaction_type=CreditTransactionType.USAGE, - metadata=Json(metadata.model_dump()), + metadata=SafeJson(metadata.model_dump()), ) # Auto top-up if balance is below threshold. @@ -439,7 +446,7 @@ async def onboarding_reward(self, user_id: str, credits: int, step: OnboardingSt amount=credits, transaction_type=CreditTransactionType.GRANT, transaction_key=f"REWARD-{user_id}-{step.value}", - metadata=Json( + metadata=SafeJson( {"reason": f"Reward for completing {step.value} onboarding step."} ), ) @@ -531,7 +538,7 @@ async def deduct_credits(self, request: stripe.Refund | stripe.Dispute): amount=-request.amount, transaction_type=CreditTransactionType.REFUND, transaction_key=request.id, - metadata=Json(request), + metadata=SafeJson(request), fail_insufficient_credits=False, ) @@ -669,7 +676,7 @@ async def _top_up_credits( is_active=False, transaction_key=key, ceiling_balance=ceiling_balance, - metadata=(Json(metadata)), + metadata=(SafeJson(metadata)), ) customer_id = await get_stripe_customer_id(user_id) @@ -693,7 +700,7 @@ async def _top_up_credits( }, ) if setup_intent.status == "succeeded": - successful_transaction = Json({"setup_intent": setup_intent}) + successful_transaction = SafeJson({"setup_intent": setup_intent}) new_transaction_key = setup_intent.id break else: @@ -711,7 +718,9 @@ async def _top_up_credits( }, ) if payment_intent.status == "succeeded": - successful_transaction = Json({"payment_intent": payment_intent}) + successful_transaction = SafeJson( + {"payment_intent": payment_intent} + ) new_transaction_key = payment_intent.id break @@ -766,7 +775,7 @@ async def top_up_intent(self, user_id: str, amount: int) -> str: transaction_type=CreditTransactionType.TOP_UP, transaction_key=checkout_session.id, is_active=False, - metadata=Json(checkout_session), + metadata=SafeJson(checkout_session), ) return checkout_session.url or "" @@ -822,7 +831,7 @@ async def fulfill_checkout( transaction_key=credit_transaction.transactionKey, new_transaction_key=new_transaction_key, user_id=credit_transaction.userId, - metadata=Json(checkout_session), + metadata=SafeJson(checkout_session), ) async def get_credits(self, user_id: str) -> int: @@ -935,7 +944,7 @@ async def get_credits(self, user_id: str) -> int: amount=max(self.num_user_credits_refill - balance, 0), transaction_type=CreditTransactionType.GRANT, transaction_key=f"MONTHLY-CREDIT-TOP-UP-{cur_time}", - metadata=Json({"reason": "Monthly credit refill"}), + metadata=SafeJson({"reason": "Monthly credit refill"}), ) return balance except UniqueViolationError: @@ -995,8 +1004,8 @@ def get_block_costs() -> dict[str, list[BlockCost]]: async def get_stripe_customer_id(user_id: str) -> str: user = await get_user_by_id(user_id) - if user.stripeCustomerId: - return user.stripeCustomerId + if user.stripe_customer_id: + return user.stripe_customer_id customer = stripe.Customer.create( name=user.name or "", @@ -1012,17 +1021,17 @@ async def get_stripe_customer_id(user_id: str) -> str: async def set_auto_top_up(user_id: str, config: AutoTopUpConfig): await User.prisma().update( where={"id": user_id}, - data={"topUpConfig": Json(config.model_dump())}, + data={"topUpConfig": SafeJson(config.model_dump())}, ) async def get_auto_top_up(user_id: str) -> AutoTopUpConfig: user = await get_user_by_id(user_id) - if not user.topUpConfig: + if not user.top_up_config: return AutoTopUpConfig(threshold=0, amount=0) - return AutoTopUpConfig.model_validate(user.topUpConfig) + return AutoTopUpConfig.model_validate(user.top_up_config) async def admin_get_user_history( diff --git a/autogpt_platform/backend/backend/data/credit_test.py b/autogpt_platform/backend/backend/data/credit_test.py index d4bf1a3a4d20..704939c74556 100644 --- a/autogpt_platform/backend/backend/data/credit_test.py +++ b/autogpt_platform/backend/backend/data/credit_test.py @@ -7,7 +7,7 @@ from backend.blocks.llm import AITextGeneratorBlock from backend.data.block import get_block from backend.data.credit import BetaUserCredit, UsageTransactionMetadata -from backend.data.execution import NodeExecutionEntry +from backend.data.execution import NodeExecutionEntry, UserContext from backend.data.user import DEFAULT_USER_ID from backend.executor.utils import block_usage_cost from backend.integrations.credentials_store import openai_credentials @@ -75,6 +75,7 @@ async def test_block_credit_usage(server: SpinTestServer): "type": openai_credentials.type, }, }, + user_context=UserContext(timezone="UTC"), ), ) assert spending_amount_1 > 0 @@ -88,6 +89,7 @@ async def test_block_credit_usage(server: SpinTestServer): node_exec_id="test_node_exec", block_id=AITextGeneratorBlock().id, inputs={"model": "gpt-4-turbo", "api_key": "owned_api_key"}, + user_context=UserContext(timezone="UTC"), ), ) assert spending_amount_2 == 0 diff --git a/autogpt_platform/backend/backend/data/db.py b/autogpt_platform/backend/backend/data/db.py index cff64b3d257b..9ac734fa71a5 100644 --- a/autogpt_platform/backend/backend/data/db.py +++ b/autogpt_platform/backend/backend/data/db.py @@ -1,6 +1,5 @@ import logging import os -import zlib from contextlib import asynccontextmanager from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse from uuid import uuid4 @@ -50,6 +49,10 @@ def add_param(url: str, key: str, value: str) -> str: logger = logging.getLogger(__name__) +def is_connected(): + return prisma.is_connected() + + @conn_retry("Prisma", "Acquiring connection") async def connect(): if prisma.is_connected(): @@ -79,17 +82,55 @@ async def disconnect(): raise ConnectionError("Failed to disconnect from Prisma.") +# Transaction timeout constant (in milliseconds) +TRANSACTION_TIMEOUT = 15000 # 15 seconds - Increased from 5s to prevent timeout errors + + @asynccontextmanager -async def transaction(): - async with prisma.tx() as tx: +async def transaction(timeout: int = TRANSACTION_TIMEOUT): + """ + Create a database transaction with optional timeout. + + Args: + timeout: Transaction timeout in milliseconds. If None, uses TRANSACTION_TIMEOUT (15s). + """ + async with prisma.tx(timeout=timeout) as tx: yield tx @asynccontextmanager -async def locked_transaction(key: str): - lock_key = zlib.crc32(key.encode("utf-8")) - async with transaction() as tx: - await tx.execute_raw("SELECT pg_advisory_xact_lock($1)", lock_key) +async def locked_transaction(key: str, timeout: int = TRANSACTION_TIMEOUT): + """ + Create a transaction and take a per-key advisory *transaction* lock. + + - Uses a 64-bit lock id via hashtextextended(key, 0) to avoid 32-bit collisions. + - Bound by lock_timeout and statement_timeout so it won't block indefinitely. + - Lock is held for the duration of the transaction and auto-released on commit/rollback. + + Args: + key: String lock key (e.g., "usr_trx_"). + timeout: Transaction/lock/statement timeout in milliseconds. + """ + async with transaction(timeout=timeout) as tx: + # Ensure we don't wait longer than desired + # Note: SET LOCAL doesn't support parameterized queries, must use string interpolation + await tx.execute_raw(f"SET LOCAL statement_timeout = '{int(timeout)}ms'") # type: ignore[arg-type] + await tx.execute_raw(f"SET LOCAL lock_timeout = '{int(timeout)}ms'") # type: ignore[arg-type] + + # Block until acquired or lock_timeout hits + try: + await tx.execute_raw( + "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", + key, + ) + except Exception as e: + # Normalize PG's lock timeout error to TimeoutError for callers + if "lock timeout" in str(e).lower(): + raise TimeoutError( + f"Could not acquire lock for key={key!r} within {timeout}ms" + ) from e + raise + yield tx diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py index 6cb27b468fea..2d9fc9c65b58 100644 --- a/autogpt_platform/backend/backend/data/execution.py +++ b/autogpt_platform/backend/backend/data/execution.py @@ -16,7 +16,6 @@ overload, ) -from prisma import Json from prisma.enums import AgentExecutionStatus from prisma.models import ( AgentGraphExecution, @@ -34,11 +33,14 @@ AgentNodeExecutionUpdateInput, AgentNodeExecutionWhereInput, ) -from pydantic import BaseModel, ConfigDict, JsonValue +from pydantic import BaseModel, ConfigDict, JsonValue, ValidationError from pydantic.fields import Field from backend.server.v2.store.exceptions import DatabaseError from backend.util import type as type_utils +from backend.util.json import SafeJson +from backend.util.models import Pagination +from backend.util.retry import func_retry from backend.util.settings import Config from backend.util.truncate import truncate @@ -88,6 +90,7 @@ def error_rate(self) -> float: class GraphExecutionMeta(BaseDbModel): + id: str # type: ignore # Override base class to make this required user_id: str graph_id: str graph_version: int @@ -134,6 +137,10 @@ class Stats(BaseModel): default=None, description="Error message if any", ) + activity_status: str | None = Field( + default=None, + description="AI-generated summary of what the agent did", + ) def to_db(self) -> GraphExecutionStats: return GraphExecutionStats( @@ -145,6 +152,7 @@ def to_db(self) -> GraphExecutionStats: node_count=self.node_exec_count, node_error_count=self.node_error_count, error=self.error, + activity_status=self.activity_status, ) stats: Stats | None @@ -189,6 +197,7 @@ def from_db(_graph_exec: AgentGraphExecution): if isinstance(stats.error, Exception) else stats.error ), + activity_status=stats.activity_status, ) if stats else None @@ -283,13 +292,14 @@ def from_db(_graph_exec: AgentGraphExecution): node_executions=node_executions, ) - def to_graph_execution_entry(self): + def to_graph_execution_entry(self, user_context: "UserContext"): return GraphExecutionEntry( user_id=self.user_id, graph_id=self.graph_id, graph_version=self.graph_version or 0, graph_exec_id=self.id, nodes_input_masks={}, # FIXME: store credentials on AgentGraphExecution + user_context=user_context, ) @@ -311,18 +321,30 @@ class NodeExecutionResult(BaseModel): @staticmethod def from_db(_node_exec: AgentNodeExecution, user_id: Optional[str] = None): - if _node_exec.executionData: - # Execution that has been queued for execution will persist its data. + try: + stats = NodeExecutionStats.model_validate(_node_exec.stats or {}) + except (ValueError, ValidationError): + stats = NodeExecutionStats() + + if stats.cleared_inputs: + input_data: BlockInput = defaultdict() + for name, messages in stats.cleared_inputs.items(): + input_data[name] = messages[-1] if messages else "" + elif _node_exec.executionData: input_data = type_utils.convert(_node_exec.executionData, dict[str, Any]) else: - # For incomplete execution, executionData will not be yet available. input_data: BlockInput = defaultdict() for data in _node_exec.Input or []: input_data[data.name] = type_utils.convert(data.data, type[Any]) output_data: CompletedBlockOutput = defaultdict(list) - for data in _node_exec.Output or []: - output_data[data.name].append(type_utils.convert(data.data, type[Any])) + + if stats.cleared_outputs: + for name, messages in stats.cleared_outputs.items(): + output_data[name].extend(messages) + else: + for data in _node_exec.Output or []: + output_data[data.name].append(type_utils.convert(data.data, type[Any])) graph_execution: AgentGraphExecution | None = _node_exec.GraphExecution if graph_execution: @@ -349,7 +371,9 @@ def from_db(_node_exec: AgentNodeExecution, user_id: Optional[str] = None): end_time=_node_exec.endedTime, ) - def to_node_execution_entry(self) -> "NodeExecutionEntry": + def to_node_execution_entry( + self, user_context: "UserContext" + ) -> "NodeExecutionEntry": return NodeExecutionEntry( user_id=self.user_id, graph_exec_id=self.graph_exec_id, @@ -358,6 +382,7 @@ def to_node_execution_entry(self) -> "NodeExecutionEntry": node_id=self.node_id, block_id=self.block_id, inputs=self.input_data, + user_context=user_context, ) @@ -365,13 +390,13 @@ def to_node_execution_entry(self) -> "NodeExecutionEntry": async def get_graph_executions( - graph_exec_id: str | None = None, - graph_id: str | None = None, - user_id: str | None = None, - statuses: list[ExecutionStatus] | None = None, - created_time_gte: datetime | None = None, - created_time_lte: datetime | None = None, - limit: int | None = None, + graph_exec_id: Optional[str] = None, + graph_id: Optional[str] = None, + user_id: Optional[str] = None, + statuses: Optional[list[ExecutionStatus]] = None, + created_time_gte: Optional[datetime] = None, + created_time_lte: Optional[datetime] = None, + limit: Optional[int] = None, ) -> list[GraphExecutionMeta]: """⚠️ **Optional `user_id` check**: MUST USE check in user-facing endpoints.""" where_filter: AgentGraphExecutionWhereInput = { @@ -399,6 +424,60 @@ async def get_graph_executions( return [GraphExecutionMeta.from_db(execution) for execution in executions] +class GraphExecutionsPaginated(BaseModel): + """Response schema for paginated graph executions.""" + + executions: list[GraphExecutionMeta] + pagination: Pagination + + +async def get_graph_executions_paginated( + user_id: str, + graph_id: Optional[str] = None, + page: int = 1, + page_size: int = 25, + statuses: Optional[list[ExecutionStatus]] = None, + created_time_gte: Optional[datetime] = None, + created_time_lte: Optional[datetime] = None, +) -> GraphExecutionsPaginated: + """Get paginated graph executions for a specific graph.""" + where_filter: AgentGraphExecutionWhereInput = { + "isDeleted": False, + "userId": user_id, + } + + if graph_id: + where_filter["agentGraphId"] = graph_id + if created_time_gte or created_time_lte: + where_filter["createdAt"] = { + "gte": created_time_gte or datetime.min.replace(tzinfo=timezone.utc), + "lte": created_time_lte or datetime.max.replace(tzinfo=timezone.utc), + } + if statuses: + where_filter["OR"] = [{"executionStatus": status} for status in statuses] + + total_count = await AgentGraphExecution.prisma().count(where=where_filter) + total_pages = (total_count + page_size - 1) // page_size + + offset = (page - 1) * page_size + executions = await AgentGraphExecution.prisma().find_many( + where=where_filter, + order={"createdAt": "desc"}, + take=page_size, + skip=offset, + ) + + return GraphExecutionsPaginated( + executions=[GraphExecutionMeta.from_db(execution) for execution in executions], + pagination=Pagination( + total_items=total_count, + total_pages=total_pages, + current_page=page, + page_size=page_size, + ), + ) + + async def get_graph_execution_meta( user_id: str, execution_id: str ) -> GraphExecutionMeta | None: @@ -482,7 +561,7 @@ async def create_graph_execution( queuedTime=datetime.now(tz=timezone.utc), Input={ "create": [ - {"name": name, "data": Json(data)} + {"name": name, "data": SafeJson(data)} for name, data in node_input.items() ] }, @@ -540,7 +619,7 @@ async def upsert_execution_input( order={"addedTime": "asc"}, include={"Input": True}, ) - json_input_data = Json(input_data) + json_input_data = SafeJson(input_data) if existing_execution: await AgentNodeExecutionInputOutput.prisma().create( @@ -583,12 +662,12 @@ async def upsert_execution_output( """ Insert AgentNodeExecutionInputOutput record for as one of AgentNodeExecution.Output. """ - data = AgentNodeExecutionInputOutputCreateInput( - name=output_name, - referencedByOutputExecId=node_exec_id, - ) + data: AgentNodeExecutionInputOutputCreateInput = { + "name": output_name, + "referencedByOutputExecId": node_exec_id, + } if output_data is not None: - data["data"] = Json(output_data) + data["data"] = SafeJson(output_data) await AgentNodeExecutionInputOutput.prisma().create(data=data) @@ -619,7 +698,7 @@ async def update_graph_execution_stats( stats_dict = stats.model_dump() if isinstance(stats_dict.get("error"), Exception): stats_dict["error"] = str(stats_dict["error"]) - update_data["stats"] = Json(stats_dict) + update_data["stats"] = SafeJson(stats_dict) if status: update_data["executionStatus"] = status @@ -630,6 +709,8 @@ async def update_graph_execution_stats( "OR": [ {"executionStatus": ExecutionStatus.RUNNING}, {"executionStatus": ExecutionStatus.QUEUED}, + # Terminated graph can be resumed. + {"executionStatus": ExecutionStatus.TERMINATED}, ], }, data=update_data, @@ -646,27 +727,6 @@ async def update_graph_execution_stats( return GraphExecution.from_db(graph_exec) -async def update_node_execution_stats( - node_exec_id: str, stats: NodeExecutionStats -) -> NodeExecutionResult: - data = stats.model_dump() - if isinstance(data["error"], Exception): - data["error"] = str(data["error"]) - - res = await AgentNodeExecution.prisma().update( - where={"id": node_exec_id}, - data={ - "stats": Json(data), - "endedTime": datetime.now(tz=timezone.utc), - }, - include=EXECUTION_RESULT_INCLUDE, - ) - if not res: - raise ValueError(f"Node execution {node_exec_id} not found.") - - return NodeExecutionResult.from_db(res) - - async def update_node_execution_status_batch( node_exec_ids: list[str], status: ExecutionStatus, @@ -714,9 +774,9 @@ def _get_update_status_data( update_data["endedTime"] = now if execution_data: - update_data["executionData"] = Json(execution_data) + update_data["executionData"] = SafeJson(execution_data) if stats: - update_data["stats"] = Json(stats) + update_data["stats"] = SafeJson(stats) return update_data @@ -817,12 +877,19 @@ async def get_latest_node_execution( # ----------------- Execution Infrastructure ----------------- # +class UserContext(BaseModel): + """Generic user context for graph execution containing user-specific settings.""" + + timezone: str + + class GraphExecutionEntry(BaseModel): user_id: str graph_exec_id: str graph_id: str graph_version: int nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None + user_context: UserContext class NodeExecutionEntry(BaseModel): @@ -833,6 +900,7 @@ class NodeExecutionEntry(BaseModel): node_id: str block_id: str inputs: BlockInput + user_context: UserContext class ExecutionQueue(Generic[T]): @@ -896,15 +964,15 @@ def event_bus_name(self) -> str: def publish(self, res: GraphExecution | NodeExecutionResult): if isinstance(res, GraphExecution): - self.publish_graph_exec_update(res) + self._publish_graph_exec_update(res) else: - self.publish_node_exec_update(res) + self._publish_node_exec_update(res) - def publish_node_exec_update(self, res: NodeExecutionResult): + def _publish_node_exec_update(self, res: NodeExecutionResult): event = NodeExecutionEvent.model_validate(res.model_dump()) self._publish(event, f"{res.user_id}/{res.graph_id}/{res.graph_exec_id}") - def publish_graph_exec_update(self, res: GraphExecution): + def _publish_graph_exec_update(self, res: GraphExecution): event = GraphExecutionEvent.model_validate(res.model_dump()) self._publish(event, f"{res.user_id}/{res.graph_id}/{res.id}") @@ -936,17 +1004,18 @@ class AsyncRedisExecutionEventBus(AsyncRedisEventBus[ExecutionEvent]): def event_bus_name(self) -> str: return config.execution_event_bus_name + @func_retry async def publish(self, res: GraphExecutionMeta | NodeExecutionResult): if isinstance(res, GraphExecutionMeta): - await self.publish_graph_exec_update(res) + await self._publish_graph_exec_update(res) else: - await self.publish_node_exec_update(res) + await self._publish_node_exec_update(res) - async def publish_node_exec_update(self, res: NodeExecutionResult): + async def _publish_node_exec_update(self, res: NodeExecutionResult): event = NodeExecutionEvent.model_validate(res.model_dump()) await self._publish(event, f"{res.user_id}/{res.graph_id}/{res.graph_exec_id}") - async def publish_graph_exec_update(self, res: GraphExecutionMeta): + async def _publish_graph_exec_update(self, res: GraphExecutionMeta): # GraphExecutionEvent requires inputs and outputs fields that GraphExecutionMeta doesn't have # Add default empty values for compatibility event_data = res.model_dump() @@ -1019,11 +1088,11 @@ async def set_execution_kv_data( userId=user_id, agentNodeExecutionId=node_exec_id, key=key, - data=Json(data) if data is not None else None, + data=SafeJson(data) if data is not None else None, ), "update": { "agentNodeExecutionId": node_exec_id, - "data": Json(data) if data is not None else None, + "data": SafeJson(data) if data is not None else None, }, }, ) diff --git a/autogpt_platform/backend/backend/data/generate_data.py b/autogpt_platform/backend/backend/data/generate_data.py new file mode 100644 index 000000000000..edaa2772c8f0 --- /dev/null +++ b/autogpt_platform/backend/backend/data/generate_data.py @@ -0,0 +1,109 @@ +import logging +from collections import defaultdict +from datetime import datetime + +from prisma.enums import AgentExecutionStatus + +from backend.data.execution import get_graph_executions +from backend.data.graph import get_graph_metadata +from backend.data.model import UserExecutionSummaryStats +from backend.server.v2.store.exceptions import DatabaseError +from backend.util.logging import TruncatedLogger + +logger = TruncatedLogger(logging.getLogger(__name__), prefix="[SummaryData]") + + +async def get_user_execution_summary_data( + user_id: str, start_time: datetime, end_time: datetime +) -> UserExecutionSummaryStats: + """Gather all summary data for a user in a time range. + + This function fetches graph executions once and aggregates all required + statistics in a single pass for efficiency. + """ + try: + # Fetch graph executions once + executions = await get_graph_executions( + user_id=user_id, + created_time_gte=start_time, + created_time_lte=end_time, + ) + + # Initialize aggregation variables + total_credits_used = 0.0 + total_executions = len(executions) + successful_runs = 0 + failed_runs = 0 + terminated_runs = 0 + execution_times = [] + agent_usage = defaultdict(int) + cost_by_graph_id = defaultdict(float) + + # Single pass through executions to aggregate all stats + for execution in executions: + # Count execution statuses (including TERMINATED as failed) + if execution.status == AgentExecutionStatus.COMPLETED: + successful_runs += 1 + elif execution.status == AgentExecutionStatus.FAILED: + failed_runs += 1 + elif execution.status == AgentExecutionStatus.TERMINATED: + terminated_runs += 1 + + # Aggregate costs from stats + if execution.stats and hasattr(execution.stats, "cost"): + cost_in_dollars = execution.stats.cost / 100 + total_credits_used += cost_in_dollars + cost_by_graph_id[execution.graph_id] += cost_in_dollars + + # Collect execution times + if execution.stats and hasattr(execution.stats, "duration"): + execution_times.append(execution.stats.duration) + + # Count agent usage + agent_usage[execution.graph_id] += 1 + + # Calculate derived stats + total_execution_time = sum(execution_times) + average_execution_time = ( + total_execution_time / len(execution_times) if execution_times else 0 + ) + + # Find most used agent + most_used_agent = "No agents used" + if agent_usage: + most_used_agent_id = max(agent_usage, key=lambda k: agent_usage[k]) + try: + graph_meta = await get_graph_metadata(graph_id=most_used_agent_id) + most_used_agent = ( + graph_meta.name if graph_meta else f"Agent {most_used_agent_id[:8]}" + ) + except Exception: + logger.warning(f"Could not get metadata for graph {most_used_agent_id}") + most_used_agent = f"Agent {most_used_agent_id[:8]}" + + # Convert graph_ids to agent names for cost breakdown + cost_breakdown = {} + for graph_id, cost in cost_by_graph_id.items(): + try: + graph_meta = await get_graph_metadata(graph_id=graph_id) + agent_name = graph_meta.name if graph_meta else f"Agent {graph_id[:8]}" + except Exception: + logger.warning(f"Could not get metadata for graph {graph_id}") + agent_name = f"Agent {graph_id[:8]}" + cost_breakdown[agent_name] = cost + + # Build the summary stats object (include terminated runs as failed) + return UserExecutionSummaryStats( + total_credits_used=total_credits_used, + total_executions=total_executions, + successful_runs=successful_runs, + failed_runs=failed_runs + terminated_runs, + most_used_agent=most_used_agent, + total_execution_time=total_execution_time, + average_execution_time=average_execution_time, + cost_breakdown=cost_breakdown, + ) + + except Exception as e: + logger.error(f"Failed to get user summary data: {e}") + raise DatabaseError(f"Failed to get user summary data: {e}") from e diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index f2ec27bd60ac..1e423feb5e45 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -3,7 +3,6 @@ from collections import defaultdict from typing import TYPE_CHECKING, Any, Literal, Optional, cast -from prisma import Json from prisma.enums import SubmissionStatus from prisma.models import AgentGraph, AgentNode, AgentNodeLink, StoreListingVersion from prisma.types import ( @@ -28,6 +27,7 @@ ) from backend.integrations.providers import ProviderName from backend.util import type as type_utils +from backend.util.json import SafeJson from .block import Block, BlockInput, BlockSchema, BlockType, get_block, get_blocks from .db import BaseDbModel, query_raw_with_schema, transaction @@ -212,9 +212,9 @@ def _generate_schema( schema_fields: list[AgentInputBlock.Input | AgentOutputBlock.Input] = [] for type_class, input_default in props: try: - schema_fields.append(type_class(**input_default)) + schema_fields.append(type_class.model_construct(**input_default)) except Exception as e: - logger.warning(f"Invalid {type_class}: {input_default}, {e}") + logger.error(f"Invalid {type_class}: {input_default}, {e}") return { "type": "object", @@ -416,6 +416,10 @@ def validate_graph( for_run: bool = False, nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, ): + """ + Validate graph structure and raise `ValueError` on issues. + For structured error reporting, use `validate_graph_get_errors`. + """ self._validate_graph(self, for_run, nodes_input_masks) for sub_graph in self.sub_graphs: self._validate_graph(sub_graph, for_run, nodes_input_masks) @@ -425,15 +429,58 @@ def _validate_graph( graph: BaseGraph, for_run: bool = False, nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, - ): - def is_tool_pin(name: str) -> bool: - return name.startswith("tools_^_") + ) -> None: + errors = GraphModel._validate_graph_get_errors( + graph, for_run, nodes_input_masks + ) + if errors: + # Just raise the first error for backward compatibility + first_error = next(iter(errors.values())) + first_field_error = next(iter(first_error.values())) + raise ValueError(first_field_error) + + def validate_graph_get_errors( + self, + for_run: bool = False, + nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, + ) -> dict[str, dict[str, str]]: + """ + Validate graph and return structured errors per node. + + Returns: dict[node_id, dict[field_name, error_message]] + """ + return { + **self._validate_graph_get_errors(self, for_run, nodes_input_masks), + **{ + node_id: error + for sub_graph in self.sub_graphs + for node_id, error in self._validate_graph_get_errors( + sub_graph, for_run, nodes_input_masks + ).items() + }, + } - def sanitize(name): - sanitized_name = name.split("_#_")[0].split("_@_")[0].split("_$_")[0] - if is_tool_pin(sanitized_name): - return "tools" - return sanitized_name + @staticmethod + def _validate_graph_get_errors( + graph: BaseGraph, + for_run: bool = False, + nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, + ) -> dict[str, dict[str, str]]: + """ + Validate graph and return structured errors per node. + + Returns: dict[node_id, dict[field_name, error_message]] + """ + # First, check for structural issues with the graph + try: + GraphModel._validate_graph_structure(graph) + except ValueError: + # If structural validation fails, we can't provide per-node errors + # so we re-raise as is + raise + + # Collect errors per node + node_errors: dict[str, dict[str, str]] = defaultdict(dict) # Validate smart decision maker nodes nodes_block = { @@ -442,7 +489,7 @@ def sanitize(name): if (block := get_block(node.block_id)) is not None } - input_links = defaultdict(list) + input_links: dict[str, list[Link]] = defaultdict(list) for link in graph.links: input_links[link.sink_id].append(link) @@ -450,17 +497,22 @@ def sanitize(name): # Nodes: required fields are filled or connected and dependencies are satisfied for node in graph.nodes: if (block := nodes_block.get(node.id)) is None: + # For invalid blocks, we still raise immediately as this is a structural issue raise ValueError(f"Invalid block {node.block_id} for node #{node.id}") node_input_mask = ( nodes_input_masks.get(node.id, {}) if nodes_input_masks else {} ) provided_inputs = set( - [sanitize(name) for name in node.input_default] - + [sanitize(link.sink_name) for link in input_links.get(node.id, [])] + [_sanitize_pin_name(name) for name in node.input_default] + + [ + _sanitize_pin_name(link.sink_name) + for link in input_links.get(node.id, []) + ] + ([name for name in node_input_mask] if node_input_mask else []) ) InputSchema = block.input_schema + for name in (required_fields := InputSchema.get_required_fields()): if ( name not in provided_inputs @@ -477,18 +529,16 @@ def sanitize(name): ] ) ): - raise ValueError( - f"Node {block.name} #{node.id} required input missing: `{name}`" - ) + node_errors[node.id][name] = "This field is required" if ( block.block_type == BlockType.INPUT and (input_key := node.input_default.get("name")) and is_credentials_field_name(input_key) ): - raise ValueError( - f"Agent input node uses reserved name '{input_key}'; " - "'credentials' and `*_credentials` are reserved input names" + node_errors[node.id]["name"] = ( + f"'{input_key}' is a reserved input name: " + "'credentials' and `*_credentials` are reserved" ) # Get input schema properties and check dependencies @@ -538,10 +588,15 @@ def has_value(node: Node, name: str): # Check for missing dependencies when dependent field is present missing_deps = [dep for dep in dependencies if not has_value(node, dep)] if missing_deps and (field_has_value or field_is_required): - raise ValueError( - f"Node {block.name} #{node.id}: Field `{field_name}` requires [{', '.join(missing_deps)}] to be set" - ) + node_errors[node.id][ + field_name + ] = f"Requires {', '.join(missing_deps)} to be set" + return node_errors + + @staticmethod + def _validate_graph_structure(graph: BaseGraph): + """Validate graph structure (links, connections, etc.)""" node_map = {v.id: v for v in graph.nodes} def is_static_output_block(nid: str) -> bool: @@ -567,7 +622,7 @@ def is_static_output_block(nid: str) -> bool: f"{prefix}, {node.block_id} is invalid block id, available blocks: {blocks}" ) - sanitized_name = sanitize(name) + sanitized_name = _sanitize_pin_name(name) vals = node.input_default if i == 0: fields = ( @@ -581,7 +636,7 @@ def is_static_output_block(nid: str) -> bool: if block.block_type not in [BlockType.AGENT] else vals.get("input_schema", {}).get("properties", {}).keys() ) - if sanitized_name not in fields and not is_tool_pin(name): + if sanitized_name not in fields and not _is_tool_pin(name): fields_msg = f"Allowed fields: {fields}" raise ValueError(f"{prefix}, `{name}` invalid, {fields_msg}") @@ -618,6 +673,17 @@ def from_db( ) +def _is_tool_pin(name: str) -> bool: + return name.startswith("tools_^_") + + +def _sanitize_pin_name(name: str) -> str: + sanitized_name = name.split("_#_")[0].split("_@_")[0].split("_$_")[0] + if _is_tool_pin(sanitized_name): + return "tools" + return sanitized_name + + class GraphMeta(Graph): user_id: str @@ -952,18 +1018,18 @@ async def fork_graph(graph_id: str, graph_version: int, user_id: str) -> GraphMo """ Forks a graph by copying it and all its nodes and links to a new graph. """ - async with transaction() as tx: - graph = await get_graph(graph_id, graph_version, user_id, True) - if not graph: - raise ValueError(f"Graph {graph_id} v{graph_version} not found") + graph = await get_graph(graph_id, graph_version, user_id, True) + if not graph: + raise ValueError(f"Graph {graph_id} v{graph_version} not found") - # Set forked from ID and version as itself as it's about ot be copied - graph.forked_from_id = graph.id - graph.forked_from_version = graph.version - graph.name = f"{graph.name} (copy)" - graph.reassign_ids(user_id=user_id, reassign_graph_id=True) - graph.validate_graph(for_run=False) + # Set forked from ID and version as itself as it's about ot be copied + graph.forked_from_id = graph.id + graph.forked_from_version = graph.version + graph.name = f"{graph.name} (copy)" + graph.reassign_ids(user_id=user_id, reassign_graph_id=True) + graph.validate_graph(for_run=False) + async with transaction() as tx: await __create_graph(tx, graph, user_id) return graph @@ -995,8 +1061,8 @@ async def __create_graph(tx, graph: Graph, user_id: str): agentGraphId=graph.id, agentGraphVersion=graph.version, agentBlockId=node.block_id, - constantInput=Json(node.input_default), - metadata=Json(node.metadata), + constantInput=SafeJson(node.input_default), + metadata=SafeJson(node.metadata), ) for graph in graphs for node in graph.nodes @@ -1121,7 +1187,7 @@ async def fix_llm_provider_credentials(): await store.update_creds(user_id, credentials) await AgentNode.prisma().update( where={"id": node_id}, - data={"constantInput": Json(node_preset_input)}, + data={"constantInput": SafeJson(node_preset_input)}, ) diff --git a/autogpt_platform/backend/backend/data/integrations.py b/autogpt_platform/backend/backend/data/integrations.py index 7bc4b3d93f31..3efb96f1b35f 100644 --- a/autogpt_platform/backend/backend/data/integrations.py +++ b/autogpt_platform/backend/backend/data/integrations.py @@ -1,7 +1,6 @@ import logging from typing import AsyncGenerator, Literal, Optional, overload -from prisma import Json from prisma.models import IntegrationWebhook from prisma.types import ( IntegrationWebhookCreateInput, @@ -17,6 +16,7 @@ from backend.integrations.webhooks.utils import webhook_ingress_url from backend.server.v2.library.model import LibraryAgentPreset from backend.util.exceptions import NotFoundError +from backend.util.json import SafeJson from .db import BaseDbModel from .graph import NodeModel @@ -90,7 +90,7 @@ async def create_webhook(webhook: Webhook) -> Webhook: webhookType=webhook.webhook_type, resource=webhook.resource, events=webhook.events, - config=Json(webhook.config), + config=SafeJson(webhook.config), secret=webhook.secret, providerWebhookId=webhook.provider_webhook_id, ) @@ -205,7 +205,7 @@ async def update_webhook( """⚠️ No `user_id` check: DO NOT USE without check in user-facing endpoints.""" data: IntegrationWebhookUpdateInput = {} if config is not None: - data["config"] = Json(config) + data["config"] = SafeJson(config) if events is not None: data["events"] = events if not data: diff --git a/autogpt_platform/backend/backend/data/model.py b/autogpt_platform/backend/backend/data/model.py index 17ba987f14f1..01602ec1d458 100644 --- a/autogpt_platform/backend/backend/data/model.py +++ b/autogpt_platform/backend/backend/data/model.py @@ -5,6 +5,7 @@ import logging from collections import defaultdict from datetime import datetime, timezone +from json import JSONDecodeError from typing import ( TYPE_CHECKING, Annotated, @@ -14,7 +15,6 @@ Generic, Literal, Optional, - TypedDict, TypeVar, cast, get_args, @@ -38,14 +38,130 @@ ValidationError, core_schema, ) +from typing_extensions import TypedDict from backend.integrations.providers import ProviderName +from backend.util.json import loads as json_loads from backend.util.settings import Secrets # Type alias for any provider name (including custom ones) AnyProviderName = str # Will be validated as ProviderName at runtime + +class User(BaseModel): + """Application-layer User model with snake_case convention.""" + + model_config = ConfigDict( + extra="forbid", + str_strip_whitespace=True, + ) + + id: str = Field(..., description="User ID") + email: str = Field(..., description="User email address") + email_verified: bool = Field(default=True, description="Whether email is verified") + name: Optional[str] = Field(None, description="User display name") + created_at: datetime = Field(..., description="When user was created") + updated_at: datetime = Field(..., description="When user was last updated") + metadata: dict[str, Any] = Field( + default_factory=dict, description="User metadata as dict" + ) + integrations: str = Field(default="", description="Encrypted integrations data") + stripe_customer_id: Optional[str] = Field(None, description="Stripe customer ID") + top_up_config: Optional["AutoTopUpConfig"] = Field( + None, description="Top up configuration" + ) + + # Notification preferences + max_emails_per_day: int = Field(default=3, description="Maximum emails per day") + notify_on_agent_run: bool = Field(default=True, description="Notify on agent run") + notify_on_zero_balance: bool = Field( + default=True, description="Notify on zero balance" + ) + notify_on_low_balance: bool = Field( + default=True, description="Notify on low balance" + ) + notify_on_block_execution_failed: bool = Field( + default=True, description="Notify on block execution failure" + ) + notify_on_continuous_agent_error: bool = Field( + default=True, description="Notify on continuous agent error" + ) + notify_on_daily_summary: bool = Field( + default=True, description="Notify on daily summary" + ) + notify_on_weekly_summary: bool = Field( + default=True, description="Notify on weekly summary" + ) + notify_on_monthly_summary: bool = Field( + default=True, description="Notify on monthly summary" + ) + + # User timezone for scheduling and time display + timezone: str = Field( + default="not-set", + description="User timezone (IANA timezone identifier or 'not-set')", + ) + + @classmethod + def from_db(cls, prisma_user: "PrismaUser") -> "User": + """Convert a database User object to application User model.""" + # Handle metadata field - convert from JSON string or dict to dict + metadata = {} + if prisma_user.metadata: + if isinstance(prisma_user.metadata, str): + try: + metadata = json_loads(prisma_user.metadata) + except (JSONDecodeError, TypeError): + metadata = {} + elif isinstance(prisma_user.metadata, dict): + metadata = prisma_user.metadata + + # Handle topUpConfig field + top_up_config = None + if prisma_user.topUpConfig: + if isinstance(prisma_user.topUpConfig, str): + try: + config_dict = json_loads(prisma_user.topUpConfig) + top_up_config = AutoTopUpConfig.model_validate(config_dict) + except (JSONDecodeError, TypeError, ValueError): + top_up_config = None + elif isinstance(prisma_user.topUpConfig, dict): + try: + top_up_config = AutoTopUpConfig.model_validate( + prisma_user.topUpConfig + ) + except ValueError: + top_up_config = None + + return cls( + id=prisma_user.id, + email=prisma_user.email, + email_verified=prisma_user.emailVerified or True, + name=prisma_user.name, + created_at=prisma_user.createdAt, + updated_at=prisma_user.updatedAt, + metadata=metadata, + integrations=prisma_user.integrations or "", + stripe_customer_id=prisma_user.stripeCustomerId, + top_up_config=top_up_config, + max_emails_per_day=prisma_user.maxEmailsPerDay or 3, + notify_on_agent_run=prisma_user.notifyOnAgentRun or True, + notify_on_zero_balance=prisma_user.notifyOnZeroBalance or True, + notify_on_low_balance=prisma_user.notifyOnLowBalance or True, + notify_on_block_execution_failed=prisma_user.notifyOnBlockExecutionFailed + or True, + notify_on_continuous_agent_error=prisma_user.notifyOnContinuousAgentError + or True, + notify_on_daily_summary=prisma_user.notifyOnDailySummary or True, + notify_on_weekly_summary=prisma_user.notifyOnWeeklySummary or True, + notify_on_monthly_summary=prisma_user.notifyOnMonthlySummary or True, + timezone=prisma_user.timezone or "not-set", + ) + + if TYPE_CHECKING: + from prisma.models import User as PrismaUser + from backend.data.block import BlockSchema T = TypeVar("T") @@ -316,15 +432,32 @@ class OAuthState(BaseModel): class UserMetadata(BaseModel): integration_credentials: list[Credentials] = Field(default_factory=list) + """⚠️ Deprecated; use `UserIntegrations.credentials` instead""" integration_oauth_states: list[OAuthState] = Field(default_factory=list) + """⚠️ Deprecated; use `UserIntegrations.oauth_states` instead""" class UserMetadataRaw(TypedDict, total=False): integration_credentials: list[dict] + """⚠️ Deprecated; use `UserIntegrations.credentials` instead""" integration_oauth_states: list[dict] + """⚠️ Deprecated; use `UserIntegrations.oauth_states` instead""" class UserIntegrations(BaseModel): + + class ManagedCredentials(BaseModel): + """Integration credentials managed by us, rather than by the user""" + + ayrshare_profile_key: Optional[SecretStr] = None + + @field_serializer("*") + def dump_secret_strings(value: Any, _info): + if isinstance(value, SecretStr): + return value.get_secret_value() + return value + + managed_credentials: ManagedCredentials = Field(default_factory=ManagedCredentials) credentials: list[Credentials] = Field(default_factory=list) oauth_states: list[OAuthState] = Field(default_factory=list) @@ -627,7 +760,7 @@ class NodeExecutionStats(BaseModel): arbitrary_types_allowed=True, ) - error: Optional[Exception | str] = None + error: Optional[BaseException | str] = None walltime: float = 0 cputime: float = 0 input_size: int = 0 @@ -638,6 +771,9 @@ class NodeExecutionStats(BaseModel): output_token_count: int = 0 extra_cost: int = 0 extra_steps: int = 0 + # Moderation fields + cleared_inputs: Optional[dict[str, list[str]]] = None + cleared_outputs: Optional[dict[str, list[str]]] = None def __iadd__(self, other: "NodeExecutionStats") -> "NodeExecutionStats": """Mutate this instance by adding another NodeExecutionStats.""" @@ -689,3 +825,24 @@ class GraphExecutionStats(BaseModel): default=0, description="Total number of errors generated" ) cost: int = Field(default=0, description="Total execution cost (cents)") + activity_status: Optional[str] = Field( + default=None, description="AI-generated summary of what the agent did" + ) + + +class UserExecutionSummaryStats(BaseModel): + """Summary of user statistics for a specific user.""" + + model_config = ConfigDict( + extra="allow", + arbitrary_types_allowed=True, + ) + + total_credits_used: float = Field(default=0) + total_executions: int = Field(default=0) + successful_runs: int = Field(default=0) + failed_runs: int = Field(default=0) + most_used_agent: str = Field(default="") + total_execution_time: float = Field(default=0) + average_execution_time: float = Field(default=0) + cost_breakdown: dict[str, float] = Field(default_factory=dict) diff --git a/autogpt_platform/backend/backend/data/notifications.py b/autogpt_platform/backend/backend/data/notifications.py index f575081be765..0be2fada98f5 100644 --- a/autogpt_platform/backend/backend/data/notifications.py +++ b/autogpt_platform/backend/backend/data/notifications.py @@ -16,6 +16,7 @@ from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator from backend.server.v2.store.exceptions import DatabaseError +from backend.util.json import SafeJson from backend.util.logging import TruncatedLogger from .db import transaction @@ -53,25 +54,19 @@ class AgentRunData(BaseNotificationData): class ZeroBalanceData(BaseNotificationData): - last_transaction: float - last_transaction_time: datetime - top_up_link: str - - @field_validator("last_transaction_time") - @classmethod - def validate_timezone(cls, value: datetime): - if value.tzinfo is None: - raise ValueError("datetime must have timezone information") - return value + agent_name: str = Field(..., description="Name of the agent") + current_balance: float = Field( + ..., description="Current balance in credits (100 = $1)" + ) + billing_page_link: str = Field(..., description="Link to billing page") + shortfall: float = Field(..., description="Amount of credits needed to continue") class LowBalanceData(BaseNotificationData): - agent_name: str = Field(..., description="Name of the agent") current_balance: float = Field( ..., description="Current balance in credits (100 = $1)" ) billing_page_link: str = Field(..., description="Link to billing page") - shortfall: float = Field(..., description="Amount of credits needed to continue") class BlockExecutionFailedData(BaseNotificationData): @@ -180,6 +175,42 @@ class RefundRequestData(BaseNotificationData): balance: int +class AgentApprovalData(BaseNotificationData): + agent_name: str + agent_id: str + agent_version: int + reviewer_name: str + reviewer_email: str + comments: str + reviewed_at: datetime + store_url: str + + @field_validator("reviewed_at") + @classmethod + def validate_timezone(cls, value: datetime): + if value.tzinfo is None: + raise ValueError("datetime must have timezone information") + return value + + +class AgentRejectionData(BaseNotificationData): + agent_name: str + agent_id: str + agent_version: int + reviewer_name: str + reviewer_email: str + comments: str + reviewed_at: datetime + resubmit_url: str + + @field_validator("reviewed_at") + @classmethod + def validate_timezone(cls, value: datetime): + if value.tzinfo is None: + raise ValueError("datetime must have timezone information") + return value + + NotificationData = Annotated[ Union[ AgentRunData, @@ -239,6 +270,8 @@ def get_notif_data_type( NotificationType.MONTHLY_SUMMARY: MonthlySummaryData, NotificationType.REFUND_REQUEST: RefundRequestData, NotificationType.REFUND_PROCESSED: RefundRequestData, + NotificationType.AGENT_APPROVED: AgentApprovalData, + NotificationType.AGENT_REJECTED: AgentRejectionData, }[notification_type] @@ -273,7 +306,7 @@ def strategy(self) -> QueueType: # These are batched by the notification service NotificationType.AGENT_RUN: QueueType.BATCH, # These are batched by the notification service, but with a backoff strategy - NotificationType.ZERO_BALANCE: QueueType.BACKOFF, + NotificationType.ZERO_BALANCE: QueueType.IMMEDIATE, NotificationType.LOW_BALANCE: QueueType.IMMEDIATE, NotificationType.BLOCK_EXECUTION_FAILED: QueueType.BACKOFF, NotificationType.CONTINUOUS_AGENT_ERROR: QueueType.BACKOFF, @@ -282,6 +315,8 @@ def strategy(self) -> QueueType: NotificationType.MONTHLY_SUMMARY: QueueType.SUMMARY, NotificationType.REFUND_REQUEST: QueueType.ADMIN, NotificationType.REFUND_PROCESSED: QueueType.ADMIN, + NotificationType.AGENT_APPROVED: QueueType.IMMEDIATE, + NotificationType.AGENT_REJECTED: QueueType.IMMEDIATE, } return BATCHING_RULES.get(self.notification_type, QueueType.IMMEDIATE) @@ -299,6 +334,8 @@ def template(self) -> str: NotificationType.MONTHLY_SUMMARY: "monthly_summary.html", NotificationType.REFUND_REQUEST: "refund_request.html", NotificationType.REFUND_PROCESSED: "refund_processed.html", + NotificationType.AGENT_APPROVED: "agent_approved.html", + NotificationType.AGENT_REJECTED: "agent_rejected.html", }[self.notification_type] @property @@ -314,6 +351,8 @@ def subject(self) -> str: NotificationType.MONTHLY_SUMMARY: "We did a lot this month!", NotificationType.REFUND_REQUEST: "[ACTION REQUIRED] You got a ${{data.amount / 100}} refund request from {{data.user_name}}", NotificationType.REFUND_PROCESSED: "Refund for ${{data.amount / 100}} to {{data.user_name}} has been processed", + NotificationType.AGENT_APPROVED: "🎉 Your agent '{{data.agent_name}}' has been approved!", + NotificationType.AGENT_REJECTED: "Your agent '{{data.agent_name}}' needs some updates", }[self.notification_type] @@ -391,14 +430,11 @@ async def create_or_add_to_user_notification_batch( notification_data: NotificationEventModel, ) -> UserNotificationBatchDTO: try: - logger.info( - f"Creating or adding to notification batch for {user_id} with type {notification_type} and data {notification_data}" - ) if not notification_data.data: raise ValueError("Notification data must be provided") # Serialize the data - json_data: Json = Json(notification_data.data.model_dump()) + json_data: Json = SafeJson(notification_data.data.model_dump()) # First try to find existing batch existing_batch = await UserNotificationBatch.prisma().find_unique( @@ -412,44 +448,40 @@ async def create_or_add_to_user_notification_batch( ) if not existing_batch: - async with transaction() as tx: - notification_event = await tx.notificationevent.create( - data=NotificationEventCreateInput( - type=notification_type, - data=json_data, - ) - ) - - # Create new batch - resp = await tx.usernotificationbatch.create( - data=UserNotificationBatchCreateInput( - userId=user_id, - type=notification_type, - Notifications={"connect": [{"id": notification_event.id}]}, - ), - include={"Notifications": True}, - ) - return UserNotificationBatchDTO.from_db(resp) - else: - async with transaction() as tx: - notification_event = await tx.notificationevent.create( - data=NotificationEventCreateInput( - type=notification_type, - data=json_data, - UserNotificationBatch={"connect": {"id": existing_batch.id}}, - ) - ) - # Add to existing batch - resp = await tx.usernotificationbatch.update( - where={"id": existing_batch.id}, - data={ - "Notifications": {"connect": [{"id": notification_event.id}]} + resp = await UserNotificationBatch.prisma().create( + data=UserNotificationBatchCreateInput( + userId=user_id, + type=notification_type, + Notifications={ + "create": [ + NotificationEventCreateInput( + type=notification_type, + data=json_data, + ) + ] }, - include={"Notifications": True}, - ) + ), + include={"Notifications": True}, + ) + return UserNotificationBatchDTO.from_db(resp) + else: + resp = await UserNotificationBatch.prisma().update( + where={"id": existing_batch.id}, + data={ + "Notifications": { + "create": [ + NotificationEventCreateInput( + type=notification_type, + data=json_data, + ) + ] + } + }, + include={"Notifications": True}, + ) if not resp: raise DatabaseError( - f"Failed to add notification event {notification_event.id} to existing batch {existing_batch.id}" + f"Failed to add notification event to existing batch {existing_batch.id}" ) return UserNotificationBatchDTO.from_db(resp) except Exception as e: diff --git a/autogpt_platform/backend/backend/data/notifications_test.py b/autogpt_platform/backend/backend/data/notifications_test.py new file mode 100644 index 000000000000..bdbf64401eb1 --- /dev/null +++ b/autogpt_platform/backend/backend/data/notifications_test.py @@ -0,0 +1,151 @@ +"""Tests for notification data models.""" + +from datetime import datetime, timezone + +import pytest +from pydantic import ValidationError + +from backend.data.notifications import AgentApprovalData, AgentRejectionData + + +class TestAgentApprovalData: + """Test cases for AgentApprovalData model.""" + + def test_valid_agent_approval_data(self): + """Test creating valid AgentApprovalData.""" + data = AgentApprovalData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="John Doe", + reviewer_email="john@example.com", + comments="Great agent, approved!", + reviewed_at=datetime.now(timezone.utc), + store_url="https://app.autogpt.com/store/test-agent-123", + ) + + assert data.agent_name == "Test Agent" + assert data.agent_id == "test-agent-123" + assert data.agent_version == 1 + assert data.reviewer_name == "John Doe" + assert data.reviewer_email == "john@example.com" + assert data.comments == "Great agent, approved!" + assert data.store_url == "https://app.autogpt.com/store/test-agent-123" + assert data.reviewed_at.tzinfo is not None + + def test_agent_approval_data_without_timezone_raises_error(self): + """Test that AgentApprovalData raises error without timezone.""" + with pytest.raises( + ValidationError, match="datetime must have timezone information" + ): + AgentApprovalData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="John Doe", + reviewer_email="john@example.com", + comments="Great agent, approved!", + reviewed_at=datetime.now(), # No timezone + store_url="https://app.autogpt.com/store/test-agent-123", + ) + + def test_agent_approval_data_with_empty_comments(self): + """Test AgentApprovalData with empty comments.""" + data = AgentApprovalData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="John Doe", + reviewer_email="john@example.com", + comments="", # Empty comments + reviewed_at=datetime.now(timezone.utc), + store_url="https://app.autogpt.com/store/test-agent-123", + ) + + assert data.comments == "" + + +class TestAgentRejectionData: + """Test cases for AgentRejectionData model.""" + + def test_valid_agent_rejection_data(self): + """Test creating valid AgentRejectionData.""" + data = AgentRejectionData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="Jane Doe", + reviewer_email="jane@example.com", + comments="Please fix the security issues before resubmitting.", + reviewed_at=datetime.now(timezone.utc), + resubmit_url="https://app.autogpt.com/build/test-agent-123", + ) + + assert data.agent_name == "Test Agent" + assert data.agent_id == "test-agent-123" + assert data.agent_version == 1 + assert data.reviewer_name == "Jane Doe" + assert data.reviewer_email == "jane@example.com" + assert data.comments == "Please fix the security issues before resubmitting." + assert data.resubmit_url == "https://app.autogpt.com/build/test-agent-123" + assert data.reviewed_at.tzinfo is not None + + def test_agent_rejection_data_without_timezone_raises_error(self): + """Test that AgentRejectionData raises error without timezone.""" + with pytest.raises( + ValidationError, match="datetime must have timezone information" + ): + AgentRejectionData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="Jane Doe", + reviewer_email="jane@example.com", + comments="Please fix the security issues.", + reviewed_at=datetime.now(), # No timezone + resubmit_url="https://app.autogpt.com/build/test-agent-123", + ) + + def test_agent_rejection_data_with_long_comments(self): + """Test AgentRejectionData with long comments.""" + long_comment = "A" * 1000 # Very long comment + data = AgentRejectionData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="Jane Doe", + reviewer_email="jane@example.com", + comments=long_comment, + reviewed_at=datetime.now(timezone.utc), + resubmit_url="https://app.autogpt.com/build/test-agent-123", + ) + + assert data.comments == long_comment + + def test_model_serialization(self): + """Test that models can be serialized and deserialized.""" + original_data = AgentRejectionData( + agent_name="Test Agent", + agent_id="test-agent-123", + agent_version=1, + reviewer_name="Jane Doe", + reviewer_email="jane@example.com", + comments="Please fix the issues.", + reviewed_at=datetime.now(timezone.utc), + resubmit_url="https://app.autogpt.com/build/test-agent-123", + ) + + # Serialize to dict + data_dict = original_data.model_dump() + + # Deserialize back + restored_data = AgentRejectionData.model_validate(data_dict) + + assert restored_data.agent_name == original_data.agent_name + assert restored_data.agent_id == original_data.agent_id + assert restored_data.agent_version == original_data.agent_version + assert restored_data.reviewer_name == original_data.reviewer_name + assert restored_data.reviewer_email == original_data.reviewer_email + assert restored_data.comments == original_data.comments + assert restored_data.reviewed_at == original_data.reviewed_at + assert restored_data.resubmit_url == original_data.resubmit_url diff --git a/autogpt_platform/backend/backend/data/onboarding.py b/autogpt_platform/backend/backend/data/onboarding.py index 0bdab3b90568..817122f90ae3 100644 --- a/autogpt_platform/backend/backend/data/onboarding.py +++ b/autogpt_platform/backend/backend/data/onboarding.py @@ -3,17 +3,16 @@ import prisma import pydantic -from prisma import Json from prisma.enums import OnboardingStep from prisma.models import UserOnboarding from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput -from backend.data import db from backend.data.block import get_blocks from backend.data.credit import get_user_credit_model from backend.data.graph import GraphModel from backend.data.model import CredentialsMetaInput from backend.server.v2.store.model import StoreAgentDetails +from backend.util.json import SafeJson # Mapping from user reason id to categories to search for when choosing agent to show REASON_MAPPING: dict[str, list[str]] = { @@ -79,7 +78,7 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate): if data.selectedStoreListingVersionId is not None: update["selectedStoreListingVersionId"] = data.selectedStoreListingVersionId if data.agentInput is not None: - update["agentInput"] = Json(data.agentInput) + update["agentInput"] = SafeJson(data.agentInput) if data.onboardingAgentExecutionId is not None: update["onboardingAgentExecutionId"] = data.onboardingAgentExecutionId if data.agentRuns is not None: @@ -95,43 +94,42 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate): async def reward_user(user_id: str, step: OnboardingStep): - async with db.locked_transaction(f"usr_trx_{user_id}-reward"): - reward = 0 - match step: - # Reward user when they clicked New Run during onboarding - # This is because they need credits before scheduling a run (next step) - # This is seen as a reward for the GET_RESULTS step in the wallet - case OnboardingStep.AGENT_NEW_RUN: - reward = 300 - case OnboardingStep.RUN_AGENTS: - reward = 300 - case OnboardingStep.MARKETPLACE_ADD_AGENT: - reward = 100 - case OnboardingStep.MARKETPLACE_RUN_AGENT: - reward = 100 - case OnboardingStep.BUILDER_SAVE_AGENT: - reward = 100 - case OnboardingStep.BUILDER_RUN_AGENT: - reward = 100 - - if reward == 0: - return - - onboarding = await get_user_onboarding(user_id) - - # Skip if already rewarded - if step in onboarding.rewardedFor: - return - - onboarding.rewardedFor.append(step) - await user_credit.onboarding_reward(user_id, reward, step) - await UserOnboarding.prisma().update( - where={"userId": user_id}, - data={ - "completedSteps": list(set(onboarding.completedSteps + [step])), - "rewardedFor": onboarding.rewardedFor, - }, - ) + reward = 0 + match step: + # Reward user when they clicked New Run during onboarding + # This is because they need credits before scheduling a run (next step) + # This is seen as a reward for the GET_RESULTS step in the wallet + case OnboardingStep.AGENT_NEW_RUN: + reward = 300 + case OnboardingStep.RUN_AGENTS: + reward = 300 + case OnboardingStep.MARKETPLACE_ADD_AGENT: + reward = 100 + case OnboardingStep.MARKETPLACE_RUN_AGENT: + reward = 100 + case OnboardingStep.BUILDER_SAVE_AGENT: + reward = 100 + case OnboardingStep.BUILDER_RUN_AGENT: + reward = 100 + + if reward == 0: + return + + onboarding = await get_user_onboarding(user_id) + + # Skip if already rewarded + if step in onboarding.rewardedFor: + return + + onboarding.rewardedFor.append(step) + await user_credit.onboarding_reward(user_id, reward, step) + await UserOnboarding.prisma().update( + where={"userId": user_id}, + data={ + "completedSteps": list(set(onboarding.completedSteps + [step])), + "rewardedFor": onboarding.rewardedFor, + }, + ) def clean_and_split(text: str) -> list[str]: diff --git a/autogpt_platform/backend/backend/data/rabbitmq.py b/autogpt_platform/backend/backend/data/rabbitmq.py index e1013cb00f18..bdf2090083d8 100644 --- a/autogpt_platform/backend/backend/data/rabbitmq.py +++ b/autogpt_platform/backend/backend/data/rabbitmq.py @@ -4,24 +4,43 @@ from typing import Awaitable, Optional import aio_pika -import aio_pika.exceptions as aio_ex import pika import pika.adapters.blocking_connection -from pika.exceptions import AMQPError from pika.spec import BasicProperties from pydantic import BaseModel -from tenacity import ( - retry, - retry_if_exception_type, - stop_after_attempt, - wait_random_exponential, -) - -from backend.util.retry import conn_retry + +from backend.util.retry import conn_retry, func_retry from backend.util.settings import Settings logger = logging.getLogger(__name__) +# RabbitMQ Connection Constants +# These constants solve specific connection stability issues observed in production + +# BLOCKED_CONNECTION_TIMEOUT (300s = 5 minutes) +# Problem: Connection can hang indefinitely if RabbitMQ server is overloaded +# Solution: Timeout and reconnect if connection is blocked for too long +# Use case: Network issues or server resource constraints +BLOCKED_CONNECTION_TIMEOUT = 300 + +# SOCKET_TIMEOUT (30s) +# Problem: Network operations can hang indefinitely on poor connections +# Solution: Fail fast on socket operations to enable quick reconnection +# Use case: Network latency, packet loss, or connectivity issues +SOCKET_TIMEOUT = 30 + +# CONNECTION_ATTEMPTS (5 attempts) +# Problem: Temporary network issues cause permanent connection failures +# Solution: More retry attempts for better resilience during long executions +# Use case: Transient network issues during service startup or long-running operations +CONNECTION_ATTEMPTS = 5 + +# RETRY_DELAY (1 second) +# Problem: Immediate reconnection attempts can overwhelm the server +# Solution: Quick retry for faster recovery while still being respectful +# Use case: Faster reconnection for long-running executions that need to resume quickly +RETRY_DELAY = 1 + class ExchangeType(str, Enum): DIRECT = "direct" @@ -117,8 +136,11 @@ def connect(self) -> None: port=self.port, virtual_host=self.config.vhost, credentials=credentials, - heartbeat=600, - blocked_connection_timeout=300, + blocked_connection_timeout=BLOCKED_CONNECTION_TIMEOUT, + socket_timeout=SOCKET_TIMEOUT, + connection_attempts=CONNECTION_ATTEMPTS, + retry_delay=RETRY_DELAY, + heartbeat=300, # 5 minute timeout (heartbeats sent every 2.5 min) ) self._connection = pika.BlockingConnection(parameters) @@ -169,12 +191,7 @@ def declare_infrastructure(self) -> None: routing_key=queue.routing_key or queue.name, ) - @retry( - retry=retry_if_exception_type((AMQPError, ConnectionError)), - wait=wait_random_exponential(multiplier=1, max=5), - stop=stop_after_attempt(5), - reraise=True, - ) + @func_retry def publish_message( self, routing_key: str, @@ -227,6 +244,8 @@ async def connect(self): login=self.username, password=self.password, virtualhost=self.config.vhost.lstrip("/"), + blocked_connection_timeout=BLOCKED_CONNECTION_TIMEOUT, + heartbeat=300, # 5 minute timeout (heartbeats sent every 2.5 min) ) self._channel = await self._connection.channel() await self._channel.set_qos(prefetch_count=1) @@ -272,12 +291,7 @@ async def declare_infrastructure(self): exchange, routing_key=queue.routing_key or queue.name ) - @retry( - retry=retry_if_exception_type((aio_ex.AMQPError, ConnectionError)), - wait=wait_random_exponential(multiplier=1, max=5), - stop=stop_after_attempt(5), - reraise=True, - ) + @func_retry async def publish_message( self, routing_key: str, diff --git a/autogpt_platform/backend/backend/data/user.py b/autogpt_platform/backend/backend/data/user.py index 6e9220376961..3b1dd296db11 100644 --- a/autogpt_platform/backend/backend/data/user.py +++ b/autogpt_platform/backend/backend/data/user.py @@ -8,19 +8,20 @@ from autogpt_libs.auth.models import DEFAULT_USER_ID from fastapi import HTTPException -from prisma import Json from prisma.enums import NotificationType -from prisma.models import User +from prisma.models import User as PrismaUser from prisma.types import JsonFilter, UserCreateInput, UserUpdateInput from backend.data.db import prisma -from backend.data.model import UserIntegrations, UserMetadata, UserMetadataRaw +from backend.data.model import User, UserIntegrations, UserMetadata from backend.data.notifications import NotificationPreference, NotificationPreferenceDTO from backend.server.v2.store.exceptions import DatabaseError from backend.util.encryption import JSONCryptor +from backend.util.json import SafeJson from backend.util.settings import Settings logger = logging.getLogger(__name__) +settings = Settings() async def get_or_create_user(user_data: dict) -> User: @@ -43,7 +44,7 @@ async def get_or_create_user(user_data: dict) -> User: ) ) - return User.model_validate(user) + return User.from_db(user) except Exception as e: raise DatabaseError(f"Failed to get or create user {user_data}: {e}") from e @@ -52,7 +53,7 @@ async def get_user_by_id(user_id: str) -> User: user = await prisma.user.find_unique(where={"id": user_id}) if not user: raise ValueError(f"User not found with ID: {user_id}") - return User.model_validate(user) + return User.from_db(user) async def get_user_email_by_id(user_id: str) -> Optional[str]: @@ -66,7 +67,7 @@ async def get_user_email_by_id(user_id: str) -> Optional[str]: async def get_user_by_email(email: str) -> Optional[User]: try: user = await prisma.user.find_unique(where={"email": email}) - return User.model_validate(user) if user else None + return User.from_db(user) if user else None except Exception as e: raise DatabaseError(f"Failed to get user by email {email}: {e}") from e @@ -90,27 +91,11 @@ async def create_default_user() -> Optional[User]: name="Default User", ) ) - return User.model_validate(user) - - -async def get_user_metadata(user_id: str) -> UserMetadata: - user = await User.prisma().find_unique_or_raise( - where={"id": user_id}, - ) - - metadata = cast(UserMetadataRaw, user.metadata) - return UserMetadata.model_validate(metadata) - - -async def update_user_metadata(user_id: str, metadata: UserMetadata): - await User.prisma().update( - where={"id": user_id}, - data={"metadata": Json(metadata.model_dump())}, - ) + return User.from_db(user) async def get_user_integrations(user_id: str) -> UserIntegrations: - user = await User.prisma().find_unique_or_raise( + user = await PrismaUser.prisma().find_unique_or_raise( where={"id": user_id}, ) @@ -125,7 +110,7 @@ async def get_user_integrations(user_id: str) -> UserIntegrations: async def update_user_integrations(user_id: str, data: UserIntegrations): encrypted_data = JSONCryptor().encrypt(data.model_dump(exclude_none=True)) - await User.prisma().update( + await PrismaUser.prisma().update( where={"id": user_id}, data={"integrations": encrypted_data}, ) @@ -133,13 +118,13 @@ async def update_user_integrations(user_id: str, data: UserIntegrations): async def migrate_and_encrypt_user_integrations(): """Migrate integration credentials and OAuth states from metadata to integrations column.""" - users = await User.prisma().find_many( + users = await PrismaUser.prisma().find_many( where={ "metadata": cast( JsonFilter, { "path": ["integration_credentials"], - "not": Json( + "not": SafeJson( {"a": "yolo"} ), # bogus value works to check if key exists }, @@ -169,15 +154,15 @@ async def migrate_and_encrypt_user_integrations(): raw_metadata.pop("integration_oauth_states", None) # Update metadata without integration data - await User.prisma().update( + await PrismaUser.prisma().update( where={"id": user.id}, - data={"metadata": Json(raw_metadata)}, + data={"metadata": SafeJson(raw_metadata)}, ) async def get_active_user_ids_in_timerange(start_time: str, end_time: str) -> list[str]: try: - users = await User.prisma().find_many( + users = await PrismaUser.prisma().find_many( where={ "AgentGraphExecutions": { "some": { @@ -207,7 +192,7 @@ async def get_active_users_ids() -> list[str]: async def get_user_notification_preference(user_id: str) -> NotificationPreference: try: - user = await User.prisma().find_unique_or_raise( + user = await PrismaUser.prisma().find_unique_or_raise( where={"id": user_id}, ) @@ -223,6 +208,8 @@ async def get_user_notification_preference(user_id: str) -> NotificationPreferen NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or False, NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or False, NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or False, + NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or False, + NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or False, } daily_limit = user.maxEmailsPerDay or 3 notification_preference = NotificationPreference( @@ -281,10 +268,18 @@ async def update_user_notification_preference( update_data["notifyOnMonthlySummary"] = data.preferences[ NotificationType.MONTHLY_SUMMARY ] + if NotificationType.AGENT_APPROVED in data.preferences: + update_data["notifyOnAgentApproved"] = data.preferences[ + NotificationType.AGENT_APPROVED + ] + if NotificationType.AGENT_REJECTED in data.preferences: + update_data["notifyOnAgentRejected"] = data.preferences[ + NotificationType.AGENT_REJECTED + ] if data.daily_limit: update_data["maxEmailsPerDay"] = data.daily_limit - user = await User.prisma().update( + user = await PrismaUser.prisma().update( where={"id": user_id}, data=update_data, ) @@ -301,6 +296,8 @@ async def update_user_notification_preference( NotificationType.DAILY_SUMMARY: user.notifyOnDailySummary or True, NotificationType.WEEKLY_SUMMARY: user.notifyOnWeeklySummary or True, NotificationType.MONTHLY_SUMMARY: user.notifyOnMonthlySummary or True, + NotificationType.AGENT_APPROVED: user.notifyOnAgentApproved or True, + NotificationType.AGENT_REJECTED: user.notifyOnAgentRejected or True, } notification_preference = NotificationPreference( user_id=user.id, @@ -322,7 +319,7 @@ async def update_user_notification_preference( async def set_user_email_verification(user_id: str, verified: bool) -> None: """Set the email verification status for a user.""" try: - await User.prisma().update( + await PrismaUser.prisma().update( where={"id": user_id}, data={"emailVerified": verified}, ) @@ -335,7 +332,7 @@ async def set_user_email_verification(user_id: str, verified: bool) -> None: async def get_user_email_verification(user_id: str) -> bool: """Get the email verification status for a user.""" try: - user = await User.prisma().find_unique_or_raise( + user = await PrismaUser.prisma().find_unique_or_raise( where={"id": user_id}, ) return user.emailVerified @@ -348,7 +345,7 @@ async def get_user_email_verification(user_id: str) -> bool: def generate_unsubscribe_link(user_id: str) -> str: """Generate a link to unsubscribe from all notifications""" # Create an HMAC using a secret key - secret_key = Settings().secrets.unsubscribe_secret_key + secret_key = settings.secrets.unsubscribe_secret_key signature = hmac.new( secret_key.encode("utf-8"), user_id.encode("utf-8"), hashlib.sha256 ).digest() @@ -359,7 +356,7 @@ def generate_unsubscribe_link(user_id: str) -> str: ).decode("utf-8") logger.info(f"Generating unsubscribe link for user {user_id}") - base_url = Settings().config.platform_base_url + base_url = settings.config.platform_base_url return f"{base_url}/api/email/unsubscribe?token={quote_plus(token)}" @@ -371,7 +368,7 @@ async def unsubscribe_user_by_token(token: str) -> None: user_id, received_signature_hex = decoded.split(":", 1) # Verify the signature - secret_key = Settings().secrets.unsubscribe_secret_key + secret_key = settings.secrets.unsubscribe_secret_key expected_signature = hmac.new( secret_key.encode("utf-8"), user_id.encode("utf-8"), hashlib.sha256 ).digest() @@ -399,3 +396,17 @@ async def unsubscribe_user_by_token(token: str) -> None: ) except Exception as e: raise DatabaseError(f"Failed to unsubscribe user by token {token}: {e}") from e + + +async def update_user_timezone(user_id: str, timezone: str) -> User: + """Update a user's timezone setting.""" + try: + user = await PrismaUser.prisma().update( + where={"id": user_id}, + data={"timezone": timezone}, + ) + if not user: + raise ValueError(f"User not found with ID: {user_id}") + return User.from_db(user) + except Exception as e: + raise DatabaseError(f"Failed to update timezone for user {user_id}: {e}") from e diff --git a/autogpt_platform/backend/backend/db.py b/autogpt_platform/backend/backend/db.py new file mode 100644 index 000000000000..5c59a98a00ef --- /dev/null +++ b/autogpt_platform/backend/backend/db.py @@ -0,0 +1,13 @@ +from backend.app import run_processes +from backend.executor import DatabaseManager + + +def main(): + """ + Run all the processes required for the AutoGPT-server REST API. + """ + run_processes(DatabaseManager()) + + +if __name__ == "__main__": + main() diff --git a/autogpt_platform/backend/backend/executor/activity_status_generator.py b/autogpt_platform/backend/backend/executor/activity_status_generator.py new file mode 100644 index 000000000000..f45acdd81f0c --- /dev/null +++ b/autogpt_platform/backend/backend/executor/activity_status_generator.py @@ -0,0 +1,434 @@ +""" +Module for generating AI-based activity status for graph executions. +""" + +import json +import logging +from typing import TYPE_CHECKING, Any, NotRequired, TypedDict + +from pydantic import SecretStr + +from backend.blocks.llm import LlmModel, llm_call +from backend.data.block import get_block +from backend.data.execution import ExecutionStatus, NodeExecutionResult +from backend.data.model import APIKeyCredentials, GraphExecutionStats +from backend.util.feature_flag import Flag, is_feature_enabled +from backend.util.retry import func_retry +from backend.util.settings import Settings +from backend.util.truncate import truncate + +if TYPE_CHECKING: + from backend.executor import DatabaseManagerAsyncClient + +logger = logging.getLogger(__name__) + + +class ErrorInfo(TypedDict): + """Type definition for error information.""" + + error: str + execution_id: str + timestamp: str + + +class InputOutputInfo(TypedDict): + """Type definition for input/output information.""" + + execution_id: str + output_data: dict[str, Any] # Used for both input and output data + timestamp: str + + +class NodeInfo(TypedDict): + """Type definition for node information.""" + + node_id: str + block_id: str + block_name: str + block_description: str + execution_count: int + error_count: int + recent_errors: list[ErrorInfo] + recent_outputs: list[InputOutputInfo] + recent_inputs: list[InputOutputInfo] + + +class NodeRelation(TypedDict): + """Type definition for node relation information.""" + + source_node_id: str + sink_node_id: str + source_name: str + sink_name: str + is_static: bool + source_block_name: NotRequired[str] # Optional, only set if block exists + sink_block_name: NotRequired[str] # Optional, only set if block exists + + +def _truncate_uuid(uuid_str: str) -> str: + """Truncate UUID to first segment to reduce payload size.""" + if not uuid_str: + return uuid_str + return uuid_str.split("-")[0] if "-" in uuid_str else uuid_str[:8] + + +async def generate_activity_status_for_execution( + graph_exec_id: str, + graph_id: str, + graph_version: int, + execution_stats: GraphExecutionStats, + db_client: "DatabaseManagerAsyncClient", + user_id: str, + execution_status: ExecutionStatus | None = None, +) -> str | None: + """ + Generate an AI-based activity status summary for a graph execution. + + This function handles all the data collection and AI generation logic, + keeping the manager integration simple. + + Args: + graph_exec_id: The graph execution ID + graph_id: The graph ID + graph_version: The graph version + execution_stats: Execution statistics + db_client: Database client for fetching data + user_id: User ID for LaunchDarkly feature flag evaluation + execution_status: The overall execution status (COMPLETED, FAILED, TERMINATED) + + Returns: + AI-generated activity status string, or None if feature is disabled + """ + # Check LaunchDarkly feature flag for AI activity status generation with full context support + if not await is_feature_enabled(Flag.AI_ACTIVITY_STATUS, user_id): + logger.debug("AI activity status generation is disabled via LaunchDarkly") + return None + + # Check if we have OpenAI API key + try: + settings = Settings() + if not settings.secrets.openai_api_key: + logger.debug( + "OpenAI API key not configured, skipping activity status generation" + ) + return None + + # Get all node executions for this graph execution + node_executions = await db_client.get_node_executions( + graph_exec_id, include_exec_data=True + ) + + # Get graph metadata and full graph structure for name, description, and links + graph_metadata = await db_client.get_graph_metadata(graph_id, graph_version) + graph = await db_client.get_graph(graph_id, graph_version) + + graph_name = graph_metadata.name if graph_metadata else f"Graph {graph_id}" + graph_description = graph_metadata.description if graph_metadata else "" + graph_links = graph.links if graph else [] + + # Build execution data summary + execution_data = _build_execution_summary( + node_executions, + execution_stats, + graph_name, + graph_description, + graph_links, + execution_status, + ) + + # Prepare prompt for AI + prompt = [ + { + "role": "system", + "content": ( + "You are an AI assistant summarizing what you just did for a user in simple, friendly language. " + "Write from the user's perspective about what they accomplished, NOT about technical execution details. " + "Focus on the ACTUAL TASK the user wanted done, not the internal workflow steps. " + "Avoid technical terms like 'workflow', 'execution', 'components', 'nodes', 'processing', etc. " + "Keep it to 3 sentences maximum. Be conversational and human-friendly.\n\n" + "IMPORTANT: Be HONEST about what actually happened:\n" + "- If the input was invalid/nonsensical, say so directly\n" + "- If the task failed, explain what went wrong in simple terms\n" + "- If errors occurred, focus on what the user needs to know\n" + "- Only claim success if the task was genuinely completed\n" + "- Don't sugar-coat failures or present them as helpful feedback\n\n" + "Understanding Errors:\n" + "- Node errors: Individual steps may fail but the overall task might still complete (e.g., one data source fails but others work)\n" + "- Graph error (in overall_status.graph_error): This means the entire execution failed and nothing was accomplished\n" + "- Even if execution shows 'completed', check if critical nodes failed that would prevent the desired outcome\n" + "- Focus on the end result the user wanted, not whether technical steps completed" + ), + }, + { + "role": "user", + "content": ( + f"A user ran '{graph_name}' to accomplish something. Based on this execution data, " + f"write what they achieved in simple, user-friendly terms:\n\n" + f"{json.dumps(execution_data, indent=2)}\n\n" + "CRITICAL: Check overall_status.graph_error FIRST - if present, the entire execution failed.\n" + "Then check individual node errors to understand partial failures.\n\n" + "Write 1-3 sentences about what the user accomplished, such as:\n" + "- 'I analyzed your resume and provided detailed feedback for the IT industry.'\n" + "- 'I couldn't analyze your resume because the input was just nonsensical text.'\n" + "- 'I failed to complete the task due to missing API access.'\n" + "- 'I extracted key information from your documents and organized it into a summary.'\n" + "- 'The task failed to run due to system configuration issues.'\n\n" + "Focus on what ACTUALLY happened, not what was attempted." + ), + }, + ] + + # Log the prompt for debugging purposes + logger.debug( + f"Sending prompt to LLM for graph execution {graph_exec_id}: {json.dumps(prompt, indent=2)}" + ) + + # Create credentials for LLM call + credentials = APIKeyCredentials( + id="openai", + provider="openai", + api_key=SecretStr(settings.secrets.openai_api_key), + title="System OpenAI", + ) + + # Make LLM call using current event loop + activity_status = await _call_llm_direct(credentials, prompt) + + logger.debug( + f"Generated activity status for {graph_exec_id}: {activity_status}" + ) + return activity_status + + except Exception as e: + logger.error( + f"Failed to generate activity status for execution {graph_exec_id}: {str(e)}" + ) + return None + + +def _build_execution_summary( + node_executions: list[NodeExecutionResult], + execution_stats: GraphExecutionStats, + graph_name: str, + graph_description: str, + graph_links: list[Any], + execution_status: ExecutionStatus | None = None, +) -> dict[str, Any]: + """Build a structured summary of execution data for AI analysis.""" + + nodes: list[NodeInfo] = [] + node_execution_counts: dict[str, int] = {} + node_error_counts: dict[str, int] = {} + node_errors: dict[str, list[ErrorInfo]] = {} + node_outputs: dict[str, list[InputOutputInfo]] = {} + node_inputs: dict[str, list[InputOutputInfo]] = {} + input_output_data: dict[str, Any] = {} + node_map: dict[str, NodeInfo] = {} + + # Process node executions + for node_exec in node_executions: + block = get_block(node_exec.block_id) + if not block: + logger.warning( + f"Block {node_exec.block_id} not found for node {node_exec.node_id}" + ) + continue + + # Track execution counts per node + if node_exec.node_id not in node_execution_counts: + node_execution_counts[node_exec.node_id] = 0 + node_execution_counts[node_exec.node_id] += 1 + + # Track errors per node and group them + if node_exec.status == ExecutionStatus.FAILED: + if node_exec.node_id not in node_error_counts: + node_error_counts[node_exec.node_id] = 0 + node_error_counts[node_exec.node_id] += 1 + + # Extract actual error message from output_data + error_message = "Unknown error" + if node_exec.output_data and isinstance(node_exec.output_data, dict): + # Check if error is in output_data + if "error" in node_exec.output_data: + error_output = node_exec.output_data["error"] + if isinstance(error_output, list) and error_output: + error_message = str(error_output[0]) + else: + error_message = str(error_output) + + # Group errors by node_id + if node_exec.node_id not in node_errors: + node_errors[node_exec.node_id] = [] + + node_errors[node_exec.node_id].append( + { + "error": error_message, + "execution_id": _truncate_uuid(node_exec.node_exec_id), + "timestamp": node_exec.add_time.isoformat(), + } + ) + + # Collect output samples for each node (latest executions) + if node_exec.output_data: + if node_exec.node_id not in node_outputs: + node_outputs[node_exec.node_id] = [] + + # Truncate output data to 100 chars to save space + truncated_output = truncate(node_exec.output_data, 100) + + node_outputs[node_exec.node_id].append( + { + "execution_id": _truncate_uuid(node_exec.node_exec_id), + "output_data": truncated_output, + "timestamp": node_exec.add_time.isoformat(), + } + ) + + # Collect input samples for each node (latest executions) + if node_exec.input_data: + if node_exec.node_id not in node_inputs: + node_inputs[node_exec.node_id] = [] + + # Truncate input data to 100 chars to save space + truncated_input = truncate(node_exec.input_data, 100) + + node_inputs[node_exec.node_id].append( + { + "execution_id": _truncate_uuid(node_exec.node_exec_id), + "output_data": truncated_input, # Reuse field name for consistency + "timestamp": node_exec.add_time.isoformat(), + } + ) + + # Build node data (only add unique nodes) + if node_exec.node_id not in node_map: + node_data: NodeInfo = { + "node_id": _truncate_uuid(node_exec.node_id), + "block_id": _truncate_uuid(node_exec.block_id), + "block_name": block.name, + "block_description": block.description or "", + "execution_count": 0, # Will be set later + "error_count": 0, # Will be set later + "recent_errors": [], # Will be set later + "recent_outputs": [], # Will be set later + "recent_inputs": [], # Will be set later + } + nodes.append(node_data) + node_map[node_exec.node_id] = node_data + + # Store input/output data for special blocks (input/output blocks) + if block.name in ["AgentInputBlock", "AgentOutputBlock", "UserInputBlock"]: + if node_exec.input_data: + input_output_data[f"{node_exec.node_id}_inputs"] = dict( + node_exec.input_data + ) + if node_exec.output_data: + input_output_data[f"{node_exec.node_id}_outputs"] = dict( + node_exec.output_data + ) + + # Add execution and error counts to node data, plus limited errors and output samples + for node in nodes: + # Use original node_id for lookups (before truncation) + original_node_id = None + for orig_id, node_data in node_map.items(): + if node_data == node: + original_node_id = orig_id + break + + if original_node_id: + node["execution_count"] = node_execution_counts.get(original_node_id, 0) + node["error_count"] = node_error_counts.get(original_node_id, 0) + + # Add limited errors for this node (latest 10 or first 5 + last 5) + if original_node_id in node_errors: + node_error_list = node_errors[original_node_id] + if len(node_error_list) <= 10: + node["recent_errors"] = node_error_list + else: + # First 5 + last 5 if more than 10 errors + node["recent_errors"] = node_error_list[:5] + node_error_list[-5:] + + # Add latest output samples (latest 3) + if original_node_id in node_outputs: + node_output_list = node_outputs[original_node_id] + # Sort by timestamp if available, otherwise take last 3 + if node_output_list and node_output_list[0].get("timestamp"): + node_output_list.sort( + key=lambda x: x.get("timestamp", ""), reverse=True + ) + node["recent_outputs"] = node_output_list[:3] + + # Add latest input samples (latest 3) + if original_node_id in node_inputs: + node_input_list = node_inputs[original_node_id] + # Sort by timestamp if available, otherwise take last 3 + if node_input_list and node_input_list[0].get("timestamp"): + node_input_list.sort( + key=lambda x: x.get("timestamp", ""), reverse=True + ) + node["recent_inputs"] = node_input_list[:3] + + # Build node relations from graph links + node_relations: list[NodeRelation] = [] + for link in graph_links: + # Include link details with source and sink information (truncated UUIDs) + relation: NodeRelation = { + "source_node_id": _truncate_uuid(link.source_id), + "sink_node_id": _truncate_uuid(link.sink_id), + "source_name": link.source_name, + "sink_name": link.sink_name, + "is_static": link.is_static if hasattr(link, "is_static") else False, + } + + # Add block names if nodes exist in our map + if link.source_id in node_map: + relation["source_block_name"] = node_map[link.source_id]["block_name"] + if link.sink_id in node_map: + relation["sink_block_name"] = node_map[link.sink_id]["block_name"] + + node_relations.append(relation) + + # Build overall summary + return { + "graph_info": {"name": graph_name, "description": graph_description}, + "nodes": nodes, + "node_relations": node_relations, + "input_output_data": input_output_data, + "overall_status": { + "total_nodes_in_graph": len(nodes), + "total_executions": execution_stats.node_count, + "total_errors": execution_stats.node_error_count, + "execution_time_seconds": execution_stats.walltime, + "has_errors": bool( + execution_stats.error or execution_stats.node_error_count > 0 + ), + "graph_error": ( + str(execution_stats.error) if execution_stats.error else None + ), + "graph_execution_status": ( + execution_status.value if execution_status else None + ), + }, + } + + +@func_retry +async def _call_llm_direct( + credentials: APIKeyCredentials, prompt: list[dict[str, str]] +) -> str: + """Make direct LLM call.""" + + response = await llm_call( + credentials=credentials, + llm_model=LlmModel.GPT4O_MINI, + prompt=prompt, + json_format=False, + max_tokens=150, + compress_prompt_to_fit=True, + ) + + if response and response.response: + return response.response.strip() + else: + return "Unable to generate activity summary" diff --git a/autogpt_platform/backend/backend/executor/activity_status_generator_test.py b/autogpt_platform/backend/backend/executor/activity_status_generator_test.py new file mode 100644 index 000000000000..08ff3a7c2ae5 --- /dev/null +++ b/autogpt_platform/backend/backend/executor/activity_status_generator_test.py @@ -0,0 +1,702 @@ +""" +Tests for activity status generator functionality. +""" + +from datetime import datetime, timezone +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from backend.blocks.llm import LLMResponse +from backend.data.execution import ExecutionStatus, NodeExecutionResult +from backend.data.model import GraphExecutionStats +from backend.executor.activity_status_generator import ( + _build_execution_summary, + _call_llm_direct, + generate_activity_status_for_execution, +) + + +@pytest.fixture +def mock_node_executions(): + """Create mock node executions for testing.""" + return [ + NodeExecutionResult( + user_id="test_user", + graph_id="test_graph", + graph_version=1, + graph_exec_id="test_exec", + node_exec_id="123e4567-e89b-12d3-a456-426614174001", + node_id="456e7890-e89b-12d3-a456-426614174002", + block_id="789e1234-e89b-12d3-a456-426614174003", + status=ExecutionStatus.COMPLETED, + input_data={"user_input": "Hello, world!"}, + output_data={"processed_input": ["Hello, world!"]}, + add_time=datetime.now(timezone.utc), + queue_time=None, + start_time=None, + end_time=None, + ), + NodeExecutionResult( + user_id="test_user", + graph_id="test_graph", + graph_version=1, + graph_exec_id="test_exec", + node_exec_id="234e5678-e89b-12d3-a456-426614174004", + node_id="567e8901-e89b-12d3-a456-426614174005", + block_id="890e2345-e89b-12d3-a456-426614174006", + status=ExecutionStatus.COMPLETED, + input_data={"data": "Hello, world!"}, + output_data={"result": ["Processed data"]}, + add_time=datetime.now(timezone.utc), + queue_time=None, + start_time=None, + end_time=None, + ), + NodeExecutionResult( + user_id="test_user", + graph_id="test_graph", + graph_version=1, + graph_exec_id="test_exec", + node_exec_id="345e6789-e89b-12d3-a456-426614174007", + node_id="678e9012-e89b-12d3-a456-426614174008", + block_id="901e3456-e89b-12d3-a456-426614174009", + status=ExecutionStatus.FAILED, + input_data={"final_data": "Processed data"}, + output_data={ + "error": ["Connection timeout: Unable to reach external service"] + }, + add_time=datetime.now(timezone.utc), + queue_time=None, + start_time=None, + end_time=None, + ), + ] + + +@pytest.fixture +def mock_execution_stats(): + """Create mock execution stats for testing.""" + return GraphExecutionStats( + walltime=2.5, + cputime=1.8, + nodes_walltime=2.0, + nodes_cputime=1.5, + node_count=3, + node_error_count=1, + cost=10, + error=None, + ) + + +@pytest.fixture +def mock_execution_stats_with_graph_error(): + """Create mock execution stats with graph-level error.""" + return GraphExecutionStats( + walltime=2.5, + cputime=1.8, + nodes_walltime=2.0, + nodes_cputime=1.5, + node_count=3, + node_error_count=1, + cost=10, + error="Graph execution failed: Invalid API credentials", + ) + + +@pytest.fixture +def mock_blocks(): + """Create mock blocks for testing.""" + input_block = MagicMock() + input_block.name = "AgentInputBlock" + input_block.description = "Handles user input" + + process_block = MagicMock() + process_block.name = "ProcessingBlock" + process_block.description = "Processes data" + + output_block = MagicMock() + output_block.name = "AgentOutputBlock" + output_block.description = "Provides output to user" + + return { + "789e1234-e89b-12d3-a456-426614174003": input_block, + "890e2345-e89b-12d3-a456-426614174006": process_block, + "901e3456-e89b-12d3-a456-426614174009": output_block, + "process_block_id": process_block, # Keep old key for different error format test + } + + +class TestBuildExecutionSummary: + """Tests for _build_execution_summary function.""" + + def test_build_summary_with_successful_execution( + self, mock_node_executions, mock_execution_stats, mock_blocks + ): + """Test building summary for successful execution.""" + # Create mock links with realistic UUIDs + mock_links = [ + MagicMock( + source_id="456e7890-e89b-12d3-a456-426614174002", + sink_id="567e8901-e89b-12d3-a456-426614174005", + source_name="output", + sink_name="input", + is_static=False, + ) + ] + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block: + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + + summary = _build_execution_summary( + mock_node_executions[:2], + mock_execution_stats, + "Test Graph", + "A test graph for processing", + mock_links, + ExecutionStatus.COMPLETED, + ) + + # Check graph info + assert summary["graph_info"]["name"] == "Test Graph" + assert summary["graph_info"]["description"] == "A test graph for processing" + + # Check nodes with per-node counts + assert len(summary["nodes"]) == 2 + assert summary["nodes"][0]["block_name"] == "AgentInputBlock" + assert summary["nodes"][0]["execution_count"] == 1 + assert summary["nodes"][0]["error_count"] == 0 + assert summary["nodes"][1]["block_name"] == "ProcessingBlock" + assert summary["nodes"][1]["execution_count"] == 1 + assert summary["nodes"][1]["error_count"] == 0 + + # Check node relations (UUIDs are truncated to first segment) + assert len(summary["node_relations"]) == 1 + assert ( + summary["node_relations"][0]["source_node_id"] == "456e7890" + ) # Truncated + assert ( + summary["node_relations"][0]["sink_node_id"] == "567e8901" + ) # Truncated + assert ( + summary["node_relations"][0]["source_block_name"] == "AgentInputBlock" + ) + assert summary["node_relations"][0]["sink_block_name"] == "ProcessingBlock" + + # Check overall status + assert summary["overall_status"]["total_nodes_in_graph"] == 2 + assert summary["overall_status"]["total_executions"] == 3 + assert summary["overall_status"]["total_errors"] == 1 + assert summary["overall_status"]["execution_time_seconds"] == 2.5 + assert summary["overall_status"]["graph_execution_status"] == "COMPLETED" + + # Check input/output data (using actual node UUIDs) + assert ( + "456e7890-e89b-12d3-a456-426614174002_inputs" + in summary["input_output_data"] + ) + assert ( + "456e7890-e89b-12d3-a456-426614174002_outputs" + in summary["input_output_data"] + ) + + def test_build_summary_with_failed_execution( + self, mock_node_executions, mock_execution_stats, mock_blocks + ): + """Test building summary for execution with failures.""" + mock_links = [] # No links for this test + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block: + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + + summary = _build_execution_summary( + mock_node_executions, + mock_execution_stats, + "Failed Graph", + "Test with failures", + mock_links, + ExecutionStatus.FAILED, + ) + + # Check that errors are now in node's recent_errors field + # Find the output node (with truncated UUID) + output_node = next( + n for n in summary["nodes"] if n["node_id"] == "678e9012" # Truncated + ) + assert output_node["error_count"] == 1 + assert output_node["execution_count"] == 1 + + # Check recent_errors field + assert "recent_errors" in output_node + assert len(output_node["recent_errors"]) == 1 + assert ( + output_node["recent_errors"][0]["error"] + == "Connection timeout: Unable to reach external service" + ) + assert ( + "execution_id" in output_node["recent_errors"][0] + ) # Should include execution ID + + def test_build_summary_with_missing_blocks( + self, mock_node_executions, mock_execution_stats + ): + """Test building summary when blocks are missing.""" + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block: + mock_get_block.return_value = None + + summary = _build_execution_summary( + mock_node_executions, + mock_execution_stats, + "Missing Blocks Graph", + "Test with missing blocks", + [], + ExecutionStatus.COMPLETED, + ) + + # Should handle missing blocks gracefully + assert len(summary["nodes"]) == 0 + # No top-level errors field anymore, errors are in nodes' recent_errors + assert summary["graph_info"]["name"] == "Missing Blocks Graph" + + def test_build_summary_with_graph_error( + self, mock_node_executions, mock_execution_stats_with_graph_error, mock_blocks + ): + """Test building summary with graph-level error.""" + mock_links = [] + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block: + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + + summary = _build_execution_summary( + mock_node_executions, + mock_execution_stats_with_graph_error, + "Graph with Error", + "Test with graph error", + mock_links, + ExecutionStatus.FAILED, + ) + + # Check that graph error is included in overall status + assert summary["overall_status"]["has_errors"] is True + assert ( + summary["overall_status"]["graph_error"] + == "Graph execution failed: Invalid API credentials" + ) + assert summary["overall_status"]["total_errors"] == 1 + assert summary["overall_status"]["graph_execution_status"] == "FAILED" + + def test_build_summary_with_different_error_formats( + self, mock_execution_stats, mock_blocks + ): + """Test building summary with different error formats.""" + # Create node executions with different error formats and realistic UUIDs + mock_executions = [ + NodeExecutionResult( + user_id="test_user", + graph_id="test_graph", + graph_version=1, + graph_exec_id="test_exec", + node_exec_id="111e2222-e89b-12d3-a456-426614174010", + node_id="333e4444-e89b-12d3-a456-426614174011", + block_id="process_block_id", + status=ExecutionStatus.FAILED, + input_data={}, + output_data={"error": ["Simple string error message"]}, + add_time=datetime.now(timezone.utc), + queue_time=None, + start_time=None, + end_time=None, + ), + NodeExecutionResult( + user_id="test_user", + graph_id="test_graph", + graph_version=1, + graph_exec_id="test_exec", + node_exec_id="555e6666-e89b-12d3-a456-426614174012", + node_id="777e8888-e89b-12d3-a456-426614174013", + block_id="process_block_id", + status=ExecutionStatus.FAILED, + input_data={}, + output_data={}, # No error in output + add_time=datetime.now(timezone.utc), + queue_time=None, + start_time=None, + end_time=None, + ), + ] + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block: + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + + summary = _build_execution_summary( + mock_executions, + mock_execution_stats, + "Error Test Graph", + "Testing error formats", + [], + ExecutionStatus.FAILED, + ) + + # Check different error formats - errors are now in nodes' recent_errors + error_nodes = [n for n in summary["nodes"] if n.get("recent_errors")] + assert len(error_nodes) == 2 + + # String error format - find node with truncated ID + string_error_node = next( + n for n in summary["nodes"] if n["node_id"] == "333e4444" # Truncated + ) + assert len(string_error_node["recent_errors"]) == 1 + assert ( + string_error_node["recent_errors"][0]["error"] + == "Simple string error message" + ) + + # No error output format - find node with truncated ID + no_error_node = next( + n for n in summary["nodes"] if n["node_id"] == "777e8888" # Truncated + ) + assert len(no_error_node["recent_errors"]) == 1 + assert no_error_node["recent_errors"][0]["error"] == "Unknown error" + + +class TestLLMCall: + """Tests for LLM calling functionality.""" + + @pytest.mark.asyncio + async def test_call_llm_direct_success(self): + """Test successful LLM call.""" + from pydantic import SecretStr + + from backend.data.model import APIKeyCredentials + + mock_response = LLMResponse( + raw_response={}, + prompt=[], + response="Agent successfully processed user input and generated response.", + tool_calls=None, + prompt_tokens=50, + completion_tokens=20, + ) + + with patch( + "backend.executor.activity_status_generator.llm_call" + ) as mock_llm_call: + mock_llm_call.return_value = mock_response + + credentials = APIKeyCredentials( + id="test", + provider="openai", + api_key=SecretStr("test_key"), + title="Test", + ) + + prompt = [{"role": "user", "content": "Test prompt"}] + + result = await _call_llm_direct(credentials, prompt) + + assert ( + result + == "Agent successfully processed user input and generated response." + ) + mock_llm_call.assert_called_once() + + @pytest.mark.asyncio + async def test_call_llm_direct_no_response(self): + """Test LLM call with no response.""" + from pydantic import SecretStr + + from backend.data.model import APIKeyCredentials + + with patch( + "backend.executor.activity_status_generator.llm_call" + ) as mock_llm_call: + mock_llm_call.return_value = None + + credentials = APIKeyCredentials( + id="test", + provider="openai", + api_key=SecretStr("test_key"), + title="Test", + ) + + prompt = [{"role": "user", "content": "Test prompt"}] + + result = await _call_llm_direct(credentials, prompt) + + assert result == "Unable to generate activity summary" + + +class TestGenerateActivityStatusForExecution: + """Tests for the main generate_activity_status_for_execution function.""" + + @pytest.mark.asyncio + async def test_generate_status_success( + self, mock_node_executions, mock_execution_stats, mock_blocks + ): + """Test successful activity status generation.""" + mock_db_client = AsyncMock() + mock_db_client.get_node_executions.return_value = mock_node_executions + + mock_graph_metadata = MagicMock() + mock_graph_metadata.name = "Test Agent" + mock_graph_metadata.description = "A test agent" + mock_db_client.get_graph_metadata.return_value = mock_graph_metadata + + mock_graph = MagicMock() + mock_graph.links = [] + mock_db_client.get_graph.return_value = mock_graph + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block, patch( + "backend.executor.activity_status_generator.Settings" + ) as mock_settings, patch( + "backend.executor.activity_status_generator._call_llm_direct" + ) as mock_llm, patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=True, + ): + + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + mock_settings.return_value.secrets.openai_api_key = "test_key" + mock_llm.return_value = ( + "I analyzed your data and provided the requested insights." + ) + + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result == "I analyzed your data and provided the requested insights." + mock_db_client.get_node_executions.assert_called_once() + mock_db_client.get_graph_metadata.assert_called_once() + mock_db_client.get_graph.assert_called_once() + mock_llm.assert_called_once() + + @pytest.mark.asyncio + async def test_generate_status_feature_disabled(self, mock_execution_stats): + """Test activity status generation when feature is disabled.""" + mock_db_client = AsyncMock() + + with patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=False, + ): + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result is None + mock_db_client.get_node_executions.assert_not_called() + + @pytest.mark.asyncio + async def test_generate_status_no_api_key(self, mock_execution_stats): + """Test activity status generation with no API key.""" + mock_db_client = AsyncMock() + + with patch( + "backend.executor.activity_status_generator.Settings" + ) as mock_settings, patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=True, + ): + mock_settings.return_value.secrets.openai_api_key = "" + + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result is None + mock_db_client.get_node_executions.assert_not_called() + + @pytest.mark.asyncio + async def test_generate_status_exception_handling(self, mock_execution_stats): + """Test activity status generation with exception.""" + mock_db_client = AsyncMock() + mock_db_client.get_node_executions.side_effect = Exception("Database error") + + with patch( + "backend.executor.activity_status_generator.Settings" + ) as mock_settings, patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=True, + ): + mock_settings.return_value.secrets.openai_api_key = "test_key" + + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result is None + + @pytest.mark.asyncio + async def test_generate_status_with_graph_name_fallback( + self, mock_node_executions, mock_execution_stats, mock_blocks + ): + """Test activity status generation with graph name fallback.""" + mock_db_client = AsyncMock() + mock_db_client.get_node_executions.return_value = mock_node_executions + mock_db_client.get_graph_metadata.return_value = None # No metadata + mock_db_client.get_graph.return_value = None # No graph + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block, patch( + "backend.executor.activity_status_generator.Settings" + ) as mock_settings, patch( + "backend.executor.activity_status_generator._call_llm_direct" + ) as mock_llm, patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=True, + ): + + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + mock_settings.return_value.secrets.openai_api_key = "test_key" + mock_llm.return_value = "Agent completed execution." + + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result == "Agent completed execution." + # Should use fallback graph name in prompt + call_args = mock_llm.call_args[0][1] # prompt argument + assert "Graph test_graph" in call_args[1]["content"] + + +class TestIntegration: + """Integration tests to verify the complete flow.""" + + @pytest.mark.asyncio + async def test_full_integration_flow( + self, mock_node_executions, mock_execution_stats, mock_blocks + ): + """Test the complete integration flow.""" + mock_db_client = AsyncMock() + mock_db_client.get_node_executions.return_value = mock_node_executions + + mock_graph_metadata = MagicMock() + mock_graph_metadata.name = "Test Integration Agent" + mock_graph_metadata.description = "Integration test agent" + mock_db_client.get_graph_metadata.return_value = mock_graph_metadata + + mock_graph = MagicMock() + mock_graph.links = [] + mock_db_client.get_graph.return_value = mock_graph + + expected_activity = "I processed user input but failed during final output generation due to system error." + + with patch( + "backend.executor.activity_status_generator.get_block" + ) as mock_get_block, patch( + "backend.executor.activity_status_generator.Settings" + ) as mock_settings, patch( + "backend.executor.activity_status_generator.llm_call" + ) as mock_llm_call, patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=True, + ): + + mock_get_block.side_effect = lambda block_id: mock_blocks.get(block_id) + mock_settings.return_value.secrets.openai_api_key = "test_key" + + mock_response = LLMResponse( + raw_response={}, + prompt=[], + response=expected_activity, + tool_calls=None, + prompt_tokens=100, + completion_tokens=30, + ) + mock_llm_call.return_value = mock_response + + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + assert result == expected_activity + + # Verify the correct data was passed to LLM + llm_call_args = mock_llm_call.call_args + prompt = llm_call_args[1]["prompt"] + + # Check system prompt + assert prompt[0]["role"] == "system" + assert "user's perspective" in prompt[0]["content"] + + # Check user prompt contains expected data + user_content = prompt[1]["content"] + assert "Test Integration Agent" in user_content + assert "user-friendly terms" in user_content.lower() + + # Verify that execution data is present in the prompt + assert "{" in user_content # Should contain JSON data + assert "overall_status" in user_content + + @pytest.mark.asyncio + async def test_manager_integration_with_disabled_feature( + self, mock_execution_stats + ): + """Test that when feature returns None, manager doesn't set activity_status.""" + mock_db_client = AsyncMock() + + with patch( + "backend.executor.activity_status_generator.is_feature_enabled", + return_value=False, + ): + result = await generate_activity_status_for_execution( + graph_exec_id="test_exec", + graph_id="test_graph", + graph_version=1, + execution_stats=mock_execution_stats, + db_client=mock_db_client, + user_id="test_user", + ) + + # Should return None when disabled + assert result is None + + # Verify no database calls were made + mock_db_client.get_node_executions.assert_not_called() + mock_db_client.get_graph_metadata.assert_not_called() + mock_db_client.get_graph.assert_not_called() diff --git a/autogpt_platform/backend/backend/executor/database.py b/autogpt_platform/backend/backend/executor/database.py index 499dd014eeab..2607b24843df 100644 --- a/autogpt_platform/backend/backend/executor/database.py +++ b/autogpt_platform/backend/backend/executor/database.py @@ -7,7 +7,6 @@ create_graph_execution, get_block_error_stats, get_execution_kv_data, - get_graph_execution, get_graph_execution_meta, get_graph_executions, get_latest_node_execution, @@ -16,12 +15,12 @@ set_execution_kv_data, update_graph_execution_start_time, update_graph_execution_stats, - update_node_execution_stats, update_node_execution_status, update_node_execution_status_batch, upsert_execution_input, upsert_execution_output, ) +from backend.data.generate_data import get_user_execution_summary_data from backend.data.graph import ( get_connected_output_nodes, get_graph, @@ -40,12 +39,18 @@ get_user_email_by_id, get_user_email_verification, get_user_integrations, - get_user_metadata, get_user_notification_preference, update_user_integrations, - update_user_metadata, ) -from backend.util.service import AppService, AppServiceClient, endpoint_to_sync, expose +from backend.server.v2.library.db import add_store_agent_to_library, list_library_agents +from backend.server.v2.store.db import get_store_agent_details, get_store_agents +from backend.util.service import ( + AppService, + AppServiceClient, + UnhealthyServiceError, + endpoint_to_sync, + expose, +) from backend.util.settings import Config config = Config() @@ -77,6 +82,11 @@ def cleanup(self): logger.info(f"[{self.service_name}] ⏳ Disconnecting Database...") self.run_and_wait(db.disconnect()) + async def health_check(self) -> str: + if not db.is_connected(): + raise UnhealthyServiceError("Database is not connected") + return await super().health_check() + @classmethod def get_port(cls) -> int: return config.database_api_port @@ -90,7 +100,6 @@ def _( return cast(Callable[Concatenate[object, P], R], expose(f)) # Executions - get_graph_execution = _(get_graph_execution) get_graph_executions = _(get_graph_executions) get_graph_execution_meta = _(get_graph_execution_meta) create_graph_execution = _(create_graph_execution) @@ -101,7 +110,6 @@ def _( update_node_execution_status_batch = _(update_node_execution_status_batch) update_graph_execution_start_time = _(update_graph_execution_start_time) update_graph_execution_stats = _(update_graph_execution_stats) - update_node_execution_stats = _(update_node_execution_stats) upsert_execution_input = _(upsert_execution_input) upsert_execution_output = _(upsert_execution_output) get_execution_kv_data = _(get_execution_kv_data) @@ -119,8 +127,6 @@ def _( get_credits = _(_get_credits, name="get_credits") # User + User Metadata + User Integrations - get_user_metadata = _(get_user_metadata) - update_user_metadata = _(update_user_metadata) get_user_integrations = _(get_user_integrations) update_user_integrations = _(update_user_integrations) @@ -141,6 +147,17 @@ def _( get_user_notification_oldest_message_in_batch ) + # Library + list_library_agents = _(list_library_agents) + add_store_agent_to_library = _(add_store_agent_to_library) + + # Store + get_store_agents = _(get_store_agents) + get_store_agent_details = _(get_store_agent_details) + + # Summary data - async + get_user_execution_summary_data = _(get_user_execution_summary_data) + class DatabaseManagerClient(AppServiceClient): d = DatabaseManager @@ -151,58 +168,34 @@ def get_service_type(cls): return DatabaseManager # Executions - get_graph_execution = _(d.get_graph_execution) get_graph_executions = _(d.get_graph_executions) get_graph_execution_meta = _(d.get_graph_execution_meta) - create_graph_execution = _(d.create_graph_execution) - get_node_execution = _(d.get_node_execution) get_node_executions = _(d.get_node_executions) - get_latest_node_execution = _(d.get_latest_node_execution) update_node_execution_status = _(d.update_node_execution_status) - update_node_execution_status_batch = _(d.update_node_execution_status_batch) update_graph_execution_start_time = _(d.update_graph_execution_start_time) update_graph_execution_stats = _(d.update_graph_execution_stats) - update_node_execution_stats = _(d.update_node_execution_stats) - upsert_execution_input = _(d.upsert_execution_input) upsert_execution_output = _(d.upsert_execution_output) - get_execution_kv_data = _(d.get_execution_kv_data) - set_execution_kv_data = _(d.set_execution_kv_data) # Graphs - get_node = _(d.get_node) - get_graph = _(d.get_graph) - get_connected_output_nodes = _(d.get_connected_output_nodes) get_graph_metadata = _(d.get_graph_metadata) # Credits spend_credits = _(d.spend_credits) get_credits = _(d.get_credits) - # User + User Metadata + User Integrations - get_user_metadata = _(d.get_user_metadata) - update_user_metadata = _(d.update_user_metadata) - get_user_integrations = _(d.get_user_integrations) - update_user_integrations = _(d.update_user_integrations) + # Block error monitoring + get_block_error_stats = _(d.get_block_error_stats) - # User Comms - async - get_active_user_ids_in_timerange = _(d.get_active_user_ids_in_timerange) + # User Emails get_user_email_by_id = _(d.get_user_email_by_id) - get_user_email_verification = _(d.get_user_email_verification) - get_user_notification_preference = _(d.get_user_notification_preference) - # Notifications - async - create_or_add_to_user_notification_batch = _( - d.create_or_add_to_user_notification_batch - ) - empty_user_notification_batch = _(d.empty_user_notification_batch) - get_all_batches_by_type = _(d.get_all_batches_by_type) - get_user_notification_batch = _(d.get_user_notification_batch) - get_user_notification_oldest_message_in_batch = _( - d.get_user_notification_oldest_message_in_batch - ) + # Library + list_library_agents = _(d.list_library_agents) + add_store_agent_to_library = _(d.add_store_agent_to_library) - # Block error monitoring - get_block_error_stats = _(d.get_block_error_stats) + # Store + get_store_agents = _(d.get_store_agents) + get_store_agent_details = _(d.get_store_agent_details) class DatabaseManagerAsyncClient(AppServiceClient): @@ -225,10 +218,36 @@ def get_service_type(cls): upsert_execution_input = d.upsert_execution_input upsert_execution_output = d.upsert_execution_output update_graph_execution_stats = d.update_graph_execution_stats - update_node_execution_stats = d.update_node_execution_stats update_node_execution_status = d.update_node_execution_status update_node_execution_status_batch = d.update_node_execution_status_batch update_user_integrations = d.update_user_integrations get_execution_kv_data = d.get_execution_kv_data set_execution_kv_data = d.set_execution_kv_data - get_block_error_stats = d.get_block_error_stats + + # User Comms + get_active_user_ids_in_timerange = d.get_active_user_ids_in_timerange + get_user_email_by_id = d.get_user_email_by_id + get_user_email_verification = d.get_user_email_verification + get_user_notification_preference = d.get_user_notification_preference + + # Notifications + create_or_add_to_user_notification_batch = ( + d.create_or_add_to_user_notification_batch + ) + empty_user_notification_batch = d.empty_user_notification_batch + get_all_batches_by_type = d.get_all_batches_by_type + get_user_notification_batch = d.get_user_notification_batch + get_user_notification_oldest_message_in_batch = ( + d.get_user_notification_oldest_message_in_batch + ) + + # Library + list_library_agents = d.list_library_agents + add_store_agent_to_library = d.add_store_agent_to_library + + # Store + get_store_agents = d.get_store_agents + get_store_agent_details = d.get_store_agent_details + + # Summary data + get_user_execution_summary_data = d.get_user_execution_summary_data diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 709b37560480..5e38c285d9fa 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -1,12 +1,10 @@ import asyncio import logging -import multiprocessing import os -import sys import threading import time from collections import defaultdict -from concurrent.futures import CancelledError, Future, ProcessPoolExecutor +from concurrent.futures import Future, ThreadPoolExecutor from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast @@ -22,16 +20,19 @@ LowBalanceData, NotificationEventModel, NotificationType, + ZeroBalanceData, ) from backend.data.rabbitmq import SyncRabbitMQ -from backend.executor.utils import LogMetadata, create_execution_queue_config +from backend.executor.activity_status_generator import ( + generate_activity_status_for_execution, +) +from backend.executor.utils import LogMetadata from backend.notifications.notifications import queue_notification -from backend.util.exceptions import InsufficientBalanceError +from backend.util.exceptions import InsufficientBalanceError, ModerationError if TYPE_CHECKING: from backend.executor import DatabaseManagerClient, DatabaseManagerAsyncClient -from autogpt_libs.utils.cache import thread_cached from prometheus_client import Gauge, start_http_server from backend.blocks.agent import AgentExecutorBlock @@ -51,24 +52,32 @@ GraphExecutionEntry, NodeExecutionEntry, NodeExecutionResult, + UserContext, ) from backend.data.graph import Link, Node from backend.executor.utils import ( + GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS, GRAPH_EXECUTION_CANCEL_QUEUE_NAME, GRAPH_EXECUTION_QUEUE_NAME, CancelExecutionEvent, ExecutionOutputEntry, NodeExecutionProgress, block_usage_cost, + create_execution_queue_config, execution_usage_cost, - get_async_execution_event_bus, - get_execution_event_bus, - get_execution_queue, parse_execution_output, validate_exec, ) from backend.integrations.creds_manager import IntegrationCredentialsManager +from backend.server.v2.AutoMod.manager import automod_manager from backend.util import json +from backend.util.clients import ( + get_async_execution_event_bus, + get_database_manager_async_client, + get_database_manager_client, + get_execution_event_bus, + get_notification_manager_client, +) from backend.util.decorator import ( async_error_logged, async_time_measured, @@ -77,9 +86,9 @@ ) from backend.util.file import clean_exec_files from backend.util.logging import TruncatedLogger, configure_logging +from backend.util.metrics import DiscordChannel from backend.util.process import AppProcess, set_service_name from backend.util.retry import continuous_retry, func_retry -from backend.util.service import get_service_client from backend.util.settings import Settings _logger = logging.getLogger(__name__) @@ -97,6 +106,22 @@ "Ratio of active graph runs to max graph workers", ) +# Thread-local storage for ExecutionProcessor instances +_tls = threading.local() + + +def init_worker(): + """Initialize ExecutionProcessor instance in thread-local storage""" + _tls.processor = ExecutionProcessor() + _tls.processor.on_graph_executor_start() + + +def execute_graph( + graph_exec_entry: "GraphExecutionEntry", cancel_event: threading.Event +): + """Execute graph using thread-local ExecutionProcessor instance""" + return _tls.processor.on_graph_execution(graph_exec_entry, cancel_event) + T = TypeVar("T") @@ -169,6 +194,9 @@ async def execute_node( "user_id": user_id, } + # Add user context from NodeExecutionEntry + extra_exec_kwargs["user_context"] = data.user_context + # Last-minute fetch credentials + acquire a system-wide read-write lock to prevent # changes during execution. ⚠️ This means a set of credentials can only be used by # one (running) block at a time; simultaneous execution of blocks using same @@ -191,12 +219,6 @@ async def execute_node( output_size += len(json.dumps(output_data)) log_metadata.debug("Node produced output", **{output_name: output_data}) yield output_name, output_data - - except Exception as e: - error_msg = str(e) - yield "error", error_msg - raise e - finally: # Ensure credentials are released even if execution fails if creds_lock and (await creds_lock.locked()) and (await creds_lock.owned()): @@ -221,6 +243,7 @@ async def _enqueue_next_nodes( graph_id: str, log_metadata: LogMetadata, nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]], + user_context: UserContext, ) -> list[NodeExecutionEntry]: async def add_enqueued_execution( node_exec_id: str, node_id: str, block_id: str, data: BlockInput @@ -239,6 +262,7 @@ async def add_enqueued_execution( node_id=node_id, block_id=block_id, inputs=data, + user_context=user_context, ) async def register_next_executions(node_link: Link) -> list[NodeExecutionEntry]: @@ -367,7 +391,7 @@ async def _register_next_executions(node_link: Link) -> list[NodeExecutionEntry] ] -class Executor: +class ExecutionProcessor: """ This class contains event handlers for the process pool executor events. @@ -390,13 +414,13 @@ class Executor: 9. Node executor enqueues the next executed nodes to the node execution queue. """ - @classmethod @async_error_logged(swallow=True) async def on_node_execution( - cls, + self, node_exec: NodeExecutionEntry, node_exec_progress: NodeExecutionProgress, - nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, + nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]], + graph_stats_pair: tuple[GraphExecutionStats, threading.Lock], ) -> NodeExecutionStats: log_metadata = LogMetadata( logger=_logger, @@ -409,40 +433,83 @@ async def on_node_execution( ) db_client = get_db_async_client() node = await db_client.get_node(node_exec.node_id) - execution_stats = NodeExecutionStats() - timing_info, _ = await cls._on_node_execution( + + timing_info, status = await self._on_node_execution( node=node, node_exec=node_exec, node_exec_progress=node_exec_progress, + stats=execution_stats, db_client=db_client, log_metadata=log_metadata, - stats=execution_stats, nodes_input_masks=nodes_input_masks, ) + if isinstance(status, BaseException): + raise status + execution_stats.walltime = timing_info.wall_time execution_stats.cputime = timing_info.cpu_time - if isinstance(execution_stats.error, Exception): - execution_stats.error = str(execution_stats.error) - exec_update = await db_client.update_node_execution_stats( - node_exec.node_exec_id, execution_stats + graph_stats, graph_stats_lock = graph_stats_pair + with graph_stats_lock: + graph_stats.node_count += 1 + execution_stats.extra_steps + graph_stats.nodes_cputime += execution_stats.cputime + graph_stats.nodes_walltime += execution_stats.walltime + graph_stats.cost += execution_stats.extra_cost + if isinstance(execution_stats.error, Exception): + graph_stats.node_error_count += 1 + + node_error = execution_stats.error + node_stats = execution_stats.model_dump() + if node_error and not isinstance(node_error, str): + node_stats["error"] = str(node_error) or node_stats.__class__.__name__ + + await async_update_node_execution_status( + db_client=db_client, + exec_id=node_exec.node_exec_id, + status=status, + stats=node_stats, ) - await send_async_execution_update(exec_update) + await async_update_graph_execution_state( + db_client=db_client, + graph_exec_id=node_exec.graph_exec_id, + stats=graph_stats, + ) + return execution_stats - @classmethod @async_time_measured async def _on_node_execution( - cls, + self, node: Node, node_exec: NodeExecutionEntry, node_exec_progress: NodeExecutionProgress, + stats: NodeExecutionStats, db_client: "DatabaseManagerAsyncClient", log_metadata: LogMetadata, - stats: NodeExecutionStats | None = None, nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, - ): + ) -> ExecutionStatus: + status = ExecutionStatus.RUNNING + + async def persist_output(output_name: str, output_data: Any) -> None: + await db_client.upsert_execution_output( + node_exec_id=node_exec.node_exec_id, + output_name=output_name, + output_data=output_data, + ) + if exec_update := await db_client.get_node_execution( + node_exec.node_exec_id + ): + await send_async_execution_update(exec_update) + + node_exec_progress.add_output( + ExecutionOutputEntry( + node=node, + node_exec_id=node_exec.node_exec_id, + data=(output_name, output_data), + ) + ) + try: log_metadata.info(f"Start node execution {node_exec.node_exec_id}") await async_update_node_execution_status( @@ -453,56 +520,69 @@ async def _on_node_execution( async for output_name, output_data in execute_node( node=node, - creds_manager=cls.creds_manager, + creds_manager=self.creds_manager, data=node_exec, execution_stats=stats, nodes_input_masks=nodes_input_masks, ): - node_exec_progress.add_output( - ExecutionOutputEntry( - node=node, - node_exec_id=node_exec.node_exec_id, - data=(output_name, output_data), - ) - ) + await persist_output(output_name, output_data) + log_metadata.info(f"Finished node execution {node_exec.node_exec_id}") - except Exception as e: - # Avoid user error being marked as an actual error. + status = ExecutionStatus.COMPLETED + + except BaseException as e: + stats.error = e + if isinstance(e, ValueError): + # Avoid user error being marked as an actual error. log_metadata.info( - f"Failed node execution {node_exec.node_exec_id}: {e}" + f"Expected failure on node execution {node_exec.node_exec_id}: {type(e).__name__} - {e}" ) - else: + status = ExecutionStatus.FAILED + elif isinstance(e, Exception): + # If the exception is not a ValueError, it is unexpected. log_metadata.exception( - f"Failed node execution {node_exec.node_exec_id}: {e}" + f"Unexpected failure on node execution {node_exec.node_exec_id}: {type(e).__name__} - {e}" + ) + status = ExecutionStatus.FAILED + else: + # CancelledError or SystemExit + log_metadata.warning( + f"Interruption error on node execution {node_exec.node_exec_id}: {type(e).__name__}" + ) + status = ExecutionStatus.TERMINATED + + finally: + if status == ExecutionStatus.FAILED and stats.error is not None: + await persist_output( + "error", str(stats.error) or type(stats.error).__name__ ) - if stats is not None: - stats.error = e + return status - @classmethod @func_retry - def on_graph_executor_start(cls): + def on_graph_executor_start(self): configure_logging() set_service_name("GraphExecutor") - cls.pid = os.getpid() - cls.creds_manager = IntegrationCredentialsManager() - cls.node_execution_loop = asyncio.new_event_loop() - cls.node_evaluation_loop = asyncio.new_event_loop() - cls.node_execution_thread = threading.Thread( - target=cls.node_execution_loop.run_forever, daemon=True + self.tid = threading.get_ident() + self.creds_manager = IntegrationCredentialsManager() + self.node_execution_loop = asyncio.new_event_loop() + self.node_evaluation_loop = asyncio.new_event_loop() + self.node_execution_thread = threading.Thread( + target=self.node_execution_loop.run_forever, daemon=True ) - cls.node_evaluation_thread = threading.Thread( - target=cls.node_evaluation_loop.run_forever, daemon=True + self.node_evaluation_thread = threading.Thread( + target=self.node_evaluation_loop.run_forever, daemon=True ) - cls.node_execution_thread.start() - cls.node_evaluation_thread.start() - logger.info(f"[GraphExecutor] {cls.pid} started") + self.node_execution_thread.start() + self.node_evaluation_thread.start() + logger.info(f"[GraphExecutor] {self.tid} started") - @classmethod @error_logged(swallow=False) def on_graph_execution( - cls, graph_exec: GraphExecutionEntry, cancel: threading.Event + self, + graph_exec: GraphExecutionEntry, + cancel: threading.Event, ): log_metadata = LogMetadata( logger=_logger, @@ -535,60 +615,92 @@ def on_graph_execution( log_metadata.info( f"⚙️ Graph execution #{graph_exec.graph_exec_id} is already running, continuing where it left off." ) + elif exec_meta.status == ExecutionStatus.FAILED: + exec_meta.status = ExecutionStatus.RUNNING + log_metadata.info( + f"⚙️ Graph execution #{graph_exec.graph_exec_id} was disturbed, continuing where it left off." + ) + update_graph_execution_state( + db_client=db_client, + graph_exec_id=graph_exec.graph_exec_id, + status=ExecutionStatus.RUNNING, + ) else: log_metadata.warning( f"Skipped graph execution {graph_exec.graph_exec_id}, the graph execution status is `{exec_meta.status}`." ) return - timing_info, (exec_stats, status, error) = cls._on_graph_execution( + if exec_meta.stats is None: + exec_stats = GraphExecutionStats() + else: + exec_stats = exec_meta.stats.to_db() + + timing_info, status = self._on_graph_execution( graph_exec=graph_exec, cancel=cancel, log_metadata=log_metadata, - execution_stats=( - exec_meta.stats.to_db() if exec_meta.stats else GraphExecutionStats() - ), + execution_stats=exec_stats, ) exec_stats.walltime += timing_info.wall_time exec_stats.cputime += timing_info.cpu_time - exec_stats.error = str(error) if error else exec_stats.error - if status not in { - ExecutionStatus.COMPLETED, - ExecutionStatus.TERMINATED, - ExecutionStatus.FAILED, - }: - raise RuntimeError( - f"Graph Execution #{graph_exec.graph_exec_id} ended with unexpected status {status}" - ) + try: + # Failure handling + if isinstance(status, BaseException): + raise status + exec_meta.status = status + + # Activity status handling + activity_status = asyncio.run_coroutine_threadsafe( + generate_activity_status_for_execution( + graph_exec_id=graph_exec.graph_exec_id, + graph_id=graph_exec.graph_id, + graph_version=graph_exec.graph_version, + execution_stats=exec_stats, + db_client=get_db_async_client(), + user_id=graph_exec.user_id, + execution_status=status, + ), + self.node_execution_loop, + ).result(timeout=60.0) + if activity_status is not None: + exec_stats.activity_status = activity_status + log_metadata.info(f"Generated activity status: {activity_status}") + else: + log_metadata.debug( + "Activity status generation disabled, not setting field" + ) - if graph_exec_result := db_client.update_graph_execution_stats( - graph_exec_id=graph_exec.graph_exec_id, - status=status, - stats=exec_stats, - ): - send_execution_update(graph_exec_result) + # Communication handling + self._handle_agent_run_notif(db_client, graph_exec, exec_stats) - cls._handle_agent_run_notif(db_client, graph_exec, exec_stats) + finally: + update_graph_execution_state( + db_client=db_client, + graph_exec_id=graph_exec.graph_exec_id, + status=exec_meta.status, + stats=exec_stats, + ) - @classmethod def _charge_usage( - cls, + self, node_exec: NodeExecutionEntry, execution_count: int, - execution_stats: GraphExecutionStats, - ): + ) -> tuple[int, int]: + total_cost = 0 + remaining_balance = 0 db_client = get_db_client() block = get_block(node_exec.block_id) if not block: logger.error(f"Block {node_exec.block_id} not found.") - return + return total_cost, 0 cost, matching_filter = block_usage_cost( block=block, input_data=node_exec.inputs ) if cost > 0: - db_client.spend_credits( + remaining_balance = db_client.spend_credits( user_id=node_exec.user_id, cost=cost, metadata=UsageTransactionMetadata( @@ -602,11 +714,11 @@ def _charge_usage( reason=f"Ran block {node_exec.block_id} {block.name}", ), ) - execution_stats.cost += cost + total_cost += cost cost, usage_count = execution_usage_cost(execution_count) if cost > 0: - db_client.spend_credits( + remaining_balance = db_client.spend_credits( user_id=node_exec.user_id, cost=cost, metadata=UsageTransactionMetadata( @@ -619,17 +731,18 @@ def _charge_usage( reason=f"Execution Cost for {usage_count} blocks of ex_id:{node_exec.graph_exec_id} g_id:{node_exec.graph_id}", ), ) - execution_stats.cost += cost + total_cost += cost + + return total_cost, remaining_balance - @classmethod @time_measured def _on_graph_execution( - cls, + self, graph_exec: GraphExecutionEntry, cancel: threading.Event, log_metadata: LogMetadata, execution_stats: GraphExecutionStats, - ) -> tuple[GraphExecutionStats, ExecutionStatus, Exception | None]: + ) -> ExecutionStatus: """ Returns: dict: The execution statistics of the graph execution. @@ -639,47 +752,11 @@ def _on_graph_execution( execution_status: ExecutionStatus = ExecutionStatus.RUNNING error: Exception | None = None db_client = get_db_client() - - def on_done_task(node_exec_id: str, result: object): - if not isinstance(result, NodeExecutionStats): - log_metadata.error(f"Unexpected result #{node_exec_id}: {type(result)}") - return - - nonlocal execution_stats - execution_stats.node_count += 1 + result.extra_steps - execution_stats.nodes_cputime += result.cputime - execution_stats.nodes_walltime += result.walltime - execution_stats.cost += result.extra_cost - if (err := result.error) and isinstance(err, Exception): - execution_stats.node_error_count += 1 - update_node_execution_status( - db_client=db_client, - exec_id=node_exec_id, - status=ExecutionStatus.FAILED, - ) - else: - update_node_execution_status( - db_client=db_client, - exec_id=node_exec_id, - status=ExecutionStatus.COMPLETED, - ) - - if _graph_exec := db_client.update_graph_execution_stats( - graph_exec_id=graph_exec.graph_exec_id, - stats=execution_stats, - ): - send_execution_update(_graph_exec) - else: - log_metadata.error( - "Callback for finished node execution " - f"#{node_exec_id} could not update execution stats " - f"for graph execution #{graph_exec.graph_exec_id}; " - f"triggered while graph exec status = {execution_status}" - ) + execution_stats_lock = threading.Lock() # State holders ---------------------------------------------------- running_node_execution: dict[str, NodeExecutionProgress] = defaultdict( - lambda: NodeExecutionProgress(on_done_task=on_done_task) + NodeExecutionProgress ) running_node_evaluation: dict[str, Future] = {} execution_queue = ExecutionQueue[NodeExecutionEntry]() @@ -693,22 +770,42 @@ def on_done_task(node_exec_id: str, result: object): amount=1, ) + # Input moderation + try: + if moderation_error := asyncio.run_coroutine_threadsafe( + automod_manager.moderate_graph_execution_inputs( + db_client=get_db_async_client(), + graph_exec=graph_exec, + ), + self.node_evaluation_loop, + ).result(timeout=30.0): + raise moderation_error + except asyncio.TimeoutError: + log_metadata.warning( + f"Input moderation timed out for graph execution {graph_exec.graph_exec_id}, bypassing moderation and continuing execution" + ) + # Continue execution without moderation + # ------------------------------------------------------------ # Pre‑populate queue --------------------------------------- # ------------------------------------------------------------ for node_exec in db_client.get_node_executions( graph_exec.graph_exec_id, - statuses=[ExecutionStatus.RUNNING, ExecutionStatus.QUEUED], + statuses=[ + ExecutionStatus.RUNNING, + ExecutionStatus.QUEUED, + ExecutionStatus.TERMINATED, + ], ): - execution_queue.add(node_exec.to_node_execution_entry()) + node_entry = node_exec.to_node_execution_entry(graph_exec.user_context) + execution_queue.add(node_entry) # ------------------------------------------------------------ # Main dispatch / polling loop ----------------------------- # ------------------------------------------------------------ while not execution_queue.empty(): if cancel.is_set(): - execution_status = ExecutionStatus.TERMINATED - return execution_stats, execution_status, error + break queued_node_exec = execution_queue.get() @@ -719,12 +816,21 @@ def on_done_task(node_exec_id: str, result: object): # Charge usage (may raise) ------------------------------ try: - cls._charge_usage( + cost, remaining_balance = self._charge_usage( node_exec=queued_node_exec, execution_count=increment_execution_count(graph_exec.user_id), - execution_stats=execution_stats, ) - except InsufficientBalanceError as error: + with execution_stats_lock: + execution_stats.cost += cost + # Check if we crossed the low balance threshold + self._handle_low_balance( + db_client=db_client, + user_id=graph_exec.user_id, + current_balance=remaining_balance, + transaction_cost=cost, + ) + except InsufficientBalanceError as balance_error: + error = balance_error # Set error to trigger FAILED status node_exec_id = queued_node_exec.node_exec_id db_client.upsert_execution_output( node_exec_id=node_exec_id, @@ -736,16 +842,15 @@ def on_done_task(node_exec_id: str, result: object): exec_id=node_exec_id, status=ExecutionStatus.FAILED, ) - execution_status = ExecutionStatus.FAILED - cls._handle_low_balance_notif( + self._handle_insufficient_funds_notif( db_client, graph_exec.user_id, graph_exec.graph_id, - execution_stats, error, ) - raise + # Gracefully stop the execution loop + break # Add input overrides ----------------------------- node_id = queued_node_exec.node_id @@ -756,12 +861,16 @@ def on_done_task(node_exec_id: str, result: object): # Kick off async node execution ------------------------- node_execution_task = asyncio.run_coroutine_threadsafe( - cls.on_node_execution( + self.on_node_execution( node_exec=queued_node_exec, node_exec_progress=running_node_execution[node_id], nodes_input_masks=nodes_input_masks, + graph_stats_pair=( + execution_stats, + execution_stats_lock, + ), ), - cls.node_execution_loop, + self.node_execution_loop, ) running_node_execution[node_id].add_task( node_exec_id=queued_node_exec.node_exec_id, @@ -772,22 +881,26 @@ def on_done_task(node_exec_id: str, result: object): while execution_queue.empty() and ( running_node_execution or running_node_evaluation ): + if cancel.is_set(): + break + # -------------------------------------------------- # Handle inflight evaluations --------------------- # -------------------------------------------------- node_output_found = False for node_id, inflight_exec in list(running_node_execution.items()): if cancel.is_set(): - execution_status = ExecutionStatus.TERMINATED - return execution_stats, execution_status, error + break # node evaluation future ----------------- if inflight_eval := running_node_evaluation.get(node_id): + if not inflight_eval.done(): + continue try: - inflight_eval.result() + inflight_eval.result(timeout=0) running_node_evaluation.pop(node_id) - except TimeoutError: - continue + except Exception as e: + log_metadata.error(f"Node eval #{node_id} failed: {e}") # node execution future --------------------------- if inflight_exec.is_done(): @@ -798,7 +911,7 @@ def on_done_task(node_exec_id: str, result: object): node_output_found = True running_node_evaluation[node_id] = ( asyncio.run_coroutine_threadsafe( - cls._process_node_output( + self._process_node_output( output=output, node_id=node_id, graph_exec=graph_exec, @@ -806,7 +919,7 @@ def on_done_task(node_exec_id: str, result: object): nodes_input_masks=nodes_input_masks, execution_queue=execution_queue, ), - cls.node_evaluation_loop, + self.node_evaluation_loop, ) ) if ( @@ -818,81 +931,124 @@ def on_done_task(node_exec_id: str, result: object): time.sleep(0.1) # loop done -------------------------------------------------- - execution_status = ExecutionStatus.COMPLETED - return execution_stats, execution_status, error - except CancelledError as exc: - execution_status = ExecutionStatus.TERMINATED - error = exc - log_metadata.exception( - f"Cancelled graph execution {graph_exec.graph_exec_id}: {error}" + # Output moderation + try: + if moderation_error := asyncio.run_coroutine_threadsafe( + automod_manager.moderate_graph_execution_outputs( + db_client=get_db_async_client(), + graph_exec_id=graph_exec.graph_exec_id, + user_id=graph_exec.user_id, + graph_id=graph_exec.graph_id, + ), + self.node_evaluation_loop, + ).result(timeout=30.0): + raise moderation_error + except asyncio.TimeoutError: + log_metadata.warning( + f"Output moderation timed out for graph execution {graph_exec.graph_exec_id}, bypassing moderation and continuing execution" + ) + # Continue execution without moderation + + # Determine final execution status based on whether there was an error or termination + if cancel.is_set(): + execution_status = ExecutionStatus.TERMINATED + elif error is not None: + execution_status = ExecutionStatus.FAILED + else: + execution_status = ExecutionStatus.COMPLETED + + if error: + execution_stats.error = str(error) or type(error).__name__ + + return execution_status + + except BaseException as e: + error = ( + e + if isinstance(e, Exception) + else Exception(f"{e.__class__.__name__}: {e}") ) - except Exception as exc: + + known_errors = (InsufficientBalanceError, ModerationError) + if isinstance(error, known_errors): + execution_stats.error = str(error) + return ExecutionStatus.FAILED + execution_status = ExecutionStatus.FAILED - error = exc log_metadata.exception( f"Failed graph execution {graph_exec.graph_exec_id}: {error}" ) - finally: - # Cancel and wait for all node executions to complete - for node_id, inflight_exec in running_node_execution.items(): - if inflight_exec.is_done(): - continue - log_metadata.info(f"Stopping node execution {node_id}") - inflight_exec.stop() - - for node_id, inflight_eval in running_node_evaluation.items(): - if inflight_eval.done(): - continue - log_metadata.info(f"Stopping node evaluation {node_id}") - inflight_eval.cancel() - - for node_id, inflight_exec in running_node_execution.items(): - if inflight_exec.is_done(): - continue - try: - inflight_exec.wait_for_cancellation(timeout=60.0) - except TimeoutError: - log_metadata.exception( - f"Node execution #{node_id} did not stop in time, " - "it may be stuck or taking too long." - ) + raise - for node_id, inflight_eval in running_node_evaluation.items(): - if inflight_eval.done(): - continue - try: - inflight_eval.result(timeout=60.0) - except TimeoutError: - log_metadata.exception( - f"Node evaluation #{node_id} did not stop in time, " - "it may be stuck or taking too long." - ) + finally: + self._cleanup_graph_execution( + execution_queue=execution_queue, + running_node_execution=running_node_execution, + running_node_evaluation=running_node_evaluation, + execution_status=execution_status, + error=error, + graph_exec_id=graph_exec.graph_exec_id, + log_metadata=log_metadata, + db_client=db_client, + ) - if execution_status in [ExecutionStatus.TERMINATED, ExecutionStatus.FAILED]: - inflight_executions = db_client.get_node_executions( - graph_exec.graph_exec_id, - statuses=[ - ExecutionStatus.QUEUED, - ExecutionStatus.RUNNING, - ], - include_exec_data=False, + @error_logged(swallow=True) + def _cleanup_graph_execution( + self, + execution_queue: ExecutionQueue[NodeExecutionEntry], + running_node_execution: dict[str, "NodeExecutionProgress"], + running_node_evaluation: dict[str, Future], + execution_status: ExecutionStatus, + error: Exception | None, + graph_exec_id: str, + log_metadata: LogMetadata, + db_client: "DatabaseManagerClient", + ) -> None: + """ + Clean up running node executions and evaluations when graph execution ends. + This method is decorated with @error_logged(swallow=True) to ensure cleanup + never fails in the finally block. + """ + # Cancel and wait for all node executions to complete + for node_id, inflight_exec in running_node_execution.items(): + if inflight_exec.is_done(): + continue + log_metadata.info(f"Stopping node execution {node_id}") + inflight_exec.stop() + + for node_id, inflight_exec in running_node_execution.items(): + try: + inflight_exec.wait_for_done(timeout=3600.0) + except TimeoutError: + log_metadata.exception( + f"Node execution #{node_id} did not stop in time, " + "it may be stuck or taking too long." ) - db_client.update_node_execution_status_batch( - [node_exec.node_exec_id for node_exec in inflight_executions], - status=execution_status, - stats={"error": str(error)} if error else None, + + # Wait the remaining inflight evaluations to finish + for node_id, inflight_eval in running_node_evaluation.items(): + try: + inflight_eval.result(timeout=3600.0) + except TimeoutError: + log_metadata.exception( + f"Node evaluation #{node_id} did not stop in time, " + "it may be stuck or taking too long." ) - for node_exec in inflight_executions: - node_exec.status = execution_status - send_execution_update(node_exec) - clean_exec_files(graph_exec.graph_exec_id) - return execution_stats, execution_status, error + while queued_execution := execution_queue.get_or_none(): + update_node_execution_status( + db_client=db_client, + exec_id=queued_execution.node_exec_id, + status=execution_status, + stats={"error": str(error)} if error else None, + ) - @classmethod + clean_exec_files(graph_exec_id) + + @async_error_logged(swallow=True) async def _process_node_output( - cls, + self, output: ExecutionOutputEntry, node_id: str, graph_exec: GraphExecutionEntry, @@ -912,44 +1068,23 @@ async def _process_node_output( """ db_client = get_db_async_client() - try: - name, data = output.data - await db_client.upsert_execution_output( - node_exec_id=output.node_exec_id, - output_name=name, - output_data=data, - ) - if exec_update := await db_client.get_node_execution(output.node_exec_id): - await send_async_execution_update(exec_update) + log_metadata.debug(f"Enqueue nodes for {node_id}: {output}") - log_metadata.debug(f"Enqueue nodes for {node_id}: {output}") - for next_execution in await _enqueue_next_nodes( - db_client=db_client, - node=output.node, - output=output.data, - user_id=graph_exec.user_id, - graph_exec_id=graph_exec.graph_exec_id, - graph_id=graph_exec.graph_id, - log_metadata=log_metadata, - nodes_input_masks=nodes_input_masks, - ): - execution_queue.add(next_execution) - except Exception as e: - log_metadata.exception(f"Failed to process node output: {e}") - await db_client.upsert_execution_output( - node_exec_id=output.node_exec_id, - output_name="error", - output_data=str(e), - ) - await async_update_node_execution_status( - db_client=db_client, - exec_id=output.node_exec_id, - status=ExecutionStatus.FAILED, - ) + for next_execution in await _enqueue_next_nodes( + db_client=db_client, + node=output.node, + output=output.data, + user_id=graph_exec.user_id, + graph_exec_id=graph_exec.graph_exec_id, + graph_id=graph_exec.graph_id, + log_metadata=log_metadata, + nodes_input_masks=nodes_input_masks, + user_context=graph_exec.user_context, + ): + execution_queue.add(next_execution) - @classmethod def _handle_agent_run_notif( - cls, + self, db_client: "DatabaseManagerClient", graph_exec: GraphExecutionEntry, exec_stats: GraphExecutionStats, @@ -985,26 +1120,25 @@ def _handle_agent_run_notif( ) ) - @classmethod - def _handle_low_balance_notif( - cls, + def _handle_insufficient_funds_notif( + self, db_client: "DatabaseManagerClient", user_id: str, graph_id: str, - exec_stats: GraphExecutionStats, e: InsufficientBalanceError, ): - shortfall = e.balance - e.amount + shortfall = abs(e.amount) - e.balance metadata = db_client.get_graph_metadata(graph_id) base_url = ( settings.config.frontend_base_url or settings.config.platform_base_url ) + queue_notification( NotificationEventModel( user_id=user_id, - type=NotificationType.LOW_BALANCE, - data=LowBalanceData( - current_balance=exec_stats.cost, + type=NotificationType.ZERO_BALANCE, + data=ZeroBalanceData( + current_balance=e.balance, billing_page_link=f"{base_url}/profile/credits", shortfall=shortfall, agent_name=metadata.name if metadata else "Unknown Agent", @@ -1012,121 +1146,289 @@ def _handle_low_balance_notif( ) ) + try: + user_email = db_client.get_user_email_by_id(user_id) + + alert_message = ( + f"❌ **Insufficient Funds Alert**\n" + f"User: {user_email or user_id}\n" + f"Agent: {metadata.name if metadata else 'Unknown Agent'}\n" + f"Current balance: ${e.balance/100:.2f}\n" + f"Attempted cost: ${abs(e.amount)/100:.2f}\n" + f"Shortfall: ${abs(shortfall)/100:.2f}\n" + f"[View User Details]({base_url}/admin/spending?search={user_email})" + ) + + get_notification_manager_client().discord_system_alert( + alert_message, DiscordChannel.PRODUCT + ) + except Exception as alert_error: + logger.error( + f"Failed to send insufficient funds Discord alert: {alert_error}" + ) + + def _handle_low_balance( + self, + db_client: "DatabaseManagerClient", + user_id: str, + current_balance: int, + transaction_cost: int, + ): + """Check and handle low balance scenarios after a transaction""" + LOW_BALANCE_THRESHOLD = settings.config.low_balance_threshold + + balance_before = current_balance + transaction_cost + + if ( + current_balance < LOW_BALANCE_THRESHOLD + and balance_before >= LOW_BALANCE_THRESHOLD + ): + base_url = ( + settings.config.frontend_base_url or settings.config.platform_base_url + ) + queue_notification( + NotificationEventModel( + user_id=user_id, + type=NotificationType.LOW_BALANCE, + data=LowBalanceData( + current_balance=current_balance, + billing_page_link=f"{base_url}/profile/credits", + ), + ) + ) + + try: + user_email = db_client.get_user_email_by_id(user_id) + alert_message = ( + f"⚠️ **Low Balance Alert**\n" + f"User: {user_email or user_id}\n" + f"Balance dropped below ${LOW_BALANCE_THRESHOLD/100:.2f}\n" + f"Current balance: ${current_balance/100:.2f}\n" + f"Transaction cost: ${transaction_cost/100:.2f}\n" + f"[View User Details]({base_url}/admin/spending?search={user_email})" + ) + get_notification_manager_client().discord_system_alert( + alert_message, DiscordChannel.PRODUCT + ) + except Exception as e: + logger.error(f"Failed to send low balance Discord alert: {e}") + class ExecutionManager(AppProcess): def __init__(self): super().__init__() self.pool_size = settings.config.num_graph_workers - self.running = True self.active_graph_runs: dict[str, tuple[Future, threading.Event]] = {} - def run(self): - pool_size_gauge.set(self.pool_size) - active_runs_gauge.set(0) - utilization_gauge.set(0) + self._executor = None + self._stop_consuming = None - self.metrics_server = threading.Thread( - target=start_http_server, - args=(settings.config.execution_manager_port,), - daemon=True, - ) - self.metrics_server.start() - logger.info(f"[{self.service_name}] Starting execution manager...") - self._run() + self._cancel_thread = None + self._cancel_client = None + self._run_thread = None + self._run_client = None - def _run(self): + @property + def cancel_thread(self) -> threading.Thread: + if self._cancel_thread is None: + self._cancel_thread = threading.Thread( + target=lambda: self._consume_execution_cancel(), + daemon=True, + ) + return self._cancel_thread + + @property + def run_thread(self) -> threading.Thread: + if self._run_thread is None: + self._run_thread = threading.Thread( + target=lambda: self._consume_execution_run(), + daemon=True, + ) + return self._run_thread + + @property + def stop_consuming(self) -> threading.Event: + if self._stop_consuming is None: + self._stop_consuming = threading.Event() + return self._stop_consuming + + @property + def executor(self) -> ThreadPoolExecutor: + if self._executor is None: + self._executor = ThreadPoolExecutor( + max_workers=self.pool_size, + initializer=init_worker, + ) + return self._executor + + @property + def cancel_client(self) -> SyncRabbitMQ: + if self._cancel_client is None: + self._cancel_client = SyncRabbitMQ(create_execution_queue_config()) + return self._cancel_client + + @property + def run_client(self) -> SyncRabbitMQ: + if self._run_client is None: + self._run_client = SyncRabbitMQ(create_execution_queue_config()) + return self._run_client + + def run(self): logger.info(f"[{self.service_name}] ⏳ Spawn max-{self.pool_size} workers...") - self.executor = ProcessPoolExecutor( - max_workers=self.pool_size, - initializer=Executor.on_graph_executor_start, - ) - threading.Thread( - target=lambda: self._consume_execution_cancel(), - daemon=True, - ).start() + pool_size_gauge.set(self.pool_size) + self._update_prompt_metrics() + start_http_server(settings.config.execution_manager_port) + + self.cancel_thread.start() + self.run_thread.start() - self._consume_execution_run() + while True: + time.sleep(1e5) @continuous_retry() def _consume_execution_cancel(self): - cancel_client = SyncRabbitMQ(create_execution_queue_config()) - cancel_client.connect() - cancel_channel = cancel_client.get_channel() - logger.info(f"[{self.service_name}] ⏳ Starting cancel message consumer...") + if self.stop_consuming.is_set() and not self.active_graph_runs: + logger.info( + f"[{self.service_name}] Stop reconnecting cancel consumer since the service is cleaned up." + ) + return + + # Check if channel is closed and force reconnection if needed + if not self.cancel_client.is_ready: + self.cancel_client.disconnect() + self.cancel_client.connect() + cancel_channel = self.cancel_client.get_channel() cancel_channel.basic_consume( queue=GRAPH_EXECUTION_CANCEL_QUEUE_NAME, on_message_callback=self._handle_cancel_message, auto_ack=True, ) + logger.info(f"[{self.service_name}] ⏳ Starting cancel message consumer...") cancel_channel.start_consuming() - raise RuntimeError(f"❌ cancel message consumer is stopped: {cancel_channel}") + if not self.stop_consuming.is_set() or self.active_graph_runs: + raise RuntimeError( + f"[{self.service_name}] ❌ cancel message consumer is stopped: {cancel_channel}" + ) + logger.info( + f"[{self.service_name}] ✅ Cancel message consumer stopped gracefully" + ) @continuous_retry() def _consume_execution_run(self): - run_client = SyncRabbitMQ(create_execution_queue_config()) - run_client.connect() - run_channel = run_client.get_channel() + # Long-running executions are handled by: + # 1. Long consumer timeout (x-consumer-timeout) allows long running agent + # 2. Enhanced connection settings (5 retries, 1s delay) for quick reconnection + # 3. Process monitoring ensures failed executors release messages back to queue + if self.stop_consuming.is_set(): + logger.info( + f"[{self.service_name}] Stop reconnecting execution consumer since the service is cleaned up." + ) + return + + # Check if channel is closed and force reconnection if needed + if not self.run_client.is_ready: + self.run_client.disconnect() + self.run_client.connect() + run_channel = self.run_client.get_channel() run_channel.basic_qos(prefetch_count=self.pool_size) + + # Configure consumer for long-running graph executions + # auto_ack=False: Don't acknowledge messages until execution completes (prevents data loss) run_channel.basic_consume( queue=GRAPH_EXECUTION_QUEUE_NAME, on_message_callback=self._handle_run_message, auto_ack=False, + consumer_tag="graph_execution_consumer", ) + run_channel.confirm_delivery() logger.info(f"[{self.service_name}] ⏳ Starting to consume run messages...") run_channel.start_consuming() - raise RuntimeError(f"❌ run message consumer is stopped: {run_channel}") + if not self.stop_consuming.is_set(): + raise RuntimeError( + f"[{self.service_name}] ❌ run message consumer is stopped: {run_channel}" + ) + logger.info(f"[{self.service_name}] ✅ Run message consumer stopped gracefully") + @error_logged(swallow=True) def _handle_cancel_message( self, - channel: BlockingChannel, - method: Basic.Deliver, - properties: BasicProperties, + _channel: BlockingChannel, + _method: Basic.Deliver, + _properties: BasicProperties, body: bytes, ): """ Called whenever we receive a CANCEL message from the queue. (With auto_ack=True, message is considered 'acked' automatically.) """ - try: - request = CancelExecutionEvent.model_validate_json(body) - graph_exec_id = request.graph_exec_id - if not graph_exec_id: - logger.warning( - f"[{self.service_name}] Cancel message missing 'graph_exec_id'" - ) - return - if graph_exec_id not in self.active_graph_runs: - logger.debug( - f"[{self.service_name}] Cancel received for {graph_exec_id} but not active." - ) - return - - _, cancel_event = self.active_graph_runs[graph_exec_id] - logger.info(f"[{self.service_name}] Received cancel for {graph_exec_id}") - if not cancel_event.is_set(): - cancel_event.set() - else: - logger.debug( - f"[{self.service_name}] Cancel already set for {graph_exec_id}" - ) + request = CancelExecutionEvent.model_validate_json(body) + graph_exec_id = request.graph_exec_id + if not graph_exec_id: + logger.warning( + f"[{self.service_name}] Cancel message missing 'graph_exec_id'" + ) + return + if graph_exec_id not in self.active_graph_runs: + logger.debug( + f"[{self.service_name}] Cancel received for {graph_exec_id} but not active." + ) + return - except Exception as e: - logger.exception(f"Error handling cancel message: {e}") + _, cancel_event = self.active_graph_runs[graph_exec_id] + logger.info(f"[{self.service_name}] Received cancel for {graph_exec_id}") + if not cancel_event.is_set(): + cancel_event.set() + else: + logger.debug( + f"[{self.service_name}] Cancel already set for {graph_exec_id}" + ) def _handle_run_message( self, - channel: BlockingChannel, + _channel: BlockingChannel, method: Basic.Deliver, - properties: BasicProperties, + _properties: BasicProperties, body: bytes, ): delivery_tag = method.delivery_tag + + @func_retry + def _ack_message(reject: bool, requeue: bool): + """Acknowledge or reject the message based on execution status.""" + + # Connection can be lost, so always get a fresh channel + channel = self.run_client.get_channel() + if reject: + channel.connection.add_callback_threadsafe( + lambda: channel.basic_nack(delivery_tag, requeue=requeue) + ) + else: + channel.connection.add_callback_threadsafe( + lambda: channel.basic_ack(delivery_tag) + ) + + # Check if we're shutting down - reject new messages but keep connection alive + if self.stop_consuming.is_set(): + logger.info( + f"[{self.service_name}] Rejecting new execution during shutdown" + ) + _ack_message(reject=True, requeue=True) + return + + # Check if we can accept more runs + self._cleanup_completed_runs() + if len(self.active_graph_runs) >= self.pool_size: + _ack_message(reject=True, requeue=True) + return + try: graph_exec_entry = GraphExecutionEntry.model_validate_json(body) except Exception as e: - logger.error(f"[{self.service_name}] Could not parse run message: {e}") - channel.basic_nack(delivery_tag, requeue=False) + logger.error( + f"[{self.service_name}] Could not parse run message: {e}, body={body}" + ) + _ack_message(reject=True, requeue=False) return graph_exec_id = graph_exec_entry.graph_exec_id @@ -1134,90 +1436,160 @@ def _handle_run_message( f"[{self.service_name}] Received RUN for graph_exec_id={graph_exec_id}" ) if graph_exec_id in self.active_graph_runs: - logger.warning( + # TODO: Make this check cluster-wide, prevent duplicate runs across executor pods. + logger.error( f"[{self.service_name}] Graph {graph_exec_id} already running; rejecting duplicate run." ) - channel.basic_nack(delivery_tag, requeue=False) + _ack_message(reject=True, requeue=False) return - cancel_event = multiprocessing.Manager().Event() - future = self.executor.submit( - Executor.on_graph_execution, graph_exec_entry, cancel_event - ) + cancel_event = threading.Event() + + future = self.executor.submit(execute_graph, graph_exec_entry, cancel_event) self.active_graph_runs[graph_exec_id] = (future, cancel_event) - active_runs_gauge.set(len(self.active_graph_runs)) - utilization_gauge.set(len(self.active_graph_runs) / self.pool_size) + self._update_prompt_metrics() def _on_run_done(f: Future): logger.info(f"[{self.service_name}] Run completed for {graph_exec_id}") try: - self.active_graph_runs.pop(graph_exec_id, None) - active_runs_gauge.set(len(self.active_graph_runs)) - utilization_gauge.set(len(self.active_graph_runs) / self.pool_size) if exec_error := f.exception(): logger.error( - f"[{self.service_name}] Execution for {graph_exec_id} failed: {exec_error}" - ) - channel.connection.add_callback_threadsafe( - lambda: channel.basic_nack(delivery_tag, requeue=True) + f"[{self.service_name}] Execution for {graph_exec_id} failed: {type(exec_error)} {exec_error}" ) + _ack_message(reject=True, requeue=True) else: - channel.connection.add_callback_threadsafe( - lambda: channel.basic_ack(delivery_tag) - ) + _ack_message(reject=False, requeue=False) except BaseException as e: logger.exception( - f"[{self.service_name}] Error acknowledging message: {e}" + f"[{self.service_name}] Error in run completion callback: {e}" ) + finally: + self._cleanup_completed_runs() future.add_done_callback(_on_run_done) - def cleanup(self): - super().cleanup() - self._on_cleanup() + def _cleanup_completed_runs(self) -> list[str]: + """Remove completed futures from active_graph_runs and update metrics""" + completed_runs = [] + for graph_exec_id, (future, _) in self.active_graph_runs.items(): + if future.done(): + completed_runs.append(graph_exec_id) + + for geid in completed_runs: + logger.info(f"[{self.service_name}] ✅ Cleaned up completed run {geid}") + self.active_graph_runs.pop(geid, None) + + self._update_prompt_metrics() + return completed_runs + + def _update_prompt_metrics(self): + active_count = len(self.active_graph_runs) + active_runs_gauge.set(active_count) + if self.stop_consuming.is_set(): + utilization_gauge.set(1.0) + else: + utilization_gauge.set(active_count / self.pool_size) + + def _stop_message_consumers( + self, thread: threading.Thread, client: SyncRabbitMQ, prefix: str + ): + try: + channel = client.get_channel() + channel.connection.add_callback_threadsafe(lambda: channel.stop_consuming()) + + try: + thread.join(timeout=300) + except TimeoutError: + logger.error( + f"{prefix} ⚠️ Run thread did not finish in time, forcing disconnect" + ) + + client.disconnect() + logger.info(f"{prefix} ✅ Run client disconnected") + except Exception as e: + logger.error(f"{prefix} ⚠️ Error disconnecting run client: {type(e)} {e}") - def _on_cleanup(self, log=logger.info): + def cleanup(self): + """Override cleanup to implement graceful shutdown with active execution waiting.""" prefix = f"[{self.service_name}][on_graph_executor_stop {os.getpid()}]" - log(f"{prefix} ⏳ Shutting down service loop...") - self.running = False + logger.info(f"{prefix} 🧹 Starting graceful shutdown...") + + # Signal the consumer thread to stop (thread-safe) + try: + self.stop_consuming.set() + run_channel = self.run_client.get_channel() + run_channel.connection.add_callback_threadsafe( + lambda: run_channel.stop_consuming() + ) + logger.info(f"{prefix} ✅ Exec consumer has been signaled to stop") + except Exception as e: + logger.error(f"{prefix} ⚠️ Error signaling consumer to stop: {type(e)} {e}") + + # Wait for active executions to complete + if self.active_graph_runs: + logger.info( + f"{prefix} ⏳ Waiting for {len(self.active_graph_runs)} active executions to complete..." + ) - log(f"{prefix} ⏳ Shutting down RabbitMQ channel...") - get_execution_queue().get_channel().stop_consuming() + max_wait = GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS + wait_interval = 5 + waited = 0 - if hasattr(self, "executor"): - log(f"{prefix} ⏳ Shutting down GraphExec pool...") + while waited < max_wait: + self._cleanup_completed_runs() + if not self.active_graph_runs: + logger.info(f"{prefix} ✅ All active executions completed") + break + else: + ids = [k.split("-")[0] for k in self.active_graph_runs.keys()] + logger.info( + f"{prefix} ⏳ Still waiting for {len(self.active_graph_runs)} executions: {ids}" + ) + + time.sleep(wait_interval) + waited += wait_interval + + if self.active_graph_runs: + logger.error( + f"{prefix} ⚠️ {len(self.active_graph_runs)} executions still running after {max_wait}s" + ) + else: + logger.info(f"{prefix} ✅ All executions completed gracefully") + + # Shutdown the executor + try: self.executor.shutdown(cancel_futures=True, wait=False) + logger.info(f"{prefix} ✅ Executor shutdown completed") + except Exception as e: + logger.error(f"{prefix} ⚠️ Error during executor shutdown: {type(e)} {e}") - log(f"{prefix} ⏳ Disconnecting Redis...") - redis.disconnect() + # Disconnect the run execution consumer + self._stop_message_consumers( + self.run_thread, + self.run_client, + prefix + " [run-consumer]", + ) + self._stop_message_consumers( + self.cancel_thread, + self.cancel_client, + prefix + " [cancel-consumer]", + ) - log(f"{prefix} ✅ Finished GraphExec cleanup") - sys.exit(0) + logger.info(f"{prefix} ✅ Finished GraphExec cleanup") # ------- UTILITIES ------- # -@thread_cached def get_db_client() -> "DatabaseManagerClient": - from backend.executor import DatabaseManagerClient + return get_database_manager_client() - # Disable health check for the service client to avoid breaking process initializer. - return get_service_client( - DatabaseManagerClient, health_check=False, request_retry=True - ) - -@thread_cached def get_db_async_client() -> "DatabaseManagerAsyncClient": - from backend.executor import DatabaseManagerAsyncClient - - # Disable health check for the service client to avoid breaking process initializer. - return get_service_client( - DatabaseManagerAsyncClient, health_check=False, request_retry=True - ) + return get_database_manager_async_client() +@func_retry async def send_async_execution_update( entry: GraphExecution | NodeExecutionResult | None, ) -> None: @@ -1226,6 +1598,7 @@ async def send_async_execution_update( await get_async_execution_event_bus().publish(entry) +@func_retry def send_execution_update(entry: GraphExecution | NodeExecutionResult | None): if entry is None: return @@ -1262,6 +1635,38 @@ def update_node_execution_status( return exec_update +async def async_update_graph_execution_state( + db_client: "DatabaseManagerAsyncClient", + graph_exec_id: str, + status: ExecutionStatus | None = None, + stats: GraphExecutionStats | None = None, +) -> GraphExecution | None: + """Sets status and fetches+broadcasts the latest state of the graph execution""" + graph_update = await db_client.update_graph_execution_stats( + graph_exec_id, status, stats + ) + if graph_update: + await send_async_execution_update(graph_update) + else: + logger.error(f"Failed to update graph execution stats for {graph_exec_id}") + return graph_update + + +def update_graph_execution_state( + db_client: "DatabaseManagerClient", + graph_exec_id: str, + status: ExecutionStatus | None = None, + stats: GraphExecutionStats | None = None, +) -> GraphExecution | None: + """Sets status and fetches+broadcasts the latest state of the graph execution""" + graph_update = db_client.update_graph_execution_stats(graph_exec_id, status, stats) + if graph_update: + send_execution_update(graph_update) + else: + logger.error(f"Failed to update graph execution stats for {graph_exec_id}") + return graph_update + + @asynccontextmanager async def synchronized(key: str, timeout: int = 60): r = await redis.get_redis_async() @@ -1285,11 +1690,3 @@ def increment_execution_count(user_id: str) -> int: if counter == 1: r.expire(k, settings.config.execution_counter_expiration_time) return counter - - -def llprint(message: str): - """ - Low-level print/log helper function for use in signal handlers. - Regular log/print statements are not allowed in signal handlers. - """ - os.write(sys.stdout.fileno(), (message + "\n").encode()) diff --git a/autogpt_platform/backend/backend/executor/manager_low_balance_test.py b/autogpt_platform/backend/backend/executor/manager_low_balance_test.py new file mode 100644 index 000000000000..d51ffb251185 --- /dev/null +++ b/autogpt_platform/backend/backend/executor/manager_low_balance_test.py @@ -0,0 +1,149 @@ +from unittest.mock import MagicMock, patch + +import pytest +from prisma.enums import NotificationType + +from backend.data.notifications import LowBalanceData +from backend.executor.manager import ExecutionProcessor +from backend.util.test import SpinTestServer + + +@pytest.mark.asyncio(loop_scope="session") +async def test_handle_low_balance_threshold_crossing(server: SpinTestServer): + """Test that _handle_low_balance triggers notification when crossing threshold.""" + + execution_processor = ExecutionProcessor() + user_id = "test-user-123" + current_balance = 400 # $4 - below $5 threshold + transaction_cost = 600 # $6 transaction + + # Mock dependencies + with patch( + "backend.executor.manager.queue_notification" + ) as mock_queue_notif, patch( + "backend.executor.manager.get_notification_manager_client" + ) as mock_get_client, patch( + "backend.executor.manager.settings" + ) as mock_settings: + + # Setup mocks + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_settings.config.low_balance_threshold = 500 # $5 threshold + mock_settings.config.frontend_base_url = "https://test.com" + + # Create mock database client + mock_db_client = MagicMock() + mock_db_client.get_user_email_by_id.return_value = "test@example.com" + + # Test the low balance handler + execution_processor._handle_low_balance( + db_client=mock_db_client, + user_id=user_id, + current_balance=current_balance, + transaction_cost=transaction_cost, + ) + + # Verify notification was queued + mock_queue_notif.assert_called_once() + notification_call = mock_queue_notif.call_args[0][0] + + # Verify notification details + assert notification_call.type == NotificationType.LOW_BALANCE + assert notification_call.user_id == user_id + assert isinstance(notification_call.data, LowBalanceData) + assert notification_call.data.current_balance == current_balance + + # Verify Discord alert was sent + mock_client.discord_system_alert.assert_called_once() + discord_message = mock_client.discord_system_alert.call_args[0][0] + assert "Low Balance Alert" in discord_message + assert "test@example.com" in discord_message + assert "$4.00" in discord_message + assert "$6.00" in discord_message + + +@pytest.mark.asyncio(loop_scope="session") +async def test_handle_low_balance_no_notification_when_not_crossing( + server: SpinTestServer, +): + """Test that no notification is sent when not crossing the threshold.""" + + execution_processor = ExecutionProcessor() + user_id = "test-user-123" + current_balance = 600 # $6 - above $5 threshold + transaction_cost = ( + 100 # $1 transaction (balance before was $7, still above threshold) + ) + + # Mock dependencies + with patch( + "backend.executor.manager.queue_notification" + ) as mock_queue_notif, patch( + "backend.executor.manager.get_notification_manager_client" + ) as mock_get_client, patch( + "backend.executor.manager.settings" + ) as mock_settings: + + # Setup mocks + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_settings.config.low_balance_threshold = 500 # $5 threshold + + # Create mock database client + mock_db_client = MagicMock() + + # Test the low balance handler + execution_processor._handle_low_balance( + db_client=mock_db_client, + user_id=user_id, + current_balance=current_balance, + transaction_cost=transaction_cost, + ) + + # Verify no notification was sent + mock_queue_notif.assert_not_called() + mock_client.discord_system_alert.assert_not_called() + + +@pytest.mark.asyncio(loop_scope="session") +async def test_handle_low_balance_no_duplicate_when_already_below( + server: SpinTestServer, +): + """Test that no notification is sent when already below threshold.""" + + execution_processor = ExecutionProcessor() + user_id = "test-user-123" + current_balance = 300 # $3 - below $5 threshold + transaction_cost = ( + 100 # $1 transaction (balance before was $4, also below threshold) + ) + + # Mock dependencies + with patch( + "backend.executor.manager.queue_notification" + ) as mock_queue_notif, patch( + "backend.executor.manager.get_notification_manager_client" + ) as mock_get_client, patch( + "backend.executor.manager.settings" + ) as mock_settings: + + # Setup mocks + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_settings.config.low_balance_threshold = 500 # $5 threshold + + # Create mock database client + mock_db_client = MagicMock() + + # Test the low balance handler + execution_processor._handle_low_balance( + db_client=mock_db_client, + user_id=user_id, + current_balance=current_balance, + transaction_cost=transaction_cost, + ) + + # Verify no notification was sent (user was already below threshold) + mock_queue_notif.assert_not_called() + mock_client.discord_system_alert.assert_not_called() diff --git a/autogpt_platform/backend/backend/executor/manager_test.py b/autogpt_platform/backend/backend/executor/manager_test.py index 03d3e89011ef..c565eedfbfe3 100644 --- a/autogpt_platform/backend/backend/executor/manager_test.py +++ b/autogpt_platform/backend/backend/executor/manager_test.py @@ -3,7 +3,6 @@ import autogpt_libs.auth.models import fastapi.responses import pytest -from prisma.models import User import backend.server.v2.library.model import backend.server.v2.store.model @@ -12,6 +11,7 @@ from backend.blocks.io import AgentInputBlock from backend.blocks.maths import CalculatorBlock, Operation from backend.data import execution, graph +from backend.data.model import User from backend.server.model import CreateGraph from backend.server.rest_api import AgentServer from backend.usecases.sample import create_test_graph, create_test_user diff --git a/autogpt_platform/backend/backend/executor/scheduler.py b/autogpt_platform/backend/backend/executor/scheduler.py index e20d37e88b25..e476b3ceed99 100644 --- a/autogpt_platform/backend/backend/executor/scheduler.py +++ b/autogpt_platform/backend/backend/executor/scheduler.py @@ -1,17 +1,23 @@ import asyncio import logging import os +import threading from enum import Enum from typing import Optional from urllib.parse import parse_qs, urlencode, urlparse, urlunparse -from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED +from apscheduler.events import ( + EVENT_JOB_ERROR, + EVENT_JOB_EXECUTED, + EVENT_JOB_MAX_INSTANCES, + EVENT_JOB_MISSED, +) from apscheduler.job import Job as JobObj from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore -from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger -from autogpt_libs.utils.cache import thread_cached +from apscheduler.util import ZoneInfo from dotenv import load_dotenv from pydantic import BaseModel, Field, ValidationError from sqlalchemy import MetaData, create_engine @@ -30,7 +36,14 @@ from backend.util.cloud_storage import cleanup_expired_files_async from backend.util.exceptions import NotAuthorizedError, NotFoundError from backend.util.logging import PrefixFilter -from backend.util.service import AppService, AppServiceClient, endpoint_to_async, expose +from backend.util.retry import func_retry +from backend.util.service import ( + AppService, + AppServiceClient, + UnhealthyServiceError, + endpoint_to_async, + expose, +) from backend.util.settings import Config @@ -60,26 +73,69 @@ def _extract_schema_from_url(database_url) -> tuple[str, str]: config = Config() +# Timeout constants +SCHEDULER_OPERATION_TIMEOUT_SECONDS = 300 # 5 minutes for scheduler operations + def job_listener(event): """Logs job execution outcomes for better monitoring.""" if event.exception: - logger.error(f"Job {event.job_id} failed.") + logger.error( + f"Job {event.job_id} failed: {type(event.exception).__name__}: {event.exception}" + ) else: logger.info(f"Job {event.job_id} completed successfully.") -@thread_cached +def job_missed_listener(event): + """Logs when jobs are missed due to scheduling issues.""" + logger.warning( + f"Job {event.job_id} was missed at scheduled time {event.scheduled_run_time}. " + f"This can happen if the scheduler is overloaded or if previous executions are still running." + ) + + +def job_max_instances_listener(event): + """Logs when jobs hit max instances limit.""" + logger.warning( + f"Job {event.job_id} execution was SKIPPED - max instances limit reached. " + f"Previous execution(s) are still running. " + f"Consider increasing max_instances or check why previous executions are taking too long." + ) + + +_event_loop: asyncio.AbstractEventLoop | None = None +_event_loop_thread: threading.Thread | None = None + + +@func_retry def get_event_loop(): - return asyncio.new_event_loop() + """Get the shared event loop.""" + if _event_loop is None: + raise RuntimeError("Event loop not initialized. Scheduler not started.") + return _event_loop + + +def run_async(coro, timeout: float = SCHEDULER_OPERATION_TIMEOUT_SECONDS): + """Run a coroutine in the shared event loop and wait for completion.""" + loop = get_event_loop() + future = asyncio.run_coroutine_threadsafe(coro, loop) + try: + return future.result(timeout=timeout) + except Exception as e: + logger.error(f"Async operation failed: {type(e).__name__}: {e}") + raise def execute_graph(**kwargs): - get_event_loop().run_until_complete(_execute_graph(**kwargs)) + """Execute graph in the shared event loop and wait for completion.""" + # Wait for completion to ensure job doesn't exit prematurely + run_async(_execute_graph(**kwargs)) async def _execute_graph(**kwargs): args = GraphExecutionJobArgs(**kwargs) + start_time = asyncio.get_event_loop().time() try: logger.info(f"Executing recurring job for graph #{args.graph_id}") graph_exec: GraphExecutionWithNodes = await execution_utils.add_graph_execution( @@ -89,16 +145,28 @@ async def _execute_graph(**kwargs): inputs=args.input_data, graph_credentials_inputs=args.input_credentials, ) + elapsed = asyncio.get_event_loop().time() - start_time logger.info( - f"Graph execution started with ID {graph_exec.id} for graph {args.graph_id}" + f"Graph execution started with ID {graph_exec.id} for graph {args.graph_id} " + f"(took {elapsed:.2f}s to create and publish)" ) + if elapsed > 10: + logger.warning( + f"Graph execution {graph_exec.id} took {elapsed:.2f}s to create/publish - " + f"this is unusually slow and may indicate resource contention" + ) except Exception as e: - logger.error(f"Error executing graph {args.graph_id}: {e}") + elapsed = asyncio.get_event_loop().time() - start_time + logger.error( + f"Error executing graph {args.graph_id} after {elapsed:.2f}s: " + f"{type(e).__name__}: {e}" + ) def cleanup_expired_files(): """Clean up expired files from cloud storage.""" - get_event_loop().run_until_complete(cleanup_expired_files_async()) + # Wait for completion + run_async(cleanup_expired_files_async()) # Monitoring functions are now imported from monitoring module @@ -154,7 +222,7 @@ def from_db( class Scheduler(AppService): - scheduler: BlockingScheduler + scheduler: BackgroundScheduler def __init__(self, register_system_tasks: bool = True): self.register_system_tasks = register_system_tasks @@ -167,10 +235,50 @@ def get_port(cls) -> int: def db_pool_size(cls) -> int: return config.scheduler_db_pool_size + async def health_check(self) -> str: + # Thread-safe health check with proper initialization handling + if not hasattr(self, "scheduler"): + raise UnhealthyServiceError("Scheduler is still initializing") + + # Check if we're in the middle of cleanup + if self.cleaned_up: + return await super().health_check() + + # Normal operation - check if scheduler is running + if not self.scheduler.running: + raise UnhealthyServiceError("Scheduler is not running") + + return await super().health_check() + def run_service(self): load_dotenv() + + # Initialize the event loop for async jobs + global _event_loop + _event_loop = asyncio.new_event_loop() + + # Use daemon thread since it should die with the main service + global _event_loop_thread + _event_loop_thread = threading.Thread( + target=_event_loop.run_forever, daemon=True, name="SchedulerEventLoop" + ) + _event_loop_thread.start() + db_schema, db_url = _extract_schema_from_url(os.getenv("DIRECT_URL")) - self.scheduler = BlockingScheduler( + # Configure executors to limit concurrency without skipping jobs + from apscheduler.executors.pool import ThreadPoolExecutor + + self.scheduler = BackgroundScheduler( + executors={ + "default": ThreadPoolExecutor( + max_workers=self.db_pool_size() + ), # Match DB pool size to prevent resource contention + }, + job_defaults={ + "coalesce": True, # Skip redundant missed jobs - just run the latest + "max_instances": 1000, # Effectively unlimited - never drop executions + "misfire_grace_time": None, # No time limit for missed jobs + }, jobstores={ Jobstores.EXECUTION.value: SQLAlchemyJobStore( engine=create_engine( @@ -196,13 +304,15 @@ def run_service(self): Jobstores.WEEKLY_NOTIFICATIONS.value: MemoryJobStore(), }, logger=apscheduler_logger, + timezone=ZoneInfo("UTC"), ) if self.register_system_tasks: # Notification PROCESS WEEKLY SUMMARY + # Runs every Monday at 9 AM UTC self.scheduler.add_job( process_weekly_summary, - CronTrigger.from_crontab("0 * * * *"), + CronTrigger.from_crontab("0 9 * * 1"), id="process_weekly_summary", kwargs={}, replace_existing=True, @@ -250,13 +360,30 @@ def run_service(self): ) self.scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR) + self.scheduler.add_listener(job_missed_listener, EVENT_JOB_MISSED) + self.scheduler.add_listener(job_max_instances_listener, EVENT_JOB_MAX_INSTANCES) self.scheduler.start() + # Keep the service running since BackgroundScheduler doesn't block + super().run_service() + def cleanup(self): super().cleanup() - logger.info("⏳ Shutting down scheduler...") if self.scheduler: - self.scheduler.shutdown(wait=False) + logger.info("⏳ Shutting down scheduler...") + self.scheduler.shutdown(wait=True) + + global _event_loop + if _event_loop: + logger.info("⏳ Closing event loop...") + _event_loop.call_soon_threadsafe(_event_loop.stop) + + global _event_loop_thread + if _event_loop_thread: + logger.info("⏳ Waiting for event loop thread to finish...") + _event_loop_thread.join(timeout=SCHEDULER_OPERATION_TIMEOUT_SECONDS) + + logger.info("Scheduler cleanup complete.") @expose def add_graph_execution_schedule( @@ -269,6 +396,20 @@ def add_graph_execution_schedule( input_credentials: dict[str, CredentialsMetaInput], name: Optional[str] = None, ) -> GraphExecutionJobInfo: + # Validate the graph before scheduling to prevent runtime failures + # We don't need the return value, just want the validation to run + run_async( + execution_utils.validate_and_construct_node_execution_input( + graph_id=graph_id, + user_id=user_id, + graph_inputs=input_data, + graph_version=graph_version, + graph_credentials_inputs=input_credentials, + ) + ) + + logger.info(f"Scheduling job for user {user_id} in UTC (cron: {cron})") + job_args = GraphExecutionJobArgs( user_id=user_id, graph_id=graph_id, @@ -281,12 +422,12 @@ def add_graph_execution_schedule( execute_graph, kwargs=job_args.model_dump(), name=name, - trigger=CronTrigger.from_crontab(cron), + trigger=CronTrigger.from_crontab(cron, timezone="UTC"), jobstore=Jobstores.EXECUTION.value, replace_existing=True, ) logger.info( - f"Added job {job.id} with cron schedule '{cron}' input data: {input_data}" + f"Added job {job.id} with cron schedule '{cron}' in UTC, input data: {input_data}" ) return GraphExecutionJobInfo.from_db(job_args, job) diff --git a/autogpt_platform/backend/backend/executor/scheduler_test.py b/autogpt_platform/backend/backend/executor/scheduler_test.py index f6b2b028c2f6..c4fa35d46c60 100644 --- a/autogpt_platform/backend/backend/executor/scheduler_test.py +++ b/autogpt_platform/backend/backend/executor/scheduler_test.py @@ -1,10 +1,9 @@ import pytest from backend.data import db -from backend.executor.scheduler import SchedulerClient from backend.server.model import CreateGraph from backend.usecases.sample import create_test_graph, create_test_user -from backend.util.service import get_service_client +from backend.util.clients import get_scheduler_client from backend.util.test import SpinTestServer @@ -17,7 +16,7 @@ async def test_agent_schedule(server: SpinTestServer): user_id=test_user.id, ) - scheduler = get_service_client(SchedulerClient) + scheduler = get_scheduler_client() schedules = await scheduler.get_execution_schedules(test_graph.id, test_user.id) assert len(schedules) == 0 diff --git a/autogpt_platform/backend/backend/executor/utils.py b/autogpt_platform/backend/backend/executor/utils.py index e27c43d63f2d..dd4b94a60c37 100644 --- a/autogpt_platform/backend/backend/executor/utils.py +++ b/autogpt_platform/backend/backend/executor/utils.py @@ -1,53 +1,61 @@ import asyncio import logging +import threading import time from collections import defaultdict from concurrent.futures import Future -from typing import TYPE_CHECKING, Any, Callable, Optional, cast +from typing import Any, Optional -from autogpt_libs.utils.cache import thread_cached -from pydantic import BaseModel, JsonValue +from pydantic import BaseModel, JsonValue, ValidationError from backend.data import execution as execution_db from backend.data import graph as graph_db -from backend.data.block import ( - Block, - BlockData, - BlockInput, - BlockSchema, - BlockType, - get_block, -) +from backend.data.block import Block, BlockData, BlockInput, BlockType, get_block from backend.data.block_cost_config import BLOCK_COSTS from backend.data.cost import BlockCostType from backend.data.db import prisma from backend.data.execution import ( - AsyncRedisExecutionEventBus, ExecutionStatus, GraphExecutionStats, GraphExecutionWithNodes, - RedisExecutionEventBus, + UserContext, ) from backend.data.graph import GraphModel, Node from backend.data.model import CredentialsMetaInput -from backend.data.rabbitmq import ( - AsyncRabbitMQ, - Exchange, - ExchangeType, - Queue, - RabbitMQConfig, - SyncRabbitMQ, +from backend.data.rabbitmq import Exchange, ExchangeType, Queue, RabbitMQConfig +from backend.data.user import get_user_by_id +from backend.util.clients import ( + get_async_execution_event_bus, + get_async_execution_queue, + get_database_manager_async_client, + get_integration_credentials_store, ) -from backend.util.exceptions import NotFoundError +from backend.util.exceptions import GraphValidationError, NotFoundError from backend.util.logging import TruncatedLogger from backend.util.mock import MockObject -from backend.util.service import get_service_client from backend.util.settings import Config from backend.util.type import convert -if TYPE_CHECKING: - from backend.executor import DatabaseManagerAsyncClient, DatabaseManagerClient - from backend.integrations.credentials_store import IntegrationCredentialsStore + +async def get_user_context(user_id: str) -> UserContext: + """ + Get UserContext for a user, always returns a valid context with timezone. + Defaults to UTC if user has no timezone set. + """ + user_context = UserContext(timezone="UTC") # Default to UTC + try: + user = await get_user_by_id(user_id) + if user and user.timezone and user.timezone != "not-set": + user_context.timezone = user.timezone + logger.debug(f"Retrieved user context: timezone={user.timezone}") + else: + logger.debug("User has no timezone set, using UTC") + except Exception as e: + logger.warning(f"Could not fetch user timezone: {e}") + # Continue with UTC as default + + return user_context + config = Config() logger = TruncatedLogger(logging.getLogger(__name__), prefix="[GraphExecutorUtil]") @@ -85,51 +93,6 @@ def __init__( ) -@thread_cached -def get_execution_event_bus() -> RedisExecutionEventBus: - return RedisExecutionEventBus() - - -@thread_cached -def get_async_execution_event_bus() -> AsyncRedisExecutionEventBus: - return AsyncRedisExecutionEventBus() - - -@thread_cached -def get_execution_queue() -> SyncRabbitMQ: - client = SyncRabbitMQ(create_execution_queue_config()) - client.connect() - return client - - -@thread_cached -async def get_async_execution_queue() -> AsyncRabbitMQ: - client = AsyncRabbitMQ(create_execution_queue_config()) - await client.connect() - return client - - -@thread_cached -def get_integration_credentials_store() -> "IntegrationCredentialsStore": - from backend.integrations.credentials_store import IntegrationCredentialsStore - - return IntegrationCredentialsStore() - - -@thread_cached -def get_db_client() -> "DatabaseManagerClient": - from backend.executor import DatabaseManagerClient - - return get_service_client(DatabaseManagerClient) - - -@thread_cached -def get_db_async_client() -> "DatabaseManagerAsyncClient": - from backend.executor import DatabaseManagerAsyncClient - - return get_service_client(DatabaseManagerAsyncClient) - - # ============ Execution Cost Helpers ============ # @@ -456,7 +419,7 @@ def validate_exec( # Last validation: Validate the input values against the schema. if error := schema.get_mismatch_error(data): error_message = f"{error_prefix} {error}" - logger.error(error_message) + logger.warning(error_message) return None, error_message return data, node_block.name @@ -466,47 +429,65 @@ async def _validate_node_input_credentials( graph: GraphModel, user_id: str, nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, -): - """Checks all credentials for all nodes of the graph""" +) -> dict[str, dict[str, str]]: + """ + Checks all credentials for all nodes of the graph and returns structured errors. + + Returns: + dict[node_id, dict[field_name, error_message]]: Credential validation errors per node + """ + credential_errors: dict[str, dict[str, str]] = defaultdict(dict) for node in graph.nodes: block = node.block # Find any fields of type CredentialsMetaInput - credentials_fields = cast( - type[BlockSchema], block.input_schema - ).get_credentials_fields() + credentials_fields = block.input_schema.get_credentials_fields() if not credentials_fields: continue for field_name, credentials_meta_type in credentials_fields.items(): - if ( - nodes_input_masks - and (node_input_mask := nodes_input_masks.get(node.id)) - and field_name in node_input_mask - ): - credentials_meta = credentials_meta_type.model_validate( - node_input_mask[field_name] - ) - elif field_name in node.input_default: - credentials_meta = credentials_meta_type.model_validate( - node.input_default[field_name] - ) - else: - raise ValueError( - f"Credentials absent for {block.name} node #{node.id} " - f"input '{field_name}'" + try: + if ( + nodes_input_masks + and (node_input_mask := nodes_input_masks.get(node.id)) + and field_name in node_input_mask + ): + credentials_meta = credentials_meta_type.model_validate( + node_input_mask[field_name] + ) + elif field_name in node.input_default: + credentials_meta = credentials_meta_type.model_validate( + node.input_default[field_name] + ) + else: + # Missing credentials + credential_errors[node.id][ + field_name + ] = "These credentials are required" + continue + except ValidationError as e: + credential_errors[node.id][field_name] = f"Invalid credentials: {e}" + continue + + try: + # Fetch the corresponding Credentials and perform sanity checks + credentials = await get_integration_credentials_store().get_creds_by_id( + user_id, credentials_meta.id ) + except Exception as e: + # Handle any errors fetching credentials + credential_errors[node.id][ + field_name + ] = f"Credentials not available: {e}" + continue - # Fetch the corresponding Credentials and perform sanity checks - credentials = await get_integration_credentials_store().get_creds_by_id( - user_id, credentials_meta.id - ) if not credentials: - raise ValueError( - f"Unknown credentials #{credentials_meta.id} " - f"for node #{node.id} input '{field_name}'" - ) + credential_errors[node.id][ + field_name + ] = f"Unknown credentials #{credentials_meta.id}" + continue + if ( credentials.provider != credentials_meta.provider or credentials.type != credentials_meta.type @@ -517,10 +498,12 @@ async def _validate_node_input_credentials( f"{credentials_meta.type}<>{credentials.type};" f"{credentials_meta.provider}<>{credentials.provider}" ) - raise ValueError( - f"Invalid credentials #{credentials.id} for node #{node.id}: " - "type/provider mismatch" - ) + credential_errors[node.id][ + field_name + ] = "Invalid credentials: type/provider mismatch" + continue + + return credential_errors def make_node_credentials_input_map( @@ -558,7 +541,37 @@ def make_node_credentials_input_map( return result -async def construct_node_execution_input( +async def validate_graph_with_credentials( + graph: GraphModel, + user_id: str, + nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, +) -> dict[str, dict[str, str]]: + """ + Validate graph including credentials and return structured errors per node. + + Returns: + dict[node_id, dict[field_name, error_message]]: Validation errors per node + """ + # Get input validation errors + node_input_errors = GraphModel.validate_graph_get_errors( + graph, for_run=True, nodes_input_masks=nodes_input_masks + ) + + # Get credential input/availability/validation errors + node_credential_input_errors = await _validate_node_input_credentials( + graph, user_id, nodes_input_masks + ) + + # Merge credential errors with structural errors + for node_id, field_errors in node_credential_input_errors.items(): + if node_id not in node_input_errors: + node_input_errors[node_id] = {} + node_input_errors[node_id].update(field_errors) + + return node_input_errors + + +async def _construct_starting_node_execution_input( graph: GraphModel, user_id: str, graph_inputs: BlockInput, @@ -580,8 +593,17 @@ async def construct_node_execution_input( list[tuple[str, BlockInput]]: A list of tuples, each containing the node ID and the corresponding input data for that node. """ - graph.validate_graph(for_run=True, nodes_input_masks=nodes_input_masks) - await _validate_node_input_credentials(graph, user_id, nodes_input_masks) + # Use new validation function that includes credentials + validation_errors = await validate_graph_with_credentials( + graph, user_id, nodes_input_masks + ) + n_error_nodes = len(validation_errors) + n_errors = sum(len(errors) for errors in validation_errors.values()) + if validation_errors: + raise GraphValidationError( + f"Graph validation failed: {n_errors} issues on {n_error_nodes} nodes", + node_errors=validation_errors, + ) nodes_input = [] for node in graph.starting_nodes: @@ -616,6 +638,67 @@ async def construct_node_execution_input( return nodes_input +async def validate_and_construct_node_execution_input( + graph_id: str, + user_id: str, + graph_inputs: BlockInput, + graph_version: Optional[int] = None, + graph_credentials_inputs: Optional[dict[str, CredentialsMetaInput]] = None, + nodes_input_masks: Optional[dict[str, dict[str, JsonValue]]] = None, +) -> tuple[GraphModel, list[tuple[str, BlockInput]], dict[str, dict[str, JsonValue]]]: + """ + Public wrapper that handles graph fetching, credential mapping, and validation+construction. + This centralizes the logic used by both scheduler validation and actual execution. + + Args: + graph_id: The ID of the graph to validate/construct. + user_id: The ID of the user. + graph_inputs: The input data for the graph execution. + graph_version: The version of the graph to use. + graph_credentials_inputs: Credentials inputs to use. + nodes_input_masks: Node inputs to use. + + Returns: + tuple[GraphModel, list[tuple[str, BlockInput]]]: Graph model and list of tuples for node execution input. + + Raises: + NotFoundError: If the graph is not found. + GraphValidationError: If the graph has validation issues. + ValueError: If there are other validation errors. + """ + if prisma.is_connected(): + gdb = graph_db + else: + gdb = get_database_manager_async_client() + + graph: GraphModel | None = await gdb.get_graph( + graph_id=graph_id, + user_id=user_id, + version=graph_version, + include_subgraphs=True, + ) + if not graph: + raise NotFoundError(f"Graph #{graph_id} not found.") + + nodes_input_masks = _merge_nodes_input_masks( + ( + make_node_credentials_input_map(graph, graph_credentials_inputs) + if graph_credentials_inputs + else {} + ), + nodes_input_masks or {}, + ) + + starting_nodes_input = await _construct_starting_node_execution_input( + graph=graph, + user_id=user_id, + graph_inputs=graph_inputs, + nodes_input_masks=nodes_input_masks, + ) + + return graph, starting_nodes_input, nodes_input_masks + + def _merge_nodes_input_masks( overrides_map_1: dict[str, dict[str, JsonValue]], overrides_map_2: dict[str, dict[str, JsonValue]], @@ -632,11 +715,6 @@ def _merge_nodes_input_masks( # ============ Execution Queue Helpers ============ # - -class CancelExecutionEvent(BaseModel): - graph_exec_id: str - - GRAPH_EXECUTION_EXCHANGE = Exchange( name="graph_execution", type=ExchangeType.DIRECT, @@ -654,6 +732,11 @@ class CancelExecutionEvent(BaseModel): ) GRAPH_EXECUTION_CANCEL_QUEUE_NAME = "graph_execution_cancel_queue" +# Graceful shutdown timeout constants +# Agent executions can run for up to 1 day, so we need a graceful shutdown period +# that allows long-running executions to complete naturally +GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS = 24 * 60 * 60 # 1 day to complete active executions + def create_execution_queue_config() -> RabbitMQConfig: """ @@ -667,6 +750,16 @@ def create_execution_queue_config() -> RabbitMQConfig: routing_key=GRAPH_EXECUTION_ROUTING_KEY, durable=True, auto_delete=False, + arguments={ + # x-consumer-timeout (1 week) + # Problem: Default 30-minute consumer timeout kills long-running graph executions + # Original error: "Consumer acknowledgement timed out after 1800000 ms (30 minutes)" + # Solution: Disable consumer timeout entirely - let graphs run indefinitely + # Safety: Heartbeat mechanism now handles dead consumer detection instead + # Use case: Graph executions that take hours to complete (AI model training, etc.) + "x-consumer-timeout": GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS + * 1000, + }, ) cancel_queue = Queue( name=GRAPH_EXECUTION_CANCEL_QUEUE_NAME, @@ -682,6 +775,10 @@ def create_execution_queue_config() -> RabbitMQConfig: ) +class CancelExecutionEvent(BaseModel): + graph_exec_id: str + + async def stop_graph_execution( user_id: str, graph_exec_id: str, @@ -695,7 +792,7 @@ async def stop_graph_execution( 3. Update execution statuses in DB and set `error` outputs to `"TERMINATED"`. """ queue_client = await get_async_execution_queue() - db = execution_db if prisma.is_connected() else get_db_async_client() + db = execution_db if prisma.is_connected() else get_database_manager_async_client() await queue_client.publish_message( routing_key="", message=CancelExecutionEvent(graph_exec_id=graph_exec_id).model_dump_json(), @@ -727,51 +824,28 @@ async def stop_graph_execution( ExecutionStatus.QUEUED, ExecutionStatus.INCOMPLETE, ]: - break + # If the graph is still on the queue, we can prevent them from being executed + # by setting the status to TERMINATED. + graph_exec.status = ExecutionStatus.TERMINATED + + await asyncio.gather( + # Update graph execution status + db.update_graph_execution_stats( + graph_exec_id=graph_exec.id, + status=ExecutionStatus.TERMINATED, + ), + # Publish graph execution event + get_async_execution_event_bus().publish(graph_exec), + ) + return if graph_exec.status == ExecutionStatus.RUNNING: await asyncio.sleep(0.1) - # Set the termination status if the graph is not stopped after the timeout. - if graph_exec := await db.get_graph_execution_meta( - execution_id=graph_exec_id, user_id=user_id - ): - # If the graph is still on the queue, we can prevent them from being executed - # by setting the status to TERMINATED. - node_execs = await db.get_node_executions( - graph_exec_id=graph_exec_id, - statuses=[ - ExecutionStatus.QUEUED, - ExecutionStatus.RUNNING, - ], - include_exec_data=False, - ) - - graph_exec.status = ExecutionStatus.TERMINATED - for node_exec in node_execs: - node_exec.status = ExecutionStatus.TERMINATED - - await asyncio.gather( - # Update node execution statuses - db.update_node_execution_status_batch( - [node_exec.node_exec_id for node_exec in node_execs], - ExecutionStatus.TERMINATED, - ), - # Publish node execution events - *[ - get_async_execution_event_bus().publish(node_exec) - for node_exec in node_execs - ], - ) - await asyncio.gather( - # Update graph execution status - db.update_graph_execution_stats( - graph_exec_id=graph_exec_id, - status=ExecutionStatus.TERMINATED, - ), - # Publish graph execution event - get_async_execution_event_bus().publish(graph_exec), - ) + raise TimeoutError( + f"Graph execution #{graph_exec_id} will need to take longer than {wait_timeout} seconds to stop. " + f"You can check the status of the execution in the UI or try again later." + ) async def add_graph_execution( @@ -801,61 +875,65 @@ async def add_graph_execution( ValueError: If the graph is not found or if there are validation errors. """ if prisma.is_connected(): - gdb = graph_db edb = execution_db else: - gdb = get_db_async_client() - edb = get_db_async_client() - - graph: GraphModel | None = await gdb.get_graph( - graph_id=graph_id, - user_id=user_id, - version=graph_version, - include_subgraphs=True, + edb = get_database_manager_async_client() + + graph, starting_nodes_input, nodes_input_masks = ( + await validate_and_construct_node_execution_input( + graph_id=graph_id, + user_id=user_id, + graph_inputs=inputs or {}, + graph_version=graph_version, + graph_credentials_inputs=graph_credentials_inputs, + nodes_input_masks=nodes_input_masks, + ) ) - if not graph: - raise NotFoundError(f"Graph #{graph_id} not found.") + graph_exec = None - nodes_input_masks = _merge_nodes_input_masks( - ( - make_node_credentials_input_map(graph, graph_credentials_inputs) - if graph_credentials_inputs - else {} - ), - nodes_input_masks or {}, - ) - starting_nodes_input = await construct_node_execution_input( - graph=graph, - user_id=user_id, - graph_inputs=inputs or {}, - nodes_input_masks=nodes_input_masks, - ) + try: + graph_exec = await edb.create_graph_execution( + user_id=user_id, + graph_id=graph_id, + graph_version=graph.version, + starting_nodes_input=starting_nodes_input, + preset_id=preset_id, + ) - graph_exec = await edb.create_graph_execution( - user_id=user_id, - graph_id=graph_id, - graph_version=graph.version, - starting_nodes_input=starting_nodes_input, - preset_id=preset_id, - ) + # Fetch user context for the graph execution + user_context = await get_user_context(user_id) - try: queue = await get_async_execution_queue() - graph_exec_entry = graph_exec.to_graph_execution_entry() + graph_exec_entry = graph_exec.to_graph_execution_entry(user_context) if nodes_input_masks: graph_exec_entry.nodes_input_masks = nodes_input_masks + + logger.info( + f"Created graph execution #{graph_exec.id} for graph " + f"#{graph_id} with {len(starting_nodes_input)} starting nodes. " + f"Now publishing to execution queue." + ) + await queue.publish_message( routing_key=GRAPH_EXECUTION_ROUTING_KEY, message=graph_exec_entry.model_dump_json(), exchange=GRAPH_EXECUTION_EXCHANGE, ) + logger.info(f"Published execution {graph_exec.id} to RabbitMQ queue") bus = get_async_execution_event_bus() await bus.publish(graph_exec) return graph_exec - except Exception as e: - logger.error(f"Unable to publish graph #{graph_id} exec #{graph_exec.id}: {e}") + except BaseException as e: + err = str(e) or type(e).__name__ + if not graph_exec: + logger.error(f"Unable to execute graph #{graph_id} failed: {err}") + raise + + logger.error( + f"Unable to publish graph #{graph_id} exec #{graph_exec.id}: {err}" + ) await edb.update_node_execution_status_batch( [node_exec.node_exec_id for node_exec in graph_exec.node_executions], ExecutionStatus.FAILED, @@ -863,7 +941,7 @@ async def add_graph_execution( await edb.update_graph_execution_stats( graph_exec_id=graph_exec.id, status=ExecutionStatus.FAILED, - stats=GraphExecutionStats(error=str(e)), + stats=GraphExecutionStats(error=err), ) raise @@ -878,19 +956,17 @@ class ExecutionOutputEntry(BaseModel): class NodeExecutionProgress: - def __init__( - self, - on_done_task: Callable[[str, object], None], - ): + def __init__(self): self.output: dict[str, list[ExecutionOutputEntry]] = defaultdict(list) self.tasks: dict[str, Future] = {} - self.on_done_task = on_done_task + self._lock = threading.Lock() def add_task(self, node_exec_id: str, task: Future): self.tasks[node_exec_id] = task def add_output(self, output: ExecutionOutputEntry): - self.output[output.node_exec_id].append(output) + with self._lock: + self.output[output.node_exec_id].append(output) def pop_output(self) -> ExecutionOutputEntry | None: exec_id = self._next_exec() @@ -900,8 +976,9 @@ def pop_output(self) -> ExecutionOutputEntry | None: if self._pop_done_task(exec_id): return self.pop_output() - if next_output := self.output[exec_id]: - return next_output.pop(0) + with self._lock: + if next_output := self.output[exec_id]: + return next_output.pop(0) return None @@ -921,7 +998,9 @@ def is_done(self, wait_time: float = 0.0) -> bool: except TimeoutError: pass except Exception as e: - logger.error(f"Task for exec ID {exec_id} failed with error: {str(e)}") + logger.error( + f"Task for exec ID {exec_id} failed with error: {e.__class__.__name__} {str(e)}" + ) pass return self.is_done(0) @@ -939,7 +1018,7 @@ def stop(self) -> list[str]: cancelled_ids.append(task_id) return cancelled_ids - def wait_for_cancellation(self, timeout: float = 5.0): + def wait_for_done(self, timeout: float = 5.0): """ Wait for all cancelled tasks to complete cancellation. @@ -949,9 +1028,12 @@ def wait_for_cancellation(self, timeout: float = 5.0): start_time = time.time() while time.time() - start_time < timeout: - # Check if all tasks are done (either completed or cancelled) - if all(task.done() for task in self.tasks.values()): - return True + while self.pop_output(): + pass + + if self.is_done(): + return + time.sleep(0.1) # Small delay to avoid busy waiting raise TimeoutError( @@ -966,14 +1048,11 @@ def _pop_done_task(self, exec_id: str) -> bool: if not task.done(): return False - if self.output[exec_id]: - return False + with self._lock: + if self.output[exec_id]: + return False - if task := self.tasks.pop(exec_id): - try: - self.on_done_task(exec_id, task.result()) - except Exception as e: - logger.error(f"Task for exec ID {exec_id} failed with error: {str(e)}") + self.tasks.pop(exec_id) return True def _next_exec(self) -> str | None: diff --git a/autogpt_platform/backend/backend/integrations/ayrshare.py b/autogpt_platform/backend/backend/integrations/ayrshare.py new file mode 100644 index 000000000000..42e069b4aca5 --- /dev/null +++ b/autogpt_platform/backend/backend/integrations/ayrshare.py @@ -0,0 +1,515 @@ +from __future__ import annotations + +import json +import logging +from enum import Enum +from typing import Any, Optional + +from pydantic import BaseModel + +from backend.util.exceptions import MissingConfigError +from backend.util.request import Requests +from backend.util.settings import Settings + +logger = logging.getLogger(__name__) + +settings = Settings() + + +class AyrshareAPIException(Exception): + def __init__(self, message: str, status_code: int): + super().__init__(message) + self.status_code = status_code + + +class SocialPlatform(str, Enum): + BLUESKY = "bluesky" + FACEBOOK = "facebook" + TWITTER = "twitter" + LINKEDIN = "linkedin" + INSTAGRAM = "instagram" + YOUTUBE = "youtube" + REDDIT = "reddit" + TELEGRAM = "telegram" + GOOGLE_MY_BUSINESS = "gmb" + PINTEREST = "pinterest" + TIKTOK = "tiktok" + SNAPCHAT = "snapchat" + THREADS = "threads" + + +class EmailConfig(BaseModel): + to: str + subject: Optional[str] = None + body: Optional[str] = None + from_name: Optional[str] = None + from_email: Optional[str] = None + + +class JWTResponse(BaseModel): + status: str + title: str + token: str + url: str + emailSent: Optional[bool] = None + expiresIn: Optional[str] = None + + +class ProfileResponse(BaseModel): + status: str + title: str + refId: str + profileKey: str + messagingActive: Optional[bool] = None + + +class PostResponse(BaseModel): + status: str + id: str + refId: str + profileTitle: str + post: str + postIds: Optional[list[PostIds]] = None + scheduleDate: Optional[str] = None + errors: Optional[list[str]] = None + + +class PostIds(BaseModel): + status: str + id: str + postUrl: str + platform: str + + +class AutoHashtag(BaseModel): + max: Optional[int] = None + position: Optional[str] = None + + +class FirstComment(BaseModel): + text: str + platforms: Optional[list[SocialPlatform]] = None + + +class AutoSchedule(BaseModel): + interval: str + platforms: Optional[list[SocialPlatform]] = None + startDate: Optional[str] = None + endDate: Optional[str] = None + + +class AutoRepost(BaseModel): + interval: str + platforms: Optional[list[SocialPlatform]] = None + startDate: Optional[str] = None + endDate: Optional[str] = None + + +class AyrshareClient: + """Client for the Ayrshare Social Media Post API""" + + API_URL = "https://api.ayrshare.com/api" + POST_ENDPOINT = f"{API_URL}/post" + PROFILES_ENDPOINT = f"{API_URL}/profiles" + JWT_ENDPOINT = f"{PROFILES_ENDPOINT}/generateJWT" + + def __init__( + self, + custom_requests: Optional[Requests] = None, + ): + if not settings.secrets.ayrshare_api_key: + raise MissingConfigError("AYRSHARE_API_KEY is not configured") + + headers: dict[str, str] = { + "Content-Type": "application/json", + "Authorization": f"Bearer {settings.secrets.ayrshare_api_key}", + } + self.headers = headers + + if custom_requests: + self._requests = custom_requests + else: + self._requests = Requests( + extra_headers=headers, + trusted_origins=["https://api.ayrshare.com"], + ) + + async def generate_jwt( + self, + private_key: str, + profile_key: str, + logout: Optional[bool] = None, + redirect: Optional[str] = None, + allowed_social: Optional[list[SocialPlatform]] = None, + verify: Optional[bool] = None, + base64: Optional[bool] = None, + expires_in: Optional[int] = None, + email: Optional[EmailConfig] = None, + ) -> JWTResponse: + """ + Generate a JSON Web Token (JWT) for use with single sign on. + + Docs: https://www.ayrshare.com/docs/apis/profiles/generate-jwt-overview + + Args: + domain: Domain of app. Must match the domain given during onboarding. + private_key: Private Key used for encryption. + profile_key: User Profile Key (not the API Key). + logout: Automatically logout the current session. + redirect: URL to redirect to when the "Done" button or logo is clicked. + allowed_social: List of social networks to display in the linking page. + verify: Verify that the generated token is valid (recommended for non-production). + base64: Whether the private key is base64 encoded. + expires_in: Token longevity in minutes (1-2880). + email: Configuration for sending Connect Accounts email. + + Returns: + JWTResponse object containing the JWT token and URL. + + Raises: + AyrshareAPIException: If the API request fails or private key is invalid. + """ + payload: dict[str, Any] = { + "domain": "id-pojeg", + "privateKey": private_key, + "profileKey": profile_key, + } + + headers = self.headers + headers["Profile-Key"] = profile_key + if logout is not None: + payload["logout"] = logout + if redirect is not None: + payload["redirect"] = redirect + if allowed_social is not None: + payload["allowedSocial"] = [p.value for p in allowed_social] + if verify is not None: + payload["verify"] = verify + if base64 is not None: + payload["base64"] = base64 + if expires_in is not None: + payload["expiresIn"] = expires_in + if email is not None: + payload["email"] = email.model_dump(exclude_none=True) + + response = await self._requests.post( + self.JWT_ENDPOINT, json=payload, headers=headers + ) + + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get("message", "Unknown error") + except json.JSONDecodeError: + error_message = response.text() + + raise AyrshareAPIException( + f"Ayrshare API request failed ({response.status}): {error_message}", + response.status, + ) + + response_data = response.json() + if response_data.get("status") != "success": + raise AyrshareAPIException( + f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}", + response.status, + ) + + return JWTResponse(**response_data) + + async def create_profile( + self, + title: str, + messaging_active: Optional[bool] = None, + hide_top_header: Optional[bool] = None, + top_header: Optional[str] = None, + disable_social: Optional[list[SocialPlatform]] = None, + team: Optional[bool] = None, + email: Optional[str] = None, + sub_header: Optional[str] = None, + tags: Optional[list[str]] = None, + ) -> ProfileResponse: + """ + Create a new User Profile under your Primary Profile. + + Docs: https://www.ayrshare.com/docs/apis/profiles/create-profile + + Args: + title: Title of the new profile. Must be unique. + messaging_active: Set to true to activate messaging for this user profile. + hide_top_header: Hide the top header on the social accounts linkage page. + top_header: Change the header on the social accounts linkage page. + disable_social: Array of social networks that are disabled for this user's profile. + team: Create a new user profile as a team member. + email: Email address for team member invite (required if team is true). + sub_header: Change the sub header on the social accounts linkage page. + tags: Array of strings to tag user profiles. + + Returns: + ProfileResponse object containing the profile details and profile key. + + Raises: + AyrshareAPIException: If the API request fails or profile title already exists. + """ + payload: dict[str, Any] = { + "title": title, + } + + if messaging_active is not None: + payload["messagingActive"] = messaging_active + if hide_top_header is not None: + payload["hideTopHeader"] = hide_top_header + if top_header is not None: + payload["topHeader"] = top_header + if disable_social is not None: + payload["disableSocial"] = [p.value for p in disable_social] + if team is not None: + payload["team"] = team + if email is not None: + payload["email"] = email + if sub_header is not None: + payload["subHeader"] = sub_header + if tags is not None: + payload["tags"] = tags + + response = await self._requests.post(self.PROFILES_ENDPOINT, json=payload) + + if not response.ok: + try: + error_data = response.json() + error_message = error_data.get("message", "Unknown error") + except json.JSONDecodeError: + error_message = response.text() + + raise AyrshareAPIException( + f"Ayrshare API request failed ({response.status}): {error_message}", + response.status, + ) + + response_data = response.json() + if response_data.get("status") != "success": + raise AyrshareAPIException( + f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}", + response.status, + ) + + return ProfileResponse(**response_data) + + async def create_post( + self, + post: str, + platforms: list[SocialPlatform], + *, + media_urls: Optional[list[str]] = None, + is_video: Optional[bool] = None, + schedule_date: Optional[str] = None, + validate_schedule: Optional[bool] = None, + first_comment: Optional[FirstComment] = None, + disable_comments: Optional[bool] = None, + shorten_links: Optional[bool] = None, + auto_schedule: Optional[AutoSchedule] = None, + auto_repost: Optional[AutoRepost] = None, + auto_hashtag: Optional[AutoHashtag | bool] = None, + unsplash: Optional[str] = None, + bluesky_options: Optional[dict[str, Any]] = None, + facebook_options: Optional[dict[str, Any]] = None, + gmb_options: Optional[dict[str, Any]] = None, + instagram_options: Optional[dict[str, Any]] = None, + linkedin_options: Optional[dict[str, Any]] = None, + pinterest_options: Optional[dict[str, Any]] = None, + reddit_options: Optional[dict[str, Any]] = None, + snapchat_options: Optional[dict[str, Any]] = None, + telegram_options: Optional[dict[str, Any]] = None, + threads_options: Optional[dict[str, Any]] = None, + tiktok_options: Optional[dict[str, Any]] = None, + twitter_options: Optional[dict[str, Any]] = None, + youtube_options: Optional[dict[str, Any]] = None, + requires_approval: Optional[bool] = None, + random_post: Optional[bool] = None, + random_media_url: Optional[bool] = None, + idempotency_key: Optional[str] = None, + notes: Optional[str] = None, + profile_key: Optional[str] = None, + ) -> PostResponse: + """ + Create a post across multiple social media platforms. + + Docs: https://www.ayrshare.com/docs/apis/post/post + + Args: + post: The post text to be published - required + platforms: List of platforms to post to (e.g. [SocialPlatform.TWITTER, SocialPlatform.FACEBOOK]) - required + media_urls: Optional list of media URLs to include - required if is_video is true + is_video: Whether the media is a video - default is false (in api docs) + schedule_date: UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) - default is None (in api docs) + validate_schedule: Whether to validate the schedule date - default is false (in api docs) + first_comment: Configuration for first comment - default is None (in api docs) + disable_comments: Whether to disable comments - default is false (in api docs) + shorten_links: Whether to shorten links - default is false (in api docs) + auto_schedule: Configuration for automatic scheduling - default is None (in api docs https://www.ayrshare.com/docs/apis/auto-schedule/overview) + auto_repost: Configuration for automatic reposting - default is None (in api docs https://www.ayrshare.com/docs/apis/post/overview#auto-repost) + auto_hashtag: Configuration for automatic hashtags - default is None (in api docs https://www.ayrshare.com/docs/apis/post/overview#auto-hashtags) + unsplash: Unsplash image configuration - default is None (in api docs https://www.ayrshare.com/docs/apis/post/overview#unsplash) + + ------------------------------------------------------------ + + bluesky_options: Bluesky-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/bluesky + facebook_options: Facebook-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/facebook + gmb_options: Google Business Profile options - https://www.ayrshare.com/docs/apis/post/social-networks/google + instagram_options: Instagram-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/instagram + linkedin_options: LinkedIn-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/linkedin + pinterest_options: Pinterest-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/pinterest + reddit_options: Reddit-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/reddit + snapchat_options: Snapchat-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/snapchat + telegram_options: Telegram-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/telegram + threads_options: Threads-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/threads + tiktok_options: TikTok-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/tiktok + twitter_options: Twitter-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/twitter + youtube_options: YouTube-specific options - https://www.ayrshare.com/docs/apis/post/social-networks/youtube + + ------------------------------------------------------------ + + + requires_approval: Whether to enable approval workflow - default is false (in api docs) + random_post: Whether to generate random post text - default is false (in api docs) + random_media_url: Whether to generate random media - default is false (in api docs) + idempotency_key: Unique ID for the post - default is None (in api docs) + notes: Additional notes for the post - default is None (in api docs) + + Returns: + PostResponse object containing the post details and status + + Raises: + AyrshareAPIException: If the API request fails + """ + + payload: dict[str, Any] = { + "post": post, + "platforms": [p.value for p in platforms], + } + + # Add optional parameters if provided + if media_urls: + payload["mediaUrls"] = media_urls + if is_video is not None: + payload["isVideo"] = is_video + if schedule_date: + payload["scheduleDate"] = schedule_date + if validate_schedule is not None: + payload["validateSchedule"] = validate_schedule + if first_comment: + first_comment_dict = first_comment.model_dump(exclude_none=True) + if first_comment.platforms: + first_comment_dict["platforms"] = [ + p.value for p in first_comment.platforms + ] + payload["firstComment"] = first_comment_dict + if disable_comments is not None: + payload["disableComments"] = disable_comments + if shorten_links is not None: + payload["shortenLinks"] = shorten_links + if auto_schedule: + auto_schedule_dict = auto_schedule.model_dump(exclude_none=True) + if auto_schedule.platforms: + auto_schedule_dict["platforms"] = [ + p.value for p in auto_schedule.platforms + ] + payload["autoSchedule"] = auto_schedule_dict + if auto_repost: + auto_repost_dict = auto_repost.model_dump(exclude_none=True) + if auto_repost.platforms: + auto_repost_dict["platforms"] = [p.value for p in auto_repost.platforms] + payload["autoRepost"] = auto_repost_dict + if auto_hashtag: + payload["autoHashtag"] = ( + auto_hashtag.model_dump(exclude_none=True) + if isinstance(auto_hashtag, AutoHashtag) + else auto_hashtag + ) + if unsplash: + payload["unsplash"] = unsplash + if bluesky_options: + payload["blueskyOptions"] = bluesky_options + if facebook_options: + payload["faceBookOptions"] = facebook_options + if gmb_options: + payload["gmbOptions"] = gmb_options + if instagram_options: + payload["instagramOptions"] = instagram_options + if linkedin_options: + payload["linkedInOptions"] = linkedin_options + if pinterest_options: + payload["pinterestOptions"] = pinterest_options + if reddit_options: + payload["redditOptions"] = reddit_options + if snapchat_options: + payload["snapchatOptions"] = snapchat_options + if telegram_options: + payload["telegramOptions"] = telegram_options + if threads_options: + payload["threadsOptions"] = threads_options + if tiktok_options: + payload["tikTokOptions"] = tiktok_options + if twitter_options: + payload["twitterOptions"] = twitter_options + if youtube_options: + payload["youTubeOptions"] = youtube_options + if requires_approval is not None: + payload["requiresApproval"] = requires_approval + if random_post is not None: + payload["randomPost"] = random_post + if random_media_url is not None: + payload["randomMediaUrl"] = random_media_url + if idempotency_key: + payload["idempotencyKey"] = idempotency_key + if notes: + payload["notes"] = notes + + headers = self.headers + if profile_key: + headers["Profile-Key"] = profile_key + + response = await self._requests.post( + self.POST_ENDPOINT, json=payload, headers=headers + ) + logger.warning(f"Ayrshare request: {payload} and headers: {headers}") + if not response.ok: + logger.error( + f"Ayrshare API request failed ({response.status}): {response.text()}" + ) + try: + error_data = response.json() + error_message = error_data.get("message", "Unknown error") + except json.JSONDecodeError: + error_message = response.text() + + raise AyrshareAPIException( + f"Ayrshare API request failed ({response.status}): {error_message}", + response.status, + ) + + response_data = response.json() + if response_data.get("status") != "success": + logger.error( + f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}" + ) + raise AyrshareAPIException( + f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}", + response.status, + ) + + # Ayrshare returns an array of posts even for single posts + # It seems like there is only ever one post in the array, and within that + # there are multiple postIds + + # There is a seperate endpoint for bulk posting, so feels safe to just take + # the first post from the array + + if len(response_data["posts"]) == 0: + logger.error("Ayrshare API returned no posts") + raise AyrshareAPIException( + "Ayrshare API returned no posts", + response.status, + ) + logger.warn(f"Ayrshare API returned posts: {response_data['posts']}") + return PostResponse(**response_data["posts"][0]) diff --git a/autogpt_platform/backend/backend/integrations/credentials_store.py b/autogpt_platform/backend/backend/integrations/credentials_store.py index f321da82fabf..75ae346d5db4 100644 --- a/autogpt_platform/backend/backend/integrations/credentials_store.py +++ b/autogpt_platform/backend/backend/integrations/credentials_store.py @@ -1,10 +1,10 @@ import base64 import hashlib import secrets +from contextlib import asynccontextmanager from datetime import datetime, timedelta, timezone from typing import Optional -from autogpt_libs.utils.cache import thread_cached from autogpt_libs.utils.synchronize import AsyncRedisKeyedMutex from pydantic import SecretStr @@ -182,6 +182,15 @@ expires_at=None, ) +enrichlayer_credentials = APIKeyCredentials( + id="d9fce73a-6c1d-4e8b-ba2e-12a456789def", + provider="enrichlayer", + api_key=SecretStr(settings.secrets.enrichlayer_api_key), + title="Use Credits for Enrichlayer", + expires_at=None, +) + + llama_api_credentials = APIKeyCredentials( id="d44045af-1c33-4833-9e19-752313214de2", provider="llama_api", @@ -190,6 +199,14 @@ expires_at=None, ) +v0_credentials = APIKeyCredentials( + id="c4e6d1a0-3b5f-4789-a8e2-9b123456789f", + provider="v0", + api_key=SecretStr(settings.secrets.v0_api_key), + title="Use Credits for v0 by Vercel", + expires_at=None, +) + DEFAULT_CREDENTIALS = [ ollama_credentials, revid_credentials, @@ -203,6 +220,7 @@ jina_credentials, unreal_credentials, open_router_credentials, + enrichlayer_credentials, fal_credentials, exa_credentials, e2b_credentials, @@ -213,6 +231,8 @@ smartlead_credentials, zerobounce_credentials, google_maps_credentials, + llama_api_credentials, + v0_credentials, ] @@ -228,18 +248,17 @@ async def locks(self) -> AsyncRedisKeyedMutex: return self._locks @property - @thread_cached def db_manager(self): if prisma.is_connected(): from backend.data import user return user else: - from backend.executor.database import DatabaseManagerAsyncClient - from backend.util.service import get_service_client + from backend.util.clients import get_database_manager_async_client - return get_service_client(DatabaseManagerAsyncClient) + return get_database_manager_async_client() + # =============== USER-MANAGED CREDENTIALS =============== # async def add_creds(self, user_id: str, credentials: Credentials) -> None: async with await self.locked_user_integrations(user_id): if await self.get_creds_by_id(user_id, credentials.id): @@ -280,6 +299,8 @@ async def get_all_creds(self, user_id: str) -> list[Credentials]: all_credentials.append(unreal_credentials) if settings.secrets.open_router_api_key: all_credentials.append(open_router_credentials) + if settings.secrets.enrichlayer_api_key: + all_credentials.append(enrichlayer_credentials) if settings.secrets.fal_api_key: all_credentials.append(fal_credentials) if settings.secrets.exa_api_key: @@ -359,6 +380,24 @@ async def delete_creds_by_id(self, user_id: str, credentials_id: str) -> None: ] await self._set_user_integration_creds(user_id, filtered_credentials) + # ============== SYSTEM-MANAGED CREDENTIALS ============== # + + async def set_ayrshare_profile_key(self, user_id: str, profile_key: str) -> None: + """Set the Ayrshare profile key for a user. + + The profile key is used to authenticate API requests to Ayrshare's social media posting service. + See https://www.ayrshare.com/docs/apis/profiles/overview for more details. + + Args: + user_id: The ID of the user to set the profile key for + profile_key: The profile key to set + """ + _profile_key = SecretStr(profile_key) + async with self.edit_user_integrations(user_id) as user_integrations: + user_integrations.managed_credentials.ayrshare_profile_key = _profile_key + + # ===================== OAUTH STATES ===================== # + async def store_state_token( self, user_id: str, provider: str, scopes: list[str], use_pkce: bool = False ) -> tuple[str, str]: @@ -375,6 +414,9 @@ async def store_state_token( scopes=scopes, ) + async with self.edit_user_integrations(user_id) as user_integrations: + user_integrations.oauth_states.append(state) + async with await self.locked_user_integrations(user_id): user_integrations = await self._get_user_integrations(user_id) @@ -393,7 +435,7 @@ def _generate_code_challenge(self) -> tuple[str, str]: Generate code challenge using SHA256 from the code verifier. Currently only SHA256 is supported.(In future if we want to support more methods we can add them here) """ - code_verifier = secrets.token_urlsafe(128) + code_verifier = secrets.token_urlsafe(96) sha256_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest() code_challenge = base64.urlsafe_b64encode(sha256_hash).decode("utf-8") return code_challenge.replace("=", ""), code_verifier @@ -428,6 +470,17 @@ async def verify_state_token( return None + # =================== GET/SET HELPERS =================== # + + @asynccontextmanager + async def edit_user_integrations(self, user_id: str): + async with await self.locked_user_integrations(user_id): + user_integrations = await self._get_user_integrations(user_id) + yield user_integrations # yield to allow edits + await self.db_manager.update_user_integrations( + user_id=user_id, data=user_integrations + ) + async def _set_user_integration_creds( self, user_id: str, credentials: list[Credentials] ) -> None: diff --git a/autogpt_platform/backend/backend/integrations/creds_manager.py b/autogpt_platform/backend/backend/integrations/creds_manager.py index d782a31dd232..e5ae0cdb72e9 100644 --- a/autogpt_platform/backend/backend/integrations/creds_manager.py +++ b/autogpt_platform/backend/backend/integrations/creds_manager.py @@ -1,4 +1,5 @@ import logging +import os from contextlib import asynccontextmanager from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Coroutine @@ -9,7 +10,7 @@ from backend.data.model import Credentials, OAuth2Credentials from backend.data.redis_client import get_redis_async from backend.integrations.credentials_store import IntegrationCredentialsStore -from backend.integrations.oauth import HANDLERS_BY_NAME +from backend.integrations.oauth import CREDENTIALS_BY_PROVIDER, HANDLERS_BY_NAME from backend.integrations.providers import ProviderName from backend.util.exceptions import MissingConfigError from backend.util.settings import Settings @@ -196,8 +197,25 @@ async def _get_provider_oauth_handler(provider_name_str: str) -> "BaseOAuthHandl if provider_name not in HANDLERS_BY_NAME: raise KeyError(f"Unknown provider '{provider_name}'") - client_id = getattr(settings.secrets, f"{provider_name.value}_client_id") - client_secret = getattr(settings.secrets, f"{provider_name.value}_client_secret") + provider_creds = CREDENTIALS_BY_PROVIDER[provider_name] + if not provider_creds.use_secrets: + # This is safe to do as we check that the env vars exist in the registry + client_id = ( + os.getenv(provider_creds.client_id_env_var) + if provider_creds.client_id_env_var + else None + ) + client_secret = ( + os.getenv(provider_creds.client_secret_env_var) + if provider_creds.client_secret_env_var + else None + ) + else: + client_id = getattr(settings.secrets, f"{provider_name.value}_client_id") + client_secret = getattr( + settings.secrets, f"{provider_name.value}_client_secret" + ) + if not (client_id and client_secret): raise MissingConfigError( f"Integration with provider '{provider_name}' is not configured", diff --git a/autogpt_platform/backend/backend/integrations/oauth/__init__.py b/autogpt_platform/backend/backend/integrations/oauth/__init__.py index 6e2febdaaf9d..137b9eadfdd0 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/__init__.py +++ b/autogpt_platform/backend/backend/integrations/oauth/__init__.py @@ -4,6 +4,7 @@ from backend.integrations.oauth.todoist import TodoistOAuthHandler +from .discord import DiscordOAuthHandler from .github import GitHubOAuthHandler from .google import GoogleOAuthHandler from .notion import NotionOAuthHandler @@ -15,6 +16,7 @@ # --8<-- [start:HANDLERS_BY_NAMEExample] # Build handlers dict with string keys for compatibility with SDK auto-registration _ORIGINAL_HANDLERS = [ + DiscordOAuthHandler, GitHubOAuthHandler, GoogleOAuthHandler, NotionOAuthHandler, diff --git a/autogpt_platform/backend/backend/integrations/oauth/discord.py b/autogpt_platform/backend/backend/integrations/oauth/discord.py new file mode 100644 index 000000000000..1d70abc3720f --- /dev/null +++ b/autogpt_platform/backend/backend/integrations/oauth/discord.py @@ -0,0 +1,175 @@ +import time +from typing import Optional +from urllib.parse import urlencode + +from backend.data.model import OAuth2Credentials +from backend.integrations.providers import ProviderName +from backend.util.request import Requests + +from .base import BaseOAuthHandler + + +class DiscordOAuthHandler(BaseOAuthHandler): + """ + Discord OAuth2 handler implementation. + + Based on the documentation at: + - https://discord.com/developers/docs/topics/oauth2 + + Discord OAuth2 tokens expire after 7 days by default and include refresh tokens. + """ + + PROVIDER_NAME = ProviderName.DISCORD + DEFAULT_SCOPES = ["identify"] # Basic user information + + def __init__(self, client_id: str, client_secret: str, redirect_uri: str): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + self.auth_base_url = "https://discord.com/oauth2/authorize" + self.token_url = "https://discord.com/api/oauth2/token" + self.revoke_url = "https://discord.com/api/oauth2/token/revoke" + + def get_login_url( + self, scopes: list[str], state: str, code_challenge: Optional[str] + ) -> str: + # Handle default scopes + scopes = self.handle_default_scopes(scopes) + + params = { + "client_id": self.client_id, + "redirect_uri": self.redirect_uri, + "response_type": "code", + "scope": " ".join(scopes), + "state": state, + } + + # Discord supports PKCE + if code_challenge: + params["code_challenge"] = code_challenge + params["code_challenge_method"] = "S256" + + return f"{self.auth_base_url}?{urlencode(params)}" + + async def exchange_code_for_tokens( + self, code: str, scopes: list[str], code_verifier: Optional[str] + ) -> OAuth2Credentials: + params = { + "code": code, + "redirect_uri": self.redirect_uri, + "grant_type": "authorization_code", + } + + # Include PKCE verifier if provided + if code_verifier: + params["code_verifier"] = code_verifier + + return await self._request_tokens(params) + + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + if not credentials.access_token: + raise ValueError("No access token to revoke") + + # Discord requires client authentication for token revocation + data = { + "token": credentials.access_token.get_secret_value(), + "token_type_hint": "access_token", + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = await Requests().post( + url=self.revoke_url, + data=data, + headers=headers, + auth=(self.client_id, self.client_secret), + ) + + # Discord returns 200 OK for successful revocation + return response.status == 200 + + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: + if not credentials.refresh_token: + return credentials + + return await self._request_tokens( + { + "refresh_token": credentials.refresh_token.get_secret_value(), + "grant_type": "refresh_token", + }, + current_credentials=credentials, + ) + + async def _request_tokens( + self, + params: dict[str, str], + current_credentials: Optional[OAuth2Credentials] = None, + ) -> OAuth2Credentials: + request_body = { + "client_id": self.client_id, + "client_secret": self.client_secret, + **params, + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + response = await Requests().post( + self.token_url, data=request_body, headers=headers + ) + token_data: dict = response.json() + + # Get username if this is a new token request + username = None + if "access_token" in token_data: + username = await self._request_username(token_data["access_token"]) + + now = int(time.time()) + new_credentials = OAuth2Credentials( + provider=self.PROVIDER_NAME, + title=current_credentials.title if current_credentials else None, + username=username, + access_token=token_data["access_token"], + scopes=token_data.get("scope", "").split() + or (current_credentials.scopes if current_credentials else []), + refresh_token=token_data.get("refresh_token"), + # Discord tokens expire after expires_in seconds (typically 7 days) + access_token_expires_at=( + now + expires_in + if (expires_in := token_data.get("expires_in", None)) + else None + ), + # Discord doesn't provide separate refresh token expiration + refresh_token_expires_at=None, + ) + + if current_credentials: + new_credentials.id = current_credentials.id + + return new_credentials + + async def _request_username(self, access_token: str) -> str | None: + """ + Fetch the username using the Discord OAuth2 @me endpoint. + """ + url = "https://discord.com/api/oauth2/@me" + headers = { + "Authorization": f"Bearer {access_token}", + } + + response = await Requests().get(url, headers=headers) + + if not response.ok: + return None + + # Get user info from the response + data = response.json() + user_info = data.get("user", {}) + + # Return username (without discriminator) + return user_info.get("username") diff --git a/autogpt_platform/backend/backend/integrations/providers.py b/autogpt_platform/backend/backend/integrations/providers.py index 2db3e19f3471..3564ad32a872 100644 --- a/autogpt_platform/backend/backend/integrations/providers.py +++ b/autogpt_platform/backend/backend/integrations/providers.py @@ -25,6 +25,7 @@ class ProviderName(str, Enum): GROQ = "groq" HTTP = "http" HUBSPOT = "hubspot" + ENRICHLAYER = "enrichlayer" IDEOGRAM = "ideogram" JINA = "jina" LLAMA_API = "llama_api" @@ -47,6 +48,7 @@ class ProviderName(str, Enum): TWITTER = "twitter" TODOIST = "todoist" UNREAL_SPEECH = "unreal_speech" + V0 = "v0" ZEROBOUNCE = "zerobounce" @classmethod diff --git a/autogpt_platform/backend/backend/integrations/webhooks/_base.py b/autogpt_platform/backend/backend/integrations/webhooks/_base.py index dd27a853dab5..9342a6417bda 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/_base.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/_base.py @@ -116,7 +116,10 @@ async def prune_webhook_if_dangling( @classmethod @abstractmethod async def validate_payload( - cls, webhook: integrations.Webhook, request: Request + cls, + webhook: integrations.Webhook, + request: Request, + credentials: Credentials | None, ) -> tuple[dict, str]: """ Validates an incoming webhook request and returns its payload and type. diff --git a/autogpt_platform/backend/backend/integrations/webhooks/compass.py b/autogpt_platform/backend/backend/integrations/webhooks/compass.py index 8a2076a1dab1..6dbfc85604f3 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/compass.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/compass.py @@ -5,6 +5,7 @@ from backend.data import integrations from backend.integrations.providers import ProviderName +from backend.sdk import Credentials from ._manual_base import ManualWebhookManagerBase @@ -22,7 +23,10 @@ class CompassWebhookManager(ManualWebhookManagerBase): @classmethod async def validate_payload( - cls, webhook: integrations.Webhook, request: Request + cls, + webhook: integrations.Webhook, + request: Request, + credentials: Credentials | None, ) -> tuple[dict, str]: payload = await request.json() event_type = CompassWebhookType.TRANSCRIPTION # currently the only type diff --git a/autogpt_platform/backend/backend/integrations/webhooks/github.py b/autogpt_platform/backend/backend/integrations/webhooks/github.py index 5d2977cacc78..a4f74249f57b 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/github.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/github.py @@ -30,7 +30,10 @@ class GithubWebhooksManager(BaseWebhooksManager): @classmethod async def validate_payload( - cls, webhook: integrations.Webhook, request: Request + cls, + webhook: integrations.Webhook, + request: Request, + credentials: Credentials | None, ) -> tuple[dict, str]: if not (event_type := request.headers.get("X-GitHub-Event")): raise HTTPException( diff --git a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py index b4e87d5080f6..bc363edd08e8 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py @@ -1,5 +1,6 @@ +import asyncio import logging -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Optional, cast, overload from backend.data.block import BlockSchema from backend.data.graph import set_node_webhook @@ -9,7 +10,7 @@ from .utils import setup_webhook_for_block if TYPE_CHECKING: - from backend.data.graph import GraphModel, NodeModel + from backend.data.graph import BaseGraph, GraphModel, Node, NodeModel from backend.data.model import Credentials from ._base import BaseWebhooksManager @@ -18,13 +19,29 @@ credentials_manager = IntegrationCredentialsManager() -async def on_graph_activate(graph: "GraphModel", user_id: str): +async def on_graph_activate(graph: "GraphModel", user_id: str) -> "GraphModel": """ Hook to be called when a graph is activated/created. ⚠️ Assuming node entities are not re-used between graph versions, ⚠️ this hook calls `on_node_activate` on all nodes in this graph. """ + graph = await _on_graph_activate(graph, user_id) + graph.sub_graphs = await asyncio.gather( + *(_on_graph_activate(sub_graph, user_id) for sub_graph in graph.sub_graphs) + ) + return graph + + +@overload +async def _on_graph_activate(graph: "GraphModel", user_id: str) -> "GraphModel": ... + + +@overload +async def _on_graph_activate(graph: "BaseGraph", user_id: str) -> "BaseGraph": ... + + +async def _on_graph_activate(graph: "BaseGraph | GraphModel", user_id: str): get_credentials = credentials_manager.cached_getter(user_id) updated_nodes = [] for new_node in graph.nodes: @@ -47,7 +64,7 @@ async def on_graph_activate(graph: "GraphModel", user_id: str): ) updated_node = await on_node_activate( - graph.user_id, new_node, credentials=node_credentials + user_id, graph.id, new_node, credentials=node_credentials ) updated_nodes.append(updated_node) @@ -94,10 +111,11 @@ async def on_graph_deactivate(graph: "GraphModel", user_id: str): async def on_node_activate( user_id: str, - node: "NodeModel", + graph_id: str, + node: "Node", *, credentials: Optional["Credentials"] = None, -) -> "NodeModel": +) -> "Node": """Hook to be called when the node is activated/created""" if node.block.webhook_config: @@ -105,7 +123,7 @@ async def on_node_activate( user_id=user_id, trigger_block=node.block, trigger_config=node.input_default, - for_graph_id=node.graph_id, + for_graph_id=graph_id, ) if new_webhook: node = await set_node_webhook(node.id, new_webhook.id) diff --git a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py index bc0337d4c52d..059f0ada0944 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py @@ -58,7 +58,10 @@ async def _register_webhook( @classmethod async def validate_payload( - cls, webhook: integrations.Webhook, request: Request + cls, + webhook: integrations.Webhook, + request: Request, + credentials: Credentials | None, ) -> tuple[dict, str]: """Validate incoming webhook payload from Slant3D""" diff --git a/autogpt_platform/backend/backend/monitoring/block_error_monitor.py b/autogpt_platform/backend/backend/monitoring/block_error_monitor.py index 8de813d06b5d..ffd2ffc8889e 100644 --- a/autogpt_platform/backend/backend/monitoring/block_error_monitor.py +++ b/autogpt_platform/backend/backend/monitoring/block_error_monitor.py @@ -8,10 +8,11 @@ from backend.data.block import get_block from backend.data.execution import ExecutionStatus, NodeExecutionResult -from backend.executor import utils as execution_utils -from backend.notifications.notifications import NotificationManagerClient +from backend.util.clients import ( + get_database_manager_client, + get_notification_manager_client, +) from backend.util.metrics import sentry_capture_error -from backend.util.service import get_service_client from backend.util.settings import Config logger = logging.getLogger(__name__) @@ -40,7 +41,7 @@ class BlockErrorMonitor: def __init__(self, include_top_blocks: int | None = None): self.config = config - self.notification_client = get_service_client(NotificationManagerClient) + self.notification_client = get_notification_manager_client() self.include_top_blocks = ( include_top_blocks if include_top_blocks is not None @@ -107,7 +108,7 @@ def _get_block_stats_from_db( ) -> dict[str, BlockStatsWithSamples]: """Get block execution stats using efficient SQL aggregation.""" - result = execution_utils.get_db_client().get_block_error_stats( + result = get_database_manager_client().get_block_error_stats( start_time, end_time ) @@ -197,7 +198,7 @@ def _get_error_samples_for_block( ) -> list[str]: """Get error samples for a specific block - just a few recent ones.""" # Only fetch a small number of recent failed executions for this specific block - executions = execution_utils.get_db_client().get_node_executions( + executions = get_database_manager_client().get_node_executions( block_ids=[block_id], statuses=[ExecutionStatus.FAILED], created_time_gte=start_time, diff --git a/autogpt_platform/backend/backend/monitoring/late_execution_monitor.py b/autogpt_platform/backend/backend/monitoring/late_execution_monitor.py index 6a0fc8796431..1e0c99cac384 100644 --- a/autogpt_platform/backend/backend/monitoring/late_execution_monitor.py +++ b/autogpt_platform/backend/backend/monitoring/late_execution_monitor.py @@ -4,10 +4,11 @@ from datetime import datetime, timedelta, timezone from backend.data.execution import ExecutionStatus -from backend.executor import utils as execution_utils -from backend.notifications.notifications import NotificationManagerClient +from backend.util.clients import ( + get_database_manager_client, + get_notification_manager_client, +) from backend.util.metrics import sentry_capture_error -from backend.util.service import get_service_client from backend.util.settings import Config logger = logging.getLogger(__name__) @@ -25,11 +26,13 @@ class LateExecutionMonitor: def __init__(self): self.config = config - self.notification_client = get_service_client(NotificationManagerClient) + self.notification_client = get_notification_manager_client() def check_late_executions(self) -> str: """Check for late executions and send alerts if found.""" - late_executions = execution_utils.get_db_client().get_graph_executions( + + # Check for QUEUED executions + queued_late_executions = get_database_manager_client().get_graph_executions( statuses=[ExecutionStatus.QUEUED], created_time_gte=datetime.now(timezone.utc) - timedelta( @@ -40,24 +43,60 @@ def check_late_executions(self) -> str: limit=1000, ) - if not late_executions: + # Check for RUNNING executions stuck for more than 24 hours + running_late_executions = get_database_manager_client().get_graph_executions( + statuses=[ExecutionStatus.RUNNING], + created_time_gte=datetime.now(timezone.utc) + - timedelta(hours=24) + - timedelta( + seconds=self.config.execution_late_notification_checkrange_secs + ), + created_time_lte=datetime.now(timezone.utc) - timedelta(hours=24), + limit=1000, + ) + + all_late_executions = queued_late_executions + running_late_executions + + if not all_late_executions: return "No late executions detected." - num_late_executions = len(late_executions) - num_users = len(set([r.user_id for r in late_executions])) + # Sort by created time (oldest first) + all_late_executions.sort(key=lambda x: x.started_at) + + num_total_late = len(all_late_executions) + num_queued = len(queued_late_executions) + num_running = len(running_late_executions) + num_users = len(set([r.user_id for r in all_late_executions])) + + # Truncate to max entries + tuncate_size = 5 + truncated_executions = all_late_executions[:tuncate_size] + was_truncated = num_total_late > tuncate_size late_execution_details = [ - f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Created At: {exec.started_at.isoformat()}`" - for exec in late_executions + f"* `Execution ID: {exec.id}, Graph ID: {exec.graph_id}v{exec.graph_version}, User ID: {exec.user_id}, Status: {exec.status}, Created At: {exec.started_at.isoformat()}`" + for exec in truncated_executions ] - error = LateExecutionException( - f"Late executions detected: {num_late_executions} late executions from {num_users} users " - f"in the last {self.config.execution_late_notification_checkrange_secs} seconds. " - f"Graph has been queued for more than {self.config.execution_late_notification_threshold_secs} seconds. " - "Please check the executor status. Details:\n" - + "\n".join(late_execution_details) + message_parts = [ + f"Late executions detected: {num_total_late} total late executions ({num_queued} QUEUED, {num_running} RUNNING) from {num_users} users.", + f"QUEUED executions have been waiting for more than {self.config.execution_late_notification_threshold_secs} seconds.", + "RUNNING executions have been running for more than 24 hours.", + "Please check the executor status.", + ] + + if was_truncated: + message_parts.append( + f"\nShowing first {tuncate_size} of {num_total_late} late executions:" + ) + else: + message_parts.append("\nDetails:") + + error_message = ( + "\n".join(message_parts) + "\n" + "\n".join(late_execution_details) ) + + error = LateExecutionException(error_message) msg = str(error) sentry_capture_error(error) diff --git a/autogpt_platform/backend/backend/monitoring/notification_monitor.py b/autogpt_platform/backend/backend/monitoring/notification_monitor.py index 3326dd91a200..db69b4d9fb42 100644 --- a/autogpt_platform/backend/backend/monitoring/notification_monitor.py +++ b/autogpt_platform/backend/backend/monitoring/notification_monitor.py @@ -2,12 +2,10 @@ import logging -from autogpt_libs.utils.cache import thread_cached from prisma.enums import NotificationType from pydantic import BaseModel -from backend.notifications.notifications import NotificationManagerClient -from backend.util.service import get_service_client +from backend.util.clients import get_notification_manager_client logger = logging.getLogger(__name__) @@ -17,11 +15,6 @@ class NotificationJobArgs(BaseModel): cron: str -@thread_cached -def get_notification_manager_client(): - return get_service_client(NotificationManagerClient) - - def process_existing_batches(**kwargs): """Process existing notification batches.""" args = NotificationJobArgs(**kwargs) diff --git a/autogpt_platform/backend/backend/notification.py b/autogpt_platform/backend/backend/notification.py new file mode 100644 index 000000000000..4a349a4929d5 --- /dev/null +++ b/autogpt_platform/backend/backend/notification.py @@ -0,0 +1,15 @@ +from backend.app import run_processes +from backend.notifications.notifications import NotificationManager + + +def main(): + """ + Run the AutoGPT-server Notification Service. + """ + run_processes( + NotificationManager(), + ) + + +if __name__ == "__main__": + main() diff --git a/autogpt_platform/backend/backend/notifications/email.py b/autogpt_platform/backend/backend/notifications/email.py index 4ac0c07760ea..84202ea6a97a 100644 --- a/autogpt_platform/backend/backend/notifications/email.py +++ b/autogpt_platform/backend/backend/notifications/email.py @@ -82,6 +82,19 @@ def send_templated( logger.error(f"Error formatting full message: {e}") raise e + # Check email size (Postmark limit is 5MB = 5,242,880 characters) + email_size = len(full_message) + if email_size > 5_000_000: # Leave some buffer + logger.warning( + f"Email size ({email_size} chars) exceeds safe limit. " + f"This should have been chunked before calling send_templated." + ) + raise ValueError( + f"Email body too large: {email_size} characters (limit: 5,242,880)" + ) + + logger.debug(f"Sending email with size: {email_size} characters") + self._send_email( user_email=user_email, user_unsubscribe_link=user_unsub_link, diff --git a/autogpt_platform/backend/backend/notifications/notifications.py b/autogpt_platform/backend/backend/notifications/notifications.py index dbbd8fe07596..399e4268374a 100644 --- a/autogpt_platform/backend/backend/notifications/notifications.py +++ b/autogpt_platform/backend/backend/notifications/notifications.py @@ -1,12 +1,9 @@ import asyncio import logging -from concurrent.futures import ProcessPoolExecutor from datetime import datetime, timedelta, timezone -from typing import Callable +from typing import Awaitable, Callable import aio_pika -from aio_pika.exceptions import QueueEmpty -from autogpt_libs.utils.cache import thread_cached from prisma.enums import NotificationType from backend.data import rabbitmq @@ -27,25 +24,19 @@ get_notif_data_type, get_summary_params_type, ) -from backend.data.rabbitmq import ( - AsyncRabbitMQ, - Exchange, - ExchangeType, - Queue, - RabbitMQConfig, - SyncRabbitMQ, -) +from backend.data.rabbitmq import Exchange, ExchangeType, Queue, RabbitMQConfig from backend.data.user import generate_unsubscribe_link from backend.notifications.email import EmailSender +from backend.util.clients import get_database_manager_async_client from backend.util.logging import TruncatedLogger -from backend.util.metrics import discord_send_alert +from backend.util.metrics import DiscordChannel, discord_send_alert from backend.util.retry import continuous_retry from backend.util.service import ( AppService, AppServiceClient, + UnhealthyServiceError, endpoint_to_sync, expose, - get_service_client, ) from backend.util.settings import Settings @@ -57,8 +48,6 @@ DEAD_LETTER_EXCHANGE = Exchange(name="dead_letter", type=ExchangeType.TOPIC) EXCHANGES = [NOTIFICATION_EXCHANGE, DEAD_LETTER_EXCHANGE] -background_executor = ProcessPoolExecutor(max_workers=2) - def create_notification_config() -> RabbitMQConfig: """Create RabbitMQ configuration for notifications""" @@ -117,27 +106,6 @@ def create_notification_config() -> RabbitMQConfig: ) -@thread_cached -def get_db(): - from backend.executor.database import DatabaseManagerClient - - return get_service_client(DatabaseManagerClient) - - -@thread_cached -def get_notification_queue() -> SyncRabbitMQ: - client = SyncRabbitMQ(create_notification_config()) - client.connect() - return client - - -@thread_cached -async def get_async_notification_queue() -> AsyncRabbitMQ: - client = AsyncRabbitMQ(create_notification_config()) - await client.connect() - return client - - def get_routing_key(event_type: NotificationType) -> str: strategy = NotificationTypeOverride(event_type).strategy """Get the appropriate routing key for an event""" @@ -162,6 +130,8 @@ def queue_notification(event: NotificationEventModel) -> NotificationResult: exchange = "notifications" routing_key = get_routing_key(event.type) + from backend.util.clients import get_notification_queue + queue = get_notification_queue() queue.publish_message( routing_key=routing_key, @@ -187,6 +157,8 @@ async def queue_notification_async(event: NotificationEventModel) -> Notificatio exchange = "notifications" routing_key = get_routing_key(event.type) + from backend.util.clients import get_async_notification_queue + queue = await get_async_notification_queue() await queue.publish_message( routing_key=routing_key, @@ -216,24 +188,33 @@ def __init__(self): @property def rabbit(self) -> rabbitmq.AsyncRabbitMQ: """Access the RabbitMQ service. Will raise if not configured.""" - if not self.rabbitmq_service: - raise RuntimeError("RabbitMQ not configured for this service") + if not hasattr(self, "rabbitmq_service") or not self.rabbitmq_service: + raise UnhealthyServiceError("RabbitMQ not configured for this service") return self.rabbitmq_service @property def rabbit_config(self) -> rabbitmq.RabbitMQConfig: """Access the RabbitMQ config. Will raise if not configured.""" if not self.rabbitmq_config: - raise RuntimeError("RabbitMQ not configured for this service") + raise UnhealthyServiceError("RabbitMQ not configured for this service") return self.rabbitmq_config + async def health_check(self) -> str: + # Service is unhealthy if RabbitMQ is not ready + if not hasattr(self, "rabbitmq_service") or not self.rabbitmq_service: + raise UnhealthyServiceError("RabbitMQ not configured for this service") + if not self.rabbitmq_service.is_ready: + raise UnhealthyServiceError("RabbitMQ channel is not ready") + return await super().health_check() + @classmethod def get_port(cls) -> int: return settings.config.notification_service_port @expose - def queue_weekly_summary(self): - background_executor.submit(lambda: asyncio.run(self._queue_weekly_summary())) + async def queue_weekly_summary(self): + # Use the existing event loop instead of creating a new one with asyncio.run() + asyncio.create_task(self._queue_weekly_summary()) async def _queue_weekly_summary(self): """Process weekly summary for specified notification types""" @@ -242,10 +223,14 @@ async def _queue_weekly_summary(self): processed_count = 0 current_time = datetime.now(tz=timezone.utc) start_time = current_time - timedelta(days=7) - users = get_db().get_active_user_ids_in_timerange( + logger.info( + f"Querying for active users between {start_time} and {current_time}" + ) + users = await get_database_manager_async_client().get_active_user_ids_in_timerange( end_time=current_time.isoformat(), start_time=start_time.isoformat(), ) + logger.info(f"Found {len(users)} active users in the last 7 days") for user in users: await self._queue_scheduled_notification( SummaryParamsEventModel( @@ -265,10 +250,15 @@ async def _queue_weekly_summary(self): logger.exception(f"Error processing weekly summary: {e}") @expose - def process_existing_batches(self, notification_types: list[NotificationType]): - background_executor.submit(self._process_existing_batches, notification_types) + async def process_existing_batches( + self, notification_types: list[NotificationType] + ): + # Use the existing event loop instead of creating a new process + asyncio.create_task(self._process_existing_batches(notification_types)) - def _process_existing_batches(self, notification_types: list[NotificationType]): + async def _process_existing_batches( + self, notification_types: list[NotificationType] + ): """Process existing batches for specified notification types""" try: processed_count = 0 @@ -276,14 +266,16 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): for notification_type in notification_types: # Get all batches for this notification type - batches = get_db().get_all_batches_by_type(notification_type) + batches = ( + await get_database_manager_async_client().get_all_batches_by_type( + notification_type + ) + ) for batch in batches: # Check if batch has aged out - oldest_message = ( - get_db().get_user_notification_oldest_message_in_batch( - batch.user_id, notification_type - ) + oldest_message = await get_database_manager_async_client().get_user_notification_oldest_message_in_batch( + batch.user_id, notification_type ) if not oldest_message: @@ -297,7 +289,9 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): # If batch has aged out, process it if oldest_message.created_at + max_delay < current_time: - recipient_email = get_db().get_user_email_by_id(batch.user_id) + recipient_email = await get_database_manager_async_client().get_user_email_by_id( + batch.user_id + ) if not recipient_email: logger.error( @@ -305,7 +299,7 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): ) continue - should_send = self._should_email_user_based_on_preference( + should_send = await self._should_email_user_based_on_preference( batch.user_id, notification_type ) @@ -314,12 +308,12 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): f"User {batch.user_id} does not want to receive {notification_type} notifications" ) # Clear the batch - get_db().empty_user_notification_batch( + await get_database_manager_async_client().empty_user_notification_batch( batch.user_id, notification_type ) continue - batch_data = get_db().get_user_notification_batch( + batch_data = await get_database_manager_async_client().get_user_notification_batch( batch.user_id, notification_type ) @@ -328,7 +322,7 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): f"Batch data not found for user {batch.user_id}" ) # Clear the batch - get_db().empty_user_notification_batch( + await get_database_manager_async_client().empty_user_notification_batch( batch.user_id, notification_type ) continue @@ -364,7 +358,7 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): ) # Clear the batch - get_db().empty_user_notification_batch( + await get_database_manager_async_client().empty_user_notification_batch( batch.user_id, notification_type ) @@ -388,16 +382,21 @@ def _process_existing_batches(self, notification_types: list[NotificationType]): } @expose - async def discord_system_alert(self, content: str): - await discord_send_alert(content) + async def discord_system_alert( + self, content: str, channel: DiscordChannel = DiscordChannel.PLATFORM + ): + await discord_send_alert(content, channel) async def _queue_scheduled_notification(self, event: SummaryParamsEventModel): """Queue a scheduled notification - exposed method for other services to call""" try: - logger.debug(f"Received Request to queue scheduled notification {event=}") + logger.info( + f"Queueing scheduled notification type={event.type} user_id={event.user_id}" + ) exchange = "notifications" routing_key = get_routing_key(event.type) + logger.info(f"Using routing key: {routing_key}") # Publish to RabbitMQ await self.rabbit.publish_message( @@ -405,110 +404,131 @@ async def _queue_scheduled_notification(self, event: SummaryParamsEventModel): message=event.model_dump_json(), exchange=next(ex for ex in EXCHANGES if ex.name == exchange), ) + logger.info(f"Successfully queued notification for user {event.user_id}") except Exception as e: logger.exception(f"Error queueing notification: {e}") - def _should_email_user_based_on_preference( + async def _should_email_user_based_on_preference( self, user_id: str, event_type: NotificationType ) -> bool: """Check if a user wants to receive a notification based on their preferences and email verification status""" - validated_email = get_db().get_user_email_verification(user_id) - preference = ( - get_db() - .get_user_notification_preference(user_id) - .preferences.get(event_type, True) + validated_email = ( + await get_database_manager_async_client().get_user_email_verification( + user_id + ) ) + preference = ( + await get_database_manager_async_client().get_user_notification_preference( + user_id + ) + ).preferences.get(event_type, True) # only if both are true, should we email this person return validated_email and preference - def _gather_summary_data( + async def _gather_summary_data( self, user_id: str, event_type: NotificationType, params: BaseSummaryParams ) -> BaseSummaryData: """Gathers the data to build a summary notification""" logger.info( - f"Gathering summary data for {user_id} and {event_type} wiht {params=}" + f"Gathering summary data for {user_id} and {event_type} with {params=}" ) - # total_credits_used = self.run_and_wait( - # get_total_credits_used(user_id, start_time, end_time) - # ) - - # total_executions = self.run_and_wait( - # get_total_executions(user_id, start_time, end_time) - # ) - - # most_used_agent = self.run_and_wait( - # get_most_used_agent(user_id, start_time, end_time) - # ) - - # execution_times = self.run_and_wait( - # get_execution_time(user_id, start_time, end_time) - # ) - - # runs = self.run_and_wait( - # get_runs(user_id, start_time, end_time) - # ) - total_credits_used = 3.0 - total_executions = 2 - most_used_agent = {"name": "Some"} - execution_times = [1, 2, 3] - runs = [{"status": "COMPLETED"}, {"status": "FAILED"}] - - successful_runs = len([run for run in runs if run["status"] == "COMPLETED"]) - failed_runs = len([run for run in runs if run["status"] != "COMPLETED"]) - average_execution_time = ( - sum(execution_times) / len(execution_times) if execution_times else 0 - ) - # cost_breakdown = self.run_and_wait( - # get_cost_breakdown(user_id, start_time, end_time) - # ) - - cost_breakdown = { - "agent1": 1.0, - "agent2": 2.0, - } - - if event_type == NotificationType.DAILY_SUMMARY and isinstance( - params, DailySummaryParams - ): - return DailySummaryData( - total_credits_used=total_credits_used, - total_executions=total_executions, - most_used_agent=most_used_agent["name"], - total_execution_time=sum(execution_times), - successful_runs=successful_runs, - failed_runs=failed_runs, - average_execution_time=average_execution_time, - cost_breakdown=cost_breakdown, - date=params.date, - ) - elif event_type == NotificationType.WEEKLY_SUMMARY and isinstance( - params, WeeklySummaryParams - ): - return WeeklySummaryData( - total_credits_used=total_credits_used, - total_executions=total_executions, - most_used_agent=most_used_agent["name"], - total_execution_time=sum(execution_times), - successful_runs=successful_runs, - failed_runs=failed_runs, - average_execution_time=average_execution_time, - cost_breakdown=cost_breakdown, - start_date=params.start_date, - end_date=params.end_date, + try: + # Get summary data from the database + summary_data = await get_database_manager_async_client().get_user_execution_summary_data( + user_id=user_id, + start_time=params.start_date, + end_time=params.end_date, ) - else: - raise ValueError("Invalid event type or params") - def _should_batch( + # Extract data from summary + total_credits_used = summary_data.total_credits_used + total_executions = summary_data.total_executions + most_used_agent = summary_data.most_used_agent + successful_runs = summary_data.successful_runs + failed_runs = summary_data.failed_runs + total_execution_time = summary_data.total_execution_time + average_execution_time = summary_data.average_execution_time + cost_breakdown = summary_data.cost_breakdown + + if event_type == NotificationType.DAILY_SUMMARY and isinstance( + params, DailySummaryParams + ): + return DailySummaryData( + total_credits_used=total_credits_used, + total_executions=total_executions, + most_used_agent=most_used_agent, + total_execution_time=total_execution_time, + successful_runs=successful_runs, + failed_runs=failed_runs, + average_execution_time=average_execution_time, + cost_breakdown=cost_breakdown, + date=params.date, + ) + elif event_type == NotificationType.WEEKLY_SUMMARY and isinstance( + params, WeeklySummaryParams + ): + return WeeklySummaryData( + total_credits_used=total_credits_used, + total_executions=total_executions, + most_used_agent=most_used_agent, + total_execution_time=total_execution_time, + successful_runs=successful_runs, + failed_runs=failed_runs, + average_execution_time=average_execution_time, + cost_breakdown=cost_breakdown, + start_date=params.start_date, + end_date=params.end_date, + ) + else: + raise ValueError("Invalid event type or params") + + except Exception as e: + logger.error(f"Failed to gather summary data: {e}") + # Return sensible defaults in case of error + if event_type == NotificationType.DAILY_SUMMARY and isinstance( + params, DailySummaryParams + ): + return DailySummaryData( + total_credits_used=0.0, + total_executions=0, + most_used_agent="No data available", + total_execution_time=0.0, + successful_runs=0, + failed_runs=0, + average_execution_time=0.0, + cost_breakdown={}, + date=params.date, + ) + elif event_type == NotificationType.WEEKLY_SUMMARY and isinstance( + params, WeeklySummaryParams + ): + return WeeklySummaryData( + total_credits_used=0.0, + total_executions=0, + most_used_agent="No data available", + total_execution_time=0.0, + successful_runs=0, + failed_runs=0, + average_execution_time=0.0, + cost_breakdown={}, + start_date=params.start_date, + end_date=params.end_date, + ) + else: + raise ValueError("Invalid event type or params") from e + + async def _should_batch( self, user_id: str, event_type: NotificationType, event: NotificationEventModel ) -> bool: - get_db().create_or_add_to_user_notification_batch(user_id, event_type, event) + await get_database_manager_async_client().create_or_add_to_user_notification_batch( + user_id, event_type, event + ) - oldest_message = get_db().get_user_notification_oldest_message_in_batch( + oldest_message = await get_database_manager_async_client().get_user_notification_oldest_message_in_batch( user_id, event_type ) if not oldest_message: @@ -538,7 +558,7 @@ def _parse_message(self, message: str) -> NotificationEventModel | None: logger.error(f"Error parsing message due to non matching schema {e}") return None - def _process_admin_message(self, message: str) -> bool: + async def _process_admin_message(self, message: str) -> bool: """Process a single notification, sending to an admin, returning whether to put into the failed queue""" try: event = self._parse_message(message) @@ -552,7 +572,7 @@ def _process_admin_message(self, message: str) -> bool: logger.exception(f"Error processing notification for admin queue: {e}") return False - def _process_immediate(self, message: str) -> bool: + async def _process_immediate(self, message: str) -> bool: """Process a single notification immediately, returning whether to put into the failed queue""" try: event = self._parse_message(message) @@ -560,12 +580,16 @@ def _process_immediate(self, message: str) -> bool: return False logger.debug(f"Processing immediate notification: {event}") - recipient_email = get_db().get_user_email_by_id(event.user_id) + recipient_email = ( + await get_database_manager_async_client().get_user_email_by_id( + event.user_id + ) + ) if not recipient_email: logger.error(f"User email not found for user {event.user_id}") return False - should_send = self._should_email_user_based_on_preference( + should_send = await self._should_email_user_based_on_preference( event.user_id, event.type ) if not should_send: @@ -587,7 +611,7 @@ def _process_immediate(self, message: str) -> bool: logger.exception(f"Error processing notification for immediate queue: {e}") return False - def _process_batch(self, message: str) -> bool: + async def _process_batch(self, message: str) -> bool: """Process a single notification with a batching strategy, returning whether to put into the failed queue""" try: event = self._parse_message(message) @@ -595,12 +619,16 @@ def _process_batch(self, message: str) -> bool: return False logger.info(f"Processing batch notification: {event}") - recipient_email = get_db().get_user_email_by_id(event.user_id) + recipient_email = ( + await get_database_manager_async_client().get_user_email_by_id( + event.user_id + ) + ) if not recipient_email: logger.error(f"User email not found for user {event.user_id}") return False - should_send = self._should_email_user_based_on_preference( + should_send = await self._should_email_user_based_on_preference( event.user_id, event.type ) if not should_send: @@ -609,12 +637,16 @@ def _process_batch(self, message: str) -> bool: ) return True - should_send = self._should_batch(event.user_id, event.type, event) + should_send = await self._should_batch(event.user_id, event.type, event) if not should_send: logger.info("Batch not old enough to send") return False - batch = get_db().get_user_notification_batch(event.user_id, event.type) + batch = ( + await get_database_manager_async_client().get_user_notification_batch( + event.user_id, event.type + ) + ) if not batch or not batch.notifications: logger.error(f"Batch not found for user {event.user_id}") return False @@ -634,20 +666,101 @@ def _process_batch(self, message: str) -> bool: for db_event in batch.notifications ] - self.email_sender.send_templated( - notification=event.type, - user_email=recipient_email, - data=batch_messages, - user_unsub_link=unsub_link, - ) - # only empty the batch if we sent the email successfully - get_db().empty_user_notification_batch(event.user_id, event.type) + # Split batch into chunks to avoid exceeding email size limits + # Start with a reasonable chunk size and adjust dynamically + MAX_EMAIL_SIZE = 4_500_000 # 4.5MB to leave buffer under 5MB limit + chunk_size = 100 # Initial chunk size + successfully_sent_count = 0 + failed_indices = [] + + i = 0 + while i < len(batch_messages): + # Try progressively smaller chunks if needed + chunk_sent = False + for attempt_size in [chunk_size, 50, 25, 10, 5, 1]: + chunk = batch_messages[i : i + attempt_size] + + try: + # Try to render the email to check its size + template = self.email_sender._get_template(event.type) + _, test_message = self.email_sender.formatter.format_email( + base_template=template.base_template, + subject_template=template.subject_template, + content_template=template.body_template, + data={"notifications": chunk}, + unsubscribe_link=f"{self.email_sender.formatter.env.globals.get('base_url', '')}/profile/settings", + ) + + if len(test_message) < MAX_EMAIL_SIZE: + # Size is acceptable, send the email + logger.info( + f"Sending email with {len(chunk)} notifications " + f"(size: {len(test_message):,} chars)" + ) + + self.email_sender.send_templated( + notification=event.type, + user_email=recipient_email, + data=chunk, + user_unsub_link=unsub_link, + ) + + # Track successful sends + successfully_sent_count += len(chunk) + + # Update chunk_size for next iteration based on success + if ( + attempt_size == chunk_size + and len(test_message) < MAX_EMAIL_SIZE * 0.7 + ): + # If we're well under limit, try larger chunks next time + chunk_size = min(chunk_size + 10, 100) + elif len(test_message) > MAX_EMAIL_SIZE * 0.9: + # If we're close to limit, use smaller chunks + chunk_size = max(attempt_size - 10, 1) + + i += len(chunk) + chunk_sent = True + break + except Exception as e: + if attempt_size == 1: + # Even single notification is too large + logger.error( + f"Single notification too large to send: {e}. " + f"Skipping notification at index {i}" + ) + failed_indices.append(i) + i += 1 + chunk_sent = True + break + # Try smaller chunk + continue + + if not chunk_sent: + # Should not reach here due to single notification handling + logger.error(f"Failed to send notifications starting at index {i}") + failed_indices.append(i) + i += 1 + + # Only empty the batch if ALL notifications were sent successfully + if successfully_sent_count == len(batch_messages): + logger.info( + f"Successfully sent all {successfully_sent_count} notifications, clearing batch" + ) + await get_database_manager_async_client().empty_user_notification_batch( + event.user_id, event.type + ) + else: + logger.warning( + f"Only sent {successfully_sent_count} of {len(batch_messages)} notifications. " + f"Failed indices: {failed_indices}. Batch will be retained for retry." + ) return True except Exception as e: logger.exception(f"Error processing notification for batch queue: {e}") return False - def _process_summary(self, message: str) -> bool: + async def _process_summary(self, message: str) -> bool: """Process a single notification with a summary strategy, returning whether to put into the failed queue""" try: logger.info(f"Processing summary notification: {message}") @@ -658,11 +771,15 @@ def _process_summary(self, message: str) -> bool: logger.info(f"Processing summary notification: {model}") - recipient_email = get_db().get_user_email_by_id(event.user_id) + recipient_email = ( + await get_database_manager_async_client().get_user_email_by_id( + event.user_id + ) + ) if not recipient_email: logger.error(f"User email not found for user {event.user_id}") return False - should_send = self._should_email_user_based_on_preference( + should_send = await self._should_email_user_based_on_preference( event.user_id, event.type ) if not should_send: @@ -671,7 +788,7 @@ def _process_summary(self, message: str) -> bool: ) return True - summary_data = self._gather_summary_data( + summary_data = await self._gather_summary_data( event.user_id, event.type, model.data ) @@ -694,36 +811,42 @@ def _process_summary(self, message: str) -> bool: logger.exception(f"Error processing notification for summary queue: {e}") return False - async def _run_queue( + async def _consume_queue( self, queue: aio_pika.abc.AbstractQueue, - process_func: Callable[[str], bool], - error_queue_name: str, + process_func: Callable[[str], Awaitable[bool]], + queue_name: str, ): - message: aio_pika.abc.AbstractMessage | None = None - try: - # This parameter "no_ack" is named like shit, think of it as "auto_ack" - message = await queue.get(timeout=1.0, no_ack=False) - result = process_func(message.body.decode()) - if result: - await message.ack() - else: - await message.reject(requeue=False) + """Continuously consume messages from a queue using async iteration""" + logger.info(f"Starting consumer for queue: {queue_name}") - except QueueEmpty: - logger.debug(f"Queue {error_queue_name} empty") - except TimeoutError: - logger.debug(f"Queue {error_queue_name} timed out") + try: + async with queue.iterator() as queue_iter: + async for message in queue_iter: + if not self.running: + break + + try: + async with message.process(): + result = await process_func(message.body.decode()) + if not result: + # Message will be rejected when exiting context without exception + raise aio_pika.exceptions.MessageProcessError( + "Processing failed" + ) + except aio_pika.exceptions.MessageProcessError: + # Let message.process() handle the rejection + pass + except Exception as e: + logger.error(f"Error processing message in {queue_name}: {e}") + # Let message.process() handle the rejection + raise + except asyncio.CancelledError: + logger.info(f"Consumer for {queue_name} cancelled") + raise except Exception as e: - if message: - logger.error( - f"Error in notification service loop, message rejected {e}" - ) - await message.reject(requeue=False) - else: - logger.exception( - f"Error in notification service loop, message unable to be rejected, and will have to be manually removed to free space in the queue: {e=}" - ) + logger.exception(f"Fatal error in consumer for {queue_name}: {e}") + raise @continuous_retry() def run_service(self): @@ -736,41 +859,60 @@ async def _run_service(self): logger.info(f"[{self.service_name}] Started notification service") - # Set up queue consumers + # Set up queue consumers with QoS settings channel = await self.rabbit.get_channel() + # Set prefetch to prevent overwhelming the service + await channel.set_qos(prefetch_count=10) + immediate_queue = await channel.get_queue("immediate_notifications") batch_queue = await channel.get_queue("batch_notifications") - admin_queue = await channel.get_queue("admin_notifications") - summary_queue = await channel.get_queue("summary_notifications") - while self.running: - try: - await self._run_queue( + # Create consumer tasks for each queue - running in parallel + consumer_tasks = [ + asyncio.create_task( + self._consume_queue( queue=immediate_queue, process_func=self._process_immediate, - error_queue_name="immediate_notifications", + queue_name="immediate_notifications", ) - await self._run_queue( + ), + asyncio.create_task( + self._consume_queue( queue=admin_queue, process_func=self._process_admin_message, - error_queue_name="admin_notifications", + queue_name="admin_notifications", ) - await self._run_queue( + ), + asyncio.create_task( + self._consume_queue( queue=batch_queue, process_func=self._process_batch, - error_queue_name="batch_notifications", + queue_name="batch_notifications", ) - await self._run_queue( + ), + asyncio.create_task( + self._consume_queue( queue=summary_queue, process_func=self._process_summary, - error_queue_name="summary_notifications", + queue_name="summary_notifications", ) - await asyncio.sleep(0.1) - except QueueEmpty as e: - logger.debug(f"Queue empty: {e}") + ), + ] + + try: + # Run all consumers concurrently + await asyncio.gather(*consumer_tasks) + except asyncio.CancelledError: + logger.info("Service shutdown requested") + # Cancel all consumer tasks + for task in consumer_tasks: + task.cancel() + # Wait for all tasks to complete cancellation + await asyncio.gather(*consumer_tasks, return_exceptions=True) + raise def cleanup(self): """Cleanup service resources""" @@ -785,6 +927,8 @@ class NotificationManagerClient(AppServiceClient): def get_service_type(cls): return NotificationManager - process_existing_batches = NotificationManager.process_existing_batches - queue_weekly_summary = NotificationManager.queue_weekly_summary + process_existing_batches = endpoint_to_sync( + NotificationManager.process_existing_batches + ) + queue_weekly_summary = endpoint_to_sync(NotificationManager.queue_weekly_summary) discord_system_alert = endpoint_to_sync(NotificationManager.discord_system_alert) diff --git a/autogpt_platform/backend/backend/notifications/templates/agent_approved.html.jinja2 b/autogpt_platform/backend/backend/notifications/templates/agent_approved.html.jinja2 new file mode 100644 index 000000000000..14ca59158069 --- /dev/null +++ b/autogpt_platform/backend/backend/notifications/templates/agent_approved.html.jinja2 @@ -0,0 +1,73 @@ +{# Agent Approved Notification Email Template #} +{# + Template variables: + data.agent_name: the name of the approved agent + data.agent_id: the ID of the agent + data.agent_version: the version of the agent + data.reviewer_name: the name of the reviewer who approved it + data.reviewer_email: the email of the reviewer + data.comments: comments from the reviewer + data.reviewed_at: when the agent was reviewed + data.store_url: URL to view the agent in the store + + Subject: 🎉 Your agent '{{ data.agent_name }}' has been approved! +#} + +{% block content %} +

+ 🎉 Congratulations! +

+ +

+ Your agent '{{ data.agent_name }}' has been approved and is now live in the store! +

+ +
+ +{% if data.comments %} +
+

+ 💬 Creator feedback area +

+

+ {{ data.comments }} +

+
+ +
+{% endif %} + +
+

+ What's Next? +

+
    +
  • Your agent is now live and discoverable in the AutoGPT Store
  • +
  • Users can find, install, and run your agent
  • +
  • You can update your agent anytime by submitting a new version
  • +
+
+ +
+ + + +
+ +
+

+ 💡 Pro Tip: Share your agent with the community! Post about it on social media, forums, or your blog to help more users discover and benefit from your creation. +

+
+ +
+ +

+ Thank you for contributing to the AutoGPT ecosystem! 🚀 +

+ +{% endblock %} \ No newline at end of file diff --git a/autogpt_platform/backend/backend/notifications/templates/agent_rejected.html.jinja2 b/autogpt_platform/backend/backend/notifications/templates/agent_rejected.html.jinja2 new file mode 100644 index 000000000000..69a808190024 --- /dev/null +++ b/autogpt_platform/backend/backend/notifications/templates/agent_rejected.html.jinja2 @@ -0,0 +1,77 @@ +{# Agent Rejected Notification Email Template #} +{# + Template variables: + data.agent_name: the name of the rejected agent + data.agent_id: the ID of the agent + data.agent_version: the version of the agent + data.reviewer_name: the name of the reviewer who rejected it + data.reviewer_email: the email of the reviewer + data.comments: comments from the reviewer explaining the rejection + data.reviewed_at: when the agent was reviewed + data.resubmit_url: URL to resubmit the agent + + Subject: Your agent '{{ data.agent_name }}' needs some updates +#} + + +{% block content %} +

+ 📝 Review Complete +

+ +

+ Your agent '{{ data.agent_name }}' needs some updates before approval. +

+ +
+ +
+

+ 💬 Creator feedback area +

+

+ {{ data.comments }} +

+
+ +
+ +
+

+ ☑ Steps to Resubmit: +

+
    +
  • Review the feedback provided above carefully
  • +
  • Make the necessary updates to your agent
  • +
  • Test your agent thoroughly to ensure it works as expected
  • +
  • Submit your updated agent for review
  • +
+
+ +
+ +
+

+ 💡 Tip: Address all the points mentioned in the feedback to increase your chances of approval in the next review. +

+
+ + + +
+

+ 🌟 Don't Give Up! Many successful agents go through multiple iterations before approval. Our review team is here to help you succeed! +

+
+ +
+ +

+ We're excited to see your improved agent submission! 🚀 +

+ +{% endblock %} \ No newline at end of file diff --git a/autogpt_platform/backend/backend/notifications/templates/low_balance.html.jinja2 b/autogpt_platform/backend/backend/notifications/templates/low_balance.html.jinja2 index d39d17cb148e..8be13d047350 100644 --- a/autogpt_platform/backend/backend/notifications/templates/low_balance.html.jinja2 +++ b/autogpt_platform/backend/backend/notifications/templates/low_balance.html.jinja2 @@ -1,9 +1,7 @@ {# Low Balance Notification Email Template #} {# Template variables: -data.agent_name: the name of the agent data.current_balance: the current balance of the user data.billing_page_link: the link to the billing page -data.shortfall: the shortfall amount #}

- Your agent "{{ data.agent_name }}" has been stopped due to low balance. + Your account balance has dropped below the recommended threshold.

Current Balance: ${{ "{:.2f}".format((data.current_balance|float)/100) }}

-

- Shortfall: ${{ "{:.2f}".format((data.shortfall|float)/100) }} -

@@ -79,7 +68,7 @@ data.shortfall: the shortfall amount margin-top: 0; margin-bottom: 5px; "> - Your agent "{{ data.agent_name }}" requires additional credits to continue running. The current operation has been canceled until your balance is replenished. + Your account requires additional credits to continue running agents. Please add credits to your account to avoid service interruption.

@@ -110,5 +99,5 @@ data.shortfall: the shortfall amount margin-bottom: 10px; font-style: italic; "> - This is an automated notification. Your agent is stopped and will need manually restarted unless set to trigger automatically. + This is an automated low balance notification. Consider adding credits soon to avoid service interruption.

diff --git a/autogpt_platform/backend/backend/notifications/templates/weekly_summary.html.jinja2 b/autogpt_platform/backend/backend/notifications/templates/weekly_summary.html.jinja2 index c561851a600d..6de381a94677 100644 --- a/autogpt_platform/backend/backend/notifications/templates/weekly_summary.html.jinja2 +++ b/autogpt_platform/backend/backend/notifications/templates/weekly_summary.html.jinja2 @@ -5,23 +5,64 @@ data.start_date: the start date of the summary data.end_date: the end date of the summary data.total_credits_used: the total credits used during the summary data.total_executions: the total number of executions during the summary -data.most_used_agent: the most used agent's nameduring the summary +data.most_used_agent: the most used agent's name during the summary data.total_execution_time: the total execution time during the summary data.successful_runs: the total number of successful runs during the summary data.failed_runs: the total number of failed runs during the summary data.average_execution_time: the average execution time during the summary -data.cost_breakdown: the cost breakdown during the summary +data.cost_breakdown: the cost breakdown during the summary (dict mapping agent names to credit amounts) #} -

Weekly Summary

+

+ Weekly Summary +

-

Start Date: {{ data.start_date }}

-

End Date: {{ data.end_date }}

-

Total Credits Used: {{ data.total_credits_used }}

-

Total Executions: {{ data.total_executions }}

-

Most Used Agent: {{ data.most_used_agent }}

-

Total Execution Time: {{ data.total_execution_time }}

-

Successful Runs: {{ data.successful_runs }}

-

Failed Runs: {{ data.failed_runs }}

-

Average Execution Time: {{ data.average_execution_time }}

-

Cost Breakdown: {{ data.cost_breakdown }}

+

+ Your Agent Activity: {{ data.start_date.strftime('%B %-d') }} – {{ data.end_date.strftime('%B %-d') }} +

+ +
+
    +
  • + Total Executions: {{ data.total_executions }} +
  • +
  • + Total Credits Used: {{ data.total_credits_used|format("%.2f") }} +
  • +
  • + Total Execution Time: {{ data.total_execution_time|format("%.1f") }} seconds +
  • +
  • + Successful Runs: {{ data.successful_runs }} +
  • +
  • + Failed Runs: {{ data.failed_runs }} +
  • +
  • + Average Execution Time: {{ data.average_execution_time|format("%.1f") }} seconds +
  • +
  • + Most Used Agent: {{ data.most_used_agent }} +
  • + {% if data.cost_breakdown %} +
  • + Cost Breakdown: +
      + {% for agent_name, credits in data.cost_breakdown.items() %} +
    • + {{ agent_name }}: {{ credits|format("%.2f") }} credits +
    • + {% endfor %} +
    +
  • + {% endif %} +
+
+ +

+ Thank you for being a part of the AutoGPT community! 🎉 +

+ +

+ Join the conversation on Discord here. +

\ No newline at end of file diff --git a/autogpt_platform/backend/backend/notifications/templates/zero_balance.html.jinja2 b/autogpt_platform/backend/backend/notifications/templates/zero_balance.html.jinja2 new file mode 100644 index 000000000000..eb6879b89802 --- /dev/null +++ b/autogpt_platform/backend/backend/notifications/templates/zero_balance.html.jinja2 @@ -0,0 +1,114 @@ +{# Low Balance Notification Email Template #} +{# Template variables: +data.agent_name: the name of the agent +data.current_balance: the current balance of the user +data.billing_page_link: the link to the billing page +data.shortfall: the shortfall amount +#} + +

+ Zero Balance Warning +

+ +

+ Your agent "{{ data.agent_name }}" has been stopped due to low balance. +

+ +
+

+ Current Balance: ${{ "{:.2f}".format((data.current_balance|float)/100) }} +

+

+ Shortfall: ${{ "{:.2f}".format((data.shortfall|float)/100) }} +

+
+ + +
+

+ Low Balance: +

+

+ Your agent "{{ data.agent_name }}" requires additional credits to continue running. The current operation has been canceled until your balance is replenished. +

+
+ + + +

+ This is an automated notification. Your agent is stopped and will need manually restarted unless set to trigger automatically. +

diff --git a/autogpt_platform/backend/backend/rest.py b/autogpt_platform/backend/backend/rest.py index 0fb1eed87533..b601144c6f40 100644 --- a/autogpt_platform/backend/backend/rest.py +++ b/autogpt_platform/backend/backend/rest.py @@ -1,6 +1,4 @@ from backend.app import run_processes -from backend.executor import DatabaseManager -from backend.notifications.notifications import NotificationManager from backend.server.rest_api import AgentServer @@ -8,11 +6,7 @@ def main(): """ Run all the processes required for the AutoGPT-server REST API. """ - run_processes( - NotificationManager(), - DatabaseManager(), - AgentServer(), - ) + run_processes(AgentServer()) if __name__ == "__main__": diff --git a/autogpt_platform/backend/backend/scheduler.py b/autogpt_platform/backend/backend/scheduler.py index 22be4bf7fd87..c22faf31afe5 100644 --- a/autogpt_platform/backend/backend/scheduler.py +++ b/autogpt_platform/backend/backend/scheduler.py @@ -6,7 +6,9 @@ def main(): """ Run all the processes required for the AutoGPT-server Scheduling System. """ - run_processes(Scheduler()) + run_processes( + Scheduler(), + ) if __name__ == "__main__": diff --git a/autogpt_platform/backend/backend/sdk/__init__.py b/autogpt_platform/backend/backend/sdk/__init__.py index ce4a82a4f0d2..666ad08835ac 100644 --- a/autogpt_platform/backend/backend/sdk/__init__.py +++ b/autogpt_platform/backend/backend/sdk/__init__.py @@ -26,7 +26,7 @@ BlockType, BlockWebhookConfig, ) -from backend.data.integrations import Webhook +from backend.data.integrations import Webhook, update_webhook from backend.data.model import APIKeyCredentials, Credentials, CredentialsField from backend.data.model import CredentialsMetaInput as _CredentialsMetaInput from backend.data.model import ( @@ -144,6 +144,7 @@ "BaseWebhooksManager", "ManualWebhookManagerBase", "Webhook", + "update_webhook", # Provider-Specific (when available) "BaseOAuthHandler", # Utilities diff --git a/autogpt_platform/backend/backend/sdk/builder.py b/autogpt_platform/backend/backend/sdk/builder.py index 3d05eeed4be0..a015560d7345 100644 --- a/autogpt_platform/backend/backend/sdk/builder.py +++ b/autogpt_platform/backend/backend/sdk/builder.py @@ -2,19 +2,27 @@ Builder class for creating provider configurations with a fluent API. """ +import logging import os from typing import Callable, List, Optional, Type from pydantic import SecretStr from backend.data.cost import BlockCost, BlockCostType -from backend.data.model import APIKeyCredentials, Credentials, UserPasswordCredentials +from backend.data.model import ( + APIKeyCredentials, + Credentials, + CredentialsType, + UserPasswordCredentials, +) from backend.integrations.oauth.base import BaseOAuthHandler from backend.integrations.webhooks._base import BaseWebhooksManager from backend.sdk.provider import OAuthConfig, Provider from backend.sdk.registry import AutoRegistry from backend.util.settings import Settings +logger = logging.getLogger(__name__) + class ProviderBuilder: """Builder for creating provider configurations.""" @@ -25,7 +33,7 @@ def __init__(self, name: str): self._webhook_manager: Optional[Type[BaseWebhooksManager]] = None self._default_credentials: List[Credentials] = [] self._base_costs: List[BlockCost] = [] - self._supported_auth_types: set = set() + self._supported_auth_types: set[CredentialsType] = set() self._api_client_factory: Optional[Callable] = None self._error_handler: Optional[Callable[[Exception], str]] = None self._default_scopes: Optional[List[str]] = None @@ -41,13 +49,26 @@ def with_oauth( client_secret_env_var: Optional[str] = None, ) -> "ProviderBuilder": """Add OAuth support.""" - self._oauth_config = OAuthConfig( - oauth_handler=handler_class, - scopes=scopes, - client_id_env_var=client_id_env_var, - client_secret_env_var=client_secret_env_var, - ) - self._supported_auth_types.add("oauth2") + if not client_id_env_var or not client_secret_env_var: + client_id_env_var = f"{self.name}_client_id".upper() + client_secret_env_var = f"{self.name}_client_secret".upper() + + if os.getenv(client_id_env_var) and os.getenv(client_secret_env_var): + self._client_id_env_var = client_id_env_var + self._client_secret_env_var = client_secret_env_var + + self._oauth_config = OAuthConfig( + oauth_handler=handler_class, + scopes=scopes, + client_id_env_var=client_id_env_var, + client_secret_env_var=client_secret_env_var, + ) + self._supported_auth_types.add("oauth2") + else: + logger.warning( + f"Provider {self.name.upper()} implements OAuth but the required env " + f"vars {client_id_env_var} and {client_secret_env_var} are not both set" + ) return self def with_api_key(self, env_var_name: str, title: str) -> "ProviderBuilder": diff --git a/autogpt_platform/backend/backend/sdk/cost_integration.py b/autogpt_platform/backend/backend/sdk/cost_integration.py index 3ede3cea1f88..3eb4b470627e 100644 --- a/autogpt_platform/backend/backend/sdk/cost_integration.py +++ b/autogpt_platform/backend/backend/sdk/cost_integration.py @@ -71,7 +71,7 @@ def register_provider_costs_for_block(block_class: Type[Block]) -> None: # Add provider's base costs to the block if provider.base_costs: - logger.info( + logger.debug( f"Registering {len(provider.base_costs)} base costs from provider {provider_name} for block {block_class.__name__}" ) block_costs.extend(provider.base_costs) @@ -79,7 +79,7 @@ def register_provider_costs_for_block(block_class: Type[Block]) -> None: # Register costs if any were found if block_costs: BLOCK_COSTS[block_class] = block_costs - logger.info( + logger.debug( f"Registered {len(block_costs)} total costs for block {block_class.__name__}" ) diff --git a/autogpt_platform/backend/backend/sdk/provider.py b/autogpt_platform/backend/backend/sdk/provider.py index 7a6f91543e18..83a24ab0219f 100644 --- a/autogpt_platform/backend/backend/sdk/provider.py +++ b/autogpt_platform/backend/backend/sdk/provider.py @@ -2,12 +2,21 @@ Provider configuration class that holds all provider-related settings. """ +import uuid from typing import Any, Callable, List, Optional, Set, Type -from pydantic import BaseModel +from pydantic import BaseModel, SecretStr from backend.data.cost import BlockCost -from backend.data.model import Credentials, CredentialsField, CredentialsMetaInput +from backend.data.model import ( + APIKeyCredentials, + Credentials, + CredentialsField, + CredentialsMetaInput, + CredentialsType, + OAuth2Credentials, + UserPasswordCredentials, +) from backend.integrations.oauth.base import BaseOAuthHandler from backend.integrations.webhooks._base import BaseWebhooksManager @@ -17,8 +26,8 @@ class OAuthConfig(BaseModel): oauth_handler: Type[BaseOAuthHandler] scopes: Optional[List[str]] = None - client_id_env_var: Optional[str] = None - client_secret_env_var: Optional[str] = None + client_id_env_var: str + client_secret_env_var: str class Provider: @@ -43,7 +52,7 @@ def __init__( webhook_manager: Optional[Type[BaseWebhooksManager]] = None, default_credentials: Optional[List[Credentials]] = None, base_costs: Optional[List[BlockCost]] = None, - supported_auth_types: Optional[Set[str]] = None, + supported_auth_types: Optional[Set[CredentialsType]] = None, api_client_factory: Optional[Callable] = None, error_handler: Optional[Callable[[Exception], str]] = None, **kwargs, @@ -59,6 +68,7 @@ def __init__( # Store any additional configuration self._extra_config = kwargs + self.test_credentials_uuid = uuid.uuid4() def credentials_field(self, **kwargs) -> CredentialsMetaInput: """Return a CredentialsField configured for this provider.""" @@ -74,9 +84,7 @@ def credentials_field(self, **kwargs) -> CredentialsMetaInput: json_schema_extra = { "credentials_provider": [self.name], "credentials_types": ( - list(self.supported_auth_types) - if self.supported_auth_types - else ["api_key"] + list(self.supported_auth_types) if self.supported_auth_types else [] ), } @@ -97,6 +105,41 @@ def credentials_field(self, **kwargs) -> CredentialsMetaInput: **kwargs, ) + def get_test_credentials(self) -> Credentials: + """Get test credentials for the provider based on supported auth types.""" + test_id = str(self.test_credentials_uuid) + + # Return credentials based on the first supported auth type + if "user_password" in self.supported_auth_types: + return UserPasswordCredentials( + id=test_id, + provider=self.name, + username=SecretStr(f"mock-{self.name}-username"), + password=SecretStr(f"mock-{self.name}-password"), + title=f"Mock {self.name.title()} credentials", + ) + elif "oauth2" in self.supported_auth_types: + return OAuth2Credentials( + id=test_id, + provider=self.name, + username=f"mock-{self.name}-username", + access_token=SecretStr(f"mock-{self.name}-access-token"), + access_token_expires_at=None, + refresh_token=SecretStr(f"mock-{self.name}-refresh-token"), + refresh_token_expires_at=None, + scopes=[f"mock-{self.name}-scope"], + title=f"Mock {self.name.title()} OAuth credentials", + ) + else: + # Default to API key credentials + return APIKeyCredentials( + id=test_id, + provider=self.name, + api_key=SecretStr(f"mock-{self.name}-api-key"), + title=f"Mock {self.name.title()} API key", + expires_at=None, + ) + def get_api(self, credentials: Credentials) -> Any: """Get API client instance for the given credentials.""" if self._api_client_factory: diff --git a/autogpt_platform/backend/backend/sdk/registry.py b/autogpt_platform/backend/backend/sdk/registry.py index d65d690644f3..888f85794989 100644 --- a/autogpt_platform/backend/backend/sdk/registry.py +++ b/autogpt_platform/backend/backend/sdk/registry.py @@ -11,6 +11,7 @@ from backend.blocks.basic import Block from backend.data.model import APIKeyCredentials, Credentials from backend.integrations.oauth.base import BaseOAuthHandler +from backend.integrations.providers import ProviderName from backend.integrations.webhooks._base import BaseWebhooksManager if TYPE_CHECKING: @@ -68,16 +69,12 @@ def register_provider(cls, provider: "Provider") -> None: not hasattr(provider.oauth_config.oauth_handler, "PROVIDER_NAME") or provider.oauth_config.oauth_handler.PROVIDER_NAME is None ): - # Import ProviderName to create dynamic enum value - from backend.integrations.providers import ProviderName - # This works because ProviderName has _missing_ method provider.oauth_config.oauth_handler.PROVIDER_NAME = ProviderName( provider.name ) cls._oauth_handlers[provider.name] = provider.oauth_config.oauth_handler - # Register OAuth credentials configuration oauth_creds = SDKOAuthCredentials( use_secrets=False, # SDK providers use custom env vars client_id_env_var=provider.oauth_config.client_id_env_var, @@ -92,8 +89,6 @@ def register_provider(cls, provider: "Provider") -> None: not hasattr(provider.webhook_manager, "PROVIDER_NAME") or provider.webhook_manager.PROVIDER_NAME is None ): - # Import ProviderName to create dynamic enum value - from backend.integrations.providers import ProviderName # This works because ProviderName has _missing_ method provider.webhook_manager.PROVIDER_NAME = ProviderName(provider.name) @@ -206,9 +201,6 @@ def patched_load(): # Add SDK-registered managers sdk_managers = cls.get_webhook_managers() if isinstance(sdk_managers, dict): - # Import ProviderName for conversion - from backend.integrations.providers import ProviderName - # Convert string keys to ProviderName for consistency for provider_str, manager in sdk_managers.items(): provider_name = ProviderName(provider_str) diff --git a/autogpt_platform/backend/backend/server/integrations/router.py b/autogpt_platform/backend/backend/server/integrations/router.py index 0e71d7f0ceb4..f7ac03c4e630 100644 --- a/autogpt_platform/backend/backend/server/integrations/router.py +++ b/autogpt_platform/backend/backend/server/integrations/router.py @@ -1,5 +1,6 @@ import asyncio import logging +from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Annotated, Awaitable, List, Literal from fastapi import ( @@ -12,7 +13,8 @@ Request, status, ) -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, SecretStr +from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR, HTTP_502_BAD_GATEWAY from backend.data.graph import get_graph, set_node_webhook from backend.data.integrations import ( @@ -27,8 +29,11 @@ CredentialsType, HostScopedCredentials, OAuth2Credentials, + UserIntegrations, ) +from backend.data.user import get_user_integrations from backend.executor.utils import add_graph_execution +from backend.integrations.ayrshare import AyrshareClient, SocialPlatform from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.oauth import CREDENTIALS_BY_PROVIDER, HANDLERS_BY_NAME from backend.integrations.providers import ProviderName @@ -39,7 +44,7 @@ get_all_provider_names, ) from backend.server.v2.library.db import set_preset_webhook, update_preset -from backend.util.exceptions import NeedConfirmation, NotFoundError +from backend.util.exceptions import MissingConfigError, NeedConfirmation, NotFoundError from backend.util.settings import Settings if TYPE_CHECKING: @@ -271,6 +276,11 @@ class CredentialsDeletionNeedsConfirmationResponse(BaseModel): message: str +class AyrshareSSOResponse(BaseModel): + sso_url: str = Field(..., description="The SSO URL for Ayrshare integration") + expires_at: datetime = Field(..., description="ISO timestamp when the URL expires") + + @router.delete("/{provider}/credentials/{cred_id}") async def delete_credentials( request: Request, @@ -327,11 +337,19 @@ async def webhook_ingress_generic( webhook_manager = get_webhook_manager(provider) try: webhook = await get_webhook(webhook_id, include_relations=True) + user_id = webhook.user_id + credentials = ( + await creds_manager.get(user_id, webhook.credentials_id) + if webhook.credentials_id + else None + ) except NotFoundError as e: logger.warning(f"Webhook payload received for unknown webhook #{webhook_id}") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) logger.debug(f"Webhook #{webhook_id}: {webhook}") - payload, event_type = await webhook_manager.validate_payload(webhook, request) + payload, event_type = await webhook_manager.validate_payload( + webhook, request, credentials + ) logger.debug( f"Validated {provider.value} {webhook.webhook_type} {event_type} event " f"with payload {payload}" @@ -548,9 +566,93 @@ def _get_provider_oauth_handler( ) -# === PROVIDER DISCOVERY ENDPOINTS === +@router.get("/ayrshare/sso_url") +async def get_ayrshare_sso_url( + user_id: Annotated[str, Depends(get_user_id)], +) -> AyrshareSSOResponse: + """ + Generate an SSO URL for Ayrshare social media integration. + Returns: + dict: Contains the SSO URL for Ayrshare integration + """ + try: + client = AyrshareClient() + except MissingConfigError: + raise HTTPException( + status_code=HTTP_500_INTERNAL_SERVER_ERROR, + detail="Ayrshare integration is not configured", + ) + # Ayrshare profile key is stored in the credentials store + # It is generated when creating a new profile, if there is no profile key, + # we create a new profile and store the profile key in the credentials store + + user_integrations: UserIntegrations = await get_user_integrations(user_id) + profile_key = user_integrations.managed_credentials.ayrshare_profile_key + + if not profile_key: + logger.debug(f"Creating new Ayrshare profile for user {user_id}") + try: + profile = await client.create_profile( + title=f"User {user_id}", messaging_active=True + ) + profile_key = profile.profileKey + await creds_manager.store.set_ayrshare_profile_key(user_id, profile_key) + except Exception as e: + logger.error(f"Error creating Ayrshare profile for user {user_id}: {e}") + raise HTTPException( + status_code=HTTP_502_BAD_GATEWAY, + detail="Failed to create Ayrshare profile", + ) + else: + logger.debug(f"Using existing Ayrshare profile for user {user_id}") + + profile_key_str = ( + profile_key.get_secret_value() + if isinstance(profile_key, SecretStr) + else str(profile_key) + ) + + private_key = settings.secrets.ayrshare_jwt_key + # Ayrshare JWT expiry is 2880 minutes (48 hours) + max_expiry_minutes = 2880 + try: + logger.debug(f"Generating Ayrshare JWT for user {user_id}") + jwt_response = await client.generate_jwt( + private_key=private_key, + profile_key=profile_key_str, + allowed_social=[ + # NOTE: We are enabling platforms one at a time + # to speed up the development process + # SocialPlatform.FACEBOOK, + SocialPlatform.TWITTER, + SocialPlatform.LINKEDIN, + SocialPlatform.INSTAGRAM, + SocialPlatform.YOUTUBE, + # SocialPlatform.REDDIT, + # SocialPlatform.TELEGRAM, + # SocialPlatform.GOOGLE_MY_BUSINESS, + # SocialPlatform.PINTEREST, + SocialPlatform.TIKTOK, + # SocialPlatform.BLUESKY, + # SocialPlatform.SNAPCHAT, + # SocialPlatform.THREADS, + ], + expires_in=max_expiry_minutes, + verify=True, + ) + except Exception as e: + logger.error(f"Error generating Ayrshare JWT for user {user_id}: {e}") + raise HTTPException( + status_code=HTTP_502_BAD_GATEWAY, detail="Failed to generate JWT" + ) + + expires_at = datetime.now(timezone.utc) + timedelta(minutes=max_expiry_minutes) + return AyrshareSSOResponse(sso_url=jwt_response.url, expires_at=expires_at) + + +# === PROVIDER DISCOVERY ENDPOINTS === @router.get("/providers", response_model=List[str]) async def list_providers() -> List[str]: """ diff --git a/autogpt_platform/backend/backend/server/integrations/utils.py b/autogpt_platform/backend/backend/server/integrations/utils.py deleted file mode 100644 index 0fa1052e5be0..000000000000 --- a/autogpt_platform/backend/backend/server/integrations/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -from supabase import Client, create_client - -from backend.util.settings import Settings - -settings = Settings() - - -def get_supabase() -> Client: - return create_client( - settings.secrets.supabase_url, settings.secrets.supabase_service_role_key - ) diff --git a/autogpt_platform/backend/backend/server/model.py b/autogpt_platform/backend/backend/server/model.py index f05c708608ec..8085cb3a81e5 100644 --- a/autogpt_platform/backend/backend/server/model.py +++ b/autogpt_platform/backend/backend/server/model.py @@ -5,6 +5,7 @@ from backend.data.api_key import APIKeyPermission, APIKeyWithoutHash from backend.data.graph import Graph +from backend.util.timezone_name import TimeZoneName class WSMethod(enum.Enum): @@ -60,21 +61,6 @@ class UpdatePermissionsRequest(pydantic.BaseModel): permissions: list[APIKeyPermission] -class Pagination(pydantic.BaseModel): - total_items: int = pydantic.Field( - description="Total number of items.", examples=[42] - ) - total_pages: int = pydantic.Field( - description="Total number of pages.", examples=[2] - ) - current_page: int = pydantic.Field( - description="Current_page page number.", examples=[1] - ) - page_size: int = pydantic.Field( - description="Number of items per page.", examples=[25] - ) - - class RequestTopUp(pydantic.BaseModel): credit_amount: int @@ -85,3 +71,12 @@ class UploadFileResponse(pydantic.BaseModel): size: int content_type: str expires_in_hours: int + + +class TimezoneResponse(pydantic.BaseModel): + # Allow "not-set" as a special value, or any valid IANA timezone + timezone: TimeZoneName | str + + +class UpdateTimezoneRequest(pydantic.BaseModel): + timezone: TimeZoneName diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 19c503a1d392..deeb85c39444 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -9,12 +9,6 @@ import pydantic import starlette.middleware.cors import uvicorn -from autogpt_libs.feature_flag.client import ( - initialize_launchdarkly, - shutdown_launchdarkly, -) -from autogpt_libs.logging.utils import generate_uvicorn_config -from autogpt_libs.utils.cache import thread_cached from fastapi.exceptions import RequestValidationError from fastapi.routing import APIRoute @@ -26,6 +20,8 @@ import backend.server.routers.v1 import backend.server.v2.admin.credit_admin_routes import backend.server.v2.admin.store_admin_routes +import backend.server.v2.builder +import backend.server.v2.builder.routes import backend.server.v2.library.db import backend.server.v2.library.model import backend.server.v2.library.routes @@ -41,6 +37,9 @@ from backend.server.external.api import external_app from backend.server.middleware.security import SecurityHeadersMiddleware from backend.util import json +from backend.util.cloud_storage import shutdown_cloud_storage_handler +from backend.util.feature_flag import initialize_launchdarkly, shutdown_launchdarkly +from backend.util.service import UnhealthyServiceError settings = backend.util.settings.Settings() logger = logging.getLogger(__name__) @@ -63,16 +62,25 @@ def launch_darkly_context(): @contextlib.asynccontextmanager async def lifespan_context(app: fastapi.FastAPI): await backend.data.db.connect() - await backend.data.block.initialize_blocks() - # SDK auto-registration is now handled by AutoRegistry.patch_integrations() - # which is called when the SDK module is imported + # Ensure SDK auto-registration is patched before initializing blocks + from backend.sdk.registry import AutoRegistry + + AutoRegistry.patch_integrations() + + await backend.data.block.initialize_blocks() await backend.data.user.migrate_and_encrypt_user_integrations() await backend.data.graph.fix_llm_provider_credentials() await backend.data.graph.migrate_llm_models(LlmModel.GPT4O) with launch_darkly_context(): yield + + try: + await shutdown_cloud_storage_handler() + except Exception as e: + logger.warning(f"Error shutting down cloud storage handler: {e}") + await backend.data.db.disconnect() @@ -189,6 +197,9 @@ async def validation_error_handler( app.include_router( backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store" ) +app.include_router( + backend.server.v2.builder.routes.router, tags=["v2"], prefix="/api/builder" +) app.include_router( backend.server.v2.admin.store_admin_routes.router, tags=["v2", "admin"], @@ -220,19 +231,10 @@ async def validation_error_handler( app.mount("/external-api", external_app) -@thread_cached -def get_db_async_client(): - from backend.executor import DatabaseManagerAsyncClient - - return backend.util.service.get_service_client( - DatabaseManagerAsyncClient, - health_check=False, - ) - - @app.get(path="/health", tags=["health"], dependencies=[]) async def health(): - await get_db_async_client().health_check_async() + if not backend.data.db.is_connected(): + raise UnhealthyServiceError("Database is not connected") return {"status": "healthy"} @@ -249,7 +251,7 @@ def run(self): server_app, host=backend.util.settings.Config().agent_api_host, port=backend.util.settings.Config().agent_api_port, - log_config=generate_uvicorn_config(), + log_config=None, ) def cleanup(self): diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index b4d8670b824d..791b6e526ed2 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -8,8 +8,6 @@ import pydantic import stripe from autogpt_libs.auth.middleware import auth_middleware -from autogpt_libs.feature_flag.client import feature_flag -from autogpt_libs.utils.cache import thread_cached from fastapi import ( APIRouter, Body, @@ -17,6 +15,7 @@ File, HTTPException, Path, + Query, Request, Response, UploadFile, @@ -51,7 +50,6 @@ get_user_credit_model, set_auto_top_up, ) -from backend.data.execution import AsyncRedisExecutionEventBus from backend.data.model import CredentialsMetaInput from backend.data.notifications import NotificationPreference, NotificationPreferenceDTO from backend.data.onboarding import ( @@ -63,9 +61,11 @@ ) from backend.data.user import ( get_or_create_user, + get_user_by_id, get_user_notification_preference, update_user_email, update_user_notification_preference, + update_user_timezone, ) from backend.executor import scheduler from backend.executor import utils as execution_utils @@ -80,22 +80,24 @@ ExecuteGraphResponse, RequestTopUp, SetGraphActiveVersion, + TimezoneResponse, UpdatePermissionsRequest, + UpdateTimezoneRequest, UploadFileResponse, ) from backend.server.utils import get_user_id +from backend.util.clients import get_scheduler_client from backend.util.cloud_storage import get_cloud_storage_handler -from backend.util.exceptions import NotFoundError -from backend.util.service import get_service_client +from backend.util.exceptions import GraphValidationError, NotFoundError from backend.util.settings import Settings +from backend.util.timezone_utils import ( + convert_cron_to_utc, + convert_utc_time_to_user_timezone, + get_user_timezone_or_utc, +) from backend.util.virus_scanner import scan_content_safe -@thread_cached -def execution_scheduler_client() -> scheduler.SchedulerClient: - return get_service_client(scheduler.SchedulerClient, health_check=False) - - def _create_file_size_error(size_bytes: int, max_size_mb: int) -> HTTPException: """Create standardized file size error response.""" return HTTPException( @@ -104,11 +106,6 @@ def _create_file_size_error(size_bytes: int, max_size_mb: int) -> HTTPException: ) -@thread_cached -def execution_event_bus() -> AsyncRedisExecutionEventBus: - return AsyncRedisExecutionEventBus() - - settings = Settings() logger = logging.getLogger(__name__) @@ -161,6 +158,35 @@ async def update_user_email_route( return {"email": email} +@v1_router.get( + "/auth/user/timezone", + summary="Get user timezone", + tags=["auth"], + dependencies=[Depends(auth_middleware)], +) +async def get_user_timezone_route( + user_data: dict = Depends(auth_middleware), +) -> TimezoneResponse: + """Get user timezone setting.""" + user = await get_or_create_user(user_data) + return TimezoneResponse(timezone=user.timezone) + + +@v1_router.post( + "/auth/user/timezone", + summary="Update user timezone", + tags=["auth"], + dependencies=[Depends(auth_middleware)], + response_model=TimezoneResponse, +) +async def update_user_timezone_route( + user_id: Annotated[str, Depends(get_user_id)], request: UpdateTimezoneRequest +) -> TimezoneResponse: + """Update user timezone. The timezone should be a valid IANA timezone identifier.""" + user = await update_user_timezone(user_id, str(request.timezone)) + return TimezoneResponse(timezone=user.timezone) + + @v1_router.get( "/auth/user/preferences", summary="Get notification preferences", @@ -470,12 +496,16 @@ async def stripe_webhook(request: Request): event = stripe.Webhook.construct_event( payload, sig_header, settings.secrets.stripe_webhook_secret ) - except ValueError: + except ValueError as e: # Invalid payload - raise HTTPException(status_code=400) - except stripe.SignatureVerificationError: + raise HTTPException( + status_code=400, detail=f"Invalid payload: {str(e) or type(e).__name__}" + ) + except stripe.SignatureVerificationError as e: # Invalid signature - raise HTTPException(status_code=400) + raise HTTPException( + status_code=400, detail=f"Invalid signature: {str(e) or type(e).__name__}" + ) if ( event["type"] == "checkout.session.completed" @@ -621,16 +651,11 @@ async def create_new_graph( graph.reassign_ids(user_id=user_id, reassign_graph_id=True) graph.validate_graph(for_run=False) - graph = await graph_db.create_graph(graph, user_id=user_id) - - # Create a library agent for the new graph - library_agent = await library_db.create_library_agent(graph, user_id) - _ = asyncio.create_task( - library_db.add_generated_agent_image(graph, library_agent.id) - ) - - graph = await on_graph_activate(graph, user_id=user_id) - return graph + # The return value of the create graph & library function is intentionally not used here, + # as the graph already valid and no sub-graphs are returned back. + await graph_db.create_graph(graph, user_id=user_id) + await library_db.create_library_agent(graph, user_id=user_id) + return await on_graph_activate(graph, user_id=user_id) @v1_router.delete( @@ -693,7 +718,15 @@ async def update_graph( # Handle deactivation of the previously active version await on_graph_deactivate(current_active_version, user_id=user_id) - return new_graph_version + # Fetch new graph version *with sub-graphs* (needed for credentials input schema) + new_graph_version_with_subgraphs = await graph_db.get_graph( + graph_id, + new_graph_version.version, + user_id=user_id, + include_subgraphs=True, + ) + assert new_graph_version_with_subgraphs # make type checker happy + return new_graph_version_with_subgraphs @v1_router.put( @@ -758,15 +791,27 @@ async def execute_graph( detail="Insufficient balance to execute the agent. Please top up your account.", ) - graph_exec = await execution_utils.add_graph_execution( - graph_id=graph_id, - user_id=user_id, - inputs=inputs, - preset_id=preset_id, - graph_version=graph_version, - graph_credentials_inputs=credentials_inputs, - ) - return ExecuteGraphResponse(graph_exec_id=graph_exec.id) + try: + graph_exec = await execution_utils.add_graph_execution( + graph_id=graph_id, + user_id=user_id, + inputs=inputs, + preset_id=preset_id, + graph_version=graph_version, + graph_credentials_inputs=credentials_inputs, + ) + return ExecuteGraphResponse(graph_exec_id=graph_exec.id) + except GraphValidationError as e: + # Return structured validation errors that the frontend can parse + raise HTTPException( + status_code=400, + detail={ + "type": "validation_error", + "message": e.message, + # TODO: only return node-specific errors if user has access to graph + "node_errors": e.node_errors, + }, + ) @v1_router.post( @@ -813,11 +858,11 @@ async def _stop_graph_run( @v1_router.get( path="/executions", - summary="Get all executions", + summary="List all executions", tags=["graphs"], dependencies=[Depends(auth_middleware)], ) -async def get_graphs_executions( +async def list_graphs_executions( user_id: Annotated[str, Depends(get_user_id)], ) -> list[execution_db.GraphExecutionMeta]: return await execution_db.get_graph_executions(user_id=user_id) @@ -825,15 +870,24 @@ async def get_graphs_executions( @v1_router.get( path="/graphs/{graph_id}/executions", - summary="Get graph executions", + summary="List graph executions", tags=["graphs"], dependencies=[Depends(auth_middleware)], ) -async def get_graph_executions( +async def list_graph_executions( graph_id: str, user_id: Annotated[str, Depends(get_user_id)], -) -> list[execution_db.GraphExecutionMeta]: - return await execution_db.get_graph_executions(graph_id=graph_id, user_id=user_id) + page: int = Query(1, ge=1, description="Page number (1-indexed)"), + page_size: int = Query( + 25, ge=1, le=100, description="Number of executions per page" + ), +) -> execution_db.GraphExecutionsPaginated: + return await execution_db.get_graph_executions_paginated( + graph_id=graph_id, + user_id=user_id, + page=page, + page_size=page_size, + ) @v1_router.get( @@ -917,16 +971,36 @@ async def create_graph_execution_schedule( detail=f"Graph #{graph_id} v{schedule_params.graph_version} not found.", ) - return await execution_scheduler_client().add_execution_schedule( + user = await get_user_by_id(user_id) + user_timezone = get_user_timezone_or_utc(user.timezone if user else None) + + # Convert cron expression from user timezone to UTC + try: + utc_cron = convert_cron_to_utc(schedule_params.cron, user_timezone) + except ValueError as e: + raise HTTPException( + status_code=400, + detail=f"Invalid cron expression for timezone {user_timezone}: {e}", + ) + + result = await get_scheduler_client().add_execution_schedule( user_id=user_id, graph_id=graph_id, graph_version=graph.version, name=schedule_params.name, - cron=schedule_params.cron, + cron=utc_cron, # Send UTC cron to scheduler input_data=schedule_params.inputs, input_credentials=schedule_params.credentials, ) + # Convert the next_run_time back to user timezone for display + if result.next_run_time: + result.next_run_time = convert_utc_time_to_user_timezone( + result.next_run_time, user_timezone + ) + + return result + @v1_router.get( path="/graphs/{graph_id}/schedules", @@ -938,11 +1012,24 @@ async def list_graph_execution_schedules( user_id: Annotated[str, Depends(get_user_id)], graph_id: str = Path(), ) -> list[scheduler.GraphExecutionJobInfo]: - return await execution_scheduler_client().get_execution_schedules( + schedules = await get_scheduler_client().get_execution_schedules( user_id=user_id, graph_id=graph_id, ) + # Get user timezone for conversion + user = await get_user_by_id(user_id) + user_timezone = get_user_timezone_or_utc(user.timezone if user else None) + + # Convert next_run_time to user timezone for display + for schedule in schedules: + if schedule.next_run_time: + schedule.next_run_time = convert_utc_time_to_user_timezone( + schedule.next_run_time, user_timezone + ) + + return schedules + @v1_router.get( path="/schedules", @@ -953,7 +1040,20 @@ async def list_graph_execution_schedules( async def list_all_graphs_execution_schedules( user_id: Annotated[str, Depends(get_user_id)], ) -> list[scheduler.GraphExecutionJobInfo]: - return await execution_scheduler_client().get_execution_schedules(user_id=user_id) + schedules = await get_scheduler_client().get_execution_schedules(user_id=user_id) + + # Get user timezone for conversion + user = await get_user_by_id(user_id) + user_timezone = get_user_timezone_or_utc(user.timezone if user else None) + + # Convert UTC next_run_time to user timezone for display + for schedule in schedules: + if schedule.next_run_time: + schedule.next_run_time = convert_utc_time_to_user_timezone( + schedule.next_run_time, user_timezone + ) + + return schedules @v1_router.delete( @@ -967,7 +1067,7 @@ async def delete_graph_execution_schedule( schedule_id: str = Path(..., description="ID of the schedule to delete"), ) -> dict[str, Any]: try: - await execution_scheduler_client().delete_schedule(schedule_id, user_id=user_id) + await get_scheduler_client().delete_schedule(schedule_id, user_id=user_id) except NotFoundError: raise HTTPException( status_code=HTTP_404_NOT_FOUND, @@ -1064,7 +1164,6 @@ async def get_api_key( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def delete_api_key( key_id: str, user_id: Annotated[str, Depends(get_user_id)] ) -> Optional[APIKeyWithoutHash]: @@ -1093,7 +1192,6 @@ async def delete_api_key( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def suspend_key( key_id: str, user_id: Annotated[str, Depends(get_user_id)] ) -> Optional[APIKeyWithoutHash]: @@ -1119,7 +1217,6 @@ async def suspend_key( tags=["api-keys"], dependencies=[Depends(auth_middleware)], ) -@feature_flag("api-keys-enabled") async def update_permissions( key_id: str, request: UpdatePermissionsRequest, diff --git a/autogpt_platform/backend/backend/server/v2/AutoMod/__init__.py b/autogpt_platform/backend/backend/server/v2/AutoMod/__init__.py new file mode 100644 index 000000000000..c721d22efa35 --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/AutoMod/__init__.py @@ -0,0 +1 @@ +# AutoMod integration for content moderation diff --git a/autogpt_platform/backend/backend/server/v2/AutoMod/manager.py b/autogpt_platform/backend/backend/server/v2/AutoMod/manager.py new file mode 100644 index 000000000000..181fcec24875 --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/AutoMod/manager.py @@ -0,0 +1,364 @@ +import asyncio +import json +import logging +from typing import TYPE_CHECKING, Any, Literal + +if TYPE_CHECKING: + from backend.executor import DatabaseManagerAsyncClient + +from pydantic import ValidationError + +from backend.data.execution import ExecutionStatus +from backend.server.v2.AutoMod.models import ( + AutoModRequest, + AutoModResponse, + ModerationConfig, +) +from backend.util.exceptions import ModerationError +from backend.util.feature_flag import Flag, is_feature_enabled +from backend.util.request import Requests +from backend.util.settings import Settings + +logger = logging.getLogger(__name__) + + +class AutoModManager: + + def __init__(self): + self.config = self._load_config() + + def _load_config(self) -> ModerationConfig: + """Load AutoMod configuration from settings""" + settings = Settings() + return ModerationConfig( + enabled=settings.config.automod_enabled, + api_url=settings.config.automod_api_url, + api_key=settings.secrets.automod_api_key, + timeout=settings.config.automod_timeout, + retry_attempts=settings.config.automod_retry_attempts, + retry_delay=settings.config.automod_retry_delay, + fail_open=settings.config.automod_fail_open, + ) + + async def moderate_graph_execution_inputs( + self, db_client: "DatabaseManagerAsyncClient", graph_exec, timeout: int = 10 + ) -> Exception | None: + """ + Complete input moderation flow for graph execution + Returns: error_if_failed (None means success) + """ + if not self.config.enabled: + return None + + # Check if AutoMod feature is enabled for this user + if not await is_feature_enabled(Flag.AUTOMOD, graph_exec.user_id): + logger.debug(f"AutoMod feature not enabled for user {graph_exec.user_id}") + return None + + # Get graph model and collect all inputs + graph_model = await db_client.get_graph( + graph_exec.graph_id, + user_id=graph_exec.user_id, + version=graph_exec.graph_version, + ) + + if not graph_model or not graph_model.nodes: + return None + + all_inputs = [] + for node in graph_model.nodes: + if node.input_default: + all_inputs.extend(str(v) for v in node.input_default.values() if v) + if (masks := graph_exec.nodes_input_masks) and (mask := masks.get(node.id)): + all_inputs.extend(str(v) for v in mask.values() if v) + + if not all_inputs: + return None + + # Combine all content and moderate directly + content = " ".join(all_inputs) + + # Run moderation + logger.warning( + f"Moderating inputs for graph execution {graph_exec.graph_exec_id}" + ) + try: + moderation_passed, content_id = await self._moderate_content( + content, + { + "user_id": graph_exec.user_id, + "graph_id": graph_exec.graph_id, + "graph_exec_id": graph_exec.graph_exec_id, + "moderation_type": "execution_input", + }, + ) + + if not moderation_passed: + logger.warning( + f"Moderation failed for graph execution {graph_exec.graph_exec_id}" + ) + # Update node statuses for frontend display before raising error + await self._update_failed_nodes_for_moderation( + db_client, graph_exec.graph_exec_id, "input", content_id + ) + + return ModerationError( + message="Execution failed due to input content moderation", + user_id=graph_exec.user_id, + graph_exec_id=graph_exec.graph_exec_id, + moderation_type="input", + content_id=content_id, + ) + + return None + + except asyncio.TimeoutError: + logger.warning( + f"Input moderation timed out for graph execution {graph_exec.graph_exec_id}, bypassing moderation" + ) + return None # Bypass moderation on timeout + except Exception as e: + logger.warning(f"Input moderation execution failed: {e}") + return ModerationError( + message="Execution failed due to input content moderation error", + user_id=graph_exec.user_id, + graph_exec_id=graph_exec.graph_exec_id, + moderation_type="input", + ) + + async def moderate_graph_execution_outputs( + self, + db_client: "DatabaseManagerAsyncClient", + graph_exec_id: str, + user_id: str, + graph_id: str, + timeout: int = 10, + ) -> Exception | None: + """ + Complete output moderation flow for graph execution + Returns: error_if_failed (None means success) + """ + if not self.config.enabled: + return None + + # Check if AutoMod feature is enabled for this user + if not await is_feature_enabled(Flag.AUTOMOD, user_id): + logger.debug(f"AutoMod feature not enabled for user {user_id}") + return None + + # Get completed executions and collect outputs + completed_executions = await db_client.get_node_executions( + graph_exec_id, statuses=[ExecutionStatus.COMPLETED], include_exec_data=True + ) + + if not completed_executions: + return None + + all_outputs = [] + for exec_entry in completed_executions: + if exec_entry.output_data: + all_outputs.extend(str(v) for v in exec_entry.output_data.values() if v) + + if not all_outputs: + return None + + # Combine all content and moderate directly + content = " ".join(all_outputs) + + # Run moderation + logger.warning(f"Moderating outputs for graph execution {graph_exec_id}") + try: + moderation_passed, content_id = await self._moderate_content( + content, + { + "user_id": user_id, + "graph_id": graph_id, + "graph_exec_id": graph_exec_id, + "moderation_type": "execution_output", + }, + ) + + if not moderation_passed: + logger.warning(f"Moderation failed for graph execution {graph_exec_id}") + # Update node statuses for frontend display before raising error + await self._update_failed_nodes_for_moderation( + db_client, graph_exec_id, "output", content_id + ) + + return ModerationError( + message="Execution failed due to output content moderation", + user_id=user_id, + graph_exec_id=graph_exec_id, + moderation_type="output", + content_id=content_id, + ) + + return None + + except asyncio.TimeoutError: + logger.warning( + f"Output moderation timed out for graph execution {graph_exec_id}, bypassing moderation" + ) + return None # Bypass moderation on timeout + except Exception as e: + logger.warning(f"Output moderation execution failed: {e}") + return ModerationError( + message="Execution failed due to output content moderation error", + user_id=user_id, + graph_exec_id=graph_exec_id, + moderation_type="output", + ) + + async def _update_failed_nodes_for_moderation( + self, + db_client: "DatabaseManagerAsyncClient", + graph_exec_id: str, + moderation_type: Literal["input", "output"], + content_id: str | None = None, + ): + """Update node execution statuses for frontend display when moderation fails""" + # Import here to avoid circular imports + from backend.executor.manager import send_async_execution_update + + if moderation_type == "input": + # For input moderation, mark queued/running/incomplete nodes as failed + target_statuses = [ + ExecutionStatus.QUEUED, + ExecutionStatus.RUNNING, + ExecutionStatus.INCOMPLETE, + ] + else: + # For output moderation, mark completed nodes as failed + target_statuses = [ExecutionStatus.COMPLETED] + + # Get the executions that need to be updated + executions_to_update = await db_client.get_node_executions( + graph_exec_id, statuses=target_statuses, include_exec_data=True + ) + + if not executions_to_update: + return + + # Create error message with content_id if available + error_message = "Failed due to content moderation" + if content_id: + error_message += f" (Moderation ID: {content_id})" + + # Prepare database update tasks + exec_updates = [] + for exec_entry in executions_to_update: + # Collect all input and output names to clear + cleared_inputs = {} + cleared_outputs = {} + + if exec_entry.input_data: + for name in exec_entry.input_data.keys(): + cleared_inputs[name] = [error_message] + + if exec_entry.output_data: + for name in exec_entry.output_data.keys(): + cleared_outputs[name] = [error_message] + + # Add update task to list + exec_updates.append( + db_client.update_node_execution_status( + exec_entry.node_exec_id, + status=ExecutionStatus.FAILED, + stats={ + "error": error_message, + "cleared_inputs": cleared_inputs, + "cleared_outputs": cleared_outputs, + }, + ) + ) + + # Execute all database updates in parallel + updated_execs = await asyncio.gather(*exec_updates) + + # Send all websocket updates in parallel + await asyncio.gather( + *[ + send_async_execution_update(updated_exec) + for updated_exec in updated_execs + ] + ) + + async def _moderate_content( + self, content: str, metadata: dict[str, Any] + ) -> tuple[bool, str | None]: + """Moderate content using AutoMod API + + Returns: + Tuple of (approval_status, content_id) + - approval_status: True if approved or timeout occurred, False if rejected + - content_id: Reference ID from moderation API, or None if not available + + Raises: + asyncio.TimeoutError: When moderation times out (should be bypassed) + """ + try: + request_data = AutoModRequest( + type="text", + content=content, + metadata=metadata, + ) + + response = await self._make_request(request_data) + + if response.success and response.status == "approved": + logger.debug( + f"Content approved for {metadata.get('graph_exec_id', 'unknown')}" + ) + return True, response.content_id + else: + reasons = [r.reason for r in response.moderation_results if r.reason] + error_msg = f"Content rejected by AutoMod: {'; '.join(reasons)}" + logger.warning(f"Content rejected: {error_msg}") + return False, response.content_id + + except asyncio.TimeoutError: + # Re-raise timeout to be handled by calling methods + logger.warning( + f"AutoMod API timeout for {metadata.get('graph_exec_id', 'unknown')}" + ) + raise + except Exception as e: + logger.error(f"AutoMod moderation error: {e}") + return self.config.fail_open, None + + async def _make_request(self, request_data: AutoModRequest) -> AutoModResponse: + """Make HTTP request to AutoMod API using the standard request utility""" + url = f"{self.config.api_url}/moderate" + headers = { + "Content-Type": "application/json", + "X-API-Key": self.config.api_key.strip(), + } + + # Create requests instance with timeout and retry configuration + requests = Requests( + extra_headers=headers, + retry_max_wait=float(self.config.timeout), + ) + + try: + response = await requests.post( + url, json=request_data.model_dump(), timeout=self.config.timeout + ) + + response_data = response.json() + return AutoModResponse.model_validate(response_data) + + except asyncio.TimeoutError: + # Re-raise timeout error to be caught by _moderate_content + raise + except (json.JSONDecodeError, ValidationError) as e: + raise Exception(f"Invalid response from AutoMod API: {e}") + except Exception as e: + # Check if this is an aiohttp timeout that we should convert + if "timeout" in str(e).lower(): + raise asyncio.TimeoutError(f"AutoMod API request timed out: {e}") + raise Exception(f"AutoMod API request failed: {e}") + + +# Global instance +automod_manager = AutoModManager() diff --git a/autogpt_platform/backend/backend/server/v2/AutoMod/models.py b/autogpt_platform/backend/backend/server/v2/AutoMod/models.py new file mode 100644 index 000000000000..23bd9fe87f6e --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/AutoMod/models.py @@ -0,0 +1,60 @@ +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class AutoModRequest(BaseModel): + """Request model for AutoMod API""" + + type: str = Field(..., description="Content type - 'text', 'image', 'video'") + content: str = Field(..., description="The content to moderate") + metadata: Optional[Dict[str, Any]] = Field( + default=None, description="Additional context about the content" + ) + + +class ModerationResult(BaseModel): + """Individual moderation result""" + + decision: str = Field( + ..., description="Moderation decision: 'approved', 'rejected', 'flagged'" + ) + reason: Optional[str] = Field(default=None, description="Reason for the decision") + + +class AutoModResponse(BaseModel): + """Response model for AutoMod API""" + + success: bool = Field(..., description="Whether the request was successful") + content_id: str = Field( + ..., description="Unique reference ID for this moderation request" + ) + status: str = Field( + ..., description="Overall status: 'approved', 'rejected', 'flagged', 'pending'" + ) + moderation_results: List[ModerationResult] = Field( + default_factory=list, description="List of moderation results" + ) + + +class ModerationConfig(BaseModel): + """Configuration for AutoMod integration""" + + enabled: bool = Field(default=True, description="Whether moderation is enabled") + api_url: str = Field(default="", description="AutoMod API base URL") + api_key: str = Field(..., description="AutoMod API key") + timeout: int = Field(default=30, description="Request timeout in seconds") + retry_attempts: int = Field(default=3, description="Number of retry attempts") + retry_delay: float = Field( + default=1.0, description="Delay between retries in seconds" + ) + fail_open: bool = Field( + default=False, + description="If True, allow execution to continue if moderation fails", + ) + moderate_inputs: bool = Field( + default=True, description="Whether to moderate block inputs" + ) + moderate_outputs: bool = Field( + default=True, description="Whether to moderate block outputs" + ) diff --git a/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes.py b/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes.py index 009c541432a8..36e05f12984a 100644 --- a/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes.py +++ b/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes.py @@ -4,11 +4,11 @@ from autogpt_libs.auth import requires_admin_user from autogpt_libs.auth.depends import get_user_id from fastapi import APIRouter, Body, Depends -from prisma import Json from prisma.enums import CreditTransactionType from backend.data.credit import admin_get_user_history, get_user_credit_model from backend.server.v2.admin.model import AddUserCreditsResponse, UserHistoryResponse +from backend.util.json import SafeJson logger = logging.getLogger(__name__) @@ -40,7 +40,7 @@ async def add_user_credits( user_id, amount, transaction_type=CreditTransactionType.GRANT, - metadata=Json({"admin_id": admin_user, "reason": comments}), + metadata=SafeJson({"admin_id": admin_user, "reason": comments}), ) return { "new_balance": new_balance, diff --git a/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes_test.py b/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes_test.py index cf2a466d5774..e970d9069c45 100644 --- a/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes_test.py +++ b/autogpt_platform/backend/backend/server/v2/admin/credit_admin_routes_test.py @@ -14,7 +14,7 @@ import backend.server.v2.admin.model as admin_model from backend.data.model import UserTransaction from backend.server.conftest import ADMIN_USER_ID, TARGET_USER_ID -from backend.server.model import Pagination +from backend.util.models import Pagination app = fastapi.FastAPI() app.include_router(credit_admin_routes.router) diff --git a/autogpt_platform/backend/backend/server/v2/admin/model.py b/autogpt_platform/backend/backend/server/v2/admin/model.py index 26cc6e6e2e2e..82f51e8e7ac8 100644 --- a/autogpt_platform/backend/backend/server/v2/admin/model.py +++ b/autogpt_platform/backend/backend/server/v2/admin/model.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from backend.data.model import UserTransaction -from backend.server.model import Pagination +from backend.util.models import Pagination class UserHistoryResponse(BaseModel): diff --git a/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py b/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py index 88f69360a40d..f37f83294805 100644 --- a/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py +++ b/autogpt_platform/backend/backend/server/v2/admin/store_admin_routes.py @@ -131,7 +131,7 @@ async def admin_download_agent_file( Raises: HTTPException: If the agent is not found or an unexpected error occurs. """ - graph_data = await backend.server.v2.store.db.get_agent( + graph_data = await backend.server.v2.store.db.get_agent_as_admin( user_id=user.user_id, store_listing_version_id=store_listing_version_id, ) diff --git a/autogpt_platform/backend/backend/server/v2/builder/db.py b/autogpt_platform/backend/backend/server/v2/builder/db.py new file mode 100644 index 000000000000..3cef2655615c --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/builder/db.py @@ -0,0 +1,376 @@ +import functools +import logging +from datetime import datetime, timedelta, timezone + +import prisma + +import backend.data.block +from backend.blocks import load_all_blocks +from backend.blocks.llm import LlmModel +from backend.data.block import Block, BlockCategory, BlockSchema +from backend.data.credit import get_block_costs +from backend.integrations.providers import ProviderName +from backend.server.v2.builder.model import ( + BlockCategoryResponse, + BlockData, + BlockResponse, + BlockType, + CountResponse, + Provider, + ProviderResponse, + SearchBlocksResponse, +) +from backend.util.models import Pagination + +logger = logging.getLogger(__name__) +llm_models = [name.name.lower().replace("_", " ") for name in LlmModel] +_static_counts_cache: dict | None = None +_suggested_blocks: list[BlockData] | None = None + + +def get_block_categories(category_blocks: int = 3) -> list[BlockCategoryResponse]: + categories: dict[BlockCategory, BlockCategoryResponse] = {} + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + # Skip disabled blocks + if block.disabled: + continue + # Skip blocks that don't have categories (all should have at least one) + if not block.categories: + continue + + # Add block to the categories + for category in block.categories: + if category not in categories: + categories[category] = BlockCategoryResponse( + name=category.name.lower(), + total_blocks=0, + blocks=[], + ) + + categories[category].total_blocks += 1 + + # Append if the category has less than the specified number of blocks + if len(categories[category].blocks) < category_blocks: + categories[category].blocks.append(block.to_dict()) + + # Sort categories by name + return sorted(categories.values(), key=lambda x: x.name) + + +def get_blocks( + *, + category: str | None = None, + type: BlockType | None = None, + provider: ProviderName | None = None, + page: int = 1, + page_size: int = 50, +) -> BlockResponse: + """ + Get blocks based on either category, type or provider. + Providing nothing fetches all block types. + """ + # Only one of category, type, or provider can be specified + if (category and type) or (category and provider) or (type and provider): + raise ValueError("Only one of category, type, or provider can be specified") + + blocks: list[Block[BlockSchema, BlockSchema]] = [] + skip = (page - 1) * page_size + take = page_size + total = 0 + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + # Skip disabled blocks + if block.disabled: + continue + # Skip blocks that don't match the category + if category and category not in {c.name.lower() for c in block.categories}: + continue + # Skip blocks that don't match the type + if ( + (type == "input" and block.block_type.value != "Input") + or (type == "output" and block.block_type.value != "Output") + or (type == "action" and block.block_type.value in ("Input", "Output")) + ): + continue + # Skip blocks that don't match the provider + if provider: + credentials_info = block.input_schema.get_credentials_fields_info().values() + if not any(provider in info.provider for info in credentials_info): + continue + + total += 1 + if skip > 0: + skip -= 1 + continue + if take > 0: + take -= 1 + blocks.append(block) + + costs = get_block_costs() + + return BlockResponse( + blocks=[{**b.to_dict(), "costs": costs.get(b.id, [])} for b in blocks], + pagination=Pagination( + total_items=total, + total_pages=(total + page_size - 1) // page_size, + current_page=page, + page_size=page_size, + ), + ) + + +def search_blocks( + include_blocks: bool = True, + include_integrations: bool = True, + query: str = "", + page: int = 1, + page_size: int = 50, +) -> SearchBlocksResponse: + """ + Get blocks based on the filter and query. + `providers` only applies for `integrations` filter. + """ + blocks: list[Block[BlockSchema, BlockSchema]] = [] + query = query.lower() + + total = 0 + skip = (page - 1) * page_size + take = page_size + block_count = 0 + integration_count = 0 + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + # Skip disabled blocks + if block.disabled: + continue + # Skip blocks that don't match the query + if ( + query not in block.name.lower() + and query not in block.description.lower() + and not _matches_llm_model(block.input_schema, query) + ): + continue + keep = False + credentials = list(block.input_schema.get_credentials_fields().values()) + if include_integrations and len(credentials) > 0: + keep = True + integration_count += 1 + if include_blocks and len(credentials) == 0: + keep = True + block_count += 1 + + if not keep: + continue + + total += 1 + if skip > 0: + skip -= 1 + continue + if take > 0: + take -= 1 + blocks.append(block) + + costs = get_block_costs() + + return SearchBlocksResponse( + blocks=BlockResponse( + blocks=[{**b.to_dict(), "costs": costs.get(b.id, [])} for b in blocks], + pagination=Pagination( + total_items=total, + total_pages=(total + page_size - 1) // page_size, + current_page=page, + page_size=page_size, + ), + ), + total_block_count=block_count, + total_integration_count=integration_count, + ) + + +def get_providers( + query: str = "", + page: int = 1, + page_size: int = 50, +) -> ProviderResponse: + providers = [] + query = query.lower() + + skip = (page - 1) * page_size + take = page_size + + all_providers = _get_all_providers() + + for provider in all_providers.values(): + if ( + query not in provider.name.value.lower() + and query not in provider.description.lower() + ): + continue + if skip > 0: + skip -= 1 + continue + if take > 0: + take -= 1 + providers.append(provider) + + total = len(all_providers) + + return ProviderResponse( + providers=providers, + pagination=Pagination( + total_items=total, + total_pages=(total + page_size - 1) // page_size, + current_page=page, + page_size=page_size, + ), + ) + + +async def get_counts(user_id: str) -> CountResponse: + my_agents = await prisma.models.LibraryAgent.prisma().count( + where={ + "userId": user_id, + "isDeleted": False, + "isArchived": False, + } + ) + counts = await _get_static_counts() + return CountResponse( + my_agents=my_agents, + **counts, + ) + + +async def _get_static_counts(): + """ + Get counts of blocks, integrations, and marketplace agents. + This is cached to avoid unnecessary database queries and calculations. + Can't use functools.cache here because the function is async. + """ + global _static_counts_cache + if _static_counts_cache is not None: + return _static_counts_cache + + all_blocks = 0 + input_blocks = 0 + action_blocks = 0 + output_blocks = 0 + integrations = 0 + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + if block.disabled: + continue + + all_blocks += 1 + + if block.block_type.value == "Input": + input_blocks += 1 + elif block.block_type.value == "Output": + output_blocks += 1 + else: + action_blocks += 1 + + credentials = list(block.input_schema.get_credentials_fields().values()) + if len(credentials) > 0: + integrations += 1 + + marketplace_agents = await prisma.models.StoreAgent.prisma().count() + + _static_counts_cache = { + "all_blocks": all_blocks, + "input_blocks": input_blocks, + "action_blocks": action_blocks, + "output_blocks": output_blocks, + "integrations": integrations, + "marketplace_agents": marketplace_agents, + } + + return _static_counts_cache + + +def _matches_llm_model(schema_cls: type[BlockSchema], query: str) -> bool: + for field in schema_cls.model_fields.values(): + if field.annotation == LlmModel: + # Check if query matches any value in llm_models + if any(query in name for name in llm_models): + return True + return False + + +@functools.cache +def _get_all_providers() -> dict[ProviderName, Provider]: + providers: dict[ProviderName, Provider] = {} + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + if block.disabled: + continue + + credentials_info = block.input_schema.get_credentials_fields_info().values() + for info in credentials_info: + for provider in info.provider: # provider is a ProviderName enum member + if provider in providers: + providers[provider].integration_count += 1 + else: + providers[provider] = Provider( + name=provider, description="", integration_count=1 + ) + return providers + + +async def get_suggested_blocks(count: int = 5) -> list[BlockData]: + global _suggested_blocks + + if _suggested_blocks is not None and len(_suggested_blocks) >= count: + return _suggested_blocks[:count] + + _suggested_blocks = [] + # Sum the number of executions for each block type + # Prisma cannot group by nested relations, so we do a raw query + # Calculate the cutoff timestamp + timestamp_threshold = datetime.now(timezone.utc) - timedelta(days=30) + + results = await prisma.get_client().query_raw( + """ + SELECT + agent_node."agentBlockId" AS block_id, + COUNT(execution.id) AS execution_count + FROM "AgentNodeExecution" execution + JOIN "AgentNode" agent_node ON execution."agentNodeId" = agent_node.id + WHERE execution."endedTime" >= $1::timestamp + GROUP BY agent_node."agentBlockId" + ORDER BY execution_count DESC; + """, + timestamp_threshold, + ) + + # Get the top blocks based on execution count + # But ignore Input and Output blocks + blocks: list[tuple[BlockData, int]] = [] + + for block_type in load_all_blocks().values(): + block: Block[BlockSchema, BlockSchema] = block_type() + if block.disabled or block.block_type in ( + backend.data.block.BlockType.INPUT, + backend.data.block.BlockType.OUTPUT, + backend.data.block.BlockType.AGENT, + ): + continue + # Find the execution count for this block + execution_count = next( + (row["execution_count"] for row in results if row["block_id"] == block.id), + 0, + ) + blocks.append((block.to_dict(), execution_count)) + # Sort blocks by execution count + blocks.sort(key=lambda x: x[1], reverse=True) + + _suggested_blocks = [block[0] for block in blocks] + + # Return the top blocks + return _suggested_blocks[:count] diff --git a/autogpt_platform/backend/backend/server/v2/builder/model.py b/autogpt_platform/backend/backend/server/v2/builder/model.py new file mode 100644 index 000000000000..7b9fe58f0a22 --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/builder/model.py @@ -0,0 +1,87 @@ +from typing import Any, Literal + +from pydantic import BaseModel + +import backend.server.v2.library.model as library_model +import backend.server.v2.store.model as store_model +from backend.integrations.providers import ProviderName +from backend.util.models import Pagination + +FilterType = Literal[ + "blocks", + "integrations", + "marketplace_agents", + "my_agents", +] + +BlockType = Literal["all", "input", "action", "output"] + +BlockData = dict[str, Any] + + +# Suggestions +class SuggestionsResponse(BaseModel): + otto_suggestions: list[str] + recent_searches: list[str] + providers: list[ProviderName] + top_blocks: list[BlockData] + + +# All blocks +class BlockCategoryResponse(BaseModel): + name: str + total_blocks: int + blocks: list[BlockData] + + model_config = {"use_enum_values": False} # <== use enum names like "AI" + + +# Input/Action/Output and see all for block categories +class BlockResponse(BaseModel): + blocks: list[BlockData] + pagination: Pagination + + +# Providers +class Provider(BaseModel): + name: ProviderName + description: str + integration_count: int + + +class ProviderResponse(BaseModel): + providers: list[Provider] + pagination: Pagination + + +# Search +class SearchRequest(BaseModel): + search_query: str | None = None + filter: list[FilterType] | None = None + by_creator: list[str] | None = None + search_id: str | None = None + page: int | None = None + page_size: int | None = None + + +class SearchBlocksResponse(BaseModel): + blocks: BlockResponse + total_block_count: int + total_integration_count: int + + +class SearchResponse(BaseModel): + items: list[BlockData | library_model.LibraryAgent | store_model.StoreAgent] + total_items: dict[FilterType, int] + page: int + more_pages: bool + + +class CountResponse(BaseModel): + all_blocks: int + input_blocks: int + action_blocks: int + output_blocks: int + integrations: int + marketplace_agents: int + my_agents: int diff --git a/autogpt_platform/backend/backend/server/v2/builder/routes.py b/autogpt_platform/backend/backend/server/v2/builder/routes.py new file mode 100644 index 000000000000..0e78af3d4b9f --- /dev/null +++ b/autogpt_platform/backend/backend/server/v2/builder/routes.py @@ -0,0 +1,239 @@ +import logging +from typing import Annotated, Sequence + +import fastapi +from autogpt_libs.auth.depends import auth_middleware, get_user_id + +import backend.server.v2.builder.db as builder_db +import backend.server.v2.builder.model as builder_model +import backend.server.v2.library.db as library_db +import backend.server.v2.library.model as library_model +import backend.server.v2.store.db as store_db +import backend.server.v2.store.model as store_model +from backend.integrations.providers import ProviderName +from backend.util.models import Pagination + +logger = logging.getLogger(__name__) + +router = fastapi.APIRouter() + + +# Taken from backend/server/v2/store/db.py +def sanitize_query(query: str | None) -> str | None: + if query is None: + return query + query = query.strip()[:100] + return ( + query.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("'", "\\'") + .replace('"', '\\"') + .replace(";", "\\;") + .replace("--", "\\--") + .replace("/*", "\\/*") + .replace("*/", "\\*/") + ) + + +@router.get( + "/suggestions", + summary="Get Builder suggestions", + dependencies=[fastapi.Depends(auth_middleware)], + response_model=builder_model.SuggestionsResponse, +) +async def get_suggestions( + user_id: Annotated[str, fastapi.Depends(get_user_id)], +) -> builder_model.SuggestionsResponse: + """ + Get all suggestions for the Blocks Menu. + """ + return builder_model.SuggestionsResponse( + otto_suggestions=[ + "What blocks do I need to get started?", + "Help me create a list", + "Help me feed my data to Google Maps", + ], + recent_searches=[ + "image generation", + "deepfake", + "competitor analysis", + ], + providers=[ + ProviderName.TWITTER, + ProviderName.GITHUB, + ProviderName.NOTION, + ProviderName.GOOGLE, + ProviderName.DISCORD, + ProviderName.GOOGLE_MAPS, + ], + top_blocks=await builder_db.get_suggested_blocks(), + ) + + +@router.get( + "/categories", + summary="Get Builder block categories", + dependencies=[fastapi.Depends(auth_middleware)], + response_model=Sequence[builder_model.BlockCategoryResponse], +) +async def get_block_categories( + blocks_per_category: Annotated[int, fastapi.Query()] = 3, +) -> Sequence[builder_model.BlockCategoryResponse]: + """ + Get all block categories with a specified number of blocks per category. + """ + return builder_db.get_block_categories(blocks_per_category) + + +@router.get( + "/blocks", + summary="Get Builder blocks", + dependencies=[fastapi.Depends(auth_middleware)], + response_model=builder_model.BlockResponse, +) +async def get_blocks( + category: Annotated[str | None, fastapi.Query()] = None, + type: Annotated[builder_model.BlockType | None, fastapi.Query()] = None, + provider: Annotated[ProviderName | None, fastapi.Query()] = None, + page: Annotated[int, fastapi.Query()] = 1, + page_size: Annotated[int, fastapi.Query()] = 50, +) -> builder_model.BlockResponse: + """ + Get blocks based on either category, type, or provider. + """ + return builder_db.get_blocks( + category=category, + type=type, + provider=provider, + page=page, + page_size=page_size, + ) + + +@router.get( + "/providers", + summary="Get Builder integration providers", + dependencies=[fastapi.Depends(auth_middleware)], + response_model=builder_model.ProviderResponse, +) +async def get_providers( + page: Annotated[int, fastapi.Query()] = 1, + page_size: Annotated[int, fastapi.Query()] = 50, +) -> builder_model.ProviderResponse: + """ + Get all integration providers with their block counts. + """ + return builder_db.get_providers( + page=page, + page_size=page_size, + ) + + +@router.post( + "/search", + summary="Builder search", + tags=["store", "private"], + dependencies=[fastapi.Depends(auth_middleware)], + response_model=builder_model.SearchResponse, +) +async def search( + options: builder_model.SearchRequest, + user_id: Annotated[str, fastapi.Depends(get_user_id)], +) -> builder_model.SearchResponse: + """ + Search for blocks (including integrations), marketplace agents, and user library agents. + """ + # If no filters are provided, then we will return all types + if not options.filter: + options.filter = [ + "blocks", + "integrations", + "marketplace_agents", + "my_agents", + ] + options.search_query = sanitize_query(options.search_query) + options.page = options.page or 1 + options.page_size = options.page_size or 50 + + # Blocks&Integrations + blocks = builder_model.SearchBlocksResponse( + blocks=builder_model.BlockResponse( + blocks=[], + pagination=Pagination.empty(), + ), + total_block_count=0, + total_integration_count=0, + ) + if "blocks" in options.filter or "integrations" in options.filter: + blocks = builder_db.search_blocks( + include_blocks="blocks" in options.filter, + include_integrations="integrations" in options.filter, + query=options.search_query or "", + page=options.page, + page_size=options.page_size, + ) + + # Library Agents + my_agents = library_model.LibraryAgentResponse( + agents=[], + pagination=Pagination.empty(), + ) + if "my_agents" in options.filter: + my_agents = await library_db.list_library_agents( + user_id=user_id, + search_term=options.search_query, + page=options.page, + page_size=options.page_size, + ) + + # Marketplace Agents + marketplace_agents = store_model.StoreAgentsResponse( + agents=[], + pagination=Pagination.empty(), + ) + if "marketplace_agents" in options.filter: + marketplace_agents = await store_db.get_store_agents( + creators=options.by_creator, + search_query=options.search_query, + page=options.page, + page_size=options.page_size, + ) + + more_pages = False + if ( + blocks.blocks.pagination.current_page < blocks.blocks.pagination.total_pages + or my_agents.pagination.current_page < my_agents.pagination.total_pages + or marketplace_agents.pagination.current_page + < marketplace_agents.pagination.total_pages + ): + more_pages = True + + return builder_model.SearchResponse( + items=blocks.blocks.blocks + my_agents.agents + marketplace_agents.agents, + total_items={ + "blocks": blocks.total_block_count, + "integrations": blocks.total_integration_count, + "marketplace_agents": marketplace_agents.pagination.total_items, + "my_agents": my_agents.pagination.total_items, + }, + page=options.page, + more_pages=more_pages, + ) + + +@router.get( + "/counts", + summary="Get Builder item counts", + dependencies=[fastapi.Depends(auth_middleware)], + response_model=builder_model.CountResponse, +) +async def get_counts( + user_id: Annotated[str, fastapi.Depends(get_user_id)], +) -> builder_model.CountResponse: + """ + Get item counts for the menu categories in the Blocks Menu. + """ + return await builder_db.get_counts(user_id) diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py index 915bd42e778e..9c69b023a0cf 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db.py +++ b/autogpt_platform/backend/backend/server/v2/library/db.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Literal, Optional @@ -8,19 +9,20 @@ import prisma.types import backend.data.graph as graph_db -import backend.server.model import backend.server.v2.library.model as library_model import backend.server.v2.store.exceptions as store_exceptions import backend.server.v2.store.image_gen as store_image_gen import backend.server.v2.store.media as store_media from backend.data.block import BlockInput -from backend.data.db import locked_transaction, transaction +from backend.data.db import transaction from backend.data.execution import get_graph_execution from backend.data.includes import library_agent_include from backend.data.model import CredentialsMetaInput from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate from backend.util.exceptions import NotFoundError +from backend.util.json import SafeJson +from backend.util.models import Pagination from backend.util.settings import Config logger = logging.getLogger(__name__) @@ -129,7 +131,7 @@ async def list_library_agents( # Return the response with only valid agents return library_model.LibraryAgentResponse( agents=valid_library_agents, - pagination=backend.server.model.Pagination( + pagination=Pagination( total_items=agent_count, total_pages=(agent_count + page_size - 1) // page_size, current_page=page, @@ -239,20 +241,24 @@ async def get_library_agent_by_graph_id( ) if not agent: return None - return library_model.LibraryAgent.from_db(agent) + + assert agent.AgentGraph # make type checker happy + # Include sub-graphs so we can make a full credentials input schema + sub_graphs = await graph_db.get_sub_graphs(agent.AgentGraph) + return library_model.LibraryAgent.from_db(agent, sub_graphs=sub_graphs) except prisma.errors.PrismaError as e: logger.error(f"Database error fetching library agent by graph ID: {e}") raise store_exceptions.DatabaseError("Failed to fetch library agent") from e async def add_generated_agent_image( - graph: graph_db.GraphModel, + graph: graph_db.BaseGraph, + user_id: str, library_agent_id: str, ) -> Optional[prisma.models.LibraryAgent]: """ Generates an image for the specified LibraryAgent and updates its record. """ - user_id = graph.user_id graph_id = graph.id # Use .jpeg here since we are generating JPEG images @@ -281,16 +287,19 @@ async def add_generated_agent_image( async def create_library_agent( graph: graph_db.GraphModel, user_id: str, -) -> library_model.LibraryAgent: + create_library_agents_for_sub_graphs: bool = True, +) -> list[library_model.LibraryAgent]: """ Adds an agent to the user's library (LibraryAgent table). Args: agent: The agent/Graph to add to the library. user_id: The user to whom the agent will be added. + create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well. Returns: - The newly created LibraryAgent record. + The newly created LibraryAgent records. + If the graph has sub-graphs, the parent graph will always be the first entry in the list. Raises: AgentNotFoundError: If the specified agent does not exist. @@ -300,26 +309,39 @@ async def create_library_agent( f"Creating library agent for graph #{graph.id} v{graph.version}; " f"user #{user_id}" ) + graph_entries = ( + [graph, *graph.sub_graphs] if create_library_agents_for_sub_graphs else [graph] + ) - try: - agent = await prisma.models.LibraryAgent.prisma().create( - data=prisma.types.LibraryAgentCreateInput( - isCreatedByUser=(user_id == graph.user_id), - useGraphIsActiveVersion=True, - User={"connect": {"id": user_id}}, - # Creator={"connect": {"id": graph.user_id}}, - AgentGraph={ - "connect": { - "graphVersionId": {"id": graph.id, "version": graph.version} - } - }, - ), - include={"AgentGraph": True}, + async with transaction() as tx: + library_agents = await asyncio.gather( + *( + prisma.models.LibraryAgent.prisma(tx).create( + data=prisma.types.LibraryAgentCreateInput( + isCreatedByUser=(user_id == user_id), + useGraphIsActiveVersion=True, + User={"connect": {"id": user_id}}, + # Creator={"connect": {"id": user_id}}, + AgentGraph={ + "connect": { + "graphVersionId": { + "id": graph_entry.id, + "version": graph_entry.version, + } + } + }, + ), + include=library_agent_include(user_id), + ) + for graph_entry in graph_entries + ) ) - return library_model.LibraryAgent.from_db(agent) - except prisma.errors.PrismaError as e: - logger.error(f"Database error creating agent in library: {e}") - raise store_exceptions.DatabaseError("Failed to create agent in library") from e + + # Generate images for the main graph and sub-graphs + for agent, graph in zip(library_agents, graph_entries): + asyncio.create_task(add_generated_agent_image(graph, user_id, agent.id)) + + return [library_model.LibraryAgent.from_db(agent) for agent in library_agents] async def update_agent_version_in_library( @@ -484,68 +506,64 @@ async def add_store_agent_to_library( ) try: - async with locked_transaction(f"add_agent_trx_{user_id}"): - store_listing_version = ( - await prisma.models.StoreListingVersion.prisma().find_unique( - where={"id": store_listing_version_id}, include={"AgentGraph": True} - ) + store_listing_version = ( + await prisma.models.StoreListingVersion.prisma().find_unique( + where={"id": store_listing_version_id}, include={"AgentGraph": True} + ) + ) + if not store_listing_version or not store_listing_version.AgentGraph: + logger.warning( + f"Store listing version not found: {store_listing_version_id}" + ) + raise store_exceptions.AgentNotFoundError( + f"Store listing version {store_listing_version_id} not found or invalid" ) - if not store_listing_version or not store_listing_version.AgentGraph: - logger.warning( - f"Store listing version not found: {store_listing_version_id}" - ) - raise store_exceptions.AgentNotFoundError( - f"Store listing version {store_listing_version_id} not found or invalid" - ) - graph = store_listing_version.AgentGraph + graph = store_listing_version.AgentGraph - # Check if user already has this agent - existing_library_agent = ( - await prisma.models.LibraryAgent.prisma().find_unique( - where={ - "userId_agentGraphId_agentGraphVersion": { - "userId": user_id, - "agentGraphId": graph.id, - "agentGraphVersion": graph.version, - } - }, - include={"AgentGraph": True}, + # Check if user already has this agent + existing_library_agent = await prisma.models.LibraryAgent.prisma().find_unique( + where={ + "userId_agentGraphId_agentGraphVersion": { + "userId": user_id, + "agentGraphId": graph.id, + "agentGraphVersion": graph.version, + } + }, + include={"AgentGraph": True}, + ) + if existing_library_agent: + if existing_library_agent.isDeleted: + # Even if agent exists it needs to be marked as not deleted + await update_library_agent( + existing_library_agent.id, user_id, is_deleted=False ) - ) - if existing_library_agent: - if existing_library_agent.isDeleted: - # Even if agent exists it needs to be marked as not deleted - await update_library_agent( - existing_library_agent.id, user_id, is_deleted=False - ) - else: - logger.debug( - f"User #{user_id} already has graph #{graph.id} " - f"v{graph.version} in their library" - ) - return library_model.LibraryAgent.from_db(existing_library_agent) - - # Create LibraryAgent entry - added_agent = await prisma.models.LibraryAgent.prisma().create( - data={ - "User": {"connect": {"id": user_id}}, - "AgentGraph": { - "connect": { - "graphVersionId": {"id": graph.id, "version": graph.version} - } - }, - "isCreatedByUser": False, - }, - include=library_agent_include(user_id), - ) - logger.debug( - f"Added graph #{graph.id} v{graph.version}" - f"for store listing version #{store_listing_version.id} " - f"to library for user #{user_id}" - ) - return library_model.LibraryAgent.from_db(added_agent) + else: + logger.debug( + f"User #{user_id} already has graph #{graph.id} " + f"v{graph.version} in their library" + ) + return library_model.LibraryAgent.from_db(existing_library_agent) + # Create LibraryAgent entry + added_agent = await prisma.models.LibraryAgent.prisma().create( + data={ + "User": {"connect": {"id": user_id}}, + "AgentGraph": { + "connect": { + "graphVersionId": {"id": graph.id, "version": graph.version} + } + }, + "isCreatedByUser": False, + }, + include=library_agent_include(user_id), + ) + logger.debug( + f"Added graph #{graph.id} v{graph.version}" + f"for store listing version #{store_listing_version.id} " + f"to library for user #{user_id}" + ) + return library_model.LibraryAgent.from_db(added_agent) except store_exceptions.AgentNotFoundError: # Reraise for external handling. raise @@ -611,7 +629,7 @@ async def list_presets( return library_model.LibraryAgentPresetResponse( presets=presets, - pagination=backend.server.model.Pagination( + pagination=Pagination( total_items=total_items, total_pages=total_pages, current_page=page, @@ -687,7 +705,7 @@ async def create_preset( InputPresets={ "create": [ prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa - name=name, data=prisma.fields.Json(data) + name=name, data=SafeJson(data) ) for name, data in { **preset.inputs, @@ -797,7 +815,7 @@ async def update_preset( update_data["InputPresets"] = { "create": [ prisma.types.AgentNodeExecutionInputOutputCreateWithoutRelationsInput( # noqa - name=name, data=prisma.fields.Json(data) + name=name, data=SafeJson(data) ) for name, data in { **inputs, @@ -872,7 +890,9 @@ async def delete_preset(user_id: str, preset_id: str) -> None: raise store_exceptions.DatabaseError("Failed to delete preset") from e -async def fork_library_agent(library_agent_id: str, user_id: str): +async def fork_library_agent( + library_agent_id: str, user_id: str +) -> library_model.LibraryAgent: """ Clones a library agent and its underyling graph and nodes (with new ids) for the given user. @@ -881,33 +901,32 @@ async def fork_library_agent(library_agent_id: str, user_id: str): user_id: The ID of the user who owns the library agent. Returns: - The forked LibraryAgent. + The forked parent (if it has sub-graphs) LibraryAgent. Raises: DatabaseError: If there's an error during the forking process. """ logger.debug(f"Forking library agent {library_agent_id} for user {user_id}") try: - async with locked_transaction(f"usr_trx_{user_id}-fork_agent"): - # Fetch the original agent - original_agent = await get_library_agent(library_agent_id, user_id) - - # Check if user owns the library agent - # TODO: once we have open/closed sourced agents this needs to be enabled ~kcze - # + update library/agents/[id]/page.tsx agent actions - # if not original_agent.can_access_graph: - # raise store_exceptions.DatabaseError( - # f"User {user_id} cannot access library agent graph {library_agent_id}" - # ) - - # Fork the underlying graph and nodes - new_graph = await graph_db.fork_graph( - original_agent.graph_id, original_agent.graph_version, user_id - ) - new_graph = await on_graph_activate(new_graph, user_id=user_id) + # Fetch the original agent + original_agent = await get_library_agent(library_agent_id, user_id) + + # Check if user owns the library agent + # TODO: once we have open/closed sourced agents this needs to be enabled ~kcze + # + update library/agents/[id]/page.tsx agent actions + # if not original_agent.can_access_graph: + # raise store_exceptions.DatabaseError( + # f"User {user_id} cannot access library agent graph {library_agent_id}" + # ) + + # Fork the underlying graph and nodes + new_graph = await graph_db.fork_graph( + original_agent.graph_id, original_agent.graph_version, user_id + ) + new_graph = await on_graph_activate(new_graph, user_id=user_id) - # Create a library agent for the new graph - return await create_library_agent(new_graph, user_id) + # Create a library agent for the new graph + return (await create_library_agent(new_graph, user_id))[0] except prisma.errors.PrismaError as e: logger.error(f"Database error cloning library agent: {e}") raise store_exceptions.DatabaseError("Failed to fork library agent") from e diff --git a/autogpt_platform/backend/backend/server/v2/library/db_test.py b/autogpt_platform/backend/backend/server/v2/library/db_test.py index 0b39b384ea6b..7cb34ed75a82 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db_test.py +++ b/autogpt_platform/backend/backend/server/v2/library/db_test.py @@ -87,7 +87,7 @@ async def test_add_agent_to_library(mocker): await connect() # Mock the transaction context - mock_transaction = mocker.patch("backend.server.v2.library.db.locked_transaction") + mock_transaction = mocker.patch("backend.server.v2.library.db.transaction") mock_transaction.return_value.__aenter__ = mocker.AsyncMock(return_value=None) mock_transaction.return_value.__aexit__ = mocker.AsyncMock(return_value=None) # Mock data diff --git a/autogpt_platform/backend/backend/server/v2/library/model.py b/autogpt_platform/backend/backend/server/v2/library/model.py index fbffbc2cc95f..3838d9a88db2 100644 --- a/autogpt_platform/backend/backend/server/v2/library/model.py +++ b/autogpt_platform/backend/backend/server/v2/library/model.py @@ -8,9 +8,9 @@ import backend.data.block as block_model import backend.data.graph as graph_model -import backend.server.model as server_model from backend.data.model import CredentialsMetaInput, is_credentials_field_name from backend.integrations.providers import ProviderName +from backend.util.models import Pagination class LibraryAgentStatus(str, Enum): @@ -51,6 +51,7 @@ class LibraryAgent(pydantic.BaseModel): description: str input_schema: dict[str, Any] # Should be BlockIOObjectSubSchema in frontend + output_schema: dict[str, Any] credentials_input_schema: dict[str, Any] | None = pydantic.Field( description="Input schema for credentials required by the agent", ) @@ -126,6 +127,7 @@ def from_db( name=graph.name, description=graph.description, input_schema=graph.input_schema, + output_schema=graph.output_schema, credentials_input_schema=( graph.credentials_input_schema if sub_graphs is not None else None ), @@ -213,7 +215,7 @@ class LibraryAgentResponse(pydantic.BaseModel): """Response schema for a list of library agents and pagination info.""" agents: list[LibraryAgent] - pagination: server_model.Pagination + pagination: Pagination class LibraryAgentPresetCreatable(pydantic.BaseModel): @@ -317,7 +319,7 @@ class LibraryAgentPresetResponse(pydantic.BaseModel): """Response schema for a list of agent presets and pagination info.""" presets: list[LibraryAgentPreset] - pagination: server_model.Pagination + pagination: Pagination class LibraryAgentFilter(str, Enum): diff --git a/autogpt_platform/backend/backend/server/v2/library/routes_test.py b/autogpt_platform/backend/backend/server/v2/library/routes_test.py index 97c20e9d922b..2681e9dfb79c 100644 --- a/autogpt_platform/backend/backend/server/v2/library/routes_test.py +++ b/autogpt_platform/backend/backend/server/v2/library/routes_test.py @@ -7,9 +7,9 @@ import pytest_mock from pytest_snapshot.plugin import Snapshot -import backend.server.model as server_model import backend.server.v2.library.model as library_model from backend.server.v2.library.routes import router as library_router +from backend.util.models import Pagination app = fastapi.FastAPI() app.include_router(library_router) @@ -50,6 +50,7 @@ async def test_get_library_agents_success( creator_name="Test Creator", creator_image_url="", input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, credentials_input_schema={"type": "object", "properties": {}}, has_external_trigger=False, status=library_model.LibraryAgentStatus.COMPLETED, @@ -68,6 +69,7 @@ async def test_get_library_agents_success( creator_name="Test Creator", creator_image_url="", input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, credentials_input_schema={"type": "object", "properties": {}}, has_external_trigger=False, status=library_model.LibraryAgentStatus.COMPLETED, @@ -77,7 +79,7 @@ async def test_get_library_agents_success( updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0), ), ], - pagination=server_model.Pagination( + pagination=Pagination( total_items=2, total_pages=1, current_page=1, page_size=50 ), ) @@ -132,6 +134,7 @@ def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture): creator_name="Test Creator", creator_image_url="", input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, credentials_input_schema={"type": "object", "properties": {}}, has_external_trigger=False, status=library_model.LibraryAgentStatus.COMPLETED, diff --git a/autogpt_platform/backend/backend/server/v2/store/db.py b/autogpt_platform/backend/backend/server/v2/store/db.py index f8eced49c1d0..d50bf5c90968 100644 --- a/autogpt_platform/backend/backend/server/v2/store/db.py +++ b/autogpt_platform/backend/backend/server/v2/store/db.py @@ -17,8 +17,21 @@ get_sub_graphs, ) from backend.data.includes import AGENT_GRAPH_INCLUDE +from backend.data.notifications import ( + AgentApprovalData, + AgentRejectionData, + NotificationEventModel, +) +from backend.notifications.notifications import queue_notification_async +from backend.util.settings import Settings logger = logging.getLogger(__name__) +settings = Settings() + + +# Constants for default admin values +DEFAULT_ADMIN_NAME = "AutoGPT Admin" +DEFAULT_ADMIN_EMAIL = "admin@autogpt.co" def sanitize_query(query: str | None) -> str | None: @@ -42,7 +55,7 @@ def sanitize_query(query: str | None) -> str | None: async def get_store_agents( featured: bool = False, - creator: str | None = None, + creators: list[str] | None = None, sorted_by: str | None = None, search_query: str | None = None, category: str | None = None, @@ -53,15 +66,15 @@ async def get_store_agents( Get PUBLIC store agents from the StoreAgent view """ logger.debug( - f"Getting store agents. featured={featured}, creator={creator}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}" + f"Getting store agents. featured={featured}, creators={creators}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}" ) sanitized_query = sanitize_query(search_query) where_clause = {} if featured: where_clause["featured"] = featured - if creator: - where_clause["creator_username"] = creator + if creators: + where_clause["creator_username"] = {"in": creators} if category: where_clause["categories"] = {"has": category} @@ -466,6 +479,8 @@ async def get_store_submissions( # internal_comments omitted for regular users reviewed_at=sub.reviewed_at, changes_summary=sub.changes_summary, + video_url=sub.video_url, + categories=sub.categories, ) submission_models.append(submission_model) @@ -546,7 +561,7 @@ async def create_store_submission( description: str = "", sub_heading: str = "", categories: list[str] = [], - changes_summary: str = "Initial Submission", + changes_summary: str | None = "Initial Submission", ) -> backend.server.v2.store.model.StoreSubmission: """ Create the first (and only) store listing and thus submission as a normal user @@ -685,6 +700,160 @@ async def create_store_submission( ) from e +async def edit_store_submission( + user_id: str, + store_listing_version_id: str, + name: str, + video_url: str | None = None, + image_urls: list[str] = [], + description: str = "", + sub_heading: str = "", + categories: list[str] = [], + changes_summary: str | None = "Update submission", +) -> backend.server.v2.store.model.StoreSubmission: + """ + Edit an existing store listing submission. + + Args: + user_id: ID of the authenticated user editing the submission + store_listing_version_id: ID of the store listing version to edit + agent_id: ID of the agent being submitted + agent_version: Version of the agent being submitted + slug: URL slug for the listing (only changeable for PENDING submissions) + name: Name of the agent + video_url: Optional URL to video demo + image_urls: List of image URLs for the listing + description: Description of the agent + sub_heading: Optional sub-heading for the agent + categories: List of categories for the agent + changes_summary: Summary of changes made in this submission + + Returns: + StoreSubmission: The updated store submission + + Raises: + SubmissionNotFoundError: If the submission is not found + UnauthorizedError: If the user doesn't own the submission + InvalidOperationError: If trying to edit a submission that can't be edited + """ + try: + # Get the current version and verify ownership + current_version = await prisma.models.StoreListingVersion.prisma().find_first( + where=prisma.types.StoreListingVersionWhereInput( + id=store_listing_version_id + ), + include={ + "StoreListing": { + "include": { + "Versions": {"order_by": {"version": "desc"}, "take": 1} + } + } + }, + ) + + if not current_version: + raise backend.server.v2.store.exceptions.SubmissionNotFoundError( + f"Store listing version not found: {store_listing_version_id}" + ) + + # Verify the user owns this submission + if ( + not current_version.StoreListing + or current_version.StoreListing.owningUserId != user_id + ): + raise backend.server.v2.store.exceptions.UnauthorizedError( + f"User {user_id} does not own submission {store_listing_version_id}" + ) + + # Currently we are not allowing user to update the agent associated with a submission + # If we allow it in future, then we need a check here to verify the agent belongs to this user. + + # Check if we can edit this submission + if current_version.submissionStatus == prisma.enums.SubmissionStatus.REJECTED: + raise backend.server.v2.store.exceptions.InvalidOperationError( + "Cannot edit a rejected submission" + ) + + # For APPROVED submissions, we need to create a new version + if current_version.submissionStatus == prisma.enums.SubmissionStatus.APPROVED: + # Create a new version for the existing listing + return await create_store_version( + user_id=user_id, + agent_id=current_version.agentGraphId, + agent_version=current_version.agentGraphVersion, + store_listing_id=current_version.storeListingId, + name=name, + video_url=video_url, + image_urls=image_urls, + description=description, + sub_heading=sub_heading, + categories=categories, + changes_summary=changes_summary, + ) + + # For PENDING submissions, we can update the existing version + elif current_version.submissionStatus == prisma.enums.SubmissionStatus.PENDING: + # Update the existing version + updated_version = await prisma.models.StoreListingVersion.prisma().update( + where={"id": store_listing_version_id}, + data=prisma.types.StoreListingVersionUpdateInput( + name=name, + videoUrl=video_url, + imageUrls=image_urls, + description=description, + categories=categories, + subHeading=sub_heading, + changesSummary=changes_summary, + ), + ) + + logger.debug( + f"Updated existing version {store_listing_version_id} for agent {current_version.agentGraphId}" + ) + + if not updated_version: + raise backend.server.v2.store.exceptions.DatabaseError( + "Failed to update store listing version" + ) + return backend.server.v2.store.model.StoreSubmission( + agent_id=current_version.agentGraphId, + agent_version=current_version.agentGraphVersion, + name=name, + sub_heading=sub_heading, + slug=current_version.StoreListing.slug, + description=description, + image_urls=image_urls, + date_submitted=updated_version.submittedAt or updated_version.createdAt, + status=updated_version.submissionStatus, + runs=0, + rating=0.0, + store_listing_version_id=updated_version.id, + changes_summary=changes_summary, + video_url=video_url, + categories=categories, + version=updated_version.version, + ) + + else: + raise backend.server.v2.store.exceptions.InvalidOperationError( + f"Cannot edit submission with status: {current_version.submissionStatus}" + ) + + except ( + backend.server.v2.store.exceptions.SubmissionNotFoundError, + backend.server.v2.store.exceptions.UnauthorizedError, + backend.server.v2.store.exceptions.AgentNotFoundError, + backend.server.v2.store.exceptions.ListingExistsError, + backend.server.v2.store.exceptions.InvalidOperationError, + ): + raise + except prisma.errors.PrismaError as e: + logger.error(f"Database error editing store submission: {e}") + raise backend.server.v2.store.exceptions.DatabaseError( + "Failed to edit store submission" + ) from e + + async def create_store_version( user_id: str, agent_id: str, @@ -696,7 +865,7 @@ async def create_store_version( description: str = "", sub_heading: str = "", categories: list[str] = [], - changes_summary: str = "Update Submission", + changes_summary: str | None = "Initial submission", ) -> backend.server.v2.store.model.StoreSubmission: """ Create a new version for an existing store listing @@ -1085,7 +1254,8 @@ async def review_store_submission( where={"id": store_listing_version_id}, include={ "StoreListing": True, - "AgentGraph": {"include": AGENT_GRAPH_INCLUDE}, + "AgentGraph": {"include": {**AGENT_GRAPH_INCLUDE, "User": True}}, + "Reviewer": True, }, ) ) @@ -1096,6 +1266,13 @@ async def review_store_submission( detail=f"Store listing version {store_listing_version_id} not found", ) + # Check if we're rejecting an already approved agent + is_rejecting_approved = ( + not is_approved + and store_listing_version.submissionStatus + == prisma.enums.SubmissionStatus.APPROVED + ) + # If approving, update the listing to indicate it has an approved version if is_approved and store_listing_version.AgentGraph: heading = f"Sub-graph of {store_listing_version.name}v{store_listing_version.agentGraphVersion}" @@ -1126,6 +1303,37 @@ async def review_store_submission( }, ) + # If rejecting an approved agent, update the StoreListing accordingly + if is_rejecting_approved: + # Check if there are other approved versions + other_approved = ( + await prisma.models.StoreListingVersion.prisma().find_first( + where={ + "storeListingId": store_listing_version.StoreListing.id, + "id": {"not": store_listing_version_id}, + "submissionStatus": prisma.enums.SubmissionStatus.APPROVED, + } + ) + ) + + if not other_approved: + # No other approved versions, update hasApprovedVersion to False + await prisma.models.StoreListing.prisma().update( + where={"id": store_listing_version.StoreListing.id}, + data={ + "hasApprovedVersion": False, + "ActiveVersion": {"disconnect": True}, + }, + ) + else: + # Set the most recent other approved version as active + await prisma.models.StoreListing.prisma().update( + where={"id": store_listing_version.StoreListing.id}, + data={ + "ActiveVersion": {"connect": {"id": other_approved.id}}, + }, + ) + submission_status = ( prisma.enums.SubmissionStatus.APPROVED if is_approved @@ -1154,6 +1362,89 @@ async def review_store_submission( f"Failed to update store listing version {store_listing_version_id}" ) + # Send email notification to the agent creator + if store_listing_version.AgentGraph and store_listing_version.AgentGraph.User: + agent_creator = store_listing_version.AgentGraph.User + reviewer = ( + store_listing_version.Reviewer + if store_listing_version.Reviewer + else None + ) + + try: + base_url = ( + settings.config.frontend_base_url + or settings.config.platform_base_url + ) + + if is_approved: + store_agent = ( + await prisma.models.StoreAgent.prisma().find_first_or_raise( + where={"storeListingVersionId": submission.id} + ) + ) + + # Send approval notification + notification_data = AgentApprovalData( + agent_name=submission.name, + agent_id=submission.agentGraphId, + agent_version=submission.agentGraphVersion, + reviewer_name=( + reviewer.name + if reviewer and reviewer.name + else DEFAULT_ADMIN_NAME + ), + reviewer_email=( + reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL + ), + comments=external_comments, + reviewed_at=submission.reviewedAt + or datetime.now(tz=timezone.utc), + store_url=f"{base_url}/marketplace/agent/{store_agent.creator_username}/{store_agent.slug}", + ) + + notification_event = NotificationEventModel[AgentApprovalData]( + user_id=agent_creator.id, + type=prisma.enums.NotificationType.AGENT_APPROVED, + data=notification_data, + ) + else: + # Send rejection notification + notification_data = AgentRejectionData( + agent_name=submission.name, + agent_id=submission.agentGraphId, + agent_version=submission.agentGraphVersion, + reviewer_name=( + reviewer.name + if reviewer and reviewer.name + else DEFAULT_ADMIN_NAME + ), + reviewer_email=( + reviewer.email if reviewer else DEFAULT_ADMIN_EMAIL + ), + comments=external_comments, + reviewed_at=submission.reviewedAt + or datetime.now(tz=timezone.utc), + resubmit_url=f"{base_url}/build?flowID={submission.agentGraphId}", + ) + + notification_event = NotificationEventModel[AgentRejectionData]( + user_id=agent_creator.id, + type=prisma.enums.NotificationType.AGENT_REJECTED, + data=notification_data, + ) + + # Queue the notification for immediate sending + await queue_notification_async(notification_event) + logger.info( + f"Queued {'approval' if is_approved else 'rejection'} notification for user {agent_creator.id} and agent {submission.name}" + ) + + except Exception as e: + logger.error(f"Failed to send email notification for agent review: {e}") + # Don't fail the review process if email sending fails + pass + # Convert to Pydantic model for consistency return backend.server.v2.store.model.StoreSubmission( agent_id=submission.agentGraphId, diff --git a/autogpt_platform/backend/backend/server/v2/store/exceptions.py b/autogpt_platform/backend/backend/server/v2/store/exceptions.py index d92196527f26..0b1020db6064 100644 --- a/autogpt_platform/backend/backend/server/v2/store/exceptions.py +++ b/autogpt_platform/backend/backend/server/v2/store/exceptions.py @@ -94,3 +94,15 @@ class SubmissionNotFoundError(StoreError): """Raised when a submission is not found""" pass + + +class InvalidOperationError(StoreError): + """Raised when an operation is not valid for the current state""" + + pass + + +class UnauthorizedError(StoreError): + """Raised when a user is not authorized to perform an action""" + + pass diff --git a/autogpt_platform/backend/backend/server/v2/store/image_gen.py b/autogpt_platform/backend/backend/server/v2/store/image_gen.py index b75536d3cd33..87b7b601dfeb 100644 --- a/autogpt_platform/backend/backend/server/v2/store/image_gen.py +++ b/autogpt_platform/backend/backend/server/v2/store/image_gen.py @@ -16,7 +16,7 @@ StyleType, UpscaleOption, ) -from backend.data.graph import Graph +from backend.data.graph import BaseGraph from backend.data.model import CredentialsMetaInput, ProviderName from backend.integrations.credentials_store import ideogram_credentials from backend.util.request import Requests @@ -34,14 +34,14 @@ class ImageStyle(str, Enum): DIGITAL_ART = "digital art" -async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image(agent: BaseGraph | AgentGraph) -> io.BytesIO: if settings.config.use_agent_image_generation_v2: return await generate_agent_image_v2(graph=agent) else: return await generate_agent_image_v1(agent=agent) -async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v2(graph: BaseGraph | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Ideogram model. Returns: @@ -82,7 +82,7 @@ async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: type=ideogram_credentials.type, ), prompt=prompt, - ideogram_model_name=IdeogramModelName.V2, + ideogram_model_name=IdeogramModelName.V3, aspect_ratio=AspectRatio.ASPECT_16_9, magic_prompt_option=MagicPromptOption.OFF, style_type=StyleType.AUTO, @@ -99,7 +99,7 @@ async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: return io.BytesIO(response.content) -async def generate_agent_image_v1(agent: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v1(agent: BaseGraph | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Flux model via Replicate API. diff --git a/autogpt_platform/backend/backend/server/v2/store/media.py b/autogpt_platform/backend/backend/server/v2/store/media.py index 0f87971b128b..88542dd2c8f2 100644 --- a/autogpt_platform/backend/backend/server/v2/store/media.py +++ b/autogpt_platform/backend/backend/server/v2/store/media.py @@ -33,30 +33,30 @@ async def check_media_exists(user_id: str, filename: str) -> str | None: if not settings.config.media_gcs_bucket_name: raise MissingConfigError("GCS media bucket is not configured") - async_client = async_storage.Storage() - bucket_name = settings.config.media_gcs_bucket_name + async with async_storage.Storage() as async_client: + bucket_name = settings.config.media_gcs_bucket_name - # Check images - image_path = f"users/{user_id}/images/{filename}" - try: - await async_client.download_metadata(bucket_name, image_path) - # If we get here, the file exists - construct public URL - return f"https://storage.googleapis.com/{bucket_name}/{image_path}" - except Exception: - # File doesn't exist, continue to check videos - pass - - # Check videos - video_path = f"users/{user_id}/videos/{filename}" - try: - await async_client.download_metadata(bucket_name, video_path) - # If we get here, the file exists - construct public URL - return f"https://storage.googleapis.com/{bucket_name}/{video_path}" - except Exception: - # File doesn't exist - pass + # Check images + image_path = f"users/{user_id}/images/{filename}" + try: + await async_client.download_metadata(bucket_name, image_path) + # If we get here, the file exists - construct public URL + return f"https://storage.googleapis.com/{bucket_name}/{image_path}" + except Exception: + # File doesn't exist, continue to check videos + pass + + # Check videos + video_path = f"users/{user_id}/videos/{filename}" + try: + await async_client.download_metadata(bucket_name, video_path) + # If we get here, the file exists - construct public URL + return f"https://storage.googleapis.com/{bucket_name}/{video_path}" + except Exception: + # File doesn't exist + pass - return None + return None async def upload_media( @@ -177,22 +177,24 @@ async def upload_media( storage_path = f"users/{user_id}/{media_type}/{unique_filename}" try: - async_client = async_storage.Storage() - bucket_name = settings.config.media_gcs_bucket_name + async with async_storage.Storage() as async_client: + bucket_name = settings.config.media_gcs_bucket_name - file_bytes = await file.read() - await scan_content_safe(file_bytes, filename=unique_filename) + file_bytes = await file.read() + await scan_content_safe(file_bytes, filename=unique_filename) - # Upload using pure async client - await async_client.upload( - bucket_name, storage_path, file_bytes, content_type=content_type - ) + # Upload using pure async client + await async_client.upload( + bucket_name, storage_path, file_bytes, content_type=content_type + ) - # Construct public URL - public_url = f"https://storage.googleapis.com/{bucket_name}/{storage_path}" + # Construct public URL + public_url = ( + f"https://storage.googleapis.com/{bucket_name}/{storage_path}" + ) - logger.info(f"Successfully uploaded file to: {storage_path}") - return public_url + logger.info(f"Successfully uploaded file to: {storage_path}") + return public_url except Exception as e: logger.error(f"GCS storage error: {str(e)}") diff --git a/autogpt_platform/backend/backend/server/v2/store/media_test.py b/autogpt_platform/backend/backend/server/v2/store/media_test.py index b75e17d0beb8..3722d2fdc3db 100644 --- a/autogpt_platform/backend/backend/server/v2/store/media_test.py +++ b/autogpt_platform/backend/backend/server/v2/store/media_test.py @@ -26,6 +26,10 @@ def mock_storage_client(mocker): mock_client = AsyncMock() mock_client.upload = AsyncMock() + # Mock context manager methods + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + # Mock the constructor to return our mock client mocker.patch( "backend.server.v2.store.media.async_storage.Storage", return_value=mock_client diff --git a/autogpt_platform/backend/backend/server/v2/store/model.py b/autogpt_platform/backend/backend/server/v2/store/model.py index ef2bf78c5e2e..a9afaa65964c 100644 --- a/autogpt_platform/backend/backend/server/v2/store/model.py +++ b/autogpt_platform/backend/backend/server/v2/store/model.py @@ -4,7 +4,7 @@ import prisma.enums import pydantic -from backend.server.model import Pagination +from backend.util.models import Pagination class MyAgent(pydantic.BaseModel): @@ -115,11 +115,9 @@ class StoreSubmission(pydantic.BaseModel): reviewed_at: datetime.datetime | None = None changes_summary: str | None = None - reviewer_id: str | None = None - review_comments: str | None = None # External comments visible to creator - internal_comments: str | None = None # Private notes for admin use only - reviewed_at: datetime.datetime | None = None - changes_summary: str | None = None + # Additional fields for editing + video_url: str | None = None + categories: list[str] = [] class StoreSubmissionsResponse(pydantic.BaseModel): @@ -161,6 +159,16 @@ class StoreSubmissionRequest(pydantic.BaseModel): changes_summary: str | None = None +class StoreSubmissionEditRequest(pydantic.BaseModel): + name: str + sub_heading: str + video_url: str | None = None + image_urls: list[str] = [] + description: str = "" + categories: list[str] = [] + changes_summary: str | None = None + + class ProfileDetails(pydantic.BaseModel): name: str username: str diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 6cc272196875..dfe14b3ef044 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -162,7 +162,7 @@ async def get_agents( try: agents = await backend.server.v2.store.db.get_store_agents( featured=featured, - creator=creator, + creators=[creator] if creator else None, sorted_by=sorted_by, search_query=search_query, category=category, @@ -409,9 +409,13 @@ async def get_my_agents( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], + page: typing.Annotated[int, fastapi.Query(ge=1)] = 1, + page_size: typing.Annotated[int, fastapi.Query(ge=1)] = 20, ): try: - agents = await backend.server.v2.store.db.get_my_agents(user_id) + agents = await backend.server.v2.store.db.get_my_agents( + user_id, page=page, page_size=page_size + ) return agents except Exception: logger.exception("Exception occurred whilst getting my agents") @@ -560,6 +564,47 @@ async def create_submission( ) +@router.put( + "/submissions/{store_listing_version_id}", + summary="Edit store submission", + tags=["store", "private"], + dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)], + response_model=backend.server.v2.store.model.StoreSubmission, +) +async def edit_submission( + store_listing_version_id: str, + submission_request: backend.server.v2.store.model.StoreSubmissionEditRequest, + user_id: typing.Annotated[ + str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) + ], +): + """ + Edit an existing store listing submission. + + Args: + store_listing_version_id (str): ID of the store listing version to edit + submission_request (StoreSubmissionRequest): The updated submission details + user_id (str): ID of the authenticated user editing the listing + + Returns: + StoreSubmission: The updated store submission + + Raises: + HTTPException: If there is an error editing the submission + """ + return await backend.server.v2.store.db.edit_store_submission( + user_id=user_id, + store_listing_version_id=store_listing_version_id, + name=submission_request.name, + video_url=submission_request.video_url, + image_urls=submission_request.image_urls, + description=submission_request.description, + sub_heading=submission_request.sub_heading, + categories=submission_request.categories, + changes_summary=submission_request.changes_summary, + ) + + @router.post( "/submissions/media", summary="Upload submission media", diff --git a/autogpt_platform/backend/backend/server/v2/store/routes_test.py b/autogpt_platform/backend/backend/server/v2/store/routes_test.py index f0ff131f2e9f..86eb82e5340d 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes_test.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes_test.py @@ -66,7 +66,7 @@ def test_get_agents_defaults( snapshot.assert_match(json.dumps(response.json(), indent=2), "def_agts") mock_db_call.assert_called_once_with( featured=False, - creator=None, + creators=None, sorted_by=None, search_query=None, category=None, @@ -113,7 +113,7 @@ def test_get_agents_featured( snapshot.assert_match(json.dumps(response.json(), indent=2), "feat_agts") mock_db_call.assert_called_once_with( featured=True, - creator=None, + creators=None, sorted_by=None, search_query=None, category=None, @@ -160,7 +160,7 @@ def test_get_agents_by_creator( snapshot.assert_match(json.dumps(response.json(), indent=2), "agts_by_creator") mock_db_call.assert_called_once_with( featured=False, - creator="specific-creator", + creators=["specific-creator"], sorted_by=None, search_query=None, category=None, @@ -207,7 +207,7 @@ def test_get_agents_sorted( snapshot.assert_match(json.dumps(response.json(), indent=2), "agts_sorted") mock_db_call.assert_called_once_with( featured=False, - creator=None, + creators=None, sorted_by="runs", search_query=None, category=None, @@ -254,7 +254,7 @@ def test_get_agents_search( snapshot.assert_match(json.dumps(response.json(), indent=2), "agts_search") mock_db_call.assert_called_once_with( featured=False, - creator=None, + creators=None, sorted_by=None, search_query="specific", category=None, @@ -300,7 +300,7 @@ def test_get_agents_category( snapshot.assert_match(json.dumps(response.json(), indent=2), "agts_category") mock_db_call.assert_called_once_with( featured=False, - creator=None, + creators=None, sorted_by=None, search_query=None, category="test-category", @@ -349,7 +349,7 @@ def test_get_agents_pagination( snapshot.assert_match(json.dumps(response.json(), indent=2), "agts_pagination") mock_db_call.assert_called_once_with( featured=False, - creator=None, + creators=None, sorted_by=None, search_query=None, category=None, @@ -551,6 +551,8 @@ def test_get_submissions_success( agent_version=1, sub_heading="Test agent subheading", slug="test-agent", + video_url="test.mp4", + categories=["test-category"], ) ], pagination=backend.server.v2.store.model.Pagination( diff --git a/autogpt_platform/backend/backend/server/ws_api.py b/autogpt_platform/backend/backend/server/ws_api.py index 38dd26dc1520..65b747c8b237 100644 --- a/autogpt_platform/backend/backend/server/ws_api.py +++ b/autogpt_platform/backend/backend/server/ws_api.py @@ -6,7 +6,6 @@ import pydantic import uvicorn from autogpt_libs.auth import parse_jwt_token -from autogpt_libs.logging.utils import generate_uvicorn_config from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect from starlette.middleware.cors import CORSMiddleware @@ -309,7 +308,7 @@ def run(self): server_app, host=Config().websocket_server_host, port=Config().websocket_server_port, - log_config=generate_uvicorn_config(), + log_config=None, ) def cleanup(self): diff --git a/autogpt_platform/backend/backend/usecases/block_autogen.py b/autogpt_platform/backend/backend/usecases/block_autogen.py index 2cae49bf1a1a..7ccd766c5edf 100644 --- a/autogpt_platform/backend/backend/usecases/block_autogen.py +++ b/autogpt_platform/backend/backend/usecases/block_autogen.py @@ -1,13 +1,12 @@ from pathlib import Path -from prisma.models import User - from backend.blocks.basic import StoreValueBlock from backend.blocks.block import BlockInstallationBlock from backend.blocks.http import SendWebRequestBlock from backend.blocks.llm import AITextGeneratorBlock from backend.blocks.text import ExtractTextInformationBlock, FillTextTemplateBlock from backend.data.graph import Graph, Link, Node, create_graph +from backend.data.model import User from backend.data.user import get_or_create_user from backend.util.test import SpinTestServer, wait_execution diff --git a/autogpt_platform/backend/backend/usecases/reddit_marketing.py b/autogpt_platform/backend/backend/usecases/reddit_marketing.py index ce702ef59022..1ec381d618ad 100644 --- a/autogpt_platform/backend/backend/usecases/reddit_marketing.py +++ b/autogpt_platform/backend/backend/usecases/reddit_marketing.py @@ -1,9 +1,8 @@ -from prisma.models import User - from backend.blocks.llm import AIStructuredResponseGeneratorBlock from backend.blocks.reddit import GetRedditPostsBlock, PostRedditCommentBlock from backend.blocks.text import FillTextTemplateBlock, MatchTextPatternBlock from backend.data.graph import Graph, Link, Node, create_graph +from backend.data.model import User from backend.data.user import get_or_create_user from backend.util.test import SpinTestServer, wait_execution diff --git a/autogpt_platform/backend/backend/usecases/sample.py b/autogpt_platform/backend/backend/usecases/sample.py index 61b5424e7ea0..234a39c9765f 100644 --- a/autogpt_platform/backend/backend/usecases/sample.py +++ b/autogpt_platform/backend/backend/usecases/sample.py @@ -1,10 +1,9 @@ -from prisma.models import User - from backend.blocks.basic import StoreValueBlock from backend.blocks.io import AgentInputBlock from backend.blocks.text import FillTextTemplateBlock from backend.data import graph from backend.data.graph import create_graph +from backend.data.model import User from backend.data.user import get_or_create_user from backend.util.test import SpinTestServer, wait_execution diff --git a/autogpt_platform/backend/backend/util/clients.py b/autogpt_platform/backend/backend/util/clients.py new file mode 100644 index 000000000000..cdc66a807d82 --- /dev/null +++ b/autogpt_platform/backend/backend/util/clients.py @@ -0,0 +1,164 @@ +""" +Centralized service client helpers with thread caching. +""" + +from functools import cache +from typing import TYPE_CHECKING + +from autogpt_libs.utils.cache import async_cache, thread_cached + +from backend.util.settings import Settings + +settings = Settings() + +if TYPE_CHECKING: + from supabase import AClient, Client + + from backend.data.execution import ( + AsyncRedisExecutionEventBus, + RedisExecutionEventBus, + ) + from backend.data.rabbitmq import AsyncRabbitMQ, SyncRabbitMQ + from backend.executor import DatabaseManagerAsyncClient, DatabaseManagerClient + from backend.executor.scheduler import SchedulerClient + from backend.integrations.credentials_store import IntegrationCredentialsStore + from backend.notifications.notifications import NotificationManagerClient + + +@thread_cached +def get_database_manager_client() -> "DatabaseManagerClient": + """Get a thread-cached DatabaseManagerClient with request retry enabled.""" + from backend.executor import DatabaseManagerClient + from backend.util.service import get_service_client + + return get_service_client(DatabaseManagerClient, request_retry=True) + + +@thread_cached +def get_database_manager_async_client() -> "DatabaseManagerAsyncClient": + """Get a thread-cached DatabaseManagerAsyncClient with request retry enabled.""" + from backend.executor import DatabaseManagerAsyncClient + from backend.util.service import get_service_client + + return get_service_client(DatabaseManagerAsyncClient, request_retry=True) + + +@thread_cached +def get_scheduler_client() -> "SchedulerClient": + """Get a thread-cached SchedulerClient.""" + from backend.executor.scheduler import SchedulerClient + from backend.util.service import get_service_client + + return get_service_client(SchedulerClient) + + +@thread_cached +def get_notification_manager_client() -> "NotificationManagerClient": + """Get a thread-cached NotificationManagerClient.""" + from backend.notifications.notifications import NotificationManagerClient + from backend.util.service import get_service_client + + return get_service_client(NotificationManagerClient) + + +# ============ Execution Event Bus Helpers ============ # + + +@thread_cached +def get_execution_event_bus() -> "RedisExecutionEventBus": + """Get a thread-cached RedisExecutionEventBus.""" + from backend.data.execution import RedisExecutionEventBus + + return RedisExecutionEventBus() + + +@thread_cached +def get_async_execution_event_bus() -> "AsyncRedisExecutionEventBus": + """Get a thread-cached AsyncRedisExecutionEventBus.""" + from backend.data.execution import AsyncRedisExecutionEventBus + + return AsyncRedisExecutionEventBus() + + +# ============ Execution Queue Helpers ============ # + + +@thread_cached +def get_execution_queue() -> "SyncRabbitMQ": + """Get a thread-cached SyncRabbitMQ execution queue client.""" + from backend.data.rabbitmq import SyncRabbitMQ + from backend.executor.utils import create_execution_queue_config + + client = SyncRabbitMQ(create_execution_queue_config()) + client.connect() + return client + + +@thread_cached +async def get_async_execution_queue() -> "AsyncRabbitMQ": + """Get a thread-cached AsyncRabbitMQ execution queue client.""" + from backend.data.rabbitmq import AsyncRabbitMQ + from backend.executor.utils import create_execution_queue_config + + client = AsyncRabbitMQ(create_execution_queue_config()) + await client.connect() + return client + + +# ============ Integration Credentials Store ============ # + + +@thread_cached +def get_integration_credentials_store() -> "IntegrationCredentialsStore": + """Get a thread-cached IntegrationCredentialsStore.""" + from backend.integrations.credentials_store import IntegrationCredentialsStore + + return IntegrationCredentialsStore() + + +# ============ Supabase Clients ============ # + + +@cache +def get_supabase() -> "Client": + """Get a process-cached synchronous Supabase client instance.""" + from supabase import create_client + + return create_client( + settings.secrets.supabase_url, settings.secrets.supabase_service_role_key + ) + + +@async_cache +async def get_async_supabase() -> "AClient": + """Get a process-cached asynchronous Supabase client instance.""" + from supabase import create_async_client + + return await create_async_client( + settings.secrets.supabase_url, settings.secrets.supabase_service_role_key + ) + + +# ============ Notification Queue Helpers ============ # + + +@thread_cached +def get_notification_queue() -> "SyncRabbitMQ": + """Get a thread-cached SyncRabbitMQ notification queue client.""" + from backend.data.rabbitmq import SyncRabbitMQ + from backend.notifications.notifications import create_notification_config + + client = SyncRabbitMQ(create_notification_config()) + client.connect() + return client + + +@thread_cached +async def get_async_notification_queue() -> "AsyncRabbitMQ": + """Get a thread-cached AsyncRabbitMQ notification queue client.""" + from backend.data.rabbitmq import AsyncRabbitMQ + from backend.notifications.notifications import create_notification_config + + client = AsyncRabbitMQ(create_notification_config()) + await client.connect() + return client diff --git a/autogpt_platform/backend/backend/util/cloud_storage.py b/autogpt_platform/backend/backend/util/cloud_storage.py index 81ab7900bdf7..27acc605bb6e 100644 --- a/autogpt_platform/backend/backend/util/cloud_storage.py +++ b/autogpt_platform/backend/backend/util/cloud_storage.py @@ -46,6 +46,20 @@ def _get_async_gcs_client(self): self._async_gcs_client = async_gcs_storage.Storage() return self._async_gcs_client + async def close(self): + """Close all client connections properly.""" + if self._async_gcs_client is not None: + await self._async_gcs_client.close() + self._async_gcs_client = None + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + def _get_sync_gcs_client(self): """Lazy initialization of sync GCS client (only for signed URLs).""" if self._sync_gcs_client is None: @@ -507,6 +521,17 @@ async def get_cloud_storage_handler() -> CloudStorageHandler: return _cloud_storage_handler +async def shutdown_cloud_storage_handler(): + """Properly shutdown the global cloud storage handler.""" + global _cloud_storage_handler + + if _cloud_storage_handler is not None: + async with _handler_lock: + if _cloud_storage_handler is not None: + await _cloud_storage_handler.close() + _cloud_storage_handler = None + + async def cleanup_expired_files_async() -> int: """ Clean up expired files from cloud storage. diff --git a/autogpt_platform/backend/backend/util/decorator.py b/autogpt_platform/backend/backend/util/decorator.py index a0b0ce91baaa..3767435646d2 100644 --- a/autogpt_platform/backend/backend/util/decorator.py +++ b/autogpt_platform/backend/backend/util/decorator.py @@ -16,6 +16,8 @@ from pydantic import BaseModel +from backend.util.logging import TruncatedLogger + class TimingInfo(BaseModel): cpu_time: float @@ -37,19 +39,25 @@ def _end_measurement( P = ParamSpec("P") T = TypeVar("T") -logger = logging.getLogger(__name__) +logger = TruncatedLogger(logging.getLogger(__name__)) -def time_measured(func: Callable[P, T]) -> Callable[P, Tuple[TimingInfo, T]]: +def time_measured( + func: Callable[P, T], +) -> Callable[P, Tuple[TimingInfo, T | BaseException]]: """ Decorator to measure the time taken by a synchronous function to execute. """ @functools.wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Tuple[TimingInfo, T]: + def wrapper( + *args: P.args, **kwargs: P.kwargs + ) -> Tuple[TimingInfo, T | BaseException]: start_wall_time, start_cpu_time = _start_measurement() try: result = func(*args, **kwargs) + except BaseException as e: + result = e finally: wall_duration, cpu_duration = _end_measurement( start_wall_time, start_cpu_time @@ -62,16 +70,20 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Tuple[TimingInfo, T]: def async_time_measured( func: Callable[P, Awaitable[T]], -) -> Callable[P, Awaitable[Tuple[TimingInfo, T]]]: +) -> Callable[P, Awaitable[Tuple[TimingInfo, T | BaseException]]]: """ Decorator to measure the time taken by an async function to execute. """ @functools.wraps(func) - async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Tuple[TimingInfo, T]: + async def async_wrapper( + *args: P.args, **kwargs: P.kwargs + ) -> Tuple[TimingInfo, T | BaseException]: start_wall_time, start_cpu_time = _start_measurement() try: result = await func(*args, **kwargs) + except BaseException as e: + result = e finally: wall_duration, cpu_duration = _end_measurement( start_wall_time, start_cpu_time @@ -120,7 +132,7 @@ def decorator(f: Callable[P, T]) -> Callable[P, T | None]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None: try: return f(*args, **kwargs) - except Exception as e: + except BaseException as e: logger.exception( f"Error when calling function {f.__name__} with arguments {args} {kwargs}: {e}" ) @@ -177,13 +189,13 @@ def async_error_logged(*, swallow: bool = True) -> ( """ def decorator( - f: Callable[P, Coroutine[Any, Any, T]] + f: Callable[P, Coroutine[Any, Any, T]], ) -> Callable[P, Coroutine[Any, Any, T | None]]: @functools.wraps(f) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None: try: return await f(*args, **kwargs) - except Exception as e: + except BaseException as e: logger.exception( f"Error when calling async function {f.__name__} with arguments {args} {kwargs}: {e}" ) diff --git a/autogpt_platform/backend/backend/util/decorator_test.py b/autogpt_platform/backend/backend/util/decorator_test.py index cdc1cc5b7577..07598c59bcaa 100644 --- a/autogpt_platform/backend/backend/util/decorator_test.py +++ b/autogpt_platform/backend/backend/util/decorator_test.py @@ -3,6 +3,7 @@ import pytest from backend.util.decorator import async_error_logged, error_logged, time_measured +from backend.util.retry import continuous_retry @time_measured @@ -72,3 +73,26 @@ async def run_test(): with pytest.raises(ValueError, match="This async error should NOT be swallowed"): asyncio.run(run_test()) + + +def test_continuous_retry_basic(): + """Test that continuous_retry decorator retries on exception.""" + + class MockManager: + def __init__(self): + self.call_count = 0 + + @continuous_retry(retry_delay=0.01) + def failing_method(self): + self.call_count += 1 + if self.call_count <= 2: + # Fail on first two calls + raise RuntimeError("Simulated failure") + return "success" + + mock_manager = MockManager() + + # Should retry and eventually succeed + result = mock_manager.failing_method() + assert result == "success" + assert mock_manager.call_count == 3 # Failed twice, succeeded on third diff --git a/autogpt_platform/backend/backend/util/exceptions.py b/autogpt_platform/backend/backend/util/exceptions.py index 8f2eacc4eaed..3a1efa0e5d85 100644 --- a/autogpt_platform/backend/backend/util/exceptions.py +++ b/autogpt_platform/backend/backend/util/exceptions.py @@ -31,3 +31,55 @@ def __init__(self, message: str, user_id: str, balance: float, amount: float): def __str__(self): """Used to display the error message in the frontend, because we str() the error when sending the execution update""" return self.message + + +class ModerationError(ValueError): + """Content moderation failure during execution""" + + user_id: str + message: str + graph_exec_id: str + moderation_type: str + content_id: str | None + + def __init__( + self, + message: str, + user_id: str, + graph_exec_id: str, + moderation_type: str = "content", + content_id: str | None = None, + ): + super().__init__(message) + self.args = (message, user_id, graph_exec_id, moderation_type, content_id) + self.message = message + self.user_id = user_id + self.graph_exec_id = graph_exec_id + self.moderation_type = moderation_type + self.content_id = content_id + + def __str__(self): + """Used to display the error message in the frontend, because we str() the error when sending the execution update""" + if self.content_id: + return f"{self.message} (Moderation ID: {self.content_id})" + return self.message + + +class GraphValidationError(ValueError): + """Structured validation error for graph validation failures""" + + def __init__( + self, message: str, node_errors: dict[str, dict[str, str]] | None = None + ): + super().__init__(message) + self.message = message + self.node_errors = node_errors or {} + + def __str__(self): + return self.message + "".join( + [ + f"\n {node_id}:" + + "".join([f"\n {k}: {e}" for k, e in errors.items()]) + for node_id, errors in self.node_errors.items() + ] + ) diff --git a/autogpt_platform/backend/backend/util/feature_flag.py b/autogpt_platform/backend/backend/util/feature_flag.py new file mode 100644 index 000000000000..9af04534793b --- /dev/null +++ b/autogpt_platform/backend/backend/util/feature_flag.py @@ -0,0 +1,257 @@ +import contextlib +import logging +from enum import Enum +from functools import wraps +from typing import Any, Awaitable, Callable, TypeVar + +import ldclient +from autogpt_libs.utils.cache import async_ttl_cache +from fastapi import HTTPException +from ldclient import Context, LDClient +from ldclient.config import Config +from typing_extensions import ParamSpec + +from backend.util.settings import Settings + +logger = logging.getLogger(__name__) + +# Load settings at module level +settings = Settings() + +P = ParamSpec("P") +T = TypeVar("T") + +_is_initialized = False + + +class Flag(str, Enum): + """ + Centralized enum for all LaunchDarkly feature flags. + + Add new flags here to ensure consistency across the codebase. + """ + + AUTOMOD = "AutoMod" + AI_ACTIVITY_STATUS = "ai-agent-execution-summary" + BETA_BLOCKS = "beta-blocks" + AGENT_ACTIVITY = "agent-activity" + + +def get_client() -> LDClient: + """Get the LaunchDarkly client singleton.""" + if not _is_initialized: + initialize_launchdarkly() + return ldclient.get() + + +def initialize_launchdarkly() -> None: + sdk_key = settings.secrets.launch_darkly_sdk_key + logger.debug( + f"Initializing LaunchDarkly with SDK key: {'present' if sdk_key else 'missing'}" + ) + + if not sdk_key: + logger.warning("LaunchDarkly SDK key not configured") + return + + config = Config(sdk_key) + ldclient.set_config(config) + + if ldclient.get().is_initialized(): + global _is_initialized + _is_initialized = True + logger.info("LaunchDarkly client initialized successfully") + else: + logger.error("LaunchDarkly client failed to initialize") + + +def shutdown_launchdarkly() -> None: + """Shutdown the LaunchDarkly client.""" + if ldclient.get().is_initialized(): + ldclient.get().close() + logger.info("LaunchDarkly client closed successfully") + + +@async_ttl_cache(maxsize=1000, ttl_seconds=86400) # 1000 entries, 24 hours TTL +async def _fetch_user_context_data(user_id: str) -> Context: + """ + Fetch user context for LaunchDarkly from Supabase. + + Args: + user_id: The user ID to fetch data for + + Returns: + LaunchDarkly Context object + """ + builder = Context.builder(user_id).kind("user").anonymous(True) + + try: + from backend.util.clients import get_supabase + + # If we have user data, update context + response = get_supabase().auth.admin.get_user_by_id(user_id) + if response and response.user: + user = response.user + builder.anonymous(False) + if user.role: + builder.set("role", user.role) + # It's weird, I know, but it is what it is. + builder.set("custom", {"role": user.role}) + if user.email: + builder.set("email", user.email) + builder.set("email_domain", user.email.split("@")[-1]) + + except Exception as e: + logger.warning(f"Failed to fetch user context for {user_id}: {e}") + + return builder.build() + + +async def get_feature_flag_value( + flag_key: str, + user_id: str, + default: Any = None, +) -> Any: + """ + Get the raw value of a feature flag for a user. + + This is the generic function that returns the actual flag value, + which could be a boolean, string, number, or JSON object. + + Args: + flag_key: The LaunchDarkly feature flag key + user_id: The user ID to evaluate the flag for + default: Default value if LaunchDarkly is unavailable or flag evaluation fails + + Returns: + The flag value from LaunchDarkly + """ + try: + client = get_client() + + # Check if client is initialized + if not client.is_initialized(): + logger.debug( + f"LaunchDarkly not initialized, using default={default} for {flag_key}" + ) + return default + + # Get user context from Supabase + context = await _fetch_user_context_data(user_id) + + # Evaluate flag + result = client.variation(flag_key, context, default) + + logger.debug( + f"Feature flag {flag_key} for user {user_id}: {result} (type: {type(result).__name__})" + ) + return result + + except Exception as e: + logger.warning( + f"LaunchDarkly flag evaluation failed for {flag_key}: {e}, using default={default}" + ) + return default + + +async def is_feature_enabled( + flag_key: Flag, + user_id: str, + default: bool = False, +) -> bool: + """ + Check if a feature flag is enabled for a user. + + Args: + flag_key: The Flag enum value + user_id: The user ID to evaluate the flag for + default: Default value if LaunchDarkly is unavailable or flag evaluation fails + + Returns: + True if feature is enabled, False otherwise + """ + result = await get_feature_flag_value(flag_key.value, user_id, default) + + # If the result is already a boolean, return it + if isinstance(result, bool): + return result + + # Log a warning if the flag is not returning a boolean + logger.warning( + f"Feature flag {flag_key} returned non-boolean value: {result} (type: {type(result).__name__}). " + f"This flag should be configured as a boolean in LaunchDarkly. Using default={default}" + ) + + # Return the default if we get a non-boolean value + # This prevents objects from being incorrectly treated as True + return default + + +def feature_flag( + flag_key: str, + default: bool = False, +) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]: + """ + Decorator for async feature flag protected endpoints. + + Args: + flag_key: The LaunchDarkly feature flag key + default: Default value if flag evaluation fails + + Returns: + Decorator that only works with async functions + """ + + def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: + @wraps(func) + async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + try: + user_id = kwargs.get("user_id") + if not user_id: + raise ValueError("user_id is required") + + if not get_client().is_initialized(): + logger.warning( + f"LaunchDarkly not initialized, using default={default}" + ) + is_enabled = default + else: + # Use the internal function directly since we have a raw string flag_key + flag_value = await get_feature_flag_value( + flag_key, str(user_id), default + ) + # Ensure we treat flag value as boolean + if isinstance(flag_value, bool): + is_enabled = flag_value + else: + # Log warning and use default for non-boolean values + logger.warning( + f"Feature flag {flag_key} returned non-boolean value: {flag_value} (type: {type(flag_value).__name__}). " + f"Using default={default}" + ) + is_enabled = default + + if not is_enabled: + raise HTTPException(status_code=404, detail="Feature not available") + + return await func(*args, **kwargs) + except Exception as e: + logger.error(f"Error evaluating feature flag {flag_key}: {e}") + raise + + return async_wrapper + + return decorator + + +@contextlib.contextmanager +def mock_flag_variation(flag_key: str, return_value: Any): + """Context manager for testing feature flags.""" + original_variation = get_client().variation + get_client().variation = lambda key, context, default: ( + return_value if key == flag_key else original_variation(key, context, default) + ) + try: + yield + finally: + get_client().variation = original_variation diff --git a/autogpt_platform/backend/backend/util/feature_flag_test.py b/autogpt_platform/backend/backend/util/feature_flag_test.py new file mode 100644 index 000000000000..9bd99809ff29 --- /dev/null +++ b/autogpt_platform/backend/backend/util/feature_flag_test.py @@ -0,0 +1,113 @@ +import pytest +from fastapi import HTTPException +from ldclient import LDClient + +from backend.util.feature_flag import ( + Flag, + feature_flag, + is_feature_enabled, + mock_flag_variation, +) + + +@pytest.fixture +def ld_client(mocker): + client = mocker.Mock(spec=LDClient) + mocker.patch("ldclient.get", return_value=client) + client.is_initialized.return_value = True + return client + + +@pytest.mark.asyncio +async def test_feature_flag_enabled(ld_client): + ld_client.variation.return_value = True + + @feature_flag("test-flag") + async def test_function(user_id: str): + return "success" + + result = await test_function(user_id="test-user") + assert result == "success" + ld_client.variation.assert_called_once() + + +@pytest.mark.asyncio +async def test_feature_flag_unauthorized_response(ld_client): + ld_client.variation.return_value = False + + @feature_flag("test-flag") + async def test_function(user_id: str): + return "success" + + with pytest.raises(HTTPException) as exc_info: + await test_function(user_id="test-user") + assert exc_info.value.status_code == 404 + + +def test_mock_flag_variation(ld_client): + with mock_flag_variation("test-flag", True): + assert ld_client.variation("test-flag", None, False) is True + + with mock_flag_variation("test-flag", False): + assert ld_client.variation("test-flag", None, True) is False + + +@pytest.mark.asyncio +async def test_is_feature_enabled(ld_client): + """Test the is_feature_enabled helper function.""" + ld_client.is_initialized.return_value = True + ld_client.variation.return_value = True + + result = await is_feature_enabled(Flag.AUTOMOD, "user123", default=False) + assert result is True + + ld_client.variation.assert_called_once() + call_args = ld_client.variation.call_args + assert call_args[0][0] == "AutoMod" # flag_key + assert call_args[0][2] is False # default value + + +@pytest.mark.asyncio +async def test_is_feature_enabled_not_initialized(ld_client): + """Test is_feature_enabled when LaunchDarkly is not initialized.""" + ld_client.is_initialized.return_value = False + + result = await is_feature_enabled(Flag.AGENT_ACTIVITY, "user123", default=True) + assert result is True # Should return default + + ld_client.variation.assert_not_called() + + +@pytest.mark.asyncio +async def test_is_feature_enabled_exception(mocker): + """Test is_feature_enabled when get_client() raises an exception.""" + mocker.patch( + "backend.util.feature_flag.get_client", + side_effect=Exception("Client error"), + ) + + result = await is_feature_enabled(Flag.AGENT_ACTIVITY, "user123", default=True) + assert result is True # Should return default + + +def test_flag_enum_values(): + """Test that Flag enum has expected values.""" + assert Flag.AUTOMOD == "AutoMod" + assert Flag.AI_ACTIVITY_STATUS == "ai-agent-execution-summary" + assert Flag.BETA_BLOCKS == "beta-blocks" + assert Flag.AGENT_ACTIVITY == "agent-activity" + + +@pytest.mark.asyncio +async def test_is_feature_enabled_with_flag_enum(mocker): + """Test is_feature_enabled function with Flag enum.""" + mock_get_feature_flag_value = mocker.patch( + "backend.util.feature_flag.get_feature_flag_value" + ) + mock_get_feature_flag_value.return_value = True + + result = await is_feature_enabled(Flag.AUTOMOD, "user123") + + assert result is True + # Should call with the flag's string value + mock_get_feature_flag_value.assert_called_once() diff --git a/autogpt_platform/backend/backend/util/json.py b/autogpt_platform/backend/backend/util/json.py index 505a7b27c837..e425483c3148 100644 --- a/autogpt_platform/backend/backend/util/json.py +++ b/autogpt_platform/backend/backend/util/json.py @@ -3,6 +3,7 @@ import jsonschema from fastapi.encoders import jsonable_encoder +from prisma import Json from pydantic import BaseModel from .type import type_match @@ -95,3 +96,19 @@ def convert_pydantic_to_json(output_data: Any) -> Any: if is_list_of_basemodels(output_data): return [item.model_dump() for item in output_data] return output_data + + +def SafeJson(data: Any) -> Json: + """Safely serialize data and return Prisma's Json type.""" + if isinstance(data, BaseModel): + return Json( + data.model_dump( + mode="json", + warnings="error", + exclude_none=True, + fallback=lambda v: None, + ) + ) + # Round-trip through JSON to ensure proper serialization with fallback for non-serializable values + json_string = dumps(data, default=lambda v: None) + return Json(json.loads(json_string)) diff --git a/autogpt_platform/backend/backend/util/metrics.py b/autogpt_platform/backend/backend/util/metrics.py index f1698873076f..ff92537d4ea6 100644 --- a/autogpt_platform/backend/backend/util/metrics.py +++ b/autogpt_platform/backend/backend/util/metrics.py @@ -1,4 +1,5 @@ import logging +from enum import Enum import sentry_sdk from pydantic import SecretStr @@ -7,14 +8,21 @@ from backend.util.settings import Settings +settings = Settings() + + +class DiscordChannel(str, Enum): + PLATFORM = "platform" # For platform/system alerts + PRODUCT = "product" # For product alerts (low balance, zero balance, etc.) + def sentry_init(): - sentry_dsn = Settings().secrets.sentry_dsn + sentry_dsn = settings.secrets.sentry_dsn sentry_sdk.init( dsn=sentry_dsn, traces_sample_rate=1.0, profiles_sample_rate=1.0, - environment=f"app:{Settings().config.app_env.value}-behave:{Settings().config.behave_as.value}", + environment=f"app:{settings.config.app_env.value}-behave:{settings.config.behave_as.value}", _experiments={"enable_logs": True}, integrations=[ LoggingIntegration(sentry_logs_level=logging.INFO), @@ -30,12 +38,12 @@ def sentry_capture_error(error: Exception): sentry_sdk.flush() -async def discord_send_alert(content: str): - from backend.blocks.discord import SendDiscordMessageBlock +async def discord_send_alert( + content: str, channel: DiscordChannel = DiscordChannel.PLATFORM +): + from backend.blocks.discord.bot_blocks import SendDiscordMessageBlock from backend.data.model import APIKeyCredentials, CredentialsMetaInput, ProviderName - from backend.util.settings import Settings - settings = Settings() creds = APIKeyCredentials( provider="discord", api_key=SecretStr(settings.secrets.discord_bot_token), @@ -43,6 +51,14 @@ async def discord_send_alert(content: str): expires_at=None, ) + # Select channel based on enum + if channel == DiscordChannel.PLATFORM: + channel_name = settings.config.platform_alert_discord_channel + elif channel == DiscordChannel.PRODUCT: + channel_name = settings.config.product_alert_discord_channel + else: + channel_name = settings.config.platform_alert_discord_channel + return await SendDiscordMessageBlock().run_once( SendDiscordMessageBlock.Input( credentials=CredentialsMetaInput( @@ -52,7 +68,7 @@ async def discord_send_alert(content: str): provider=ProviderName.DISCORD, ), message_content=content, - channel_name=settings.config.platform_alert_discord_channel, + channel_name=channel_name, ), "status", credentials=creds, diff --git a/autogpt_platform/backend/backend/util/models.py b/autogpt_platform/backend/backend/util/models.py new file mode 100644 index 000000000000..a4b21af90043 --- /dev/null +++ b/autogpt_platform/backend/backend/util/models.py @@ -0,0 +1,29 @@ +""" +Shared models and types used across the backend to avoid circular imports. +""" + +import pydantic + + +class Pagination(pydantic.BaseModel): + total_items: int = pydantic.Field( + description="Total number of items.", examples=[42] + ) + total_pages: int = pydantic.Field( + description="Total number of pages.", examples=[2] + ) + current_page: int = pydantic.Field( + description="Current_page page number.", examples=[1] + ) + page_size: int = pydantic.Field( + description="Number of items per page.", examples=[25] + ) + + @staticmethod + def empty() -> "Pagination": + return Pagination( + total_items=0, + total_pages=0, + current_page=0, + page_size=0, + ) diff --git a/autogpt_platform/backend/backend/util/process.py b/autogpt_platform/backend/backend/util/process.py index 1b145c8ba7b0..4e37f960d863 100644 --- a/autogpt_platform/backend/backend/util/process.py +++ b/autogpt_platform/backend/backend/util/process.py @@ -48,10 +48,9 @@ def run(self): """ pass - @classmethod @property - def service_name(cls) -> str: - return cls.__name__ + def service_name(self) -> str: + return self.__class__.__name__ @abstractmethod def cleanup(self): @@ -61,12 +60,6 @@ def cleanup(self): """ pass - def health_check(self) -> str: - """ - A method to check the health of the process. - """ - return "OK" - def execute_run_command(self, silent): signal.signal(signal.SIGTERM, self._self_terminate) signal.signal(signal.SIGINT, self._self_terminate) @@ -79,19 +72,30 @@ def execute_run_command(self, silent): set_service_name(self.service_name) logger.info(f"[{self.service_name}] Starting...") self.run() - except (KeyboardInterrupt, SystemExit) as e: - logger.warning(f"[{self.service_name}] Terminated: {e}; quitting...") + except BaseException as e: + logger.warning( + f"[{self.service_name}] Termination request: {type(e).__name__}; {e} executing cleanup." + ) finally: - if not self.cleaned_up: - self.cleanup() - self.cleaned_up = True + self.cleanup() logger.info(f"[{self.service_name}] Terminated.") + @staticmethod + def llprint(message: str): + """ + Low-level print/log helper function for use in signal handlers. + Regular log/print statements are not allowed in signal handlers. + """ + os.write(sys.stdout.fileno(), (message + "\n").encode()) + def _self_terminate(self, signum: int, frame): if not self.cleaned_up: - self.cleanup() self.cleaned_up = True - sys.exit(0) + sys.exit(0) + else: + self.llprint( + f"[{self.service_name}] Received exit signal {signum}, but cleanup is already underway." + ) # Methods that are executed OUTSIDE the process # @@ -123,7 +127,6 @@ def start(self, background: bool = False, silent: bool = False, **proc_args) -> **proc_args, ) self.process.start() - self.health_check() logger.info(f"[{self.service_name}] started with PID {self.process.pid}") return self.process.pid or 0 diff --git a/autogpt_platform/backend/backend/util/retry.py b/autogpt_platform/backend/backend/util/retry.py index 03d36d4fa45d..c586281af9c2 100644 --- a/autogpt_platform/backend/backend/util/retry.py +++ b/autogpt_platform/backend/backend/util/retry.py @@ -6,13 +6,83 @@ from functools import wraps from uuid import uuid4 -from tenacity import retry, stop_after_attempt, wait_exponential +from tenacity import ( + retry, + retry_if_not_exception_type, + stop_after_attempt, + wait_exponential_jitter, +) from backend.util.process import get_service_name logger = logging.getLogger(__name__) +def _create_retry_callback(context: str = ""): + """Create a retry callback with optional context.""" + + def callback(retry_state): + attempt_number = retry_state.attempt_number + exception = retry_state.outcome.exception() + func_name = getattr(retry_state.fn, "__name__", "unknown") + + prefix = f"{context}: " if context else "" + + if retry_state.outcome.failed and retry_state.next_action is None: + # Final failure + logger.error( + f"{prefix}Giving up after {attempt_number} attempts for '{func_name}': " + f"{type(exception).__name__}: {exception}" + ) + else: + # Retry attempt + logger.warning( + f"{prefix}Retry attempt {attempt_number} for '{func_name}': " + f"{type(exception).__name__}: {exception}" + ) + + return callback + + +def create_retry_decorator( + max_attempts: int = 5, + exclude_exceptions: tuple[type[BaseException], ...] = (), + max_wait: float = 30.0, + context: str = "", + reraise: bool = True, +): + """ + Create a preconfigured retry decorator with sensible defaults. + + Uses exponential backoff with jitter by default. + + Args: + max_attempts: Maximum number of attempts (default: 5) + exclude_exceptions: Tuple of exception types to not retry on + max_wait: Maximum wait time in seconds (default: 30) + context: Optional context string for log messages + reraise: Whether to reraise the final exception (default: True) + + Returns: + Configured retry decorator + """ + if exclude_exceptions: + return retry( + stop=stop_after_attempt(max_attempts), + wait=wait_exponential_jitter(max=max_wait), + before_sleep=_create_retry_callback(context), + reraise=reraise, + retry=retry_if_not_exception_type(exclude_exceptions), + ) + else: + return retry( + stop=stop_after_attempt(max_attempts), + wait=wait_exponential_jitter(max=max_wait), + before_sleep=_create_retry_callback(context), + reraise=reraise, + ) + + def _log_prefix(resource_name: str, conn_id: str): """ Returns a prefix string for logging purposes. @@ -26,8 +96,6 @@ def conn_retry( resource_name: str, action_name: str, max_retry: int = 5, - multiplier: int = 1, - min_wait: float = 1, max_wait: float = 30, ): conn_id = str(uuid4()) @@ -35,13 +103,20 @@ def conn_retry( def on_retry(retry_state): prefix = _log_prefix(resource_name, conn_id) exception = retry_state.outcome.exception() - logger.warning(f"{prefix} {action_name} failed: {exception}. Retrying now...") + + if retry_state.outcome.failed and retry_state.next_action is None: + logger.error(f"{prefix} {action_name} failed after retries: {exception}") + else: + logger.warning( + f"{prefix} {action_name} failed: {exception}. Retrying now..." + ) def decorator(func): is_coroutine = asyncio.iscoroutinefunction(func) + # Use static retry configuration retry_decorator = retry( - stop=stop_after_attempt(max_retry + 1), - wait=wait_exponential(multiplier=multiplier, min=min_wait, max=max_wait), + stop=stop_after_attempt(max_retry + 1), # +1 for the initial attempt + wait=wait_exponential_jitter(max=max_wait), before_sleep=on_retry, reraise=True, ) @@ -76,11 +151,8 @@ async def async_wrapper(*args, **kwargs): return decorator -func_retry = retry( - reraise=False, - stop=stop_after_attempt(5), - wait=wait_exponential(multiplier=1, min=1, max=30), -) +# Preconfigured retry decorator for general functions +func_retry = create_retry_decorator(max_attempts=5) def continuous_retry(*, retry_delay: float = 1.0): @@ -89,14 +161,21 @@ def decorator(func): @wraps(func) def sync_wrapper(*args, **kwargs): + counter = 0 while True: try: return func(*args, **kwargs) except Exception as exc: - logger.exception( - "%s failed with %s — retrying in %.2f s", + counter += 1 + if counter % 10 == 0: + log = logger.exception + else: + log = logger.warning + log( + "%s failed for the %s times, error: [%s] — retrying in %.2fs", func.__name__, - exc, + counter, + str(exc) or type(exc).__name__, retry_delay, ) time.sleep(retry_delay) @@ -104,13 +183,20 @@ def sync_wrapper(*args, **kwargs): @wraps(func) async def async_wrapper(*args, **kwargs): while True: + counter = 0 try: return await func(*args, **kwargs) except Exception as exc: - logger.exception( - "%s failed with %s — retrying in %.2f s", + counter += 1 + if counter % 10 == 0: + log = logger.exception + else: + log = logger.warning + log( + "%s failed for the %s times, error: [%s] — retrying in %.2fs", func.__name__, - exc, + counter, + str(exc) or type(exc).__name__, retry_delay, ) await asyncio.sleep(retry_delay) diff --git a/autogpt_platform/backend/backend/util/retry_test.py b/autogpt_platform/backend/backend/util/retry_test.py index d3192f4f9fac..29b6a192aa93 100644 --- a/autogpt_platform/backend/backend/util/retry_test.py +++ b/autogpt_platform/backend/backend/util/retry_test.py @@ -8,7 +8,7 @@ def test_conn_retry_sync_function(): retry_count = 0 - @conn_retry("Test", "Test function", max_retry=2, max_wait=0.1, min_wait=0.1) + @conn_retry("Test", "Test function", max_retry=2, max_wait=0.1) def test_function(): nonlocal retry_count retry_count -= 1 @@ -30,7 +30,7 @@ def test_function(): async def test_conn_retry_async_function(): retry_count = 0 - @conn_retry("Test", "Test function", max_retry=2, max_wait=0.1, min_wait=0.1) + @conn_retry("Test", "Test function", max_retry=2, max_wait=0.1) async def test_function(): nonlocal retry_count await asyncio.sleep(1) diff --git a/autogpt_platform/backend/backend/util/service.py b/autogpt_platform/backend/backend/util/service.py index 1096101107ac..97ff054fbf11 100644 --- a/autogpt_platform/backend/backend/util/service.py +++ b/autogpt_platform/backend/backend/util/service.py @@ -1,4 +1,6 @@ import asyncio +import concurrent +import concurrent.futures import inspect import logging import os @@ -24,18 +26,12 @@ import uvicorn from fastapi import FastAPI, Request, responses from pydantic import BaseModel, TypeAdapter, create_model -from tenacity import ( - retry, - retry_if_exception_type, - stop_after_attempt, - wait_exponential_jitter, -) import backend.util.exceptions as exceptions from backend.util.json import to_dict from backend.util.metrics import sentry_init from backend.util.process import AppProcess, get_service_name -from backend.util.retry import conn_retry +from backend.util.retry import conn_retry, create_retry_decorator from backend.util.settings import Config logger = logging.getLogger(__name__) @@ -48,6 +44,34 @@ api_comm_timeout = config.pyro_client_comm_timeout api_call_timeout = config.rpc_client_call_timeout + +def _validate_no_prisma_objects(obj: Any, path: str = "result") -> None: + """ + Recursively validate that no Prisma objects are being returned from service methods. + This enforces proper separation of layers - only application models should cross service boundaries. + """ + if obj is None: + return + + # Check if it's a Prisma model object + if hasattr(obj, "__class__") and hasattr(obj.__class__, "__module__"): + module_name = obj.__class__.__module__ + if module_name and "prisma.models" in module_name: + raise ValueError( + f"Prisma object {obj.__class__.__name__} found in {path}. " + "Service methods must return application models, not Prisma objects. " + f"Use {obj.__class__.__name__}.from_db() to convert to application model." + ) + + # Recursively check collections + if isinstance(obj, (list, tuple)): + for i, item in enumerate(obj): + _validate_no_prisma_objects(item, f"{path}[{i}]") + elif isinstance(obj, dict): + for key, value in obj.items(): + _validate_no_prisma_objects(value, f"{path}['{key}']") + + P = ParamSpec("P") R = TypeVar("R") EXPOSED_FLAG = "__exposed__" @@ -73,11 +97,11 @@ def get_port(cls) -> int: @classmethod def get_host(cls) -> str: source_host = os.environ.get(f"{get_service_name().upper()}_HOST", api_host) - target_host = os.environ.get(f"{cls.service_name.upper()}_HOST", api_host) + target_host = os.environ.get(f"{cls.__name__.upper()}_HOST", api_host) if source_host == target_host and source_host != api_host: logger.warning( - f"Service {cls.service_name} is the same host as the source service." + f"Service {cls.__name__} is the same host as the source service." f"Use the localhost of {api_host} instead." ) return api_host @@ -100,12 +124,46 @@ class RemoteCallError(BaseModel): args: Optional[Tuple[Any, ...]] = None +class UnhealthyServiceError(ValueError): + def __init__( + self, message: str = "Service is unhealthy or not ready", log: bool = True + ): + msg = f"[{get_service_name()}] - {message}" + super().__init__(msg) + self.message = msg + if log: + logger.error(self.message) + + def __str__(self): + return self.message + + +class HTTPClientError(Exception): + """Exception for HTTP client errors (4xx status codes) that should not be retried.""" + + def __init__(self, status_code: int, message: str): + self.status_code = status_code + super().__init__(f"HTTP {status_code}: {message}") + + +class HTTPServerError(Exception): + """Exception for HTTP server errors (5xx status codes) that can be retried.""" + + def __init__(self, status_code: int, message: str): + self.status_code = status_code + super().__init__(f"HTTP {status_code}: {message}") + + EXCEPTION_MAPPING = { e.__name__: e for e in [ ValueError, + RuntimeError, TimeoutError, ConnectionError, + UnhealthyServiceError, + HTTPClientError, + HTTPServerError, *[ ErrorType for _, ErrorType in inspect.getmembers(exceptions) @@ -119,6 +177,12 @@ class RemoteCallError(BaseModel): class AppService(BaseAppService, ABC): fastapi_app: FastAPI + log_level: str = "info" + + def set_log_level(self, log_level: str): + """Set the uvicorn log level. Returns self for chaining.""" + self.log_level = log_level + return self @staticmethod def _handle_internal_http_error(status_code: int = 500, log_error: bool = True): @@ -172,17 +236,21 @@ def _create_fastapi_endpoint(self, func: Callable) -> Callable: if asyncio.iscoroutinefunction(f): async def async_endpoint(body: RequestBodyModel): # type: ignore #RequestBodyModel being variable - return await f( + result = await f( **{name: getattr(body, name) for name in type(body).model_fields} ) + _validate_no_prisma_objects(result, f"{func.__name__} result") + return result return async_endpoint else: def sync_endpoint(body: RequestBodyModel): # type: ignore #RequestBodyModel being variable - return f( + result = f( **{name: getattr(body, name) for name in type(body).model_fields} ) + _validate_no_prisma_objects(result, f"{func.__name__} result") + return result return sync_endpoint @@ -191,16 +259,24 @@ def __start_fastapi(self): logger.info( f"[{self.service_name}] Starting RPC server at http://{api_host}:{self.get_port()}" ) + server = uvicorn.Server( uvicorn.Config( self.fastapi_app, host=api_host, port=self.get_port(), - log_level="warning", + log_config=None, # Explicitly None to avoid uvicorn replacing the logger. + log_level=self.log_level, ) ) self.shared_event_loop.run_until_complete(server.serve()) + async def health_check(self) -> str: + """ + A method to check the health of the process. + """ + return "OK" + def run(self): sentry_init() super().run() @@ -265,35 +341,28 @@ def close(self): def get_service_client( service_client_type: Type[ASC], call_timeout: int | None = api_call_timeout, - health_check: bool = True, - request_retry: bool | int = False, + request_retry: bool = False, ) -> ASC: def _maybe_retry(fn: Callable[..., R]) -> Callable[..., R]: """Decorate *fn* with tenacity retry when enabled.""" - nonlocal request_retry - - if isinstance(request_retry, int): - retry_attempts = request_retry - request_retry = True - else: - retry_attempts = api_comm_retry - if not request_retry: return fn - return retry( - reraise=True, - stop=stop_after_attempt(retry_attempts), - wait=wait_exponential_jitter(max=4.0), - retry=retry_if_exception_type( - ( - httpx.ConnectError, - httpx.ReadTimeout, - httpx.WriteTimeout, - httpx.ConnectTimeout, - httpx.RemoteProtocolError, - ) + # Use preconfigured retry decorator for service communication + return create_retry_decorator( + max_attempts=api_comm_retry, + max_wait=5.0, + context="Service communication", + exclude_exceptions=( + # Don't retry these specific exceptions that won't be fixed by retrying + ValueError, # Invalid input/parameters + KeyError, # Missing required data + TypeError, # Wrong data types + AttributeError, # Missing attributes + asyncio.CancelledError, # Task was cancelled + concurrent.futures.CancelledError, # Future was cancelled + HTTPClientError, # HTTP 4xx client errors - don't retry ), )(fn) @@ -303,57 +372,162 @@ def __init__(self) -> None: host = service_type.get_host() port = service_type.get_port() self.base_url = f"http://{host}:{port}".rstrip("/") + self._connection_failure_count = 0 + self._last_client_reset = 0 - @cached_property - def sync_client(self) -> httpx.Client: + def _create_sync_client(self) -> httpx.Client: return httpx.Client( base_url=self.base_url, timeout=call_timeout, + limits=httpx.Limits( + max_keepalive_connections=200, # 10x default for async concurrent calls + max_connections=500, # High limit for burst handling + keepalive_expiry=30.0, # Keep connections alive longer + ), ) - @cached_property - def async_client(self) -> httpx.AsyncClient: + def _create_async_client(self) -> httpx.AsyncClient: return httpx.AsyncClient( base_url=self.base_url, timeout=call_timeout, + limits=httpx.Limits( + max_keepalive_connections=200, # 10x default for async concurrent calls + max_connections=500, # High limit for burst handling + keepalive_expiry=30.0, # Keep connections alive longer + ), ) + @cached_property + def sync_client(self) -> httpx.Client: + return self._create_sync_client() + + @cached_property + def async_client(self) -> httpx.AsyncClient: + return self._create_async_client() + + def _handle_connection_error(self, error: Exception) -> None: + """Handle connection errors and implement self-healing""" + self._connection_failure_count += 1 + current_time = time.time() + + # If we've had 3+ failures, and it's been more than 30 seconds since last reset + if ( + self._connection_failure_count >= 3 + and current_time - self._last_client_reset > 30 + ): + + logger.warning( + f"Connection failures detected ({self._connection_failure_count}), recreating HTTP clients" + ) + + # Clear cached clients to force recreation on next access + # Only recreate when there's actually a problem + if hasattr(self, "sync_client"): + delattr(self, "sync_client") + if hasattr(self, "async_client"): + delattr(self, "async_client") + + # Reset counters + self._connection_failure_count = 0 + self._last_client_reset = current_time + def _handle_call_method_response( self, *, response: httpx.Response, method_name: str ) -> Any: try: response.raise_for_status() + # Reset failure count on successful response + self._connection_failure_count = 0 return response.json() except httpx.HTTPStatusError as e: - logger.error(f"HTTP error in {method_name}: {e.response.text}") - error = RemoteCallError.model_validate(e.response.json()) - # DEBUG HELP: if you made a custom exception, make sure you override self.args to be how to make your exception - raise EXCEPTION_MAPPING.get(error.type, Exception)( - *(error.args or [str(e)]) - ) + status_code = e.response.status_code + + # Try to parse the error response as RemoteCallError for mapped exceptions + error_response = None + try: + error_response = RemoteCallError.model_validate(e.response.json()) + except Exception: + pass + + # If we successfully parsed a mapped exception type, re-raise it + if error_response and error_response.type in EXCEPTION_MAPPING: + exception_class = EXCEPTION_MAPPING[error_response.type] + args = error_response.args or [str(e)] + raise exception_class(*args) + + # Otherwise categorize by HTTP status code + if 400 <= status_code < 500: + # Client errors (4xx) - wrap to prevent retries + raise HTTPClientError(status_code, str(e)) + elif 500 <= status_code < 600: + # Server errors (5xx) - wrap but allow retries + raise HTTPServerError(status_code, str(e)) + else: + # Other status codes (1xx, 2xx, 3xx) - re-raise original error + raise e @_maybe_retry def _call_method_sync(self, method_name: str, **kwargs: Any) -> Any: - return self._handle_call_method_response( - method_name=method_name, - response=self.sync_client.post(method_name, json=to_dict(kwargs)), - ) + try: + return self._handle_call_method_response( + method_name=method_name, + response=self.sync_client.post(method_name, json=to_dict(kwargs)), + ) + except (httpx.ConnectError, httpx.ConnectTimeout) as e: + self._handle_connection_error(e) + raise @_maybe_retry async def _call_method_async(self, method_name: str, **kwargs: Any) -> Any: - return self._handle_call_method_response( - method_name=method_name, - response=await self.async_client.post( - method_name, json=to_dict(kwargs) - ), - ) + try: + return self._handle_call_method_response( + method_name=method_name, + response=await self.async_client.post( + method_name, json=to_dict(kwargs) + ), + ) + except (httpx.ConnectError, httpx.ConnectTimeout) as e: + self._handle_connection_error(e) + raise async def aclose(self) -> None: - self.sync_client.close() - await self.async_client.aclose() + if hasattr(self, "sync_client"): + self.sync_client.close() + if hasattr(self, "async_client"): + await self.async_client.aclose() def close(self) -> None: - self.sync_client.close() + if hasattr(self, "sync_client"): + self.sync_client.close() + # Note: Cannot close async client synchronously + + def __del__(self): + """Cleanup HTTP clients on garbage collection to prevent resource leaks.""" + try: + if hasattr(self, "sync_client"): + self.sync_client.close() + if hasattr(self, "async_client"): + # Note: Can't await in __del__, so we just close sync + # The async client will be cleaned up by garbage collection + import warnings + + warnings.warn( + "DynamicClient async client not explicitly closed. " + "Call aclose() before destroying the client.", + ResourceWarning, + stacklevel=2, + ) + except Exception: + # Silently ignore cleanup errors in __del__ + pass + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.aclose() def _get_params( self, signature: inspect.Signature, *args: Any, **kwargs: Any @@ -403,8 +577,6 @@ def sync_method(*args: P.args, **kwargs: P.kwargs): return sync_method client = cast(ASC, DynamicClient()) - if health_check and hasattr(client, "health_check"): - client.health_check() return client diff --git a/autogpt_platform/backend/backend/util/service_test.py b/autogpt_platform/backend/backend/util/service_test.py index 3d8eb29419cf..1683c642208b 100644 --- a/autogpt_platform/backend/backend/util/service_test.py +++ b/autogpt_platform/backend/backend/util/service_test.py @@ -1,8 +1,15 @@ +import time +from functools import cached_property +from unittest.mock import Mock + +import httpx import pytest from backend.util.service import ( AppService, AppServiceClient, + HTTPClientError, + HTTPServerError, endpoint_to_async, expose, get_service_client, @@ -11,9 +18,16 @@ TEST_SERVICE_PORT = 8765 +def wait_for_service_ready(service_client_type, timeout_seconds=30): + """Helper method to wait for a service to be ready using health check with retry.""" + client = get_service_client(service_client_type, request_retry=True) + client.health_check() # This will retry until service is ready + + class ServiceTest(AppService): def __init__(self): super().__init__() + self.fail_count = 0 def cleanup(self): pass @@ -22,6 +36,15 @@ def cleanup(self): def get_port(cls) -> int: return TEST_SERVICE_PORT + def __enter__(self): + # Start the service + result = super().__enter__() + + # Wait for the service to be ready + wait_for_service_ready(ServiceTestClient) + + return result + @expose def add(self, a: int, b: int) -> int: return a + b @@ -37,6 +60,19 @@ async def add_async(a: int, b: int) -> int: return self.run_and_wait(add_async(a, b)) + @expose + def failing_add(self, a: int, b: int) -> int: + """Method that fails 2 times then succeeds - for testing retry logic""" + self.fail_count += 1 + if self.fail_count <= 2: + raise RuntimeError(f"Intended error for testing {self.fail_count}/2") + return a + b + + @expose + def always_failing_add(self, a: int, b: int) -> int: + """Method that always fails - for testing no retry when disabled""" + raise RuntimeError("Intended error for testing") + class ServiceTestClient(AppServiceClient): @classmethod @@ -46,6 +82,8 @@ def get_service_type(cls): add = ServiceTest.add subtract = ServiceTest.subtract fun_with_async = ServiceTest.fun_with_async + failing_add = ServiceTest.failing_add + always_failing_add = ServiceTest.always_failing_add add_async = endpoint_to_async(ServiceTest.add) subtract_async = endpoint_to_async(ServiceTest.subtract) @@ -60,3 +98,395 @@ async def test_service_creation(server): assert client.fun_with_async(5, 3) == 8 assert await client.add_async(5, 3) == 8 assert await client.subtract_async(10, 4) == 6 + + +class TestDynamicClientConnectionHealing: + """Test the DynamicClient connection healing logic""" + + def setup_method(self): + """Setup for each test method""" + # Create a mock service client type + self.mock_service_type = Mock() + self.mock_service_type.get_host.return_value = "localhost" + self.mock_service_type.get_port.return_value = 8000 + + self.mock_service_client_type = Mock() + self.mock_service_client_type.get_service_type.return_value = ( + self.mock_service_type + ) + + # Create our test client with the real DynamicClient logic + self.client = self._create_test_client() + + def _create_test_client(self): + """Create a test client that mimics the real DynamicClient""" + + class TestClient: + def __init__(self, service_client_type): + service_type = service_client_type.get_service_type() + host = service_type.get_host() + port = service_type.get_port() + self.base_url = f"http://{host}:{port}".rstrip("/") + self._connection_failure_count = 0 + self._last_client_reset = 0 + + def _create_sync_client(self) -> httpx.Client: + return Mock(spec=httpx.Client) + + def _create_async_client(self) -> httpx.AsyncClient: + return Mock(spec=httpx.AsyncClient) + + @cached_property + def sync_client(self) -> httpx.Client: + return self._create_sync_client() + + @cached_property + def async_client(self) -> httpx.AsyncClient: + return self._create_async_client() + + def _handle_connection_error(self, error: Exception) -> None: + """Handle connection errors and implement self-healing""" + self._connection_failure_count += 1 + current_time = time.time() + + # If we've had 3+ failures and it's been more than 30 seconds since last reset + if ( + self._connection_failure_count >= 3 + and current_time - self._last_client_reset > 30 + ): + + # Clear cached clients to force recreation on next access + if hasattr(self, "sync_client"): + delattr(self, "sync_client") + if hasattr(self, "async_client"): + delattr(self, "async_client") + + # Reset counters + self._connection_failure_count = 0 + self._last_client_reset = current_time + + return TestClient(self.mock_service_client_type) + + def test_client_caching(self): + """Test that clients are cached via @cached_property""" + # Get clients multiple times + sync1 = self.client.sync_client + sync2 = self.client.sync_client + async1 = self.client.async_client + async2 = self.client.async_client + + # Should return same instances (cached) + assert sync1 is sync2, "Sync clients should be cached" + assert async1 is async2, "Async clients should be cached" + + def test_connection_error_counting(self): + """Test that connection errors are counted correctly""" + initial_count = self.client._connection_failure_count + + # Simulate connection errors + self.client._handle_connection_error(Exception("Connection failed")) + assert self.client._connection_failure_count == initial_count + 1 + + self.client._handle_connection_error(Exception("Connection failed")) + assert self.client._connection_failure_count == initial_count + 2 + + def test_no_reset_before_threshold(self): + """Test that clients are NOT reset before reaching failure threshold""" + # Get initial clients + sync_before = self.client.sync_client + async_before = self.client.async_client + + # Simulate 2 failures (below threshold of 3) + self.client._handle_connection_error(Exception("Connection failed")) + self.client._handle_connection_error(Exception("Connection failed")) + + # Clients should still be the same (no reset) + sync_after = self.client.sync_client + async_after = self.client.async_client + + assert ( + sync_before is sync_after + ), "Sync client should not be reset before threshold" + assert ( + async_before is async_after + ), "Async client should not be reset before threshold" + assert self.client._connection_failure_count == 2 + + def test_no_reset_within_time_window(self): + """Test that clients are NOT reset if within the 30-second window""" + # Get initial clients + sync_before = self.client.sync_client + async_before = self.client.async_client + + # Set last reset to recent time (within 30 seconds) + self.client._last_client_reset = time.time() - 10 # 10 seconds ago + + # Simulate 3+ failures + for _ in range(3): + self.client._handle_connection_error(Exception("Connection failed")) + + # Clients should still be the same (no reset due to time window) + sync_after = self.client.sync_client + async_after = self.client.async_client + + assert ( + sync_before is sync_after + ), "Sync client should not be reset within time window" + assert ( + async_before is async_after + ), "Async client should not be reset within time window" + assert self.client._connection_failure_count == 3 + + def test_reset_after_threshold_and_time(self): + """Test that clients ARE reset after threshold failures and time window""" + # Get initial clients + sync_before = self.client.sync_client + async_before = self.client.async_client + + # Set last reset to old time (beyond 30 seconds) + self.client._last_client_reset = time.time() - 60 # 60 seconds ago + + # Simulate 3+ failures to trigger reset + for _ in range(3): + self.client._handle_connection_error(Exception("Connection failed")) + + # Clients should be different (reset occurred) + sync_after = self.client.sync_client + async_after = self.client.async_client + + assert ( + sync_before is not sync_after + ), "Sync client should be reset after threshold" + assert ( + async_before is not async_after + ), "Async client should be reset after threshold" + assert ( + self.client._connection_failure_count == 0 + ), "Failure count should be reset" + + def test_reset_counters_after_healing(self): + """Test that counters are properly reset after healing""" + # Set up for reset + self.client._last_client_reset = time.time() - 60 + self.client._connection_failure_count = 5 + + # Trigger reset + self.client._handle_connection_error(Exception("Connection failed")) + + # Check counters are reset + assert self.client._connection_failure_count == 0 + assert self.client._last_client_reset > time.time() - 5 # Recently reset + + +class TestConnectionHealingIntegration: + """Integration tests for the complete connection healing workflow""" + + def test_failure_count_reset_on_success(self): + """Test that failure count would be reset on successful requests""" + + # This simulates what happens in _handle_call_method_response + class ClientWithSuccessHandling: + def __init__(self): + self._connection_failure_count = 5 + + def _handle_successful_response(self): + # This is what happens in the real _handle_call_method_response + self._connection_failure_count = 0 + + client = ClientWithSuccessHandling() + client._handle_successful_response() + assert client._connection_failure_count == 0 + + def test_thirty_second_window_timing(self): + """Test that the 30-second window works as expected""" + current_time = time.time() + + # Test cases for the timing logic + test_cases = [ + (current_time - 10, False), # 10 seconds ago - should NOT reset + (current_time - 29, False), # 29 seconds ago - should NOT reset + (current_time - 31, True), # 31 seconds ago - should reset + (current_time - 60, True), # 60 seconds ago - should reset + ] + + for last_reset_time, should_reset in test_cases: + failure_count = 3 # At threshold + time_condition = current_time - last_reset_time > 30 + should_trigger_reset = failure_count >= 3 and time_condition + + assert ( + should_trigger_reset == should_reset + ), f"Time window logic failed for {current_time - last_reset_time} seconds ago" + + +def test_cached_property_behavior(): + """Test that @cached_property works as expected for our use case""" + creation_count = 0 + + class TestCachedProperty: + @cached_property + def expensive_resource(self): + nonlocal creation_count + creation_count += 1 + return f"resource-{creation_count}" + + obj = TestCachedProperty() + + # First access should create + resource1 = obj.expensive_resource + assert creation_count == 1 + + # Second access should return cached + resource2 = obj.expensive_resource + assert creation_count == 1 # No additional creation + assert resource1 is resource2 + + # Deleting the cached property should allow recreation + delattr(obj, "expensive_resource") + resource3 = obj.expensive_resource + assert creation_count == 2 # New creation + assert resource1 != resource3 + + +def test_service_with_runtime_error_retries(server): + """Test a real service method that throws RuntimeError and gets retried""" + with ServiceTest(): + # Get client with retry enabled + client = get_service_client(ServiceTestClient, request_retry=True) + + # This should succeed after retries (fails 2 times, succeeds on 3rd try) + result = client.failing_add(5, 3) + assert result == 8 + + +def test_service_no_retry_when_disabled(server): + """Test that retry doesn't happen when disabled""" + with ServiceTest(): + # Get client with retry disabled + client = get_service_client(ServiceTestClient, request_retry=False) + + # This should fail immediately without retry + with pytest.raises(RuntimeError, match="Intended error for testing"): + client.always_failing_add(5, 3) + + +class TestHTTPErrorRetryBehavior: + """Test that HTTP client errors (4xx) are not retried but server errors (5xx) can be.""" + + # Note: These tests access private methods for testing internal behavior + # Type ignore comments are used to suppress warnings about accessing private methods + + def test_http_client_error_not_retried(self): + """Test that 4xx errors are wrapped as HTTPClientError and not retried.""" + # Create a mock response with 404 status + mock_response = Mock() + mock_response.status_code = 404 + mock_response.json.return_value = {"message": "Not found"} + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "404 Not Found", request=Mock(), response=mock_response + ) + + # Create client + client = get_service_client(ServiceTestClient) + dynamic_client = client + + # Test the _handle_call_method_response directly + with pytest.raises(HTTPClientError) as exc_info: + dynamic_client._handle_call_method_response( # type: ignore[attr-defined] + response=mock_response, method_name="test_method" + ) + + assert exc_info.value.status_code == 404 + assert "404" in str(exc_info.value) + + def test_http_server_error_can_be_retried(self): + """Test that 5xx errors are wrapped as HTTPServerError and can be retried.""" + # Create a mock response with 500 status + mock_response = Mock() + mock_response.status_code = 500 + mock_response.json.return_value = {"message": "Internal server error"} + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "500 Internal Server Error", request=Mock(), response=mock_response + ) + + # Create client + client = get_service_client(ServiceTestClient) + dynamic_client = client + + # Test the _handle_call_method_response directly + with pytest.raises(HTTPServerError) as exc_info: + dynamic_client._handle_call_method_response( # type: ignore[attr-defined] + response=mock_response, method_name="test_method" + ) + + assert exc_info.value.status_code == 500 + assert "500" in str(exc_info.value) + + def test_mapped_exception_preserves_original_type(self): + """Test that mapped exceptions preserve their original type regardless of HTTP status.""" + # Create a mock response with ValueError in the remote call error + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "type": "ValueError", + "args": ["Invalid parameter value"], + } + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "400 Bad Request", request=Mock(), response=mock_response + ) + + # Create client + client = get_service_client(ServiceTestClient) + dynamic_client = client + + # Test the _handle_call_method_response directly + with pytest.raises(ValueError) as exc_info: + dynamic_client._handle_call_method_response( # type: ignore[attr-defined] + response=mock_response, method_name="test_method" + ) + + assert "Invalid parameter value" in str(exc_info.value) + + def test_client_error_status_codes_coverage(self): + """Test that various 4xx status codes are all wrapped as HTTPClientError.""" + client_error_codes = [400, 401, 403, 404, 405, 409, 422, 429] + + for status_code in client_error_codes: + mock_response = Mock() + mock_response.status_code = status_code + mock_response.json.return_value = {"message": f"Error {status_code}"} + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + f"{status_code} Error", request=Mock(), response=mock_response + ) + + client = get_service_client(ServiceTestClient) + dynamic_client = client + + with pytest.raises(HTTPClientError) as exc_info: + dynamic_client._handle_call_method_response( # type: ignore + response=mock_response, method_name="test_method" + ) + + assert exc_info.value.status_code == status_code + + def test_server_error_status_codes_coverage(self): + """Test that various 5xx status codes are all wrapped as HTTPServerError.""" + server_error_codes = [500, 501, 502, 503, 504, 505] + + for status_code in server_error_codes: + mock_response = Mock() + mock_response.status_code = status_code + mock_response.json.return_value = {"message": f"Error {status_code}"} + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + f"{status_code} Error", request=Mock(), response=mock_response + ) + + client = get_service_client(ServiceTestClient) + dynamic_client = client + + with pytest.raises(HTTPServerError) as exc_info: + dynamic_client._handle_call_method_response( # type: ignore + response=mock_response, method_name="test_method" + ) + + assert exc_info.value.status_code == status_code diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index fdf35128e572..89f13f965b62 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -68,7 +68,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): description="The default timeout in seconds, for Pyro client connections.", ) pyro_client_comm_retry: int = Field( - default=3, + default=5, description="The default number of retries for Pyro client connections.", ) rpc_client_call_timeout: int = Field( @@ -95,6 +95,10 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): default=500, description="Maximum number of credits above the balance to be auto-approved.", ) + low_balance_threshold: int = Field( + default=500, + description="Credit threshold for low balance notifications (100 = $1, default 500 = $5)", + ) refund_notification_email: str = Field( default="refund@agpt.co", description="Email address to send refund notifications to.", @@ -250,6 +254,10 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): default="local-alerts", description="The Discord channel for the platform", ) + product_alert_discord_channel: str = Field( + default="product-alerts", + description="The Discord channel for product alerts (low balance, zero balance, etc.)", + ) clamav_service_host: str = Field( default="localhost", @@ -295,6 +303,32 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): description="Maximum file size in MB for file uploads (1-1024 MB)", ) + # AutoMod configuration + automod_enabled: bool = Field( + default=False, + description="Whether AutoMod content moderation is enabled", + ) + automod_api_url: str = Field( + default="", + description="AutoMod API base URL - Make sure it ends in /api", + ) + automod_timeout: int = Field( + default=30, + description="Timeout in seconds for AutoMod API requests", + ) + automod_retry_attempts: int = Field( + default=3, + description="Number of retry attempts for AutoMod API requests", + ) + automod_retry_delay: float = Field( + default=1.0, + description="Delay between retries for AutoMod API requests in seconds", + ) + automod_fail_open: bool = Field( + default=False, + description="If True, allow execution to continue if AutoMod fails", + ) + @field_validator("platform_base_url", "frontend_base_url") @classmethod def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str: @@ -334,7 +368,7 @@ def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str: description="Maximum message size limit for communication with the message bus", ) - backend_cors_allow_origins: List[str] = Field(default_factory=list) + backend_cors_allow_origins: List[str] = Field(default=["http://localhost:3000"]) @field_validator("backend_cors_allow_origins") @classmethod @@ -439,6 +473,10 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): twitter_client_secret: str = Field( default="", description="Twitter/X OAuth client secret" ) + discord_client_id: str = Field(default="", description="Discord OAuth client ID") + discord_client_secret: str = Field( + default="", description="Discord OAuth client secret" + ) openai_api_key: str = Field(default="", description="OpenAI API key") aiml_api_key: str = Field(default="", description="'AI/ML API' key") @@ -446,6 +484,7 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): groq_api_key: str = Field(default="", description="Groq API key") open_router_api_key: str = Field(default="", description="Open Router API Key") llama_api_key: str = Field(default="", description="Llama API Key") + v0_api_key: str = Field(default="", description="v0 by Vercel API key") reddit_client_id: str = Field(default="", description="Reddit client ID") reddit_client_secret: str = Field(default="", description="Reddit client secret") @@ -495,9 +534,20 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): apollo_api_key: str = Field(default="", description="Apollo API Key") smartlead_api_key: str = Field(default="", description="SmartLead API Key") zerobounce_api_key: str = Field(default="", description="ZeroBounce API Key") + enrichlayer_api_key: str = Field(default="", description="Enrichlayer API Key") - # Add more secret fields as needed + # AutoMod API credentials + automod_api_key: str = Field(default="", description="AutoMod API key") + # LaunchDarkly feature flags + launch_darkly_sdk_key: str = Field( + default="", + description="The LaunchDarkly SDK key for feature flag management", + ) + + ayrshare_api_key: str = Field(default="", description="Ayrshare API Key") + ayrshare_jwt_key: str = Field(default="", description="Ayrshare private Key") + # Add more secret fields as needed model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", diff --git a/autogpt_platform/backend/backend/util/test.py b/autogpt_platform/backend/backend/util/test.py index 72a39d40270a..e2e4cc79b028 100644 --- a/autogpt_platform/backend/backend/util/test.py +++ b/autogpt_platform/backend/backend/util/test.py @@ -9,6 +9,7 @@ from backend.data.execution import ( ExecutionStatus, NodeExecutionResult, + UserContext, get_graph_execution, ) from backend.data.model import _BaseCredentials @@ -138,6 +139,7 @@ async def async_mock( "graph_exec_id": str(uuid.uuid4()), "node_exec_id": str(uuid.uuid4()), "user_id": str(uuid.uuid4()), + "user_context": UserContext(timezone="UTC"), # Default for tests } input_model = cast(type[BlockSchema], block.input_schema) credentials_input_fields = input_model.get_credentials_fields() diff --git a/autogpt_platform/backend/backend/util/test_json.py b/autogpt_platform/backend/backend/util/test_json.py new file mode 100644 index 000000000000..9e8602ea0868 --- /dev/null +++ b/autogpt_platform/backend/backend/util/test_json.py @@ -0,0 +1,217 @@ +import datetime +from typing import Any, Optional + +from prisma import Json +from pydantic import BaseModel + +from backend.util.json import SafeJson + + +class SamplePydanticModel(BaseModel): + name: str + age: Optional[int] = None + timestamp: Optional[datetime.datetime] = None + metadata: Optional[dict] = None + + +class SampleModelWithNonSerializable(BaseModel): + name: str + func: Any = None # Could contain non-serializable data + data: Optional[dict] = None + + +class TestSafeJson: + """Test cases for SafeJson function.""" + + def test_safejson_returns_json_type(self): + """Test that SafeJson returns a proper Json instance.""" + data = {"test": "value"} + result = SafeJson(data) + assert isinstance(result, Json) + + def test_simple_dict_serialization(self): + """Test basic dictionary serialization.""" + data = {"name": "John", "age": 30, "active": True} + result = SafeJson(data) + assert isinstance(result, Json) + + def test_unicode_handling(self): + """Test that Unicode characters are handled properly.""" + data = { + "name": "café", + "emoji": "🎉", + "chinese": "你好", + "arabic": "مرحبا", + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_nested_data_structures(self): + """Test complex nested data structures.""" + data = { + "user": { + "name": "Alice", + "preferences": { + "theme": "dark", + "notifications": ["email", "push"], + }, + }, + "metadata": { + "tags": ["important", "urgent"], + "scores": [8.5, 9.2, 7.8], + }, + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_pydantic_model_basic(self): + """Test basic Pydantic model serialization.""" + model = SamplePydanticModel(name="John", age=30) + result = SafeJson(model) + assert isinstance(result, Json) + + def test_pydantic_model_with_none_values(self): + """Test Pydantic model with None values (should be excluded).""" + model = SamplePydanticModel(name="John", age=None, timestamp=None) + result = SafeJson(model) + assert isinstance(result, Json) + # The actual Json content should exclude None values due to exclude_none=True + + def test_pydantic_model_with_datetime(self): + """Test Pydantic model with datetime field.""" + now = datetime.datetime.now() + model = SamplePydanticModel(name="John", age=25, timestamp=now) + result = SafeJson(model) + assert isinstance(result, Json) + + def test_non_serializable_values_in_dict(self): + """Test that non-serializable values in dict are converted to None.""" + data = { + "name": "test", + "function": lambda x: x, # Non-serializable + "datetime": datetime.datetime.now(), # Non-serializable + "valid_data": "this should work", + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_pydantic_model_with_non_serializable_fallback(self): + """Test Pydantic model with non-serializable field using fallback.""" + model = SampleModelWithNonSerializable( + name="test", + func=lambda x: x, # Non-serializable + data={"valid": "data"}, + ) + result = SafeJson(model) + assert isinstance(result, Json) + + def test_empty_data_structures(self): + """Test empty data structures.""" + test_cases = [ + {}, # Empty dict + [], # Empty list + "", # Empty string + None, # None value + ] + + for data in test_cases: + result = SafeJson(data) + assert isinstance(result, Json) + + def test_complex_mixed_data(self): + """Test complex mixed data with various types.""" + data = { + "string": "test", + "integer": 42, + "float": 3.14, + "boolean": True, + "none_value": None, + "list": [1, 2, "three", {"nested": "dict"}], + "nested_dict": { + "level2": { + "level3": ["deep", "nesting", 123], + } + }, + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_list_of_pydantic_models(self): + """Test list containing Pydantic models.""" + models = [ + SamplePydanticModel(name="Alice", age=25), + SamplePydanticModel(name="Bob", age=30), + ] + data = {"users": models} + result = SafeJson(data) + assert isinstance(result, Json) + + def test_edge_case_circular_reference_protection(self): + """Test that circular references don't cause infinite loops.""" + # Note: This test assumes the underlying json.dumps handles circular refs + # by raising an exception, which our fallback should handle + data = {} + data["self"] = data # Create circular reference + + # This should either work with fallback or raise a reasonable error + try: + result = SafeJson(data) + assert isinstance(result, Json) + except (ValueError, RecursionError): + # If it raises an error, that's also acceptable behavior + pass + + def test_large_data_structure(self): + """Test with a reasonably large data structure.""" + data = { + "items": [ + {"id": i, "name": f"item_{i}", "active": i % 2 == 0} for i in range(100) + ], + "metadata": { + "total": 100, + "generated_at": "2024-01-01T00:00:00Z", + "tags": ["auto", "generated", "test"], + }, + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_special_characters_and_encoding(self): + """Test various special characters and encoding scenarios.""" + data = { + "quotes": 'He said "Hello world!"', + "backslashes": "C:\\Users\\test\\file.txt", + "newlines": "Line 1\nLine 2\nLine 3", + "tabs": "Column1\tColumn2\tColumn3", + "unicode_escape": "\u0048\u0065\u006c\u006c\u006f", # "Hello" + "mixed": "Test with émojis 🚀 and ñúméríçs", + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_numeric_edge_cases(self): + """Test various numeric edge cases.""" + data = { + "zero": 0, + "negative": -42, + "large_int": 999999999999999999, + "small_float": 0.000001, + "large_float": 1e10, + "infinity": float("inf"), # This might become None due to fallback + "negative_infinity": float( + "-inf" + ), # This might become None due to fallback + } + result = SafeJson(data) + assert isinstance(result, Json) + + def test_boolean_and_null_values(self): + """Test boolean and null value handling.""" + data = { + "true_value": True, + "false_value": False, + "null_value": None, + "mixed_list": [True, False, None, "string", 42], + } + result = SafeJson(data) + assert isinstance(result, Json) diff --git a/autogpt_platform/backend/backend/util/timezone_name.py b/autogpt_platform/backend/backend/util/timezone_name.py new file mode 100644 index 000000000000..1bd4caf1960a --- /dev/null +++ b/autogpt_platform/backend/backend/util/timezone_name.py @@ -0,0 +1,115 @@ +""" +Time zone name validation and serialization. + +This file is adapted from pydantic-extra-types: +https://github.com/pydantic/pydantic-extra-types/blob/main/pydantic_extra_types/timezone_name.py + +The MIT License (MIT) + +Copyright (c) 2023 Samuel Colvin and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Modifications: +- Modified to always use pytz for timezone data to ensure consistency across environments +- Removed zoneinfo support to prevent environment-specific timezone lists +""" + +from __future__ import annotations + +from typing import Any, Callable, cast + +import pytz +from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler +from pydantic_core import PydanticCustomError, core_schema + +# Cache the timezones at module level to avoid repeated computation +ALL_TIMEZONES: set[str] = set(pytz.all_timezones) + + +def get_timezones() -> set[str]: + """Get timezones from pytz for consistency across all environments.""" + # Return cached timezone set + return ALL_TIMEZONES + + +class TimeZoneNameSettings(type): + def __new__( + cls, name: str, bases: tuple[type, ...], dct: dict[str, Any], **kwargs: Any + ) -> type[TimeZoneName]: + dct["strict"] = kwargs.pop("strict", True) + return cast("type[TimeZoneName]", super().__new__(cls, name, bases, dct)) + + def __init__( + cls, name: str, bases: tuple[type, ...], dct: dict[str, Any], **kwargs: Any + ) -> None: + super().__init__(name, bases, dct) + cls.strict = kwargs.get("strict", True) + + +def timezone_name_settings( + **kwargs: Any, +) -> Callable[[type[TimeZoneName]], type[TimeZoneName]]: + def wrapper(cls: type[TimeZoneName]) -> type[TimeZoneName]: + cls.strict = kwargs.get("strict", True) + return cls + + return wrapper + + +@timezone_name_settings(strict=True) +class TimeZoneName(str): + """TimeZoneName is a custom string subclass for validating and serializing timezone names.""" + + __slots__: list[str] = [] + allowed_values: set[str] = set(get_timezones()) + allowed_values_list: list[str] = sorted(allowed_values) + allowed_values_upper_to_correct: dict[str, str] = { + val.upper(): val for val in allowed_values + } + strict: bool + + @classmethod + def _validate( + cls, __input_value: str, _: core_schema.ValidationInfo + ) -> TimeZoneName: + if __input_value not in cls.allowed_values: + if not cls.strict: + upper_value = __input_value.strip().upper() + if upper_value in cls.allowed_values_upper_to_correct: + return cls(cls.allowed_values_upper_to_correct[upper_value]) + raise PydanticCustomError("TimeZoneName", "Invalid timezone name.") + return cls(__input_value) + + @classmethod + def __get_pydantic_core_schema__( + cls, _: type[Any], __: GetCoreSchemaHandler + ) -> core_schema.AfterValidatorFunctionSchema: + return core_schema.with_info_after_validator_function( + cls._validate, + core_schema.str_schema(min_length=1), + ) + + @classmethod + def __get_pydantic_json_schema__( + cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> dict[str, Any]: + json_schema = handler(schema) + json_schema.update({"enum": cls.allowed_values_list}) + return json_schema diff --git a/autogpt_platform/backend/backend/util/timezone_utils.py b/autogpt_platform/backend/backend/util/timezone_utils.py new file mode 100644 index 000000000000..6a6c438085e4 --- /dev/null +++ b/autogpt_platform/backend/backend/util/timezone_utils.py @@ -0,0 +1,148 @@ +""" +Timezone conversion utilities for API endpoints. +Handles conversion between user timezones and UTC for scheduler operations. +""" + +import logging +from datetime import datetime +from typing import Optional +from zoneinfo import ZoneInfo + +from croniter import croniter + +logger = logging.getLogger(__name__) + + +def convert_cron_to_utc(cron_expr: str, user_timezone: str) -> str: + """ + Convert a cron expression from user timezone to UTC. + + NOTE: This is a simplified conversion that only adjusts minute and hour fields. + Complex cron expressions with specific day/month/weekday patterns may not + convert accurately due to timezone offset variations throughout the year. + + Args: + cron_expr: Cron expression in user timezone + user_timezone: User's IANA timezone identifier + + Returns: + Cron expression adjusted for UTC execution + + Raises: + ValueError: If timezone or cron expression is invalid + """ + try: + user_tz = ZoneInfo(user_timezone) + utc_tz = ZoneInfo("UTC") + + # Split the cron expression into its five fields + cron_fields = cron_expr.strip().split() + if len(cron_fields) != 5: + raise ValueError( + "Cron expression must have 5 fields (minute hour day month weekday)" + ) + + # Get the current time in the user's timezone + now_user = datetime.now(user_tz) + + # Get the next scheduled time in user timezone + cron = croniter(cron_expr, now_user) + next_user_time = cron.get_next(datetime) + + # Convert to UTC + next_utc_time = next_user_time.astimezone(utc_tz) + + # Adjust minute and hour fields for UTC, keep day/month/weekday as in original + utc_cron_parts = [ + str(next_utc_time.minute), + str(next_utc_time.hour), + cron_fields[2], # day of month + cron_fields[3], # month + cron_fields[4], # day of week + ] + + utc_cron = " ".join(utc_cron_parts) + + logger.debug( + f"Converted cron '{cron_expr}' from {user_timezone} to UTC: '{utc_cron}'" + ) + return utc_cron + + except Exception as e: + logger.error( + f"Failed to convert cron expression '{cron_expr}' from {user_timezone} to UTC: {e}" + ) + raise ValueError(f"Invalid cron expression or timezone: {e}") + + +def convert_utc_time_to_user_timezone(utc_time_str: str, user_timezone: str) -> str: + """ + Convert a UTC datetime string to user timezone. + + Args: + utc_time_str: ISO format datetime string in UTC + user_timezone: User's IANA timezone identifier + + Returns: + ISO format datetime string in user timezone + """ + try: + # Parse the time string + parsed_time = datetime.fromisoformat(utc_time_str.replace("Z", "+00:00")) + + user_tz = ZoneInfo(user_timezone) + + # If the time already has timezone info, convert it to user timezone + if parsed_time.tzinfo is not None: + # Convert to user timezone regardless of source timezone + user_time = parsed_time.astimezone(user_tz) + return user_time.isoformat() + + # If no timezone info, treat as UTC and convert to user timezone + parsed_time = parsed_time.replace(tzinfo=ZoneInfo("UTC")) + user_time = parsed_time.astimezone(user_tz) + return user_time.isoformat() + + except Exception as e: + logger.error( + f"Failed to convert UTC time '{utc_time_str}' to {user_timezone}: {e}" + ) + # Return original time if conversion fails + return utc_time_str + + +def validate_timezone(timezone: str) -> bool: + """ + Validate if a timezone string is a valid IANA timezone identifier. + + Args: + timezone: Timezone string to validate + + Returns: + True if valid, False otherwise + """ + try: + ZoneInfo(timezone) + return True + except Exception: + return False + + +def get_user_timezone_or_utc(user_timezone: Optional[str]) -> str: + """ + Get user timezone or default to UTC if invalid/missing. + + Args: + user_timezone: User's timezone preference + + Returns: + Valid timezone string (user's preference or UTC fallback) + """ + if not user_timezone or user_timezone == "not-set": + return "UTC" + + if validate_timezone(user_timezone): + return user_timezone + + logger.warning(f"Invalid user timezone '{user_timezone}', falling back to UTC") + return "UTC" diff --git a/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql b/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql new file mode 100644 index 000000000000..1bc3b5293693 --- /dev/null +++ b/autogpt_platform/backend/migrations/20250721073830_add_preset_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "AgentNodeExecutionInputOutput_agentPresetId_idx" ON "AgentNodeExecutionInputOutput"("agentPresetId"); diff --git a/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql b/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql new file mode 100644 index 000000000000..86f443b56d3b --- /dev/null +++ b/autogpt_platform/backend/migrations/20250721081856_add_missing_fk_indexes_remove_unused_indexes/migration.sql @@ -0,0 +1,109 @@ +-- DropIndex +DROP INDEX IF EXISTS "APIKey_key_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "APIKey_prefix_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "APIKey_status_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_agent_graph_execution_agent"; + +-- DropIndex +DROP INDEX IF EXISTS "AnalyticsDetails_type_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "AnalyticsMetrics_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "IntegrationWebhook_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "Profile_username_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_review_version"; + +-- DropIndex +DROP INDEX IF EXISTS "User_email_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "User_id_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "UserOnboarding_userId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_version_status"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_slv_agent"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_version_approved_listing"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_agentId_owningUserId_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_isDeleted_isApproved_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListing_isDeleted_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "StoreListingVersion_agentId_agentVersion_isDeleted_idx"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_store_listing_approved"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_slv_categories_gin"; + +-- DropIndex +DROP INDEX IF EXISTS "idx_profile_user"; + +-- CreateIndex +CREATE INDEX "APIKey_prefix_name_idx" ON "APIKey"("prefix", "name"); + +-- CreateIndex +CREATE INDEX "AgentGraph_forkedFromId_forkedFromVersion_idx" ON "AgentGraph"("forkedFromId", "forkedFromVersion"); + +-- CreateIndex +CREATE INDEX "AgentGraphExecution_agentPresetId_idx" ON "AgentGraphExecution"("agentPresetId"); + +-- CreateIndex +CREATE INDEX "AgentNodeExecution_agentNodeId_executionStatus_idx" ON "AgentNodeExecution"("agentNodeId", "executionStatus"); + +-- CreateIndex +CREATE INDEX "AgentPreset_agentGraphId_agentGraphVersion_idx" ON "AgentPreset"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "AgentPreset_webhookId_idx" ON "AgentPreset"("webhookId"); + +-- CreateIndex +CREATE INDEX "LibraryAgent_agentGraphId_agentGraphVersion_idx" ON "LibraryAgent"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "LibraryAgent_creatorId_idx" ON "LibraryAgent"("creatorId"); + +-- CreateIndex +CREATE INDEX "StoreListing_agentGraphId_agentGraphVersion_idx" ON "StoreListing"("agentGraphId", "agentGraphVersion"); + +-- CreateIndex +CREATE INDEX "StoreListingReview_reviewByUserId_idx" ON "StoreListingReview"("reviewByUserId"); + +-- CreateIndex (Materialized View Performance Indexes) +CREATE INDEX IF NOT EXISTS "idx_mv_review_stats_rating" ON "mv_review_stats" ("avg_rating" DESC); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "idx_mv_review_stats_count" ON "mv_review_stats" ("review_count" DESC); + +-- RenameIndex (only if exists) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'analyticsDetails') THEN + ALTER INDEX "analyticsDetails" RENAME TO "AnalyticsDetails_userId_type_idx"; + END IF; +END $$; diff --git a/autogpt_platform/backend/migrations/20250805111135_add_video_url_and_categories_to_store_submission_view/migration.sql b/autogpt_platform/backend/migrations/20250805111135_add_video_url_and_categories_to_store_submission_view/migration.sql new file mode 100644 index 000000000000..c5e8962df212 --- /dev/null +++ b/autogpt_platform/backend/migrations/20250805111135_add_video_url_and_categories_to_store_submission_view/migration.sql @@ -0,0 +1,41 @@ +-- Drop the existing view +DROP VIEW IF EXISTS "StoreSubmission"; + +-- Recreate the view with the new fields +CREATE VIEW "StoreSubmission" AS +SELECT + sl.id AS listing_id, + sl."owningUserId" AS user_id, + slv."agentGraphId" AS agent_id, + slv.version AS agent_version, + sl.slug, + COALESCE(slv.name, '') AS name, + slv."subHeading" AS sub_heading, + slv.description, + slv."imageUrls" AS image_urls, + slv."submittedAt" AS date_submitted, + slv."submissionStatus" AS status, + COALESCE(ar.run_count, 0::bigint) AS runs, + COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating, + slv.id AS store_listing_version_id, + slv."reviewerId" AS reviewer_id, + slv."reviewComments" AS review_comments, + slv."internalComments" AS internal_comments, + slv."reviewedAt" AS reviewed_at, + slv."changesSummary" AS changes_summary, + -- Add the two new fields: + slv."videoUrl" AS video_url, + slv.categories +FROM "StoreListing" sl + JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id + LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id + LEFT JOIN ( + SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count + FROM "AgentGraphExecution" + GROUP BY "AgentGraphExecution"."agentGraphId" + ) ar ON ar."agentGraphId" = slv."agentGraphId" +WHERE sl."isDeleted" = false +GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv.version, sl.slug, slv.name, + slv."subHeading", slv.description, slv."imageUrls", slv."submittedAt", + slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments", + slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count; \ No newline at end of file diff --git a/autogpt_platform/backend/migrations/20250819163527_add_user_timezone/migration.sql b/autogpt_platform/backend/migrations/20250819163527_add_user_timezone/migration.sql new file mode 100644 index 000000000000..87bbed7dfb0e --- /dev/null +++ b/autogpt_platform/backend/migrations/20250819163527_add_user_timezone/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'not-set' + CHECK (timezone = 'not-set' OR now() AT TIME ZONE timezone IS NOT NULL); diff --git a/autogpt_platform/backend/migrations/20250824000317_add_notifications_for_approved_and_denied_agents/migration.sql b/autogpt_platform/backend/migrations/20250824000317_add_notifications_for_approved_and_denied_agents/migration.sql new file mode 100644 index 000000000000..5e56f1f96429 --- /dev/null +++ b/autogpt_platform/backend/migrations/20250824000317_add_notifications_for_approved_and_denied_agents/migration.sql @@ -0,0 +1,14 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "NotificationType" ADD VALUE 'AGENT_APPROVED'; +ALTER TYPE "NotificationType" ADD VALUE 'AGENT_REJECTED'; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "notifyOnAgentApproved" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "notifyOnAgentRejected" BOOLEAN NOT NULL DEFAULT true; diff --git a/autogpt_platform/backend/poetry.lock b/autogpt_platform/backend/poetry.lock index 167fd971a45a..f429f057e504 100644 --- a/autogpt_platform/backend/poetry.lock +++ b/autogpt_platform/backend/poetry.lock @@ -223,14 +223,14 @@ files = [ [[package]] name = "anthropic" -version = "0.57.1" +version = "0.59.0" description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306"}, - {file = "anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0"}, + {file = "anthropic-0.59.0-py3-none-any.whl", hash = "sha256:cbc8b3dccef66ad6435c4fa1d317e5ebb092399a4b88b33a09dc4bf3944c3183"}, + {file = "anthropic-0.59.0.tar.gz", hash = "sha256:d710d1ef0547ebbb64b03f219e44ba078e83fc83752b96a9b22e9726b523fd8f"}, ] [package.dependencies] @@ -243,7 +243,7 @@ sniffio = "*" typing-extensions = ">=4.10,<5" [package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.6)"] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] vertex = ["google-auth[requests] (>=2,<3)"] @@ -331,10 +331,70 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +[[package]] +name = "audioop-lts" +version = "0.2.2" +description = "LTS Port of Python audioop" +optional = false +python-versions = ">=3.13" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd"}, + {file = "audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0"}, +] + [[package]] name = "autogpt-libs" version = "0.2.0" -description = "Shared libraries across NextGen AutoGPT" +description = "Shared libraries across AutoGPT Platform" optional = false python-versions = ">=3.10,<4.0" groups = ["main"] @@ -352,6 +412,7 @@ pydantic-settings = "^2.10.1" pyjwt = "^2.10.1" pytest-asyncio = "^1.1.0" pytest-mock = "^3.14.1" +redis = "^6.2.0" supabase = "^2.16.0" uvicorn = "^0.35.0" @@ -467,6 +528,26 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.5)"] +[[package]] +name = "browserbase" +version = "1.4.0" +description = "The official Python library for the Browserbase API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "browserbase-1.4.0-py3-none-any.whl", hash = "sha256:ea9f1fb4a88921975b8b9606835c441a59d8ce82ce00313a6d48bbe8e30f79fb"}, + {file = "browserbase-1.4.0.tar.gz", hash = "sha256:e2ed36f513c8630b94b826042c4bb9f497c333f3bd28e5b76cb708c65b4318a0"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.10,<5" + [[package]] name = "build" version = "1.2.2.post1" @@ -801,6 +882,22 @@ files = [ {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, ] +[[package]] +name = "croniter" +version = "6.0.0" +description = "croniter provides iteration for datetime object with cron like format" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" +groups = ["main"] +files = [ + {file = "croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368"}, + {file = "croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = ">2021.1" + [[package]] name = "cryptography" version = "43.0.3" @@ -904,6 +1001,7 @@ files = [ [package.dependencies] aiohttp = ">=3.7.4,<4" +audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} [package.extras] dev = ["black (==22.6)", "typing_extensions (>=4.3,<5)"] @@ -1078,6 +1176,25 @@ files = [ dnspython = ">=2.0.0" idna = ">=2.0.0" +[[package]] +name = "exa-py" +version = "1.14.20" +description = "Python SDK for Exa API." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "exa_py-1.14.20-py3-none-any.whl", hash = "sha256:e0ed9d99c3c494a0e6903e11a0f6fb773b3b23d0cd802380cf58efc97d9d332d"}, + {file = "exa_py-1.14.20.tar.gz", hash = "sha256:423789a0635b7a4ecd5f56d6b4a0dfb01126fa45ce1e04106c0bb96b7d551ebf"}, +] + +[package.dependencies] +httpx = ">=0.28.1" +openai = ">=1.48" +pydantic = ">=2.10.6" +requests = ">=2.32.3" +typing-extensions = ">=4.12.2" + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -1211,6 +1328,26 @@ files = [ [package.dependencies] packaging = ">=20" +[[package]] +name = "firecrawl-py" +version = "2.16.3" +description = "Python SDK for Firecrawl API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "firecrawl_py-2.16.3-py3-none-any.whl", hash = "sha256:94bb46af5e0df6c8ec414ac999a5355c0f5a46f15fd1cf5a02a3b31062db0aa8"}, + {file = "firecrawl_py-2.16.3.tar.gz", hash = "sha256:5fd063ef4acc4c4be62648f1e11467336bc127780b3afc28d39078a012e6a14c"}, +] + +[package.dependencies] +aiohttp = "*" +nest-asyncio = "*" +pydantic = "*" +python-dotenv = "*" +requests = "*" +websockets = "*" + [[package]] name = "flake8" version = "7.3.0" @@ -1342,6 +1479,46 @@ files = [ {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, ] +[[package]] +name = "fsspec" +version = "2025.7.0" +description = "File-system specification" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21"}, + {file = "fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff (>=0.5)"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] +tqdm = ["tqdm"] + [[package]] name = "gcloud-aio-auth" version = "5.4.2" @@ -1402,7 +1579,10 @@ grpcio-status = [ {version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, ] -proto-plus = ">=1.22.3,<2.0.0" +proto-plus = [ + {version = ">=1.22.3,<2.0.0"}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, +] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" @@ -1414,14 +1594,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] [[package]] name = "google-api-python-client" -version = "2.176.0" +version = "2.177.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "google_api_python_client-2.176.0-py3-none-any.whl", hash = "sha256:e22239797f1d085341e12cd924591fc65c56d08e0af02549d7606092e6296510"}, - {file = "google_api_python_client-2.176.0.tar.gz", hash = "sha256:2b451cdd7fd10faeb5dd20f7d992f185e1e8f4124c35f2cdcc77c843139a4cf1"}, + {file = "google_api_python_client-2.177.0-py3-none-any.whl", hash = "sha256:f2f50f11105ab883eb9b6cf38ec54ea5fd4b429249f76444bec90deba5be79b3"}, + {file = "google_api_python_client-2.177.0.tar.gz", hash = "sha256:9ffd2b57d68f5afa7e6ac64e2c440534eaa056cbb394812a62ff94723c31b50e"}, ] [package.dependencies] @@ -1508,7 +1688,10 @@ files = [ [package.dependencies] google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -proto-plus = ">=1.22.3,<2.0.0" +proto-plus = [ + {version = ">=1.22.3,<2.0.0"}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, +] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [[package]] @@ -1569,6 +1752,7 @@ opentelemetry-api = ">=1.9.0" proto-plus = [ {version = ">=1.22.0,<2.0.0"}, {version = ">=1.22.2,<2.0.0", markers = "python_version >= \"3.11\""}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1736,7 +1920,6 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, @@ -1947,6 +2130,28 @@ files = [ hpack = ">=4.1,<5" hyperframe = ">=6.1,<7" +[[package]] +name = "hf-xet" +version = "1.1.8" +description = "Fast transfer of large files with the Hugging Face Hub." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" +files = [ + {file = "hf_xet-1.1.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3d5f82e533fc51c7daad0f9b655d9c7811b5308e5890236828bd1dd3ed8fea74"}, + {file = "hf_xet-1.1.8-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e2dba5896bca3ab61d0bef4f01a1647004de59640701b37e37eaa57087bbd9d"}, + {file = "hf_xet-1.1.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfe5700bc729be3d33d4e9a9b5cc17a951bf8c7ada7ba0c9198a6ab2053b7453"}, + {file = "hf_xet-1.1.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:09e86514c3c4284ed8a57d6b0f3d089f9836a0af0a1ceb3c9dd664f1f3eaefef"}, + {file = "hf_xet-1.1.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4a9b99ab721d385b83f4fc8ee4e0366b0b59dce03b5888a86029cc0ca634efbf"}, + {file = "hf_xet-1.1.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25b9d43333bbef39aeae1616789ec329c21401a7fe30969d538791076227b591"}, + {file = "hf_xet-1.1.8-cp37-abi3-win_amd64.whl", hash = "sha256:4171f31d87b13da4af1ed86c98cf763292e4720c088b4957cf9d564f92904ca9"}, + {file = "hf_xet-1.1.8.tar.gz", hash = "sha256:62a0043e441753bbc446dcb5a3fe40a4d03f5fb9f13589ef1df9ab19252beb53"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "hpack" version = "4.1.0" @@ -2089,6 +2294,45 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "huggingface-hub" +version = "0.34.4" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a"}, + {file = "huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +hf-xet = {version = ">=1.1.3,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] +inference = ["aiohttp"] +mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] +oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] +quality = ["libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "ruff (>=0.9.0)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors[torch]", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + [[package]] name = "hyperframe" version = "6.1.0" @@ -2442,14 +2686,14 @@ files = [ [[package]] name = "jsonschema" -version = "4.24.1" +version = "4.25.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema-4.24.1-py3-none-any.whl", hash = "sha256:6b916866aa0b61437785f1277aa2cbd63512e8d4b47151072ef13292049b4627"}, - {file = "jsonschema-4.24.1.tar.gz", hash = "sha256:fe45a130cc7f67cd0d67640b4e7e3e2e666919462ae355eda238296eafeb4b5d"}, + {file = "jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716"}, + {file = "jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f"}, ] [package.dependencies] @@ -2460,7 +2704,7 @@ rpds-py = ">=0.7.1" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-specifications" @@ -2548,6 +2792,62 @@ dynamodb = ["boto3 (>=1.9.71)"] redis = ["redis (>=2.10.5)"] test-filesource = ["pyyaml (>=5.3.1)", "watchdog (>=3.0.0)"] +[[package]] +name = "litellm" +version = "1.74.15.post2" +description = "Library to easily interface with LLM API providers" +optional = false +python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" +groups = ["main"] +files = [ + {file = "litellm-1.74.15.post2.tar.gz", hash = "sha256:8eddb1c8a6a5a7048f8ba16e652aba23d6ca996dd87cb853c874ba375aa32479"}, +] + +[package.dependencies] +aiohttp = ">=3.10" +click = "*" +httpx = ">=0.23.0" +importlib-metadata = ">=6.8.0" +jinja2 = ">=3.1.2,<4.0.0" +jsonschema = ">=4.22.0,<5.0.0" +openai = ">=1.68.2" +pydantic = ">=2.5.0,<3.0.0" +python-dotenv = ">=0.2.0" +tiktoken = ">=0.7.0" +tokenizers = "*" + +[package.extras] +caching = ["diskcache (>=5.6.1,<6.0.0)"] +extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] +mlflow = ["mlflow (>3.1.4) ; python_version >= \"3.10\""] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.16)", "litellm-proxy-extras (==0.2.16)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"] +semantic-router = ["semantic-router ; python_version >= \"3.9\""] +utils = ["numpydoc"] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + [[package]] name = "markupsafe" version = "3.0.2" @@ -2631,16 +2931,28 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mem0ai" -version = "0.1.114" +version = "0.1.115" description = "Long-term memory for AI Agents" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ - {file = "mem0ai-0.1.114-py3-none-any.whl", hash = "sha256:dfb7f0079ee282f5d9782e220f6f09707bcf5e107925d1901dbca30d8dd83f9b"}, - {file = "mem0ai-0.1.114.tar.gz", hash = "sha256:b27886132eaec78544e8b8b54f0b14a36728f3c99da54cb7cb417150e2fad7e1"}, + {file = "mem0ai-0.1.115-py3-none-any.whl", hash = "sha256:29310bd5bcab644f7a4dbf87bd1afd878eb68458a2fb36cfcbf20bdff46fbdaf"}, + {file = "mem0ai-0.1.115.tar.gz", hash = "sha256:147a6593604188acd30281c40171112aed9f16e196fa528627430c15e00f1e32"}, ] [package.dependencies] @@ -2901,6 +3213,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -3076,14 +3400,14 @@ pydantic = ">=2.9" [[package]] name = "openai" -version = "1.97.0" +version = "1.97.1" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.97.0-py3-none-any.whl", hash = "sha256:a1c24d96f4609f3f7f51c9e1c2606d97cc6e334833438659cfd687e9c972c610"}, - {file = "openai-1.97.0.tar.gz", hash = "sha256:0be349569ccaa4fb54f97bb808423fd29ccaeb1246ee1be762e0c81a47bae0aa"}, + {file = "openai-1.97.1-py3-none-any.whl", hash = "sha256:4e96bbdf672ec3d44968c9ea39d2c375891db1acc1794668d8149d5fa6000606"}, + {file = "openai-1.97.1.tar.gz", hash = "sha256:a744b27ae624e3d4135225da9b1c89c107a2a7e5bc4c93e5b7b5214772ce7a4e"}, ] [package.dependencies] @@ -3507,6 +3831,28 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] +[[package]] +name = "playwright" +version = "1.54.0" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "playwright-1.54.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d"}, + {file = "playwright-1.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02"}, + {file = "playwright-1.54.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9"}, + {file = "playwright-1.54.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc"}, + {file = "playwright-1.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc"}, + {file = "playwright-1.54.0-py3-none-win32.whl", hash = "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56"}, + {file = "playwright-1.54.0-py3-none-win_amd64.whl", hash = "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2"}, + {file = "playwright-1.54.0-py3-none-win_arm64.whl", hash = "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75"}, +] + +[package.dependencies] +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=13,<14" + [[package]] name = "pluggy" version = "1.6.0" @@ -4341,6 +4687,24 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pyee" +version = "13.0.0" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498"}, + {file = "pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio ; python_version >= \"3.4\"", "pytest-trio ; python_version >= \"3.7\"", "sphinx", "toml", "tox", "trio", "trio ; python_version > \"3.6\"", "trio-typing ; python_version > \"3.6\"", "twine", "twisted", "validate-pyproject[all]"] + [[package]] name = "pyflakes" version = "3.4.0" @@ -4717,6 +5081,7 @@ grpcio = ">=1.41.0" httpx = {version = ">=0.20.0", extras = ["http2"]} numpy = [ {version = ">=1.21", markers = "python_version >= \"3.10\" and python_version < \"3.12\""}, + {version = ">=2.1.0", markers = "python_version >= \"3.13\""}, {version = ">=1.26", markers = "python_version == \"3.12\""}, ] portalocker = ">=2.7.0,<3.0.0" @@ -4837,38 +5202,40 @@ all = ["numpy"] [[package]] name = "realtime" -version = "2.5.3" +version = "2.6.0" description = "" optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "realtime-2.5.3-py3-none-any.whl", hash = "sha256:eb0994636946eff04c4c7f044f980c8c633c7eb632994f549f61053a474ac970"}, - {file = "realtime-2.5.3.tar.gz", hash = "sha256:0587594f3bc1c84bf007ff625075b86db6528843e03250dc84f4f2808be3d99a"}, + {file = "realtime-2.6.0-py3-none-any.whl", hash = "sha256:a0512d71044c2621455bc87d1c171739967edc161381994de54e0989ca6c348e"}, + {file = "realtime-2.6.0.tar.gz", hash = "sha256:f68743cff85d3113659fa19835a868674e720465649bf833e1cd47d7da0f7bbd"}, ] [package.dependencies] -typing-extensions = ">=4.14.0,<5.0.0" +pydantic = ">=2.11.7,<3.0.0" +typing-extensions = ">=4.14.0" websockets = ">=11,<16" [[package]] name = "redis" -version = "5.2.1" +version = "6.2.0" description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, - {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, + {file = "redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e"}, + {file = "redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"}, ] [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>=3.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] [[package]] name = "referencing" @@ -5065,6 +5432,25 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rich" +version = "14.1.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rpds-py" version = "0.26.0" @@ -5293,14 +5679,14 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.33.0" +version = "2.33.2" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "sentry_sdk-2.33.0-py2.py3-none-any.whl", hash = "sha256:a762d3f19a1c240e16c98796f2a5023f6e58872997d5ae2147ac3ed378b23ec2"}, - {file = "sentry_sdk-2.33.0.tar.gz", hash = "sha256:cdceed05e186846fdf80ceea261fe0a11ebc93aab2f228ed73d076a07804152e"}, + {file = "sentry_sdk-2.33.2-py2.py3-none-any.whl", hash = "sha256:8d57a3b4861b243aa9d558fda75509ad487db14f488cbdb6c78c614979d77632"}, + {file = "sentry_sdk-2.33.2.tar.gz", hash = "sha256:e85002234b7b8efac9b74c2d91dbd4f8f3970dc28da8798e39530e65cb740f94"}, ] [package.dependencies] @@ -5518,6 +5904,34 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "stagehand" +version = "0.5.1" +description = "Python SDK for Stagehand" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "stagehand-0.5.1-py3-none-any.whl", hash = "sha256:97f88ef31c4b2ad448c2ef341a3cff074b83e40c60872d8f49f79c82f9249174"}, + {file = "stagehand-0.5.1.tar.gz", hash = "sha256:312611f776a5f93f3a10a3cae87cd4881eb020c999a87394b21bff2b123fdfc3"}, +] + +[package.dependencies] +anthropic = ">=0.51.0" +browserbase = ">=1.4.0" +httpx = ">=0.24.0" +litellm = ">=1.72.0,<1.75.0" +nest-asyncio = ">=1.6.0" +openai = ">=1.83.0,<1.99.6" +playwright = ">=1.42.1" +pydantic = ">=1.10.0" +python-dotenv = ">=1.0.0" +requests = ">=2.31.0" +rich = ">=13.7.0" + +[package.extras] +dev = ["black (>=23.3.0)", "isort (>=5.12.0)", "mypy (>=1.3.0)", "psutil (>=5.9.0)", "pytest (>=7.3.1)", "pytest-asyncio (>=0.21.0)", "pytest-cov (>=4.1.0)", "pytest-mock (>=3.10.0)", "ruff"] + [[package]] name = "starlette" version = "0.47.1" @@ -5589,23 +6003,23 @@ typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""} [[package]] name = "supabase" -version = "2.16.0" +version = "2.17.0" description = "Supabase client for Python." optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ - {file = "supabase-2.16.0-py3-none-any.whl", hash = "sha256:99065caab3d90a56650bf39fbd0e49740995da3738ab28706c61bd7f2401db55"}, - {file = "supabase-2.16.0.tar.gz", hash = "sha256:98f3810158012d4ec0e3083f2e5515f5e10b32bd71e7d458662140e963c1d164"}, + {file = "supabase-2.17.0-py3-none-any.whl", hash = "sha256:2dd804fae8850cebccc9ab8711c2ee9e2f009e847f4c95c092a4423778e3c3f6"}, + {file = "supabase-2.17.0.tar.gz", hash = "sha256:3207314b540db7e3339fa2500bd977541517afb4d20b7ff93a89b97a05f9df38"}, ] [package.dependencies] -gotrue = ">=2.11.0,<3.0.0" +gotrue = "2.12.3" httpx = ">=0.26,<0.29" -postgrest = ">0.19,<1.2" -realtime = ">=2.4.0,<2.6.0" -storage3 = ">=0.10,<0.13" -supafunc = ">=0.9,<0.11" +postgrest = "1.1.1" +realtime = "2.6.0" +storage3 = "0.12.0" +supafunc = "0.10.1" [[package]] name = "supafunc" @@ -5721,6 +6135,39 @@ files = [ [package.dependencies] requests = ">=2.32.3,<3.0.0" +[[package]] +name = "tokenizers" +version = "0.21.4" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133"}, + {file = "tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2"}, + {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff"}, + {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2"}, + {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78"}, + {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b"}, + {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24"}, + {file = "tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0"}, + {file = "tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597"}, + {file = "tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880"}, +] + +[package.dependencies] +huggingface-hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] + [[package]] name = "tomli" version = "2.2.1" @@ -6518,14 +6965,14 @@ propcache = ">=0.2.1" [[package]] name = "youtube-transcript-api" -version = "1.1.1" +version = "1.2.1" description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!" optional = false python-versions = "<3.14,>=3.8" groups = ["main"] files = [ - {file = "youtube_transcript_api-1.1.1-py3-none-any.whl", hash = "sha256:a438a824d67c0885855047e2b38993abdd4f59b69a983cf27b50a06c9d564064"}, - {file = "youtube_transcript_api-1.1.1.tar.gz", hash = "sha256:2e1162d45ece14223a58a4a39176c464fdd33d5ebdd6def18ebb038dea62f667"}, + {file = "youtube_transcript_api-1.2.1-py3-none-any.whl", hash = "sha256:4852356c8459aceab73f8f05e0f7fc4762d5f278e7e34bd6359fce7427df853b"}, + {file = "youtube_transcript_api-1.2.1.tar.gz", hash = "sha256:7c16ba3e981dd7ab4c0f00f42e5a69b19bdb9f13324c60bd6cc8f97701699900"}, ] [package.dependencies] @@ -6682,5 +7129,5 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" -python-versions = ">=3.10,<3.13" -content-hash = "ebf7138cb90da2b3dad731f6e93c4704ee07560fe0a017ff40dfe483a8274104" +python-versions = ">=3.10,<3.14" +content-hash = "06ca298144a75656a31df830153013cc372f47bed0fd04c8cb02373a4f40c38b" diff --git a/autogpt_platform/backend/pyproject.toml b/autogpt_platform/backend/pyproject.toml index 294d69b05eeb..424e7635e795 100644 --- a/autogpt_platform/backend/pyproject.toml +++ b/autogpt_platform/backend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autogpt-platform-backend" -version = "0.4.9" +version = "0.6.22" description = "A platform for building AI-powered agentic workflows" authors = ["AutoGPT "] readme = "README.md" @@ -8,10 +8,11 @@ packages = [{ include = "backend", format = "sdist" }] [tool.poetry.dependencies] -python = ">=3.10,<3.13" +python = ">=3.10,<3.14" aio-pika = "^9.5.5" +aiohttp = "^3.10.0" aiodns = "^3.5.0" -anthropic = "^0.57.1" +anthropic = "^0.59.0" apscheduler = "^3.11.0" autogpt-libs = { path = "../autogpt_libs", develop = true } bleach = { extras = ["css"], version = "^6.2.0" } @@ -22,7 +23,7 @@ e2b-code-interpreter = "^1.5.2" fastapi = "^0.116.1" feedparser = "^6.0.11" flake8 = "^7.3.0" -google-api-python-client = "^2.176.0" +google-api-python-client = "^2.177.0" google-auth-oauthlib = "^1.2.2" google-cloud-storage = "^3.2.0" googlemaps = "^4.10.0" @@ -31,12 +32,12 @@ groq = "^0.30.0" html2text = "^2024.2.26" jinja2 = "^3.1.6" jsonref = "^1.1.0" -jsonschema = "^4.22.0" +jsonschema = "^4.25.0" launchdarkly-server-sdk = "^9.12.0" -mem0ai = "^0.1.114" +mem0ai = "^0.1.115" moviepy = "^2.1.2" ollama = "^0.5.1" -openai = "^1.97.0" +openai = "^1.97.1" pika = "^1.3.2" pinecone = "^7.3.0" poetry = "2.1.1" # CHECK DEPENDABOT SUPPORT BEFORE UPGRADING @@ -52,19 +53,19 @@ pytest = "^8.4.1" pytest-asyncio = "^1.1.0" python-dotenv = "^1.1.1" python-multipart = "^0.0.20" -redis = "^5.2.0" +redis = "^6.2.0" replicate = "^1.0.6" -sentry-sdk = {extras = ["anthropic", "fastapi", "launchdarkly", "openai", "sqlalchemy"], version = "^2.33.0"} +sentry-sdk = {extras = ["anthropic", "fastapi", "launchdarkly", "openai", "sqlalchemy"], version = "^2.33.2"} sqlalchemy = "^2.0.40" strenum = "^0.4.9" stripe = "^11.5.0" -supabase = "2.16.0" +supabase = "2.17.0" tenacity = "^9.1.2" todoist-api-python = "^2.1.7" tweepy = "^4.16.0" uvicorn = { extras = ["standard"], version = "^0.35.0" } websockets = "^15.0" -youtube-transcript-api = "^1.1.1" +youtube-transcript-api = "^1.2.1" zerobouncesdk = "^1.1.2" # NOTE: please insert new dependencies in their alphabetical location pytest-snapshot = "^0.9.0" @@ -74,6 +75,10 @@ aioclamd = "^1.0.0" setuptools = "^80.9.0" gcloud-aio-storage = "^9.5.0" pandas = "^2.3.1" +firecrawl-py = "^2.16.3" +exa-py = "^1.14.20" +croniter = "^6.0.0" +stagehand = "^0.5.1" [tool.poetry.group.dev.dependencies] aiohappyeyeballs = "^2.6.1" @@ -97,8 +102,10 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] app = "backend.app:main" rest = "backend.rest:main" +db = "backend.db:main" ws = "backend.ws:main" scheduler = "backend.scheduler:main" +notification = "backend.notification:main" executor = "backend.exec:main" cli = "backend.cli:main" format = "linter:format" @@ -127,4 +134,3 @@ filterwarnings = [ [tool.ruff] target-version = "py310" - diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 7e8b53bdc769..13c3d2c52982 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -33,6 +33,10 @@ model User { notifyOnDailySummary Boolean @default(true) notifyOnWeeklySummary Boolean @default(true) notifyOnMonthlySummary Boolean @default(true) + notifyOnAgentApproved Boolean @default(true) + notifyOnAgentRejected Boolean @default(true) + + timezone String @default("not-set") // Relations @@ -53,9 +57,6 @@ model User { APIKeys APIKey[] IntegrationWebhooks IntegrationWebhook[] NotificationBatches UserNotificationBatch[] - - @@index([id]) - @@index([email]) } enum OnboardingStep { @@ -98,8 +99,6 @@ model UserOnboarding { userId String @unique User User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) } // This model describes the Agent Graph/Flow (Multi Agent System). @@ -135,6 +134,7 @@ model AgentGraph { @@id(name: "graphVersionId", [id, version]) @@index([userId, isActive]) + @@index([forkedFromId, forkedFromVersion]) } //////////////////////////////////////////////////////////// @@ -176,6 +176,8 @@ model AgentPreset { isDeleted Boolean @default(false) @@index([userId]) + @@index([agentGraphId, agentGraphVersion]) + @@index([webhookId]) } enum NotificationType { @@ -189,6 +191,8 @@ enum NotificationType { MONTHLY_SUMMARY REFUND_REQUEST REFUND_PROCESSED + AGENT_APPROVED + AGENT_REJECTED } model NotificationEvent { @@ -248,6 +252,8 @@ model LibraryAgent { isDeleted Boolean @default(false) @@unique([userId, agentGraphId, agentGraphVersion]) + @@index([agentGraphId, agentGraphVersion]) + @@index([creatorId]) } //////////////////////////////////////////////////////////// @@ -361,6 +367,7 @@ model AgentGraphExecution { @@index([agentGraphId, agentGraphVersion]) @@index([userId]) @@index([createdAt]) + @@index([agentPresetId]) } // This model describes the execution of an AgentNode. @@ -386,6 +393,7 @@ model AgentNodeExecution { stats Json? @@index([agentGraphExecutionId, agentNodeId, executionStatus]) + @@index([agentNodeId, executionStatus]) @@index([addedTime, queuedTime]) } @@ -411,15 +419,17 @@ model AgentNodeExecutionInputOutput { @@index([referencedByOutputExecId]) // Composite index for `upsert_execution_input`. @@index([name, time]) + @@index([agentPresetId]) } model AgentNodeExecutionKeyValueData { - userId String - key String - agentNodeExecutionId String - data Json? - createdAt DateTime @default(now()) - updatedAt DateTime? @updatedAt + userId String + key String + agentNodeExecutionId String + data Json? + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + @@id([userId, key]) } @@ -444,8 +454,6 @@ model IntegrationWebhook { AgentNodes AgentNode[] AgentPresets AgentPreset[] - - @@index([userId]) } model AnalyticsDetails { @@ -469,8 +477,7 @@ model AnalyticsDetails { // Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc. dataIndex String? - @@index([userId, type], name: "analyticsDetails") - @@index([type]) + @@index([userId, type]) } //////////////////////////////////////////////////////////// @@ -494,8 +501,6 @@ model AnalyticsMetrics { // Link to User model userId String User User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) } //////////////////////////////////////////////////////////// @@ -581,7 +586,6 @@ model Profile { LibraryAgents LibraryAgent[] - @@index([username]) @@index([userId]) } @@ -599,21 +603,13 @@ view Creator { agent_runs Int is_featured Boolean - // Note: Prisma doesn't support indexes on views, but the following indexes exist in the database: - // - // Optimized indexes (partial indexes to reduce size and improve performance): - // - idx_profile_user on Profile(userId) - // - idx_store_listing_approved on StoreListing(owningUserId) WHERE isDeleted = false AND hasApprovedVersion = true - // - idx_store_listing_version_status on StoreListingVersion(storeListingId) WHERE submissionStatus = 'APPROVED' - // - idx_slv_categories_gin - GIN index on StoreListingVersion(categories) WHERE submissionStatus = 'APPROVED' - // - idx_slv_agent on StoreListingVersion(agentGraphId, agentGraphVersion) WHERE submissionStatus = 'APPROVED' - // - idx_store_listing_review_version on StoreListingReview(storeListingVersionId) - // - idx_store_listing_version_approved_listing on StoreListingVersion(storeListingId, version) WHERE submissionStatus = 'APPROVED' - // - idx_agent_graph_execution_agent on AgentGraphExecution(agentGraphId) - // // Materialized views used (refreshed every 15 minutes via pg_cron): // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId + // * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId + // * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup + // * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance + // * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance // // Query strategy: Uses CTEs to efficiently aggregate creator statistics leveraging materialized views } @@ -638,28 +634,13 @@ view StoreAgent { rating Float versions String[] - // Note: Prisma doesn't support indexes on views, but the following indexes exist in the database: - // - // Optimized indexes (partial indexes to reduce size and improve performance): - // - idx_store_listing_approved on StoreListing(owningUserId) WHERE isDeleted = false AND hasApprovedVersion = true - // - idx_store_listing_version_status on StoreListingVersion(storeListingId) WHERE submissionStatus = 'APPROVED' - // - idx_slv_categories_gin - GIN index on StoreListingVersion(categories) WHERE submissionStatus = 'APPROVED' for array searches - // - idx_slv_agent on StoreListingVersion(agentGraphId, agentGraphVersion) WHERE submissionStatus = 'APPROVED' - // - idx_store_listing_review_version on StoreListingReview(storeListingVersionId) - // - idx_store_listing_version_approved_listing on StoreListingVersion(storeListingId, version) WHERE submissionStatus = 'APPROVED' - // - idx_agent_graph_execution_agent on AgentGraphExecution(agentGraphId) - // - idx_profile_user on Profile(userId) - // - // Additional indexes from earlier migrations: - // - StoreListing_agentId_owningUserId_idx - // - StoreListing_isDeleted_isApproved_idx (replaced by idx_store_listing_approved) - // - StoreListing_isDeleted_idx - // - StoreListing_agentId_key (unique on agentGraphId) - // - StoreListingVersion_agentId_agentVersion_isDeleted_idx - // // Materialized views used (refreshed every 15 minutes via pg_cron): // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId + // * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId + // * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup + // * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance + // * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance // // Query strategy: Uses CTE for version aggregation and joins with materialized views for performance } @@ -684,6 +665,8 @@ view StoreSubmission { internal_comments String? reviewed_at DateTime? changes_summary String? + video_url String? + categories String[] // Index or unique are not applied to views } @@ -747,6 +730,7 @@ model StoreListing { @@unique([owningUserId, slug]) // Used in the view query @@index([isDeleted, hasApprovedVersion]) + @@index([agentGraphId, agentGraphVersion]) } model StoreListingVersion { @@ -820,6 +804,7 @@ model StoreListingReview { comments String? @@unique([storeListingVersionId, reviewByUserId]) + @@index([reviewByUserId]) } enum SubmissionStatus { @@ -855,9 +840,7 @@ model APIKey { userId String User User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([key]) - @@index([prefix]) - @@index([status]) + @@index([prefix, name]) @@index([userId, status]) } diff --git a/autogpt_platform/backend/snapshots/lib_agts_search b/autogpt_platform/backend/snapshots/lib_agts_search index e0f168ddd7db..f19d42896ba0 100644 --- a/autogpt_platform/backend/snapshots/lib_agts_search +++ b/autogpt_platform/backend/snapshots/lib_agts_search @@ -15,6 +15,10 @@ "type": "object", "properties": {} }, + "output_schema": { + "type": "object", + "properties": {} + }, "credentials_input_schema": { "type": "object", "properties": {} @@ -40,6 +44,10 @@ "type": "object", "properties": {} }, + "output_schema": { + "type": "object", + "properties": {} + }, "credentials_input_schema": { "type": "object", "properties": {} diff --git a/autogpt_platform/backend/snapshots/sub_success b/autogpt_platform/backend/snapshots/sub_success index 3d10ea70e2ec..5f46f4a43444 100644 --- a/autogpt_platform/backend/snapshots/sub_success +++ b/autogpt_platform/backend/snapshots/sub_success @@ -20,7 +20,11 @@ "review_comments": null, "internal_comments": null, "reviewed_at": null, - "changes_summary": null + "changes_summary": null, + "video_url": "test.mp4", + "categories": [ + "test-category" + ] } ], "pagination": { diff --git a/autogpt_platform/backend/test/__init__.py b/autogpt_platform/backend/test/__init__.py new file mode 100644 index 000000000000..67d696d6d016 --- /dev/null +++ b/autogpt_platform/backend/test/__init__.py @@ -0,0 +1 @@ +# This file makes the test directory a Python module diff --git a/autogpt_platform/backend/test/blocks/test_gmail.py b/autogpt_platform/backend/test/blocks/test_gmail.py index 6be1c914d461..5c086c6e0b0e 100644 --- a/autogpt_platform/backend/test/blocks/test_gmail.py +++ b/autogpt_platform/backend/test/blocks/test_gmail.py @@ -1,6 +1,8 @@ import base64 from unittest.mock import Mock, patch +import pytest + from backend.blocks.google.gmail import GmailReadBlock @@ -16,7 +18,8 @@ def _encode_base64(self, text: str) -> str: """Helper to encode text as base64 URL-safe.""" return base64.urlsafe_b64encode(text.encode("utf-8")).decode("utf-8") - def test_single_part_text_plain(self): + @pytest.mark.asyncio + async def test_single_part_text_plain(self): """Test parsing single-part text/plain email.""" body_text = "This is a plain text email body." msg = { @@ -27,10 +30,11 @@ def test_single_part_text_plain(self): }, } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == body_text - def test_multipart_alternative_plain_and_html(self): + @pytest.mark.asyncio + async def test_multipart_alternative_plain_and_html(self): """Test parsing multipart/alternative with both plain and HTML parts.""" plain_text = "This is the plain text version." html_text = "

This is the HTML version.

" @@ -52,11 +56,12 @@ def test_multipart_alternative_plain_and_html(self): }, } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) # Should prefer plain text over HTML assert result == plain_text - def test_html_only_email(self): + @pytest.mark.asyncio + async def test_html_only_email(self): """Test parsing HTML-only email with conversion to plain text.""" html_text = ( "

Hello World

This is HTML content.

" @@ -75,11 +80,12 @@ def test_html_only_email(self): mock_converter.handle.return_value = "Hello World\n\nThis is HTML content." mock_html2text.return_value = mock_converter - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert "Hello World" in result assert "This is HTML content" in result - def test_html_fallback_when_html2text_unavailable(self): + @pytest.mark.asyncio + async def test_html_fallback_when_html2text_unavailable(self): """Test fallback to raw HTML when html2text is not available.""" html_text = "

HTML content

" @@ -92,10 +98,11 @@ def test_html_fallback_when_html2text_unavailable(self): } with patch("html2text.HTML2Text", side_effect=ImportError): - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == html_text - def test_nested_multipart_structure(self): + @pytest.mark.asyncio + async def test_nested_multipart_structure(self): """Test parsing deeply nested multipart structure.""" plain_text = "Nested plain text content." @@ -117,10 +124,11 @@ def test_nested_multipart_structure(self): }, } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == plain_text - def test_attachment_body_content(self): + @pytest.mark.asyncio + async def test_attachment_body_content(self): """Test parsing email where body is stored as attachment.""" attachment_data = self._encode_base64("Body content from attachment.") @@ -137,10 +145,11 @@ def test_attachment_body_content(self): "data": attachment_data } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == "Body content from attachment." - def test_no_readable_body(self): + @pytest.mark.asyncio + async def test_no_readable_body(self): """Test email with no readable body content.""" msg = { "id": "test_msg_7", @@ -150,10 +159,11 @@ def test_no_readable_body(self): }, } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == "This email does not contain a readable body." - def test_base64_padding_handling(self): + @pytest.mark.asyncio + async def test_base64_padding_handling(self): """Test proper handling of base64 data with missing padding.""" # Create base64 data with missing padding text = "Test content" @@ -164,7 +174,8 @@ def test_base64_padding_handling(self): result = self.gmail_block._decode_base64(encoded_no_padding) assert result == text - def test_recursion_depth_limit(self): + @pytest.mark.asyncio + async def test_recursion_depth_limit(self): """Test that recursion depth is properly limited.""" # Create a deeply nested structure that would exceed the limit @@ -184,21 +195,24 @@ def create_nested_part(depth): "payload": create_nested_part(0), } - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) # Should return fallback message due to depth limit assert result == "This email does not contain a readable body." - def test_malformed_base64_handling(self): + @pytest.mark.asyncio + async def test_malformed_base64_handling(self): """Test handling of malformed base64 data.""" result = self.gmail_block._decode_base64("invalid_base64_data!!!") assert result is None - def test_empty_data_handling(self): + @pytest.mark.asyncio + async def test_empty_data_handling(self): """Test handling of empty or None data.""" assert self.gmail_block._decode_base64("") is None assert self.gmail_block._decode_base64(None) is None - def test_attachment_download_failure(self): + @pytest.mark.asyncio + async def test_attachment_download_failure(self): """Test handling of attachment download failure.""" msg = { "id": "test_msg_9", @@ -213,5 +227,5 @@ def test_attachment_download_failure(self): Exception("Download failed") ) - result = self.gmail_block._get_email_body(msg, self.mock_service) + result = await self.gmail_block._get_email_body(msg, self.mock_service) assert result == "This email does not contain a readable body." diff --git a/autogpt_platform/backend/test/e2e_test_data.py b/autogpt_platform/backend/test/e2e_test_data.py index dbb5c10919a8..b183d83d59ec 100644 --- a/autogpt_platform/backend/test/e2e_test_data.py +++ b/autogpt_platform/backend/test/e2e_test_data.py @@ -30,29 +30,29 @@ # Import API functions from the backend from backend.data.user import get_or_create_user -from backend.server.integrations.utils import get_supabase from backend.server.v2.library.db import create_library_agent, create_preset from backend.server.v2.library.model import LibraryAgentPresetCreatable from backend.server.v2.store.db import create_store_submission, review_store_submission +from backend.util.clients import get_supabase faker = Faker() # Constants for data generation limits (reduced for E2E tests) -NUM_USERS = 10 -NUM_AGENT_BLOCKS = 20 -MIN_GRAPHS_PER_USER = 10 -MAX_GRAPHS_PER_USER = 10 -MIN_NODES_PER_GRAPH = 2 -MAX_NODES_PER_GRAPH = 4 -MIN_PRESETS_PER_USER = 1 -MAX_PRESETS_PER_USER = 2 -MIN_AGENTS_PER_USER = 10 -MAX_AGENTS_PER_USER = 10 -MIN_EXECUTIONS_PER_GRAPH = 1 -MAX_EXECUTIONS_PER_GRAPH = 5 -MIN_REVIEWS_PER_VERSION = 1 -MAX_REVIEWS_PER_VERSION = 3 +NUM_USERS = 15 +NUM_AGENT_BLOCKS = 30 +MIN_GRAPHS_PER_USER = 15 +MAX_GRAPHS_PER_USER = 15 +MIN_NODES_PER_GRAPH = 3 +MAX_NODES_PER_GRAPH = 6 +MIN_PRESETS_PER_USER = 2 +MAX_PRESETS_PER_USER = 3 +MIN_AGENTS_PER_USER = 15 +MAX_AGENTS_PER_USER = 15 +MIN_EXECUTIONS_PER_GRAPH = 2 +MAX_EXECUTIONS_PER_GRAPH = 8 +MIN_REVIEWS_PER_VERSION = 2 +MAX_REVIEWS_PER_VERSION = 5 def get_image(): @@ -76,6 +76,23 @@ def get_video_url(): return f"https://www.youtube.com/watch?v={video_id}" +def get_category(): + """Generate a random category from the predefined list.""" + categories = [ + "productivity", + "writing", + "development", + "data", + "marketing", + "research", + "creative", + "business", + "personal", + "other", + ] + return random.choice(categories) + + class TestDataCreator: """Creates test data using API functions for E2E tests.""" @@ -87,6 +104,7 @@ def __init__(self): self.store_submissions: List[Dict[str, Any]] = [] self.api_keys: List[Dict[str, Any]] = [] self.presets: List[Dict[str, Any]] = [] + self.profiles: List[Dict[str, Any]] = [] async def create_test_users(self) -> List[Dict[str, Any]]: """Create test users using Supabase client.""" @@ -331,6 +349,8 @@ async def create_test_graphs(self) -> List[Dict[str, Any]]: if is_dummy_input: graph_name = f"DummyInput {graph_name}" + graph_name = f"{graph_name} Agents" + graph = Graph( id=graph_id, name=graph_name, @@ -386,8 +406,10 @@ async def create_test_library_agents(self) -> List[Dict[str, Any]]: ) if graph: # Use the API function to create library agent - library_agent = await create_library_agent(graph, user["id"]) - library_agents.append(library_agent.model_dump()) + library_agents.extend( + v.model_dump() + for v in await create_library_agent(graph, user["id"]) + ) except Exception as e: print(f"Error creating library agent: {e}") continue @@ -461,6 +483,63 @@ async def create_test_api_keys(self) -> List[Dict[str, Any]]: self.api_keys = api_keys return api_keys + async def update_test_profiles(self) -> List[Dict[str, Any]]: + """Update existing user profiles to make some into featured creators.""" + print("Updating user profiles to create featured creators...") + + # Get all existing profiles (auto-created when users were created) + existing_profiles = await prisma.profile.find_many( + where={"userId": {"in": [user["id"] for user in self.users]}} + ) + + if not existing_profiles: + print("No existing profiles found. Profiles may not be auto-created.") + return [] + + profiles = [] + # Select about 70% of users to become creators (update their profiles) + num_creators = max(1, int(len(existing_profiles) * 0.7)) + selected_profiles = random.sample( + existing_profiles, min(num_creators, len(existing_profiles)) + ) + + # Mark about 50% of creators as featured (more for testing) + num_featured = max(2, int(num_creators * 0.5)) + num_featured = min( + num_featured, len(selected_profiles) + ) # Don't exceed available profiles + featured_profile_ids = set( + random.sample([p.id for p in selected_profiles], num_featured) + ) + + for profile in selected_profiles: + try: + is_featured = profile.id in featured_profile_ids + + # Update the profile with creator data + updated_profile = await prisma.profile.update( + where={"id": profile.id}, + data={ + "name": faker.name(), + "username": faker.user_name() + + str(random.randint(100, 999)), # Ensure uniqueness + "description": faker.text(max_nb_chars=200), + "links": [faker.url() for _ in range(random.randint(1, 3))], + "avatarUrl": get_image(), + "isFeatured": is_featured, + }, + ) + + if updated_profile: + profiles.append(updated_profile.model_dump()) + + except Exception as e: + print(f"Error updating profile {profile.id}: {e}") + continue + + self.profiles = profiles + return profiles + async def create_test_store_submissions(self) -> List[Dict[str, Any]]: """Create test store submissions using the API function.""" print("Creating test store submissions...") @@ -468,6 +547,74 @@ async def create_test_store_submissions(self) -> List[Dict[str, Any]]: submissions = [] approved_submissions = [] + # Create a special test submission for test123@gmail.com + test_user = next( + (user for user in self.users if user["email"] == "test123@gmail.com"), None + ) + if test_user: + # Special test data for consistent testing + test_submission_data = { + "user_id": test_user["id"], + "agent_id": self.agent_graphs[0]["id"], # Use first available graph + "agent_version": 1, + "slug": "test-agent-submission", + "name": "Test Agent Submission", + "sub_heading": "A test agent for frontend testing", + "video_url": "https://www.youtube.com/watch?v=test123", + "image_urls": [ + "https://picsum.photos/200/300", + "https://picsum.photos/200/301", + "https://picsum.photos/200/302", + ], + "description": "This is a test agent submission specifically created for frontend testing purposes.", + "categories": ["test", "demo", "frontend"], + "changes_summary": "Initial test submission", + } + + try: + test_submission = await create_store_submission(**test_submission_data) + submissions.append(test_submission.model_dump()) + print("✅ Created special test store submission for test123@gmail.com") + + # Randomly approve, reject, or leave pending the test submission + if test_submission.store_listing_version_id: + random_value = random.random() + if random_value < 0.4: # 40% chance to approve + approved_submission = await review_store_submission( + store_listing_version_id=test_submission.store_listing_version_id, + is_approved=True, + external_comments="Test submission approved", + internal_comments="Auto-approved test submission", + reviewer_id=test_user["id"], + ) + approved_submissions.append(approved_submission.model_dump()) + print("✅ Approved test store submission") + + # Mark approved submission as featured + await prisma.storelistingversion.update( + where={"id": test_submission.store_listing_version_id}, + data={"isFeatured": True}, + ) + print("🌟 Marked test agent as FEATURED") + elif random_value < 0.7: # 30% chance to reject (40% to 70%) + await review_store_submission( + store_listing_version_id=test_submission.store_listing_version_id, + is_approved=False, + external_comments="Test submission rejected - needs improvements", + internal_comments="Auto-rejected test submission for E2E testing", + reviewer_id=test_user["id"], + ) + print("❌ Rejected test store submission") + else: # 30% chance to leave pending (70% to 100%) + print("⏳ Left test submission pending for review") + + except Exception as e: + print(f"Error creating test store submission: {e}") + import traceback + + traceback.print_exc() + + # Create regular submissions for all users for user in self.users: # Get available graphs for this specific user user_graphs = [ @@ -481,7 +628,7 @@ async def create_test_store_submissions(self) -> List[Dict[str, Any]]: continue # Create exactly 4 store submissions per user - for _ in range(4): + for submission_index in range(4): graph = random.choice(user_graphs) try: @@ -500,32 +647,87 @@ async def create_test_store_submissions(self) -> List[Dict[str, Any]]: video_url=get_video_url() if random.random() < 0.3 else None, image_urls=[get_image() for _ in range(3)], description=faker.text(), - categories=[faker.word() for _ in range(3)], + categories=[ + get_category() + ], # Single category from predefined list changes_summary="Initial E2E test submission", ) submissions.append(submission.model_dump()) print(f"✅ Created store submission: {submission.name}") - # Approve the submission so it appears in the store + # Randomly approve, reject, or leave pending the submission if submission.store_listing_version_id: - try: - # Pick a random user as the reviewer (admin) - reviewer_id = random.choice(self.users)["id"] - - approved_submission = await review_store_submission( - store_listing_version_id=submission.store_listing_version_id, - is_approved=True, - external_comments="Auto-approved for E2E testing", - internal_comments="Automatically approved by E2E test data script", - reviewer_id=reviewer_id, - ) - approved_submissions.append( - approved_submission.model_dump() - ) - print(f"✅ Approved store submission: {submission.name}") - except Exception as e: + random_value = random.random() + if random_value < 0.4: # 40% chance to approve + try: + # Pick a random user as the reviewer (admin) + reviewer_id = random.choice(self.users)["id"] + + approved_submission = await review_store_submission( + store_listing_version_id=submission.store_listing_version_id, + is_approved=True, + external_comments="Auto-approved for E2E testing", + internal_comments="Automatically approved by E2E test data script", + reviewer_id=reviewer_id, + ) + approved_submissions.append( + approved_submission.model_dump() + ) + print( + f"✅ Approved store submission: {submission.name}" + ) + + # Mark some agents as featured during creation (30% chance) + # More likely for creators and first submissions + is_creator = user["id"] in [ + p.get("userId") for p in self.profiles + ] + feature_chance = ( + 0.5 if is_creator else 0.2 + ) # 50% for creators, 20% for others + + if random.random() < feature_chance: + try: + await prisma.storelistingversion.update( + where={ + "id": submission.store_listing_version_id + }, + data={"isFeatured": True}, + ) + print( + f"🌟 Marked agent as FEATURED: {submission.name}" + ) + except Exception as e: + print( + f"Warning: Could not mark submission as featured: {e}" + ) + + except Exception as e: + print( + f"Warning: Could not approve submission {submission.name}: {e}" + ) + elif random_value < 0.7: # 30% chance to reject (40% to 70%) + try: + # Pick a random user as the reviewer (admin) + reviewer_id = random.choice(self.users)["id"] + + await review_store_submission( + store_listing_version_id=submission.store_listing_version_id, + is_approved=False, + external_comments="Submission rejected - needs improvements", + internal_comments="Automatically rejected by E2E test data script", + reviewer_id=reviewer_id, + ) + print( + f"❌ Rejected store submission: {submission.name}" + ) + except Exception as e: + print( + f"Warning: Could not reject submission {submission.name}: {e}" + ) + else: # 30% chance to leave pending (70% to 100%) print( - f"Warning: Could not approve submission {submission.name}: {e}" + f"⏳ Left submission pending for review: {submission.name}" ) except Exception as e: @@ -594,6 +796,9 @@ async def create_all_test_data(self): # Create API keys await self.create_test_api_keys() + # Update user profiles to create featured creators + await self.update_test_profiles() + # Create store submissions await self.create_test_store_submissions() @@ -615,7 +820,10 @@ async def create_all_test_data(self): print(f"✅ Agent blocks available: {len(self.agent_blocks)}") print(f"✅ Agent graphs created: {len(self.agent_graphs)}") print(f"✅ Library agents created: {len(self.library_agents)}") - print(f"✅ Store submissions created: {len(self.store_submissions)}") + print(f"✅ Creator profiles updated: {len(self.profiles)} (some featured)") + print( + f"✅ Store submissions created: {len(self.store_submissions)} (some marked as featured during creation)" + ) print(f"✅ API keys created: {len(self.api_keys)}") print(f"✅ Presets created: {len(self.presets)}") print("\n🚀 Your E2E test database is ready to use!") diff --git a/autogpt_platform/backend/test/sdk/test_sdk_patching.py b/autogpt_platform/backend/test/sdk/test_sdk_patching.py index 21c2d72fb79f..42ea47bb434a 100644 --- a/autogpt_platform/backend/test/sdk/test_sdk_patching.py +++ b/autogpt_platform/backend/test/sdk/test_sdk_patching.py @@ -14,6 +14,7 @@ AutoRegistry, BaseOAuthHandler, BaseWebhooksManager, + Credentials, ProviderBuilder, ) @@ -34,7 +35,7 @@ class MockWebhookManager(BaseWebhooksManager): PROVIDER_NAME = ProviderName.GITHUB @classmethod - async def validate_payload(cls, webhook, request): + async def validate_payload(cls, webhook, request, credentials: Credentials | None): return {}, "test_event" async def _register_webhook(self, *args, **kwargs): diff --git a/autogpt_platform/backend/test/sdk/test_sdk_registry.py b/autogpt_platform/backend/test/sdk/test_sdk_registry.py index 701e1d75e1a7..4970ba5933eb 100644 --- a/autogpt_platform/backend/test/sdk/test_sdk_registry.py +++ b/autogpt_platform/backend/test/sdk/test_sdk_registry.py @@ -9,6 +9,7 @@ 5. Block configuration association """ +import os from unittest.mock import MagicMock, Mock, patch import pytest @@ -61,20 +62,29 @@ class TestOAuthHandler(BaseOAuthHandler): from backend.sdk.provider import OAuthConfig - provider = Provider( - name="oauth_provider", - oauth_config=OAuthConfig(oauth_handler=TestOAuthHandler), - webhook_manager=None, - default_credentials=[], - base_costs=[], - supported_auth_types={"oauth2"}, - ) + # Set environment variables so OAuth handler gets registered + with patch.dict( + os.environ, + {"TEST_CLIENT_ID": "test_id", "TEST_CLIENT_SECRET": "test_secret"}, + ): + provider = Provider( + name="oauth_provider", + oauth_config=OAuthConfig( + oauth_handler=TestOAuthHandler, + client_id_env_var="TEST_CLIENT_ID", + client_secret_env_var="TEST_CLIENT_SECRET", + ), + webhook_manager=None, + default_credentials=[], + base_costs=[], + supported_auth_types={"oauth2"}, + ) - AutoRegistry.register_provider(provider) + AutoRegistry.register_provider(provider) - # Verify OAuth handler is registered - assert "oauth_provider" in AutoRegistry._oauth_handlers - assert AutoRegistry._oauth_handlers["oauth_provider"] == TestOAuthHandler + # Verify OAuth handler is registered + assert "oauth_provider" in AutoRegistry._oauth_handlers + assert AutoRegistry._oauth_handlers["oauth_provider"] == TestOAuthHandler def test_provider_with_webhook_manager(self): """Test provider registration with webhook manager.""" @@ -170,32 +180,45 @@ class TestOAuth2(BaseOAuthHandler): from backend.sdk.provider import OAuthConfig - provider1 = Provider( - name="provider1", - oauth_config=OAuthConfig(oauth_handler=TestOAuth1), - webhook_manager=None, - default_credentials=[], - base_costs=[], - supported_auth_types={"oauth2"}, - ) + # Set environment variables so OAuth handlers get registered + with patch.dict( + os.environ, + {"TEST_CLIENT_ID": "test_id", "TEST_CLIENT_SECRET": "test_secret"}, + ): + provider1 = Provider( + name="provider1", + oauth_config=OAuthConfig( + oauth_handler=TestOAuth1, + client_id_env_var="TEST_CLIENT_ID", + client_secret_env_var="TEST_CLIENT_SECRET", + ), + webhook_manager=None, + default_credentials=[], + base_costs=[], + supported_auth_types={"oauth2"}, + ) - provider2 = Provider( - name="provider2", - oauth_config=OAuthConfig(oauth_handler=TestOAuth2), - webhook_manager=None, - default_credentials=[], - base_costs=[], - supported_auth_types={"oauth2"}, - ) + provider2 = Provider( + name="provider2", + oauth_config=OAuthConfig( + oauth_handler=TestOAuth2, + client_id_env_var="TEST_CLIENT_ID", + client_secret_env_var="TEST_CLIENT_SECRET", + ), + webhook_manager=None, + default_credentials=[], + base_costs=[], + supported_auth_types={"oauth2"}, + ) - AutoRegistry.register_provider(provider1) - AutoRegistry.register_provider(provider2) + AutoRegistry.register_provider(provider1) + AutoRegistry.register_provider(provider2) - handlers = AutoRegistry.get_oauth_handlers() - assert "provider1" in handlers - assert "provider2" in handlers - assert handlers["provider1"] == TestOAuth1 - assert handlers["provider2"] == TestOAuth2 + handlers = AutoRegistry.get_oauth_handlers() + assert "provider1" in handlers + assert "provider2" in handlers + assert handlers["provider1"] == TestOAuth1 + assert handlers["provider2"] == TestOAuth2 def test_block_configuration_registration(self): """Test registering block configuration.""" @@ -316,15 +339,22 @@ def test_provider_builder_with_oauth(self): class TestOAuth(BaseOAuthHandler): PROVIDER_NAME = ProviderName.GITHUB - provider = ( - ProviderBuilder("oauth_test") - .with_oauth(TestOAuth, scopes=["read", "write"]) - .build() - ) + with patch.dict( + os.environ, + { + "OAUTH_TEST_CLIENT_ID": "test_id", + "OAUTH_TEST_CLIENT_SECRET": "test_secret", + }, + ): + provider = ( + ProviderBuilder("oauth_test") + .with_oauth(TestOAuth, scopes=["read", "write"]) + .build() + ) - assert provider.oauth_config is not None - assert provider.oauth_config.oauth_handler == TestOAuth - assert "oauth2" in provider.supported_auth_types + assert provider.oauth_config is not None + assert provider.oauth_config.oauth_handler == TestOAuth + assert "oauth2" in provider.supported_auth_types def test_provider_builder_with_webhook(self): """Test building a provider with webhook manager.""" @@ -395,34 +425,43 @@ def client_factory(): def error_handler(exc): return str(exc) - provider = ( - ProviderBuilder("complete_test") - .with_api_key("COMPLETE_API_KEY", "Complete API Key") - .with_oauth(TestOAuth, scopes=["read"]) - .with_webhook_manager(TestWebhook) - .with_base_cost(100, BlockCostType.RUN) - .with_api_client(client_factory) - .with_error_handler(error_handler) - .with_config(custom_setting="value") - .build() - ) - - # Verify all settings - assert provider.name == "complete_test" - assert "api_key" in provider.supported_auth_types - assert "oauth2" in provider.supported_auth_types - assert provider.oauth_config is not None - assert provider.oauth_config.oauth_handler == TestOAuth - assert provider.webhook_manager == TestWebhook - assert len(provider.base_costs) == 1 - assert provider._api_client_factory == client_factory - assert provider._error_handler == error_handler - assert provider.get_config("custom_setting") == "value" # from with_config + # Set environment variables for OAuth to be registered + with patch.dict( + os.environ, + { + "COMPLETE_TEST_CLIENT_ID": "test_id", + "COMPLETE_TEST_CLIENT_SECRET": "test_secret", + "COMPLETE_API_KEY": "test_api_key", + }, + ): + provider = ( + ProviderBuilder("complete_test") + .with_api_key("COMPLETE_API_KEY", "Complete API Key") + .with_oauth(TestOAuth, scopes=["read"]) + .with_webhook_manager(TestWebhook) + .with_base_cost(100, BlockCostType.RUN) + .with_api_client(client_factory) + .with_error_handler(error_handler) + .with_config(custom_setting="value") + .build() + ) - # Verify it's registered - assert AutoRegistry.get_provider("complete_test") == provider - assert "complete_test" in AutoRegistry._oauth_handlers - assert "complete_test" in AutoRegistry._webhook_managers + # Verify all settings + assert provider.name == "complete_test" + assert "api_key" in provider.supported_auth_types + assert "oauth2" in provider.supported_auth_types + assert provider.oauth_config is not None + assert provider.oauth_config.oauth_handler == TestOAuth + assert provider.webhook_manager == TestWebhook + assert len(provider.base_costs) == 1 + assert provider._api_client_factory == client_factory + assert provider._error_handler == error_handler + assert provider.get_config("custom_setting") == "value" # from with_config + + # Verify it's registered + assert AutoRegistry.get_provider("complete_test") == provider + assert "complete_test" in AutoRegistry._oauth_handlers + assert "complete_test" in AutoRegistry._webhook_managers class TestSDKImports: diff --git a/autogpt_platform/backend/test/sdk/test_sdk_webhooks.py b/autogpt_platform/backend/test/sdk/test_sdk_webhooks.py index 511448c97fbc..65101c8fe647 100644 --- a/autogpt_platform/backend/test/sdk/test_sdk_webhooks.py +++ b/autogpt_platform/backend/test/sdk/test_sdk_webhooks.py @@ -19,6 +19,7 @@ BlockOutput, BlockSchema, BlockWebhookConfig, + Credentials, CredentialsField, CredentialsMetaInput, Field, @@ -45,7 +46,9 @@ class WebhookType(str, Enum): TEST = "test" @classmethod - async def validate_payload(cls, webhook, request): + async def validate_payload( + cls, webhook, request, credentials: Credentials | None = None + ): """Validate incoming webhook payload.""" # Mock implementation payload = {"test": "data"} diff --git a/autogpt_platform/db/docker/.env.example b/autogpt_platform/db/docker/.env.example deleted file mode 100644 index bb74500874d3..000000000000 --- a/autogpt_platform/db/docker/.env.example +++ /dev/null @@ -1,123 +0,0 @@ -############ -# Secrets -# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION -############ - -POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password -JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long -ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE -SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q -DASHBOARD_USERNAME=supabase -DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated -SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq -VAULT_ENC_KEY=your-encryption-key-32-chars-min - - -############ -# Database - You can change these to any PostgreSQL database that has logical replication enabled. -############ - -POSTGRES_HOST=db -POSTGRES_DB=postgres -POSTGRES_PORT=5432 -# default user is postgres - - -############ -# Supavisor -- Database pooler -############ -POOLER_PROXY_PORT_TRANSACTION=6543 -POOLER_DEFAULT_POOL_SIZE=20 -POOLER_MAX_CLIENT_CONN=100 -POOLER_TENANT_ID=your-tenant-id - - -############ -# API Proxy - Configuration for the Kong Reverse proxy. -############ - -KONG_HTTP_PORT=8000 -KONG_HTTPS_PORT=8443 - - -############ -# API - Configuration for PostgREST. -############ - -PGRST_DB_SCHEMAS=public,storage,graphql_public - - -############ -# Auth - Configuration for the GoTrue authentication server. -############ - -## General -SITE_URL=http://localhost:3000 -ADDITIONAL_REDIRECT_URLS= -JWT_EXPIRY=3600 -DISABLE_SIGNUP=false -API_EXTERNAL_URL=http://localhost:8000 - -## Mailer Config -MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify" -MAILER_URLPATHS_INVITE="/auth/v1/verify" -MAILER_URLPATHS_RECOVERY="/auth/v1/verify" -MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify" - -## Email auth -ENABLE_EMAIL_SIGNUP=true -ENABLE_EMAIL_AUTOCONFIRM=false -SMTP_ADMIN_EMAIL=admin@example.com -SMTP_HOST=supabase-mail -SMTP_PORT=2500 -SMTP_USER=fake_mail_user -SMTP_PASS=fake_mail_password -SMTP_SENDER_NAME=fake_sender -ENABLE_ANONYMOUS_USERS=false - -## Phone auth -ENABLE_PHONE_SIGNUP=true -ENABLE_PHONE_AUTOCONFIRM=true - - -############ -# Studio - Configuration for the Dashboard -############ - -STUDIO_DEFAULT_ORGANIZATION=Default Organization -STUDIO_DEFAULT_PROJECT=Default Project - -STUDIO_PORT=3000 -# replace if you intend to use Studio outside of localhost -SUPABASE_PUBLIC_URL=http://localhost:8000 - -# Enable webp support -IMGPROXY_ENABLE_WEBP_DETECTION=true - -# Add your OpenAI API key to enable SQL Editor Assistant -OPENAI_API_KEY= - - -############ -# Functions - Configuration for Functions -############ -# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet. -FUNCTIONS_VERIFY_JWT=false - - -############ -# Logs - Configuration for Logflare -# Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction -############ - -LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key - -# Change vector.toml sinks to reflect this change -LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key - -# Docker socket location - this value will differ depending on your OS -DOCKER_SOCKET_LOCATION=/var/run/docker.sock - -# Google Cloud Project details -GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID -GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER diff --git a/autogpt_platform/db/docker/.gitignore b/autogpt_platform/db/docker/.gitignore index a1e9dc61e05f..a8c501f97b5f 100644 --- a/autogpt_platform/db/docker/.gitignore +++ b/autogpt_platform/db/docker/.gitignore @@ -1,5 +1,4 @@ volumes/db/data volumes/storage -.env test.http docker-compose.override.yml diff --git a/autogpt_platform/db/docker/docker-compose.yml b/autogpt_platform/db/docker/docker-compose.yml index fb0f62a5f816..9774cccb331e 100644 --- a/autogpt_platform/db/docker/docker-compose.yml +++ b/autogpt_platform/db/docker/docker-compose.yml @@ -5,8 +5,101 @@ # Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans # Reset everything: ./reset.sh +# Environment Variable Loading Order (first → last, later overrides earlier): +# 1. ../../.env.default - Default values for all Supabase settings +# 2. ../../.env - User's custom configuration (if exists) +# 3. ./.env - Local overrides specific to db/docker (if exists) +# 4. environment key - Service-specific overrides defined below +# 5. Shell environment - Variables exported before running docker compose + name: supabase +# Common env_file configuration for all Supabase services +x-supabase-env-files: &supabase-env-files + env_file: + - ../../.env.default # Base defaults from platform root + - path: ../../.env # User overrides from platform root (optional) + required: false + - path: ./.env # Local overrides for db/docker (optional) + required: false + +# Common Supabase environment - hardcoded defaults to avoid variable substitution +x-supabase-env: &supabase-env + # Core PostgreSQL settings + POSTGRES_PASSWORD: your-super-secret-and-long-postgres-password + POSTGRES_HOST: db + POSTGRES_PORT: "5432" + POSTGRES_DB: postgres + + # Authentication & Security + JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q + DASHBOARD_USERNAME: supabase + DASHBOARD_PASSWORD: this_password_is_insecure_and_should_be_updated + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + VAULT_ENC_KEY: your-encryption-key-32-chars-min + + # URLs and Endpoints + SITE_URL: http://localhost:3000 + API_EXTERNAL_URL: http://localhost:8000 + SUPABASE_PUBLIC_URL: http://localhost:8000 + ADDITIONAL_REDIRECT_URLS: "" + + # Feature Flags + DISABLE_SIGNUP: "false" + ENABLE_EMAIL_SIGNUP: "true" + ENABLE_EMAIL_AUTOCONFIRM: "false" + ENABLE_ANONYMOUS_USERS: "false" + ENABLE_PHONE_SIGNUP: "true" + ENABLE_PHONE_AUTOCONFIRM: "true" + FUNCTIONS_VERIFY_JWT: "false" + IMGPROXY_ENABLE_WEBP_DETECTION: "true" + + # Email/SMTP Configuration + SMTP_ADMIN_EMAIL: admin@example.com + SMTP_HOST: supabase-mail + SMTP_PORT: "2500" + SMTP_USER: fake_mail_user + SMTP_PASS: fake_mail_password + SMTP_SENDER_NAME: fake_sender + + # Mailer URLs + MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify + MAILER_URLPATHS_INVITE: /auth/v1/verify + MAILER_URLPATHS_RECOVERY: /auth/v1/verify + MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify + + # JWT Settings + JWT_EXPIRY: "3600" + + # Database Schemas + PGRST_DB_SCHEMAS: public,storage,graphql_public + + # Studio Settings + STUDIO_DEFAULT_ORGANIZATION: Default Organization + STUDIO_DEFAULT_PROJECT: Default Project + + # Logging + LOGFLARE_API_KEY: your-super-secret-and-long-logflare-key + + # Pooler Settings + POOLER_DEFAULT_POOL_SIZE: "20" + POOLER_MAX_CLIENT_CONN: "100" + POOLER_TENANT_ID: your-tenant-id + POOLER_PROXY_PORT_TRANSACTION: "6543" + + # Kong Ports + KONG_HTTP_PORT: "8000" + KONG_HTTPS_PORT: "8443" + + # Docker + DOCKER_SOCKET_LOCATION: /var/run/docker.sock + + # Google Cloud (if needed) + GOOGLE_PROJECT_ID: GOOGLE_PROJECT_ID + GOOGLE_PROJECT_NUMBER: GOOGLE_PROJECT_NUMBER + services: studio: @@ -24,24 +117,24 @@ services: timeout: 10s interval: 5s retries: 3 - depends_on: - analytics: - condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service STUDIO_PG_META_URL: http://meta:8080 - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: your-super-secret-and-long-postgres-password - DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} - DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} + DEFAULT_ORGANIZATION_NAME: Default Organization + DEFAULT_PROJECT_NAME: Default Project + OPENAI_API_KEY: "" SUPABASE_URL: http://kong:8000 - SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} - SUPABASE_ANON_KEY: ${ANON_KEY} - SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} - AUTH_JWT_SECRET: ${JWT_SECRET} + SUPABASE_PUBLIC_URL: http://localhost:8000 + SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + SUPABASE_SERVICE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q + AUTH_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_API_KEY: your-super-secret-and-long-logflare-key LOGFLARE_URL: http://analytics:4000 NEXT_PUBLIC_ENABLE_LOGS: true # Comment to use Big Query backend for analytics @@ -54,15 +147,15 @@ services: image: kong:2.8.1 restart: unless-stopped ports: - - ${KONG_HTTP_PORT}:8000/tcp - - ${KONG_HTTPS_PORT}:8443/tcp + - 8000:8000/tcp + - 8443:8443/tcp volumes: # https://github.com/supabase/supabase/issues/12661 - ./volumes/api/kong.yml:/home/kong/temp.yml:ro - depends_on: - analytics: - condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml # https://github.com/supabase/cli/issues/14 @@ -70,10 +163,10 @@ services: KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k - SUPABASE_ANON_KEY: ${ANON_KEY} - SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} - DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} - DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + SUPABASE_SERVICE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q + DASHBOARD_USERNAME: supabase + DASHBOARD_PASSWORD: this_password_is_insecure_and_should_be_updated # https://unix.stackexchange.com/a/294837 entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' @@ -98,48 +191,49 @@ services: db: # Disable this if you are using an external Postgres database condition: service_healthy - analytics: - condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service GOTRUE_API_HOST: 0.0.0.0 GOTRUE_API_PORT: 9999 - API_EXTERNAL_URL: ${API_EXTERNAL_URL} + API_EXTERNAL_URL: http://localhost:8000 GOTRUE_DB_DRIVER: postgres - GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:your-super-secret-and-long-postgres-password@db:5432/postgres - GOTRUE_SITE_URL: ${SITE_URL} - GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} - GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + GOTRUE_SITE_URL: http://localhost:3000 + GOTRUE_URI_ALLOW_LIST: "" + GOTRUE_DISABLE_SIGNUP: false GOTRUE_JWT_ADMIN_ROLES: service_role GOTRUE_JWT_AUD: authenticated GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated - GOTRUE_JWT_EXP: ${JWT_EXPIRY} - GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_JWT_EXP: 3600 + GOTRUE_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long - GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} - GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} - GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + GOTRUE_EXTERNAL_EMAIL_ENABLED: true + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: false + GOTRUE_MAILER_AUTOCONFIRM: false # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true # GOTRUE_SMTP_MAX_FREQUENCY: 1s - GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} - GOTRUE_SMTP_HOST: ${SMTP_HOST} - GOTRUE_SMTP_PORT: ${SMTP_PORT} - GOTRUE_SMTP_USER: ${SMTP_USER} - GOTRUE_SMTP_PASS: ${SMTP_PASS} - GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} - GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} - GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} - GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} - GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} - - GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} - GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + GOTRUE_SMTP_ADMIN_EMAIL: admin@example.com + GOTRUE_SMTP_HOST: supabase-mail + GOTRUE_SMTP_PORT: 2500 + GOTRUE_SMTP_USER: fake_mail_user + GOTRUE_SMTP_PASS: fake_mail_password + GOTRUE_SMTP_SENDER_NAME: fake_sender + GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify + + GOTRUE_EXTERNAL_PHONE_ENABLED: true + GOTRUE_SMS_AUTOCONFIRM: true # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" @@ -168,16 +262,17 @@ services: db: # Disable this if you are using an external Postgres database condition: service_healthy - analytics: - condition: service_healthy + <<: *supabase-env-files environment: - PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + <<: *supabase-env + # Keep any existing environment variables specific to that service + PGRST_DB_URI: postgres://authenticator:your-super-secret-and-long-postgres-password@db:5432/postgres + PGRST_DB_SCHEMAS: public,storage,graphql_public PGRST_DB_ANON_ROLE: anon - PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long PGRST_DB_USE_LEGACY_GUCS: "false" - PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} - PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + PGRST_APP_SETTINGS_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + PGRST_APP_SETTINGS_JWT_EXP: 3600 command: [ "postgrest" @@ -192,8 +287,6 @@ services: db: # Disable this if you are using an external Postgres database condition: service_healthy - analytics: - condition: service_healthy healthcheck: test: [ @@ -204,23 +297,26 @@ services: "-o", "/dev/null", "-H", - "Authorization: Bearer ${ANON_KEY}", + "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE", "http://localhost:4000/api/tenants/realtime-dev/health" ] timeout: 5s interval: 5s retries: 3 + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service PORT: 4000 - DB_HOST: ${POSTGRES_HOST} - DB_PORT: ${POSTGRES_PORT} + DB_HOST: db + DB_PORT: 5432 DB_USER: supabase_admin - DB_PASSWORD: ${POSTGRES_PASSWORD} - DB_NAME: ${POSTGRES_DB} + DB_PASSWORD: your-super-secret-and-long-postgres-password + DB_NAME: postgres DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' DB_ENC_KEY: supabaserealtime - API_JWT_SECRET: ${JWT_SECRET} - SECRET_KEY_BASE: ${SECRET_KEY_BASE} + API_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq ERL_AFLAGS: -proto_dist inet_tcp DNS_NODES: "''" RLIMIT_NOFILE: "10000" @@ -256,12 +352,15 @@ services: condition: service_started imgproxy: condition: service_started + <<: *supabase-env-files environment: - ANON_KEY: ${ANON_KEY} - SERVICE_KEY: ${SERVICE_ROLE_KEY} + <<: *supabase-env + # Keep any existing environment variables specific to that service + ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + SERVICE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q POSTGREST_URL: http://rest:3000 - PGRST_JWT_SECRET: ${JWT_SECRET} - DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + DATABASE_URL: postgres://supabase_storage_admin:your-super-secret-and-long-postgres-password@db:5432/postgres FILE_SIZE_LIMIT: 52428800 STORAGE_BACKEND: file FILE_STORAGE_BACKEND_PATH: /var/lib/storage @@ -288,11 +387,14 @@ services: timeout: 5s interval: 5s retries: 3 + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service IMGPROXY_BIND: ":5001" IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_USE_ETAG: "true" - IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + IMGPROXY_ENABLE_WEBP_DETECTION: true meta: container_name: supabase-meta @@ -302,15 +404,16 @@ services: db: # Disable this if you are using an external Postgres database condition: service_healthy - analytics: - condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service PG_META_PORT: 8080 - PG_META_DB_HOST: ${POSTGRES_HOST} - PG_META_DB_PORT: ${POSTGRES_PORT} - PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_HOST: db + PG_META_DB_PORT: 5432 + PG_META_DB_NAME: postgres PG_META_DB_USER: supabase_admin - PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + PG_META_DB_PASSWORD: your-super-secret-and-long-postgres-password functions: container_name: supabase-edge-functions @@ -318,17 +421,17 @@ services: restart: unless-stopped volumes: - ./volumes/functions:/home/deno/functions:Z - depends_on: - analytics: - condition: service_healthy + <<: *supabase-env-files environment: - JWT_SECRET: ${JWT_SECRET} + <<: *supabase-env + # Keep any existing environment variables specific to that service + JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long SUPABASE_URL: http://kong:8000 - SUPABASE_ANON_KEY: ${ANON_KEY} - SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} - SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + SUPABASE_SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q + SUPABASE_DB_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 - VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + VERIFY_JWT: "false" command: [ "start", @@ -362,26 +465,29 @@ services: db: # Disable this if you are using an external Postgres database condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service LOGFLARE_NODE_HOST: 127.0.0.1 DB_USERNAME: supabase_admin DB_DATABASE: _supabase - DB_HOSTNAME: ${POSTGRES_HOST} - DB_PORT: ${POSTGRES_PORT} - DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_HOSTNAME: db + DB_PORT: 5432 + DB_PASSWORD: your-super-secret-and-long-postgres-password DB_SCHEMA: _analytics - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_API_KEY: your-super-secret-and-long-logflare-key LOGFLARE_SINGLE_TENANT: true LOGFLARE_SUPABASE_MODE: true LOGFLARE_MIN_CLUSTER_SIZE: 1 # Comment variables to use Big Query backend for analytics - POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + POSTGRES_BACKEND_URL: postgresql://supabase_admin:your-super-secret-and-long-postgres-password@db:5432/_supabase POSTGRES_BACKEND_SCHEMA: _analytics LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true # Uncomment to use Big Query backend for analytics - # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} - # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} + # GOOGLE_PROJECT_ID: GOOGLE_PROJECT_ID + # GOOGLE_PROJECT_NUMBER: GOOGLE_PROJECT_NUMBER # Comment out everything below this point if you are using an external Postgres database db: @@ -419,19 +525,19 @@ services: interval: 5s timeout: 5s retries: 10 - depends_on: - vector: - condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service POSTGRES_HOST: /var/run/postgresql - PGPORT: ${POSTGRES_PORT} - POSTGRES_PORT: ${POSTGRES_PORT} - PGPASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - PGDATABASE: ${POSTGRES_DB} - POSTGRES_DB: ${POSTGRES_DB} - JWT_SECRET: ${JWT_SECRET} - JWT_EXP: ${JWT_EXPIRY} + PGPORT: 5432 + POSTGRES_PORT: 5432 + PGPASSWORD: your-super-secret-and-long-postgres-password + POSTGRES_PASSWORD: your-super-secret-and-long-postgres-password + PGDATABASE: postgres + POSTGRES_DB: postgres + JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + JWT_EXP: 3600 command: [ "postgres", @@ -447,7 +553,7 @@ services: restart: unless-stopped volumes: - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro - - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro + - /var/run/docker.sock:/var/run/docker.sock:ro healthcheck: test: [ @@ -461,8 +567,11 @@ services: timeout: 5s interval: 5s retries: 3 + <<: *supabase-env-files environment: - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + <<: *supabase-env + # Keep any existing environment variables specific to that service + LOGFLARE_API_KEY: your-super-secret-and-long-logflare-key command: [ "--config", @@ -475,8 +584,8 @@ services: image: supabase/supavisor:2.4.12 restart: unless-stopped ports: - - ${POSTGRES_PORT}:5432 - - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + - 5432:5432 + - 6543:6543 volumes: - ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro healthcheck: @@ -498,22 +607,25 @@ services: condition: service_healthy analytics: condition: service_healthy + <<: *supabase-env-files environment: + <<: *supabase-env + # Keep any existing environment variables specific to that service PORT: 4000 - POSTGRES_PORT: ${POSTGRES_PORT} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase + POSTGRES_PORT: 5432 + POSTGRES_DB: postgres + POSTGRES_PASSWORD: your-super-secret-and-long-postgres-password + DATABASE_URL: ecto://supabase_admin:your-super-secret-and-long-postgres-password@db:5432/_supabase CLUSTER_POSTGRES: true - SECRET_KEY_BASE: ${SECRET_KEY_BASE} - VAULT_ENC_KEY: ${VAULT_ENC_KEY} - API_JWT_SECRET: ${JWT_SECRET} - METRICS_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + VAULT_ENC_KEY: your-encryption-key-32-chars-min + API_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long + METRICS_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long REGION: local ERL_AFLAGS: -proto_dist inet_tcp - POOLER_TENANT_ID: ${POOLER_TENANT_ID} - POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} - POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} + POOLER_TENANT_ID: your-tenant-id + POOLER_DEFAULT_POOL_SIZE: 20 + POOLER_MAX_CLIENT_CONN: 100 POOLER_POOL_MODE: transaction command: [ diff --git a/autogpt_platform/db/docker/reset.sh b/autogpt_platform/db/docker/reset.sh index d5f3a41dae06..2efe8983206e 100755 --- a/autogpt_platform/db/docker/reset.sh +++ b/autogpt_platform/db/docker/reset.sh @@ -34,11 +34,11 @@ else echo "No .env file found. Skipping .env removal step..." fi -if [ -f ".env.example" ]; then - echo "Copying .env.example to .env..." - cp .env.example .env +if [ -f ".env.default" ]; then + echo "Copying .env.default to .env..." + cp .env.default .env else - echo ".env.example file not found. Skipping .env reset step..." + echo ".env.default file not found. Skipping .env reset step..." fi echo "Cleanup complete!" \ No newline at end of file diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml index 474c95fcb519..c4e9a14e7b60 100644 --- a/autogpt_platform/docker-compose.platform.yml +++ b/autogpt_platform/docker-compose.platform.yml @@ -1,9 +1,39 @@ +# Environment Variable Loading Order (first → last, later overrides earlier): +# 1. backend/.env.default - Default values for all settings +# 2. backend/.env - User's custom configuration (if exists) +# 3. environment key - Docker-specific overrides defined below +# 4. Shell environment - Variables exported before running docker compose +# 5. CLI arguments - docker compose run -e VAR=value + +# Common backend environment - Docker service names +x-backend-env: + &backend-env # Docker internal service hostnames (override localhost defaults) + PYRO_HOST: "0.0.0.0" + AGENTSERVER_HOST: rest_server + SCHEDULER_HOST: scheduler_server + DATABASEMANAGER_HOST: database_manager + EXECUTIONMANAGER_HOST: executor + NOTIFICATIONMANAGER_HOST: notification_server + CLAMAV_SERVICE_HOST: clamav + DB_HOST: db + REDIS_HOST: redis + RABBITMQ_HOST: rabbitmq + # Override Supabase URL for Docker network + SUPABASE_URL: http://kong:8000 + +# Common env_file configuration for backend services +x-backend-env-files: &backend-env-files + env_file: + - backend/.env.default # Base defaults (always exists) + - path: backend/.env # User overrides (optional) + required: false + services: migrate: build: context: ../ dockerfile: autogpt_platform/backend/Dockerfile - target: server + target: migrate command: ["sh", "-c", "poetry run prisma migrate deploy"] develop: watch: @@ -20,10 +50,11 @@ services: - app-network restart: on-failure healthcheck: - test: ["CMD", "poetry", "run", "prisma", "migrate", "status"] - interval: 10s - timeout: 5s - retries: 5 + test: ["CMD-SHELL", "poetry run prisma migrate status | grep -q 'No pending migrations' || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s redis: image: redis:latest @@ -73,34 +104,14 @@ services: condition: service_completed_successfully rabbitmq: condition: service_healthy - # scheduler_server: - # condition: service_healthy + <<: *backend-env-files environment: - - SUPABASE_URL=http://kong:8000 - - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q - - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - DIRECT_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - REDIS_HOST=redis - - REDIS_PORT=6379 - - RABBITMQ_HOST=rabbitmq - - RABBITMQ_PORT=5672 - - RABBITMQ_DEFAULT_USER=rabbitmq_user_default - - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 - - REDIS_PASSWORD=password - - ENABLE_AUTH=true - - PYRO_HOST=0.0.0.0 - - SCHEDULER_HOST=scheduler_server - - EXECUTIONMANAGER_HOST=executor - - NOTIFICATIONMANAGER_HOST=rest_server - - CLAMAV_SERVICE_HOST=clamav - - NEXT_PUBLIC_FRONTEND_BASE_URL=http://localhost:3000 - - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] - - ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!! - - UNSUBSCRIBE_SECRET_KEY=HlP8ivStJjmbf6NKi78m_3FnOogut0t5ckzjsIqeaio= # DO NOT USE IN PRODUCTION!! + <<: *backend-env + # Service-specific overrides + DATABASE_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + DIRECT_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform ports: - "8006:8006" - - "8007:8007" networks: - app-network @@ -124,26 +135,14 @@ services: condition: service_healthy migrate: condition: service_completed_successfully + database_manager: + condition: service_started + <<: *backend-env-files environment: - - DATABASEMANAGER_HOST=rest_server - - SUPABASE_URL=http://kong:8000 - - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q - - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - DIRECT_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - REDIS_HOST=redis - - REDIS_PORT=6379 - - REDIS_PASSWORD=password - - RABBITMQ_HOST=rabbitmq - - RABBITMQ_PORT=5672 - - RABBITMQ_DEFAULT_USER=rabbitmq_user_default - - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 - - ENABLE_AUTH=true - - PYRO_HOST=0.0.0.0 - - AGENTSERVER_HOST=rest_server - - NOTIFICATIONMANAGER_HOST=rest_server - - CLAMAV_SERVICE_HOST=clamav - - ENCRYPTION_KEY=dvziYgz0KSK8FENhju0ZYi8-fRTfAdlz6YLhdB_jhNw= # DO NOT USE IN PRODUCTION!! + <<: *backend-env + # Service-specific overrides + DATABASE_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + DIRECT_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform ports: - "8002:8002" networks: @@ -165,31 +164,48 @@ services: condition: service_healthy redis: condition: service_healthy - # rabbitmq: - # condition: service_healthy migrate: condition: service_completed_successfully + database_manager: + condition: service_started + <<: *backend-env-files environment: - - DATABASEMANAGER_HOST=rest_server - - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - DIRECT_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - REDIS_HOST=redis - - REDIS_PORT=6379 - - REDIS_PASSWORD=password - # - RABBITMQ_HOST=rabbitmq - # - RABBITMQ_PORT=5672 - # - RABBITMQ_DEFAULT_USER=rabbitmq_user_default - # - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 - - ENABLE_AUTH=true - - PYRO_HOST=0.0.0.0 - - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] - + <<: *backend-env + # Service-specific overrides + DATABASE_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + DIRECT_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform ports: - "8001:8001" networks: - app-network + database_manager: + build: + context: ../ + dockerfile: autogpt_platform/backend/Dockerfile + target: server + command: ["python", "-m", "backend.db"] + develop: + watch: + - path: ./ + target: autogpt_platform/backend/ + action: rebuild + depends_on: + db: + condition: service_healthy + migrate: + condition: service_completed_successfully + <<: *backend-env-files + environment: + <<: *backend-env + # Service-specific overrides + DATABASE_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + DIRECT_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + ports: + - "8005:8005" + networks: + - app-network + scheduler_server: build: context: ../ @@ -210,6 +226,8 @@ services: condition: service_healthy migrate: condition: service_completed_successfully + database_manager: + condition: service_started # healthcheck: # test: # [ @@ -223,56 +241,70 @@ services: # interval: 10s # timeout: 10s # retries: 5 + <<: *backend-env-files environment: - - DATABASEMANAGER_HOST=rest_server - - NOTIFICATIONMANAGER_HOST=rest_server - - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - DIRECT_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - - REDIS_HOST=redis - - REDIS_PORT=6379 - - REDIS_PASSWORD=password - - RABBITMQ_HOST=rabbitmq - - RABBITMQ_PORT=5672 - - RABBITMQ_DEFAULT_USER=rabbitmq_user_default - - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 - - ENABLE_AUTH=true - - PYRO_HOST=0.0.0.0 - - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] - + <<: *backend-env + # Service-specific overrides + DATABASE_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform + DIRECT_URL: postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform ports: - "8003:8003" networks: - app-network -# frontend: -# build: -# context: ../ -# dockerfile: autogpt_platform/frontend/Dockerfile -# target: dev -# depends_on: -# db: -# condition: service_healthy -# rest_server: -# condition: service_started -# websocket_server: -# condition: service_started -# migrate: -# condition: service_completed_successfully -# environment: -# - NEXT_PUBLIC_SUPABASE_URL=http://kong:8000 -# - NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE -# - DATABASE_URL=postgresql://agpt_user:pass123@postgres:5432/postgres?connect_timeout=60&schema=platform -# - DIRECT_URL=postgresql://agpt_user:pass123@postgres:5432/postgres?connect_timeout=60&schema=platform -# - NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api -# - NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws -# - NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market -# - NEXT_PUBLIC_BEHAVE_AS=LOCAL -# ports: -# - "3000:3000" -# networks: -# - app-network - + notification_server: + build: + context: ../ + dockerfile: autogpt_platform/backend/Dockerfile + target: server + command: ["python", "-m", "backend.notification"] + develop: + watch: + - path: ./ + target: autogpt_platform/backend/ + action: rebuild + depends_on: + db: + condition: service_healthy + rabbitmq: + condition: service_healthy + migrate: + condition: service_completed_successfully + database_manager: + condition: service_started + <<: *backend-env-files + environment: + <<: *backend-env + ports: + - "8007:8007" + networks: + - app-network + frontend: + build: + context: ../ + dockerfile: autogpt_platform/frontend/Dockerfile + target: prod + depends_on: + db: + condition: service_healthy + migrate: + condition: service_completed_successfully + ports: + - "3000:3000" + networks: + - app-network + # Load environment variables in order (later overrides earlier) + env_file: + - path: ./frontend/.env.default # Base defaults (always exists) + - path: ./frontend/.env # User overrides (optional) + required: false + environment: + # Server-side environment variables (Docker service names) + # These override the localhost URLs from env files when running in Docker + AUTH_CALLBACK_URL: http://rest_server:8006/auth/callback + SUPABASE_URL: http://kong:8000 + AGPT_SERVER_URL: http://rest_server:8006/api + AGPT_WS_SERVER_URL: ws://websocket_server:8001/ws networks: app-network: driver: bridge diff --git a/autogpt_platform/docker-compose.yml b/autogpt_platform/docker-compose.yml index 07810424ede4..1860252f4686 100644 --- a/autogpt_platform/docker-compose.yml +++ b/autogpt_platform/docker-compose.yml @@ -20,6 +20,7 @@ x-supabase-services: - app-network - shared-network + services: # AGPT services migrate: @@ -58,12 +59,24 @@ services: file: ./docker-compose.platform.yml service: websocket_server + database_manager: + <<: *agpt-services + extends: + file: ./docker-compose.platform.yml + service: database_manager + scheduler_server: <<: *agpt-services extends: file: ./docker-compose.platform.yml service: scheduler_server + notification_server: + <<: *agpt-services + extends: + file: ./docker-compose.platform.yml + service: notification_server + clamav: <<: *agpt-services image: clamav/clamav-debian:latest @@ -84,19 +97,13 @@ services: timeout: 10s retries: 3 - # frontend: - # <<: *agpt-services - # extends: - # file: ./docker-compose.platform.yml - # service: frontend - - # Supabase services - studio: - <<: *supabase-services + frontend: + <<: *agpt-services extends: - file: ./db/docker/docker-compose.yml - service: studio + file: ./docker-compose.platform.yml + service: frontend + # Supabase services (minimal: auth + db + kong) kong: <<: *supabase-services extends: @@ -111,61 +118,35 @@ services: environment: GOTRUE_MAILER_AUTOCONFIRM: true - rest: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: rest - - realtime: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: realtime - - storage: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: storage - - imgproxy: + db: <<: *supabase-services extends: file: ./db/docker/docker-compose.yml - service: imgproxy + service: db + ports: + - 5432:5432 # We don't use Supavisor locally, so we expose the db directly. + # Studio and its dependencies for local development only meta: <<: *supabase-services + profiles: + - local extends: file: ./db/docker/docker-compose.yml service: meta - functions: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: functions - - analytics: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: analytics - - db: - <<: *supabase-services - extends: - file: ./db/docker/docker-compose.yml - service: db - ports: - - ${POSTGRES_PORT}:5432 # We don't use Supavisor locally, so we expose the db directly. - - vector: + studio: <<: *supabase-services + profiles: + - local extends: file: ./db/docker/docker-compose.yml - service: vector + service: studio + depends_on: + meta: + condition: service_healthy + # environment: + # NEXT_PUBLIC_ENABLE_LOGS: false # Disable analytics/logging features deps: <<: *supabase-services @@ -174,13 +155,24 @@ services: image: busybox command: /bin/true depends_on: - - studio - kong - auth - - meta - - analytics - db - - vector + - studio - redis - rabbitmq - clamav + - migrate + + deps_backend: + <<: *agpt-services + profiles: + - local + image: busybox + command: /bin/true + depends_on: + - deps + - rest_server + - executor + - websocket_server + - database_manager diff --git a/autogpt_platform/frontend/.env.default b/autogpt_platform/frontend/.env.default new file mode 100644 index 000000000000..175567c5b2ae --- /dev/null +++ b/autogpt_platform/frontend/.env.default @@ -0,0 +1,20 @@ + NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000 + NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + + NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api + NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws + NEXT_PUBLIC_FRONTEND_BASE_URL=http://localhost:3000 + + NEXT_PUBLIC_APP_ENV=local + NEXT_PUBLIC_BEHAVE_AS=LOCAL + + NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false + NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=687ab1372f497809b131e06e + + NEXT_PUBLIC_SHOW_BILLING_PAGE=false + NEXT_PUBLIC_TURNSTILE=disabled + NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true + + NEXT_PUBLIC_GA_MEASUREMENT_ID=G-FH2XK2W4GN + NEXT_PUBLIC_PW_TEST=true + \ No newline at end of file diff --git a/autogpt_platform/frontend/.env.example b/autogpt_platform/frontend/.env.example deleted file mode 100644 index 5e1edb8a86a8..000000000000 --- a/autogpt_platform/frontend/.env.example +++ /dev/null @@ -1,40 +0,0 @@ -NEXT_PUBLIC_FRONTEND_BASE_URL=http://localhost:3000 - -NEXT_PUBLIC_AUTH_CALLBACK_URL=http://localhost:8006/auth/callback -NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api -NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws -NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market -NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false -NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=687910b9638a3d099c11ab7f # Local environment on Launch darkly -NEXT_PUBLIC_APP_ENV=local - -NEXT_PUBLIC_AGPT_SERVER_BASE_URL=http://localhost:8006 - -## Locale settings - -NEXT_PUBLIC_DEFAULT_LOCALE=en -NEXT_PUBLIC_LOCALES=en,es - -## Supabase credentials - -NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000 -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE - -## OAuth Callback URL -## This should be {domain}/auth/callback -## Only used if you're using Supabase and OAuth -AUTH_CALLBACK_URL="${NEXT_PUBLIC_FRONTEND_BASE_URL}/auth/callback" -GA_MEASUREMENT_ID=G-FH2XK2W4GN - -# When running locally, set NEXT_PUBLIC_BEHAVE_AS=CLOUD to use the a locally hosted marketplace (as is typical in development, and the cloud deployment), otherwise set it to LOCAL to have the marketplace open in a new tab -NEXT_PUBLIC_BEHAVE_AS=LOCAL -NEXT_PUBLIC_SHOW_BILLING_PAGE=false - -## Cloudflare Turnstile (CAPTCHA) Configuration -## Get these from the Cloudflare Turnstile dashboard: https://dash.cloudflare.com/?to=/:account/turnstile -## This is the frontend site key -NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY= -NEXT_PUBLIC_TURNSTILE=disabled - -# Devtools -NEXT_PUBLIC_REACT_QUERY_DEVTOOL=true diff --git a/autogpt_platform/frontend/.gitignore b/autogpt_platform/frontend/.gitignore index 93625c94e865..4150a70bcde7 100644 --- a/autogpt_platform/frontend/.gitignore +++ b/autogpt_platform/frontend/.gitignore @@ -31,6 +31,7 @@ yarn.lock package-lock.json # local env files +.env .env*.local # vercel @@ -53,4 +54,7 @@ storybook-static *.ignore.* *.ign.* !.npmrc -.cursorrules \ No newline at end of file +.cursorrules + +# Generated API files +src/app/api/__generated__/ \ No newline at end of file diff --git a/autogpt_platform/frontend/Dockerfile b/autogpt_platform/frontend/Dockerfile index 583dbcada94d..0b6fe4491b03 100644 --- a/autogpt_platform/frontend/Dockerfile +++ b/autogpt_platform/frontend/Dockerfile @@ -5,18 +5,16 @@ RUN corepack enable COPY autogpt_platform/frontend/package.json autogpt_platform/frontend/pnpm-lock.yaml ./ RUN --mount=type=cache,target=/root/.local/share/pnpm pnpm install --frozen-lockfile -# Dev stage -FROM base AS dev -ENV NODE_ENV=development -ENV HOSTNAME=0.0.0.0 -COPY autogpt_platform/frontend/ . -EXPOSE 3000 -CMD ["pnpm", "run", "dev", "--hostname", "0.0.0.0"] - # Build stage for prod FROM base AS build + COPY autogpt_platform/frontend/ . -ENV SKIP_STORYBOOK_TESTS=true +RUN if [ -f .env ]; then \ + cat .env.default .env > .env.merged && mv .env.merged .env; \ + else \ + cp .env.default .env; \ + fi +RUN pnpm run generate:api RUN pnpm build # Prod stage - based on NextJS reference Dockerfile https://github.com/vercel/next.js/blob/64271354533ed16da51be5dce85f0dbd15f17517/examples/with-docker/Dockerfile diff --git a/autogpt_platform/frontend/README.md b/autogpt_platform/frontend/README.md index 94c65a6e3b21..ebc67644550d 100644 --- a/autogpt_platform/frontend/README.md +++ b/autogpt_platform/frontend/README.md @@ -18,31 +18,58 @@ Make sure you have Node.js 16.10+ installed. Corepack is included with Node.js b > > Then follow the setup steps below. -### Setup +## Setup -1. **Enable corepack** (run this once on your system): +### 1. **Enable corepack** (run this once on your system): - ```bash - corepack enable - ``` +```bash +corepack enable +``` - This enables corepack to automatically manage pnpm based on the `packageManager` field in `package.json`. +This enables corepack to automatically manage pnpm based on the `packageManager` field in `package.json`. -2. **Install dependencies**: +### 2. **Install dependencies**: - ```bash - pnpm i - ``` +```bash +pnpm i +``` -3. **Start the development server**: - ```bash - pnpm dev - ``` +### 3. **Start the development server**: + +#### Running the Front-end & Back-end separately -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +We recommend this approach if you are doing active development on the project. First spin up the Back-end: + +```bash +# on `autogpt_platform` +docker compose --profile local up deps_backend -d +# on `autogpt_platform/backend` +poetry run app +``` + +Then start the Front-end: + +```bash +# on `autogpt_platform/frontend` +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. If the server starts on `http://localhost:3001` it means the Front-end is already running via Docker. You have to kill the container then or do `docker compose down`. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +#### Running both the Front-end and Back-end via Docker + +If you run: + +```bash +# on `autogpt_platform` +docker compose up -d +``` + +It will spin up the Back-end and Front-end via Docker. The Front-end will start on port `3000`. This might not be +what you want when actively contributing to the Front-end as you won't have direct/easy access to the Next.js dev server. + ### Subsequent Runs For subsequent development sessions, you only need to run: @@ -60,12 +87,12 @@ Every time a new Front-end dependency is added by you or others, you will need t - `pnpm start` - Start production server - `pnpm lint` - Run ESLint and Prettier checks - `pnpm format` - Format code with Prettier -- `pnpm type-check` - Run TypeScript type checking +- `pnpm types` - Run TypeScript type checking - `pnpm test` - Run Playwright tests - `pnpm test-ui` - Run Playwright tests with UI - `pnpm fetch:openapi` - Fetch OpenAPI spec from backend - `pnpm generate:api-client` - Generate API client from OpenAPI spec -- `pnpm generate:api-all` - Fetch OpenAPI spec and generate API client +- `pnpm generate:api` - Fetch OpenAPI spec and generate API client This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. @@ -88,7 +115,7 @@ This project uses an auto-generated API client powered by [**Orval**](https://or ```bash # Fetch OpenAPI spec from backend and generate client -pnpm generate:api-all +pnpm generate:api # Only fetch the OpenAPI spec pnpm fetch:openapi @@ -207,6 +234,47 @@ The Orval configuration is located in `autogpt_platform/frontend/orval.config.ts For more details, see the [Orval documentation](https://orval.dev/) or check the configuration file. +## 🚩 Feature Flags + +This project uses [LaunchDarkly](https://launchdarkly.com/) for feature flags, allowing us to control feature rollouts and A/B testing. + +### Using Feature Flags + +#### Check if a feature is enabled + +```typescript +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; + +function MyComponent() { + const isAgentActivityEnabled = useGetFlag(Flag.AGENT_ACTIVITY); + + if (!isAgentActivityEnabled) { + return null; // Hide feature + } + + return
Feature is enabled!
; +} +``` + +#### Protect entire components + +```typescript +import { withFeatureFlag } from "@/services/feature-flags/with-feature-flag"; + +const MyFeaturePage = withFeatureFlag(MyPageComponent, "my-feature-flag"); +``` + +### Testing with Feature Flags + +For local development or running Playwright tests locally, use mocked feature flags by setting `NEXT_PUBLIC_PW_TEST=true` in your `.env` file. This bypasses LaunchDarkly and uses the mock values defined in the code. + +### Adding New Flags + +1. Add the flag to the `Flag` enum in `use-get-flag.ts` +2. Add the flag type to `FlagValues` type +3. Add mock value to `mockFlags` for testing +4. Configure the flag in LaunchDarkly dashboard + ## 🚚 Deploy TODO diff --git a/autogpt_platform/frontend/orval.config.ts b/autogpt_platform/frontend/orval.config.ts index b37290cd818e..434ba70a4c6c 100644 --- a/autogpt_platform/frontend/orval.config.ts +++ b/autogpt_platform/frontend/orval.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ usePrefetch: true, // Will add more as their use cases arise }, + useDates: true, operations: { "getV2List library agents": { query: { @@ -34,6 +35,12 @@ export default defineConfig({ useInfiniteQueryParam: "page", }, }, + "getV1List graph executions": { + query: { + useInfinite: true, + useInfiniteQueryParam: "page", + }, + }, }, }, }, diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 5b34a7bb4f87..3bf5ff14e84b 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -3,13 +3,13 @@ "version": "0.3.4", "private": true, "scripts": { - "dev": "next dev --turbo", - "build": "cross-env pnpm run generate:api-client && SKIP_STORYBOOK_TESTS=true next build", + "dev": "pnpm run generate:api:force && next dev --turbo", + "build": "next build", "start": "next start", "start:standalone": "cd .next/standalone && node server.js", "lint": "next lint && prettier --check .", - "format": "prettier --write .", - "type-check": "tsc --noEmit", + "format": "next lint --fix; prettier --write .", + "types": "tsc --noEmit", "test": "next build --turbo && playwright test", "test-ui": "next build --turbo && playwright test --ui", "test:no-build": "playwright test", @@ -18,76 +18,76 @@ "build-storybook": "storybook build", "test-storybook": "test-storybook", "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"", - "fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/app/api/openapi.json && prettier --write ./src/app/api/openapi.json", - "generate:api-client": "orval --config ./orval.config.ts", - "generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client" + "generate:api": "npx --yes tsx ./scripts/generate-api-queries.ts && orval --config ./orval.config.ts", + "generate:api:force": "npx --yes tsx ./scripts/generate-api-queries.ts --force && orval --config ./orval.config.ts" }, "browserslist": [ "defaults" ], "dependencies": { "@faker-js/faker": "9.9.0", - "@hookform/resolvers": "5.1.1", - "@next/third-parties": "15.3.5", + "@hookform/resolvers": "5.2.1", + "@next/third-parties": "15.4.6", "@phosphor-icons/react": "2.1.10", - "@radix-ui/react-alert-dialog": "1.1.14", + "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.2", - "@radix-ui/react-collapsible": "1.1.11", - "@radix-ui/react-context-menu": "2.2.15", - "@radix-ui/react-dialog": "1.1.14", - "@radix-ui/react-dropdown-menu": "2.1.15", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-icons": "1.3.2", "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-popover": "1.1.14", - "@radix-ui/react-radio-group": "1.3.7", - "@radix-ui/react-scroll-area": "1.2.9", - "@radix-ui/react-select": "2.2.5", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.5", - "@radix-ui/react-tabs": "1.1.12", - "@radix-ui/react-toast": "1.2.14", - "@radix-ui/react-tooltip": "1.2.7", - "@sentry/nextjs": "9.35.0", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-tooltip": "1.2.8", + "@sentry/nextjs": "9.42.0", "@supabase/ssr": "0.6.1", - "@supabase/supabase-js": "2.50.3", - "@tanstack/react-query": "5.81.5", + "@supabase/supabase-js": "2.55.0", + "@tanstack/react-query": "5.85.3", "@tanstack/react-table": "8.21.3", "@types/jaro-winkler": "0.2.4", - "@xyflow/react": "12.8.1", - "ajv": "8.17.1", + "@xyflow/react": "12.8.3", "boring-avatars": "1.11.2", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", "cookie": "1.0.2", "date-fns": "4.1.0", - "dotenv": "17.2.0", + "dotenv": "17.2.1", "elliptic": "6.6.1", "embla-carousel-react": "8.6.0", - "framer-motion": "12.23.0", + "framer-motion": "12.23.12", "geist": "1.4.2", "jaro-winkler": "0.2.8", "launchdarkly-react-client-sdk": "3.8.1", "lodash": "4.17.21", - "lucide-react": "0.525.0", + "lucide-react": "0.539.0", "moment": "2.30.1", - "next": "15.3.5", + "next": "15.5.2", "next-themes": "0.4.6", + "nuqs": "2.4.3", "party-js": "2.2.0", "react": "18.3.1", - "react-day-picker": "9.8.0", + "react-day-picker": "9.8.1", "react-dom": "18.3.1", "react-drag-drop-files": "2.4.0", - "react-hook-form": "7.60.0", + "react-hook-form": "7.62.0", "react-icons": "5.5.0", "react-markdown": "9.0.3", "react-modal": "3.16.3", - "react-shepherd": "6.1.8", + "react-shepherd": "6.1.9", + "react-window": "1.8.11", "recharts": "2.15.3", - "shepherd.js": "14.5.0", - "sonner": "2.0.6", + "shepherd.js": "14.5.1", + "sonner": "2.0.7", "tailwind-merge": "2.6.0", "tailwindcss-animate": "1.0.7", "uuid": "11.1.0", @@ -95,41 +95,42 @@ "zod": "3.25.76" }, "devDependencies": { - "@chromatic-com/storybook": "4.0.1", - "@playwright/test": "1.54.1", - "@storybook/addon-a11y": "9.0.16", - "@storybook/addon-docs": "9.0.16", - "@storybook/addon-links": "9.0.16", - "@storybook/addon-onboarding": "9.0.16", - "@storybook/nextjs": "9.0.16", - "@tanstack/eslint-plugin-query": "5.81.2", - "@tanstack/react-query-devtools": "5.83.0", + "@chromatic-com/storybook": "4.1.0", + "@playwright/test": "1.54.2", + "@storybook/addon-a11y": "9.1.2", + "@storybook/addon-docs": "9.1.2", + "@storybook/addon-links": "9.1.2", + "@storybook/addon-onboarding": "9.1.2", + "@storybook/nextjs": "9.1.2", + "@tanstack/eslint-plugin-query": "5.83.1", + "@tanstack/react-query-devtools": "5.84.2", "@types/canvas-confetti": "1.9.0", "@types/lodash": "4.17.20", "@types/negotiator": "0.6.4", - "@types/node": "24.0.14", + "@types/node": "24.2.1", "@types/react": "18.3.17", "@types/react-dom": "18.3.5", "@types/react-modal": "3.16.3", + "@types/react-window": "1.8.8", "axe-playwright": "2.1.0", - "chromatic": "13.1.2", + "chromatic": "13.1.3", "concurrently": "9.2.0", "cross-env": "7.0.3", "eslint": "8.57.1", - "eslint-config-next": "15.3.5", - "eslint-plugin-storybook": "9.0.16", + "eslint-config-next": "15.4.6", + "eslint-plugin-storybook": "9.1.2", "import-in-the-middle": "1.14.2", "msw": "2.10.4", "msw-storybook-addon": "2.0.5", - "orval": "7.10.0", + "orval": "7.11.2", "pbkdf2": "3.1.3", "postcss": "8.5.6", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "0.6.14", "require-in-the-middle": "7.5.2", - "storybook": "9.0.16", + "storybook": "9.1.2", "tailwindcss": "3.4.17", - "typescript": "5.8.3" + "typescript": "5.9.2" }, "msw": { "workerDirectory": [ diff --git a/autogpt_platform/frontend/playwright.config.ts b/autogpt_platform/frontend/playwright.config.ts index 58bed015a571..66a76a910bd2 100644 --- a/autogpt_platform/frontend/playwright.config.ts +++ b/autogpt_platform/frontend/playwright.config.ts @@ -24,25 +24,28 @@ export default defineConfig({ /* use more workers on CI. */ workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [["html"], ["line"]], + reporter: [["list"], ["html", { open: "never" }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:3000/", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", screenshot: "only-on-failure", bypassCSP: true, + + /* Helps debugging failures */ + trace: "retain-on-failure", + video: "retain-on-failure", }, /* Maximum time one test can run for */ timeout: 25000, /* Configure web server to start automatically */ webServer: { - command: "NEXT_PUBLIC_PW_TEST=true pnpm start", + command: "pnpm start", url: "http://localhost:3000", - reuseExistingServer: !process.env.CI, + reuseExistingServer: true, }, /* Configure projects for major browsers */ diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index 0dd33827099a..f0da34d23046 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -12,35 +12,35 @@ importers: specifier: 9.9.0 version: 9.9.0 '@hookform/resolvers': - specifier: 5.1.1 - version: 5.1.1(react-hook-form@7.60.0(react@18.3.1)) + specifier: 5.2.1 + version: 5.2.1(react-hook-form@7.62.0(react@18.3.1)) '@next/third-parties': - specifier: 15.3.5 - version: 15.3.5(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 15.4.6 + version: 15.4.6(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@phosphor-icons/react': specifier: 2.1.10 version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-alert-dialog': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-avatar': specifier: 1.1.10 version: 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': - specifier: 1.3.2 - version: 1.3.2(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.3.3 + version: 1.3.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': - specifier: 1.1.11 - version: 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.12 + version: 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-context-menu': - specifier: 2.2.15 - version: 2.2.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.2.16 + version: 2.2.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': - specifier: 2.1.15 - version: 2.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.1.16 + version: 2.1.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: 1.3.2 version: 1.3.2(react@18.3.1) @@ -48,17 +48,17 @@ importers: specifier: 2.1.7 version: 2.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': - specifier: 1.3.7 - version: 1.3.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.3.8 + version: 1.3.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': - specifier: 1.2.9 - version: 1.2.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.2.10 + version: 1.2.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': - specifier: 2.2.5 - version: 2.2.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.2.6 + version: 2.2.6(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: 1.1.7 version: 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -66,29 +66,29 @@ importers: specifier: 1.2.3 version: 1.2.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-switch': - specifier: 1.2.5 - version: 1.2.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.2.6 + version: 1.2.6(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': - specifier: 1.1.12 - version: 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.1.13 + version: 1.1.13(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toast': - specifier: 1.2.14 - version: 1.2.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.2.15 + version: 1.2.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': - specifier: 1.2.7 - version: 1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.2.8 + version: 1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': - specifier: 9.35.0 - version: 9.35.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(esbuild@0.25.6)) + specifier: 9.42.0 + version: 9.42.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.1(esbuild@0.25.9)) '@supabase/ssr': specifier: 0.6.1 - version: 0.6.1(@supabase/supabase-js@2.50.3) + version: 0.6.1(@supabase/supabase-js@2.55.0) '@supabase/supabase-js': - specifier: 2.50.3 - version: 2.50.3 + specifier: 2.55.0 + version: 2.55.0 '@tanstack/react-query': - specifier: 5.81.5 - version: 5.81.5(react@18.3.1) + specifier: 5.85.3 + version: 5.85.3(react@18.3.1) '@tanstack/react-table': specifier: 8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -96,11 +96,8 @@ importers: specifier: 0.2.4 version: 0.2.4 '@xyflow/react': - specifier: 12.8.1 - version: 12.8.1(@types/react@18.3.17)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - ajv: - specifier: 8.17.1 - version: 8.17.1 + specifier: 12.8.3 + version: 12.8.3(@types/react@18.3.17)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) boring-avatars: specifier: 1.11.2 version: 1.11.2 @@ -120,8 +117,8 @@ importers: specifier: 4.1.0 version: 4.1.0 dotenv: - specifier: 17.2.0 - version: 17.2.0 + specifier: 17.2.1 + version: 17.2.1 elliptic: specifier: 6.6.1 version: 6.6.1 @@ -129,11 +126,11 @@ importers: specifier: 8.6.0 version: 8.6.0(react@18.3.1) framer-motion: - specifier: 12.23.0 - version: 12.23.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 12.23.12 + version: 12.23.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: 1.4.2 - version: 1.4.2(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.4.2(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) jaro-winkler: specifier: 0.2.8 version: 0.2.8 @@ -144,17 +141,20 @@ importers: specifier: 4.17.21 version: 4.17.21 lucide-react: - specifier: 0.525.0 - version: 0.525.0(react@18.3.1) + specifier: 0.539.0 + version: 0.539.0(react@18.3.1) moment: specifier: 2.30.1 version: 2.30.1 next: - specifier: 15.3.5 - version: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 15.4.6 + version: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nuqs: + specifier: 2.4.3 + version: 2.4.3(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) party-js: specifier: 2.2.0 version: 2.2.0 @@ -162,8 +162,8 @@ importers: specifier: 18.3.1 version: 18.3.1 react-day-picker: - specifier: 9.8.0 - version: 9.8.0(react@18.3.1) + specifier: 9.8.1 + version: 9.8.1(react@18.3.1) react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) @@ -171,8 +171,8 @@ importers: specifier: 2.4.0 version: 2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-hook-form: - specifier: 7.60.0 - version: 7.60.0(react@18.3.1) + specifier: 7.62.0 + version: 7.62.0(react@18.3.1) react-icons: specifier: 5.5.0 version: 5.5.0(react@18.3.1) @@ -183,17 +183,20 @@ importers: specifier: 3.16.3 version: 3.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-shepherd: - specifier: 6.1.8 - version: 6.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + specifier: 6.1.9 + version: 6.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + react-window: + specifier: 1.8.11 + version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: 2.15.3 version: 2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) shepherd.js: - specifier: 14.5.0 - version: 14.5.0 + specifier: 14.5.1 + version: 14.5.1 sonner: - specifier: 2.0.6 - version: 2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: 2.6.0 version: 2.6.0 @@ -211,32 +214,32 @@ importers: version: 3.25.76 devDependencies: '@chromatic-com/storybook': - specifier: 4.0.1 - version: 4.0.1(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: 4.1.0 + version: 4.1.0(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@playwright/test': - specifier: 1.54.1 - version: 1.54.1 + specifier: 1.54.2 + version: 1.54.2 '@storybook/addon-a11y': - specifier: 9.0.16 - version: 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: 9.1.2 + version: 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@storybook/addon-docs': - specifier: 9.0.16 - version: 9.0.16(@types/react@18.3.17)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: 9.1.2 + version: 9.1.2(@types/react@18.3.17)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@storybook/addon-links': - specifier: 9.0.16 - version: 9.0.16(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: 9.1.2 + version: 9.1.2(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@storybook/addon-onboarding': - specifier: 9.0.16 - version: 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + specifier: 9.1.2 + version: 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@storybook/nextjs': - specifier: 9.0.16 - version: 9.0.16(esbuild@0.25.6)(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.6)) + specifier: 9.1.2 + version: 9.1.2(esbuild@0.25.9)(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.1(esbuild@0.25.9)) '@tanstack/eslint-plugin-query': - specifier: 5.81.2 - version: 5.81.2(eslint@8.57.1)(typescript@5.8.3) + specifier: 5.83.1 + version: 5.83.1(eslint@8.57.1)(typescript@5.9.2) '@tanstack/react-query-devtools': - specifier: 5.83.0 - version: 5.83.0(@tanstack/react-query@5.81.5(react@18.3.1))(react@18.3.1) + specifier: 5.84.2 + version: 5.84.2(@tanstack/react-query@5.85.3(react@18.3.1))(react@18.3.1) '@types/canvas-confetti': specifier: 1.9.0 version: 1.9.0 @@ -247,8 +250,8 @@ importers: specifier: 0.6.4 version: 0.6.4 '@types/node': - specifier: 24.0.14 - version: 24.0.14 + specifier: 24.2.1 + version: 24.2.1 '@types/react': specifier: 18.3.17 version: 18.3.17 @@ -258,12 +261,15 @@ importers: '@types/react-modal': specifier: 3.16.3 version: 3.16.3 + '@types/react-window': + specifier: 1.8.8 + version: 1.8.8 axe-playwright: specifier: 2.1.0 - version: 2.1.0(playwright@1.54.1) + version: 2.1.0(playwright@1.54.2) chromatic: - specifier: 13.1.2 - version: 13.1.2 + specifier: 13.1.3 + version: 13.1.3 concurrently: specifier: 9.2.0 version: 9.2.0 @@ -274,23 +280,23 @@ importers: specifier: 8.57.1 version: 8.57.1 eslint-config-next: - specifier: 15.3.5 - version: 15.3.5(eslint@8.57.1)(typescript@5.8.3) + specifier: 15.4.6 + version: 15.4.6(eslint@8.57.1)(typescript@5.9.2) eslint-plugin-storybook: - specifier: 9.0.16 - version: 9.0.16(eslint@8.57.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) + specifier: 9.1.2 + version: 9.1.2(eslint@8.57.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2) import-in-the-middle: specifier: 1.14.2 version: 1.14.2 msw: specifier: 2.10.4 - version: 2.10.4(@types/node@24.0.14)(typescript@5.8.3) + version: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) msw-storybook-addon: specifier: 2.0.5 - version: 2.0.5(msw@2.10.4(@types/node@24.0.14)(typescript@5.8.3)) + version: 2.0.5(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)) orval: - specifier: 7.10.0 - version: 7.10.0(openapi-types@12.1.3) + specifier: 7.11.2 + version: 7.11.2(openapi-types@12.1.3) pbkdf2: specifier: 3.1.3 version: 3.1.3 @@ -307,19 +313,19 @@ importers: specifier: 7.5.2 version: 7.5.2 storybook: - specifier: 9.0.16 - version: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + specifier: 9.1.2 + version: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) tailwindcss: specifier: 3.4.17 version: 3.4.17 typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.2 + version: 5.9.2 packages: - '@adobe/css-tools@4.4.3': - resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -345,8 +351,8 @@ packages: peerDependencies: openapi-types: '>=7' - '@asyncapi/specs@6.8.1': - resolution: {integrity: sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==} + '@asyncapi/specs@6.9.0': + resolution: {integrity: sha512-gatFEH2hfJXWmv3vogIjBZfiIbPRC/ISn9UEHZZLZDdMBO0USxt3AFgCC9AY1P+eNE7zjXddXCIT7gz32XOK4g==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} @@ -356,12 +362,12 @@ packages: resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': - resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -372,8 +378,8 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.27.1': - resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -401,8 +407,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -443,16 +449,16 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.27.1': - resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} engines: {node: '>=6.0.0'} hasBin: true @@ -480,8 +486,8 @@ packages: peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': - resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -568,14 +574,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.27.1': - resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.0': - resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} + '@babel/plugin-transform-classes@7.28.3': + resolution: {integrity: sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -790,8 +796,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.0': - resolution: {integrity: sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==} + '@babel/plugin-transform-regenerator@7.28.3': + resolution: {integrity: sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -808,8 +814,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-runtime@7.28.0': - resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==} + '@babel/plugin-transform-runtime@7.28.3': + resolution: {integrity: sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -874,8 +880,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.28.0': - resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + '@babel/preset-env@7.28.3': + resolution: {integrity: sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -897,20 +903,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.0': - resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.0': - resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@bundled-es-modules/cookie@2.0.1': @@ -922,23 +928,23 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@chromatic-com/storybook@4.0.1': - resolution: {integrity: sha512-GQXe5lyZl3yLewLJQyFXEpOp2h+mfN2bPrzYaOFNCJjO4Js9deKbRHTOSaiP2FRwZqDLdQwy2+SEGeXPZ94yYw==} + '@chromatic-com/storybook@4.1.0': + resolution: {integrity: sha512-B9XesFX5lQUdP81/QBTtkiYOFqEsJwQpzkZlcYPm2n/L1S/8ZabSPbz6NoY8hOJTXWZ2p7grygUlxyGy+gAvfQ==} engines: {node: '>=20.0.0', yarn: '>=1.22.18'} peerDependencies: - storybook: ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 + storybook: ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 || ^9.2.0-0 '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} - '@emnapi/core@1.4.4': - resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@emnapi/runtime@1.4.4': - resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} - '@emnapi/wasi-threads@1.0.3': - resolution: {integrity: sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==} + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} '@emotion/is-prop-valid@1.2.2': resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} @@ -949,158 +955,158 @@ packages: '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1130,26 +1136,26 @@ packages: resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@floating-ui/core@1.7.1': - resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.1': - resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + '@floating-ui/dom@1.7.3': + resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} - '@floating-ui/react-dom@2.1.3': - resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} + '@floating-ui/react-dom@2.1.5': + resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.9': - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.6.0': - resolution: {integrity: sha512-KaeJvPNofTEZR9EzVNp/GQzbQqkGfjiu6k3CXKvhVTX+8OoAKSX/k7qxLKOX3B0yh2XqVAc93rsOu48CGt2Qug==} + '@gerrit0/mini-shiki@3.9.2': + resolution: {integrity: sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==} - '@hookform/resolvers@5.1.1': - resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} + '@hookform/resolvers@5.2.1': + resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==} peerDependencies: react-hook-form: ^7.55.0 @@ -1170,128 +1176,134 @@ packages: resolution: {integrity: sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==} engines: {node: '>=16.0.0'} - '@ibm-cloud/openapi-ruleset@1.31.1': - resolution: {integrity: sha512-3WK2FREmDA2aadCjD71PE7tx5evyvmhg80ts1kXp2IzXIA0ZJ7guGM66tj40kxaqwpMSGchwEnnfYswntav76g==} + '@ibm-cloud/openapi-ruleset@1.31.2': + resolution: {integrity: sha512-g3YYNTiX6zW7quFvDD9szu+54oHj6+4vz8g3/ikOacVsVEX072CvhjX9zRZf1WH4zDXv8KbprsxV+osZQbXPlg==} engines: {node: '>=16.0.0'} - '@img/sharp-darwin-arm64@0.34.2': - resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.2': - resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': - resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.1.0': - resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.1.0': - resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.1.0': - resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.1.0': - resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.1.0': - resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.1.0': - resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': - resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.1.0': - resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.2': - resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.2': - resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.34.2': - resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.2': - resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.2': - resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.2': - resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.2': - resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.2': - resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.2': - resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.2': - resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] - '@inquirer/confirm@5.1.13': - resolution: {integrity: sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==} + '@inquirer/confirm@5.1.14': + resolution: {integrity: sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1299,8 +1311,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.14': - resolution: {integrity: sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==} + '@inquirer/core@10.1.15': + resolution: {integrity: sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1308,12 +1320,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.12': - resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} engines: {node: '>=18'} - '@inquirer/type@3.0.7': - resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1325,35 +1337,21 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.10': - resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -1382,72 +1380,72 @@ packages: '@types/react': '>=16' react: '>=16' - '@mswjs/interceptors@0.39.2': - resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==} + '@mswjs/interceptors@0.39.6': + resolution: {integrity: sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@0.2.11': - resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} '@neoconfetti/react@1.0.0': resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} - '@next/env@15.3.5': - resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} + '@next/env@15.4.6': + resolution: {integrity: sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==} - '@next/eslint-plugin-next@15.3.5': - resolution: {integrity: sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w==} + '@next/eslint-plugin-next@15.4.6': + resolution: {integrity: sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==} - '@next/swc-darwin-arm64@15.3.5': - resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} + '@next/swc-darwin-arm64@15.4.6': + resolution: {integrity: sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.3.5': - resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} + '@next/swc-darwin-x64@15.4.6': + resolution: {integrity: sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.3.5': - resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} + '@next/swc-linux-arm64-gnu@15.4.6': + resolution: {integrity: sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.3.5': - resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} + '@next/swc-linux-arm64-musl@15.4.6': + resolution: {integrity: sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.3.5': - resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} + '@next/swc-linux-x64-gnu@15.4.6': + resolution: {integrity: sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.3.5': - resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} + '@next/swc-linux-x64-musl@15.4.6': + resolution: {integrity: sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.3.5': - resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} + '@next/swc-win32-arm64-msvc@15.4.6': + resolution: {integrity: sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.3.5': - resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} + '@next/swc-win32-x64-msvc@15.4.6': + resolution: {integrity: sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@next/third-parties@15.3.5': - resolution: {integrity: sha512-ef2tj6caWSoUy9yM7bGA0uDECJbru8XALIukOn1ZXBeSlOq8+5TTVEB0+xyJVpEDPXIB9w/CVIQw0e0cRIJHVQ==} + '@next/third-parties@15.4.6': + resolution: {integrity: sha512-1BVymp3iVCMKvfIAlU1yptJuVnwiiyOs852WqPmj8bHPK7dcJN7vGEvH2uIKy9yBOi2UXZevlRT5njPDLZDHYg==} peerDependencies: next: ^13.0.0 || ^14.0.0 || ^15.0.0 react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -1655,8 +1653,8 @@ packages: resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.34.0': - resolution: {integrity: sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==} + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.40.1': @@ -1665,35 +1663,35 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@orval/angular@7.10.0': - resolution: {integrity: sha512-M89GKo/PibxYXvOKp9+i6BLxhEW8YsO+evwuV2kMbDGNS3RiYDwzmMBcA9SVL7m8CumeZoxNEAXsupzq96ZAXA==} + '@orval/angular@7.11.2': + resolution: {integrity: sha512-v7I3MXlc1DTFHZlCo10uqBmss/4puXi1EbYdlYGfeZ2sYQiwtRFEYAMnSIxHzMtdtI4jd7iDEH0fZRA7W6yloA==} - '@orval/axios@7.10.0': - resolution: {integrity: sha512-AB6BjEwyguIcH8olzOTFPvwUP8z63yP4Jfl3T2UoeFchK04KqWqxbUoxmDG9xVQ79uMs/uOrb0X+GFwdZ56gAg==} + '@orval/axios@7.11.2': + resolution: {integrity: sha512-X5TJTFofCeJrQcHWoH0wz/032DBhPOQuZUUOPYO3DItOnq9/nfHJYKnUfg13wtYw0LVjCxyTZpeGLUBZnY804A==} - '@orval/core@7.10.0': - resolution: {integrity: sha512-Lm7HY4Kwzehe+2HNfi+Ov/IZ+m3nj3NskVGvOyJDAqaaHB7G/xydSCtgELG32ur4G+M/XmwChAjoP4TCNVh0VA==} + '@orval/core@7.11.2': + resolution: {integrity: sha512-5k2j4ro53yZ3J+tGMu3LpLgVb2OBtxNDgyrJik8qkrFyuORBLx/a+AJRFoPYwZmtnMZzzRXoH4J/fbpW5LXIyg==} - '@orval/fetch@7.10.0': - resolution: {integrity: sha512-bWcXPmARcXhXRveBtUnkfPlkUcLEzfGaflAdqN4CtScS48LgNrXXtuyt2BV2wvEXAavCWIhnRyQvz2foTU4U8Q==} + '@orval/fetch@7.11.2': + resolution: {integrity: sha512-FuupASqk4Dn8ZET7u5Ra5djKy22KfRfec60zRR/o5+L5iQkWKEe/A5DBT1PwjTMnp9789PEGlFPQjZNwMG98Tg==} - '@orval/hono@7.10.0': - resolution: {integrity: sha512-bOxTdZxx2BpGQf7fFuCeeUe//ZYDWc6Yz9WOhj3HrnsD06xTRKFWVBi/QZ29QcAPxqwunu/VWwbqoiHHuuX3bA==} + '@orval/hono@7.11.2': + resolution: {integrity: sha512-SddhKMYMB/dJH3YQx3xi0Zd+4tfhrEkqJdqQaYLXgENJiw0aGbdaZTdY6mb/e6qP38TTK6ME2PkYOqwkl2DQ7g==} - '@orval/mcp@7.10.0': - resolution: {integrity: sha512-ztLXGOSxK7jFwPKAeYPR85BjKRh3KTClKEnM2MFmo2FHHojn72DPXRPCmy0Wbw5Ee+JOxK2kIpyx+HZi9XVxiA==} + '@orval/mcp@7.11.2': + resolution: {integrity: sha512-9kGKko8wLuCbeETp8Pd8lXLtBpLzEJfR2kl2m19AI3nAoHXE/Tnn3KgjMIg0qvCcsRXGXdYJB7wfxy2URdAxVA==} - '@orval/mock@7.10.0': - resolution: {integrity: sha512-vkEWCaKEyMfWGJF5MtxVzl+blwc9vYzwdYxMoSdjA5yS2dNBrdNlt1aLtb4+aoI1jgBgpCg/OB7VtWaL5QYidA==} + '@orval/mock@7.11.2': + resolution: {integrity: sha512-+uRq6BT6NU2z0UQtgeD6FMuLAxQ5bjJ5PZK3AsbDYFRSmAWUWoeaQcoWyF38F4t7ez779beGs3AlUg+z0Ec4rQ==} - '@orval/query@7.10.0': - resolution: {integrity: sha512-DBVg8RyKWSQKhr5Zfvxx5XICUdDUkG4MJKSd4BQCrRjUWgN6vwGunMEKyfnjpS5mFUSCkwWD/I3rTkjW6aysJA==} + '@orval/query@7.11.2': + resolution: {integrity: sha512-C/it+wNfcDtuvpB6h/78YwWU+Rjk7eU1Av8jAoGnvxMRli4nnzhSZ83HMILGhYQbE9WcfNZxQJ6OaBoTWqACPg==} - '@orval/swr@7.10.0': - resolution: {integrity: sha512-ZdApomZQhJ5ZogjJgBK+haeCOP9gUaMaGKGjTVJr86jJaygDcKn54Ok1quiDUCbX42Eye+cgmQJeKeZvqnPohA==} + '@orval/swr@7.11.2': + resolution: {integrity: sha512-95GkKLVy67xJvsiVvK4nTOsCpebWM54FvQdKQaqlJ0FGCNUbqDjVRwBKbjP6dLc/B3wTmBAWlFSLbdVmjGCTYg==} - '@orval/zod@7.10.0': - resolution: {integrity: sha512-AB/508IBMlVDBcGvlq+ASz7DvqU3nhoDnIeBCyjwNfQwhYzREU0qqiFBnH0XAW70c6SCMf9/bIcYbw8GAx/zxA==} + '@orval/zod@7.11.2': + resolution: {integrity: sha512-4MzTg5Wms8/LlM3CbYu80dvCbP88bVlQjnYsBdFXuEv0K2GYkBCAhVOrmXCVrPXE89neV6ABkvWQeuKZQpkdxQ==} '@phosphor-icons/react@2.1.10': resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} @@ -1706,8 +1704,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.54.1': - resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} + '@playwright/test@1.54.2': + resolution: {integrity: sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==} engines: {node: '>=18'} hasBin: true @@ -1737,19 +1735,19 @@ packages: webpack-plugin-serve: optional: true - '@prisma/instrumentation@6.10.1': - resolution: {integrity: sha512-JC8qzgEDuFKjuBsqrZvXHINUb12psnE6Qy3q5p2MBhalC1KW1MBBUwuonx6iS5TCfCdtNslHft8uc2r+EdLWWg==} + '@prisma/instrumentation@6.11.1': + resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==} peerDependencies: '@opentelemetry/api': ^1.8 '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - '@radix-ui/primitive@1.1.2': - resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - '@radix-ui/react-alert-dialog@1.1.14': - resolution: {integrity: sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==} + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1787,8 +1785,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-checkbox@1.3.2': - resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==} + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1800,8 +1798,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collapsible@1.1.11': - resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==} + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1835,8 +1833,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context-menu@2.2.15': - resolution: {integrity: sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==} + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1857,8 +1855,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.14': - resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1879,8 +1877,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.10': - resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1892,8 +1890,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.15': - resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1905,8 +1903,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.2': - resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1954,8 +1952,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menu@2.1.15': - resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1967,8 +1965,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.14': - resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1980,8 +1978,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.2.7': - resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2006,8 +2004,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.4': - resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2032,8 +2030,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.3.7': - resolution: {integrity: sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==} + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2045,8 +2043,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.10': - resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2058,8 +2056,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.9': - resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2071,8 +2069,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.2.5': - resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2106,8 +2104,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-switch@1.2.5': - resolution: {integrity: sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==} + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2119,8 +2117,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tabs@1.1.12': - resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2132,8 +2130,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toast@1.2.14': - resolution: {integrity: sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==} + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2145,8 +2143,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.2.7': - resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2273,98 +2271,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.35.0': - resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.35.0': - resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==} + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.35.0': - resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==} + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.35.0': - resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==} + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.35.0': - resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==} + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.35.0': - resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==} + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': - resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==} + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.35.0': - resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==} + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.35.0': - resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==} + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.35.0': - resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==} + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': - resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==} + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': - resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==} + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.35.0': - resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==} + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.35.0': - resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==} + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.35.0': - resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==} + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.35.0': - resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==} + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.35.0': - resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==} + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.35.0': - resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==} + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.35.0': - resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==} + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} cpu: [x64] os: [win32] @@ -2377,132 +2380,149 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@sentry-internal/browser-utils@9.35.0': - resolution: {integrity: sha512-75/zOArDQ4ASgndKGQo0m0v8P921eq/Q/sJvR14NopzwuwAchBhjziixWCwxKgvoA20eg3OGwMIkzztxmdp2Tw==} + '@sentry-internal/browser-utils@9.42.0': + resolution: {integrity: sha512-kHDPrLSlb9kMKKUNWVUwMbUjZN3o4aBUux9hRTf2HeDA4Uo8O7Ln4XAC7tMCJ+cB016Z2RnnqH3mLdZV7J72/w==} engines: {node: '>=18'} - '@sentry-internal/feedback@9.35.0': - resolution: {integrity: sha512-IKaZWUmqqqLucuJ5EGgwdrBdvP3l3STXvgKsLmW2l+s9WYbvfPPHukZhUULYRsXleQKXnOuz44WQmwNeZYQutw==} + '@sentry-internal/feedback@9.42.0': + resolution: {integrity: sha512-7WisZVBKnsr+19CFReFnMHe/Lgd9xqn5CBJfBdRng4hyYSiw988Zdr5xwp2wh1ESM0fxqxy6kSe1NPztIbbiVw==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@9.35.0': - resolution: {integrity: sha512-nXxrEIkpn+FBxYsD4JPQStEGQWF0j0Rs0LoCyuB1e2QeEg6Pipqg4DIjWDjZyeUAsdoaUsIRhWbMK5OBWUuudw==} + '@sentry-internal/replay-canvas@9.42.0': + resolution: {integrity: sha512-rvP2zjfR9x57u8fVFetkwXnZSXazJRLTFDbirFplggkCKeGNTDJmLBsejUNOkwGiXzcui0fuFEQElu2nF97nxw==} engines: {node: '>=18'} - '@sentry-internal/replay@9.35.0': - resolution: {integrity: sha512-veGNAXeHXULzkGPudMg5iFqkW4wFD/qVbQSr+s0q3+IZ7vJ+Eql+eBDZEKrfKYIBdNOf5POr+KaEBMpMGCbEkQ==} + '@sentry-internal/replay@9.42.0': + resolution: {integrity: sha512-teKxrVeT8JOYs9Hd4t0jI0X9NP2Ky6iVgTItN07mUD6yOS9se2ZXzmNzXevoqICX6WsnhHDeWY7krvmJ5QCVEg==} engines: {node: '>=18'} - '@sentry/babel-plugin-component-annotate@3.5.0': - resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==} + '@sentry/babel-plugin-component-annotate@3.6.1': + resolution: {integrity: sha512-zmvUa4RpzDG3LQJFpGCE8lniz8Rk1Wa6ZvvK+yEH+snZeaHHRbSnAQBMR607GOClP+euGHNO2YtaY4UAdNTYbg==} engines: {node: '>= 14'} - '@sentry/browser@9.35.0': - resolution: {integrity: sha512-m1fRwMa1vik6VFAAz6RlJUUU+0+Uo+QIKJWWOx9calb11Zt4wIg9wvox7TOgMd8KPt3sefPXIPM38A+uixyXYw==} + '@sentry/browser@9.42.0': + resolution: {integrity: sha512-85RgFSMDS24JD3nSqA4LpDlVGTxVGwYeqCwI6pRM0CH9pz6G+0OESRhTDccj+rv+kr8vcvWl/LUklJkoswH4kw==} engines: {node: '>=18'} - '@sentry/bundler-plugin-core@3.5.0': - resolution: {integrity: sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==} + '@sentry/bundler-plugin-core@3.6.1': + resolution: {integrity: sha512-/ubWjPwgLep84sUPzHfKL2Ns9mK9aQrEX4aBFztru7ygiJidKJTxYGtvjh4dL2M1aZ0WRQYp+7PF6+VKwdZXcQ==} engines: {node: '>= 14'} - '@sentry/cli-darwin@2.42.2': - resolution: {integrity: sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==} + '@sentry/cli-darwin@2.52.0': + resolution: {integrity: sha512-ieQs/p4yTHT27nBzy0wtAb8BSISfWlpXdgsACcwXimYa36NJRwyCqgOXUaH/BYiTdwWSHpuANbUHGJW6zljzxw==} engines: {node: '>=10'} os: [darwin] - '@sentry/cli-linux-arm64@2.42.2': - resolution: {integrity: sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==} + '@sentry/cli-linux-arm64@2.52.0': + resolution: {integrity: sha512-RxT5uzxjCkcvplmx0bavJIEYerRex2Rg/2RAVBdVvWLKFOcmeerTn/VVxPZVuDIVMVyjlZsteWPYwfUm+Ia3wQ==} engines: {node: '>=10'} cpu: [arm64] - os: [linux, freebsd] + os: [linux, freebsd, android] - '@sentry/cli-linux-arm@2.42.2': - resolution: {integrity: sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==} + '@sentry/cli-linux-arm@2.52.0': + resolution: {integrity: sha512-tWMLU+hj+iip5Akx+S76biAOE1eMMWTDq8c0MqMv/ahHgb6/HiVngMcUsp59Oz3EczJGbTkcnS3vRTDodEcMDw==} engines: {node: '>=10'} cpu: [arm] - os: [linux, freebsd] + os: [linux, freebsd, android] - '@sentry/cli-linux-i686@2.42.2': - resolution: {integrity: sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==} + '@sentry/cli-linux-i686@2.52.0': + resolution: {integrity: sha512-sKcJmIg7QWFtlNU5Bs5OZprwdIzzyYMRpFkWioPZ4TE82yvP1+2SAX31VPUlTx+7NLU6YVEWNwvSxh8LWb7iOw==} engines: {node: '>=10'} cpu: [x86, ia32] - os: [linux, freebsd] + os: [linux, freebsd, android] - '@sentry/cli-linux-x64@2.42.2': - resolution: {integrity: sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==} + '@sentry/cli-linux-x64@2.52.0': + resolution: {integrity: sha512-aPZ7bP02zGkuEqTiOAm4np/ggfgtzrq4ti1Xze96Csi/DV3820SCfLrPlsvcvnqq7x69IL9cI3kXjdEpgrfGxw==} engines: {node: '>=10'} cpu: [x64] - os: [linux, freebsd] + os: [linux, freebsd, android] + + '@sentry/cli-win32-arm64@2.52.0': + resolution: {integrity: sha512-90hrB5XdwJVhRpCmVrEcYoKW8nl5/V9OfVvOGeKUPvUkApLzvsInK74FYBZEVyAn1i/NdUv+Xk9q2zqUGK1aLQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] - '@sentry/cli-win32-i686@2.42.2': - resolution: {integrity: sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==} + '@sentry/cli-win32-i686@2.52.0': + resolution: {integrity: sha512-HXlSE4CaLylNrELx4KVmOQjV5bURCNuky6sjCWiTH7HyDqHEak2Rk8iLE0JNLj5RETWMvmaZnZZFfmyGlY1opg==} engines: {node: '>=10'} cpu: [x86, ia32] os: [win32] - '@sentry/cli-win32-x64@2.42.2': - resolution: {integrity: sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==} + '@sentry/cli-win32-x64@2.52.0': + resolution: {integrity: sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@sentry/cli@2.42.2': - resolution: {integrity: sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==} + '@sentry/cli@2.52.0': + resolution: {integrity: sha512-PXyo7Yv7+rVMSBGZfI/eFEzzhiKedTs25sDCjz4a3goAZ/F5R5tn3MKq30pnze5wNnoQmLujAa0uUjfNcWP+uQ==} engines: {node: '>= 10'} hasBin: true - '@sentry/core@9.35.0': - resolution: {integrity: sha512-bdAtzVQZ/wn4L/m8r2OUCCG/NWr0Q8dyZDwdwvINJaMbyhDRUdQh/MWjrz+id/3JoOL1LigAyTV1h4FJDGuwUQ==} + '@sentry/core@9.42.0': + resolution: {integrity: sha512-AsfB2eklY09GGsCLC2r0pvh/h3tgr9Co3CB7XisEfzhoQH9RaEb0XeIVLyfo+503ktdlPTjH24j4Zpts4y0Jmg==} engines: {node: '>=18'} - '@sentry/nextjs@9.35.0': - resolution: {integrity: sha512-6qrCXhArnp3DmCmadl150W7HU5UUmvKBlNm3jUdW+LqrzL4afkeWZ/0T62YHnL5zOOd7xDn6uoxgKsJeVXO5qQ==} + '@sentry/nextjs@9.42.0': + resolution: {integrity: sha512-hnjvh330LQlYLTFnJjiCu2VHwqLDycPv9P1fJlhl4aYbX0wmGh6yKLwuImpyU7zI3olZg6GvgMz8LXkFr13m1A==} engines: {node: '>=18'} peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 - '@sentry/node@9.35.0': - resolution: {integrity: sha512-7ifFqTsa3BtZGRAgqoWqYf7OJizKSyEzQlSixgBc253wyYWiLaVJ15By9Y4ozd+PbgpOPqfDN5B45Y+OxtQnQw==} + '@sentry/node-core@9.42.0': + resolution: {integrity: sha512-j0zLLatut3tY+KdHqAn1t2lih+RnR2sDUJagq+swZZFgja0nsWybm3kzPN4n2aRB7yLvjU40n8oj8vi2qBK41g==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/resources': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@sentry/node@9.42.0': + resolution: {integrity: sha512-SrfSTy570zk1ucRy5qSZ94eXj7E26ZAJ1jS7mJtUFLu2fwJt39qtbqfDncXneBJcKzLvXE6WSLVlH/WfwQ5lKg==} engines: {node: '>=18'} - '@sentry/opentelemetry@9.35.0': - resolution: {integrity: sha512-XJmSC71KaN+qwYf5EEobLDyWum4FijpIjnpTVTYOrq037uUCpxJEGtgQHq0X+DE/ycVUX/Og2PiAgTeCQEYfDg==} + '@sentry/opentelemetry@9.42.0': + resolution: {integrity: sha512-RdF2Pps9XH+oQpb/yBzG4+RyrQc5eJ55zi+kzY1cG5asPxqKfgBrniy9Q2szy3YJpvN73T//aPrasXuCTgWohg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 '@opentelemetry/core': ^1.30.1 || ^2.0.0 - '@opentelemetry/instrumentation': ^0.57.1 || ^0.200.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 '@opentelemetry/semantic-conventions': ^1.34.0 - '@sentry/react@9.35.0': - resolution: {integrity: sha512-zoLcucRYhSLKGYJ0b06MBVF+s3DvLK3YY651sf9boV071tWZs6Q8FDDD3E+pgw8t+ngL+6kB989Ns2HhyLyYIQ==} + '@sentry/react@9.42.0': + resolution: {integrity: sha512-U/KTQrtVMAfeuY77jrVldRIEsEK9dRKbqTmKR9Ajd9BAOQlW9RBklvcRyGJ0AHRWt29TZPKLTcZ8uuy9P9/1Ng==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/vercel-edge@9.35.0': - resolution: {integrity: sha512-cPQG+nvcdP5V4gRbr+WsdlA7wZ0ECFkX2DNIAVeZodeMMXX2ZSM3LzcX9yc60Nhr/e8hKCVMZNh4VW3ENtfQRQ==} + '@sentry/vercel-edge@9.42.0': + resolution: {integrity: sha512-HD9yH8ItlnM3bhn4DAmI8unqemI4ws/7UmuL/q/S5kdYrnOIkwIi3+EcFa1qBXuXcLx5+w7lsRlbGeIvfWDaYg==} engines: {node: '>=18'} - '@sentry/webpack-plugin@3.5.0': - resolution: {integrity: sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ==} + '@sentry/webpack-plugin@3.6.1': + resolution: {integrity: sha512-F2yqwbdxfCENMN5u4ih4WfOtGjW56/92DBC0bU6un7Ns/l2qd+wRONIvrF+58rl/VkCFfMlUtZTVoKGRyMRmHA==} engines: {node: '>= 14'} peerDependencies: webpack: '>=4.40.0' - '@shikijs/engine-oniguruma@3.6.0': - resolution: {integrity: sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==} + '@shikijs/engine-oniguruma@3.9.2': + resolution: {integrity: sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==} - '@shikijs/langs@3.6.0': - resolution: {integrity: sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==} + '@shikijs/langs@3.9.2': + resolution: {integrity: sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==} - '@shikijs/themes@3.6.0': - resolution: {integrity: sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==} + '@shikijs/themes@3.9.2': + resolution: {integrity: sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==} - '@shikijs/types@3.6.0': - resolution: {integrity: sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==} + '@shikijs/types@3.9.2': + resolution: {integrity: sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2583,48 +2603,48 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@storybook/addon-a11y@9.0.16': - resolution: {integrity: sha512-pi9ipxhs9bA2yCHDGp2+yWy6E2LywDFTqWcFh3aw/LRxnlRTf52QiVJkWpJbNFEXgk4QrKVrAruf9LLiXpTcOA==} + '@storybook/addon-a11y@9.1.2': + resolution: {integrity: sha512-CwFwpneZO8GvxaMygkNUEJ0ti2U6Q7waZ/NG71tRQzTWGMasbc27rUTvLf654mQen+MkSOt/MbceASkyvK2mdw==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 - '@storybook/addon-docs@9.0.16': - resolution: {integrity: sha512-/ZXaxMC/JqL0cnVuyPHXdJhNvgCrKvxcnM3ACdgBLsEIGcIqegPF+Ahkb2f9sjU36sR7ihT81cL/7cUvQwzd4Q==} + '@storybook/addon-docs@9.1.2': + resolution: {integrity: sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 - '@storybook/addon-links@9.0.16': - resolution: {integrity: sha512-8jiFKeW7gTGZ/WhBnB38dTVFDapyiyLMCvktIcrsVrJTBbj/bC1+ts7OoeuJCs7/EN+2zn9vf+V17ZqI0I2sIA==} + '@storybook/addon-links@9.1.2': + resolution: {integrity: sha512-drAWdhn5cRo5WcaORoCYfJ6tgTAw1m+ZJb1ICyNtTU6i/0nErV8jJjt7AziUcUIyzaGVJAkAMNC3+R4uDPSFDA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.16 + storybook: ^9.1.2 peerDependenciesMeta: react: optional: true - '@storybook/addon-onboarding@9.0.16': - resolution: {integrity: sha512-69BPJ9fGNGpDAcGvNJ58V5uQOmpHkQMLcgp/ON3NepoCHiSReUzojB6wV8Ag13PUZmvWXVnE14SWKBZp93xTFQ==} + '@storybook/addon-onboarding@9.1.2': + resolution: {integrity: sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 - '@storybook/builder-webpack5@9.0.16': - resolution: {integrity: sha512-Hb45cgUr6aYK24HBt42EGApbJVosunt7x6DTjawHD2BAqpfSxo6urLnKFIhx8H+af88XN37MTbp5bNozvw4WPA==} + '@storybook/builder-webpack5@9.1.2': + resolution: {integrity: sha512-FnnQewn5GxoR7GJk11Wi8qytd0KOZr5P108UlymdZ8gMbSCjfQ7XXutOhXJqXElQ85fWwvH2wDB14PKNR01HqQ==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@storybook/core-webpack@9.0.16': - resolution: {integrity: sha512-U1SiqRywp6pu0DNy1uSH6b6B4aw7clYvICf+HNRzULYfS6V7ZHD2VaBmcf1xgx0aqVEPUGv6f46oZr2pzmcjGw==} + '@storybook/core-webpack@9.1.2': + resolution: {integrity: sha512-3471YOQOpuAqO3u+tgTG3L7P/08icB3L8apRRRA+L8CFg8D07aVybzHZf5vo8Owf+xynbnv7YUYHy4nl8I/lTA==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 - '@storybook/csf-plugin@9.0.16': - resolution: {integrity: sha512-MSmfPwI0j1mMAc+R3DVkVBQf2KLzaVn2SLdEwweesx63Nh9j3zu9CqKEa0zOuDX1lR2M0DZU0lV6K4sc2EYI4A==} + '@storybook/csf-plugin@9.1.2': + resolution: {integrity: sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==} peerDependencies: - storybook: ^9.0.16 + storybook: ^9.1.2 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -2636,14 +2656,14 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - '@storybook/nextjs@9.0.16': - resolution: {integrity: sha512-OGBtokHdJ2hRDB1Lq42gq4/0WBq6Byd2SEh/lpEGnDeQrDBr3RsF6sLwwxlD4AP8EFovQSMI4vLMFIMAWC/ITA==} + '@storybook/nextjs@9.1.2': + resolution: {integrity: sha512-eriA+yUbGMTf0FkXtZHCr6fgH6Tq4JwsPtGqoz4kpgiqtHKb6EIou6A/5HOeas6iDajkem5Qnip+cD7OPVD6dg==} engines: {node: '>=20.0.0'} peerDependencies: next: ^14.1.0 || ^15.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.16 + storybook: ^9.1.2 typescript: '*' webpack: ^5.0.0 peerDependenciesMeta: @@ -2652,13 +2672,13 @@ packages: webpack: optional: true - '@storybook/preset-react-webpack@9.0.16': - resolution: {integrity: sha512-esAGMKxeXfbW2O86u9foSOGegGQz78335u9RiCKaHXz6gl/xsoE60q3EEQhYxftO630YRfIXbZY7+N9rvbs78w==} + '@storybook/preset-react-webpack@9.1.2': + resolution: {integrity: sha512-JOZb1xVR9Dub+AU5kCOtent1tGP+zVn1akseUP10eP6TfGkilaEBr4IvRsuuGJ8H8Ipyxp+UPsAbDzxrt0B08w==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.16 + storybook: ^9.1.2 typescript: '*' peerDependenciesMeta: typescript: @@ -2670,27 +2690,27 @@ packages: typescript: '>= 4.x' webpack: '>= 4' - '@storybook/react-dom-shim@9.0.16': - resolution: {integrity: sha512-5aIK+31R41mRUvDB4vmBv8hwh3IVHIk/Zbs6kkWF2a+swOsB2+a06aLX21lma4/0T/AuFVXHWat0+inQ4nrXRg==} + '@storybook/react-dom-shim@9.1.2': + resolution: {integrity: sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.16 + storybook: ^9.1.2 - '@storybook/react@9.0.16': - resolution: {integrity: sha512-1jk9fBe8vEoZrba9cK19ZDdZgYMXUNl3Egjj5RsTMYMc1L2mtIu9o56VyK/1V4Q52N9IyawHvmIIuxc5pCZHkQ==} + '@storybook/react@9.1.2': + resolution: {integrity: sha512-VVXu1HrhDExj/yj+heFYc8cgIzBruXy1UYT3LW0WiJyadgzYz3J41l/Lf/j2FCppyxwlXb19Uv51plb1F1C77w==} engines: {node: '>=20.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^9.0.16 + storybook: ^9.1.2 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@supabase/auth-js@2.70.0': - resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==} + '@supabase/auth-js@2.71.1': + resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==} '@supabase/functions-js@2.4.5': resolution: {integrity: sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==} @@ -2702,45 +2722,42 @@ packages: '@supabase/postgrest-js@1.19.4': resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} - '@supabase/realtime-js@2.11.15': - resolution: {integrity: sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==} + '@supabase/realtime-js@2.15.1': + resolution: {integrity: sha512-edRFa2IrQw50kNntvUyS38hsL7t2d/psah6om6aNTLLcWem0R6bOUq7sk7DsGeSlNfuwEwWn57FdYSva6VddYw==} '@supabase/ssr@0.6.1': resolution: {integrity: sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==} peerDependencies: '@supabase/supabase-js': ^2.43.4 - '@supabase/storage-js@2.7.1': - resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + '@supabase/storage-js@2.11.0': + resolution: {integrity: sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==} - '@supabase/supabase-js@2.50.3': - resolution: {integrity: sha512-Ld42AbfSXKnbCE2ObRvrGC5wj9OrfTOzswQZg0OcGQGx+QqcWYN/IqsLqrt4gCFrD57URbNRfGESSWzchzKAuQ==} - - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@supabase/supabase-js@2.55.0': + resolution: {integrity: sha512-Y1uV4nEMjQV1x83DGn7+Z9LOisVVRlY1geSARrUHbXWgbyKLZ6/08dvc0Us1r6AJ4tcKpwpCZWG9yDQYo1JgHg==} '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tanstack/eslint-plugin-query@5.81.2': - resolution: {integrity: sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A==} + '@tanstack/eslint-plugin-query@5.83.1': + resolution: {integrity: sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@tanstack/query-core@5.81.5': - resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} + '@tanstack/query-core@5.85.3': + resolution: {integrity: sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==} - '@tanstack/query-devtools@5.81.2': - resolution: {integrity: sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==} + '@tanstack/query-devtools@5.84.0': + resolution: {integrity: sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==} - '@tanstack/react-query-devtools@5.83.0': - resolution: {integrity: sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ==} + '@tanstack/react-query-devtools@5.84.2': + resolution: {integrity: sha512-ojJ66QoW9noqK35Lsmfqpfucj6wuOxLL2TYwEwpvU+iUQ5R/7TKpapWvpy9kZyNSl0mxv5mpS+ImfR8aL8/x3g==} peerDependencies: - '@tanstack/react-query': ^5.83.0 + '@tanstack/react-query': ^5.84.2 react: ^18 || ^19 - '@tanstack/react-query@5.81.5': - resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} + '@tanstack/react-query@5.85.3': + resolution: {integrity: sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==} peerDependencies: react: ^18 || ^19 @@ -2755,12 +2772,12 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.6.3': - resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + '@testing-library/jest-dom@6.7.0': + resolution: {integrity: sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} '@testing-library/user-event@14.6.1': @@ -2769,8 +2786,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tybys/wasm-util@0.9.0': - resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2784,8 +2801,8 @@ packages: '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} @@ -2859,9 +2876,6 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2901,8 +2915,8 @@ packages: '@types/negotiator@0.6.4': resolution: {integrity: sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A==} - '@types/node@24.0.14': - resolution: {integrity: sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==} + '@types/node@24.2.1': + resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2927,6 +2941,9 @@ packages: '@types/react-modal@3.16.3': resolution: {integrity: sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==} + '@types/react-window@1.8.8': + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + '@types/react@18.3.17': resolution: {integrity: sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==} @@ -2963,203 +2980,177 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.36.0': - resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==} + '@typescript-eslint/eslint-plugin@8.39.1': + resolution: {integrity: sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.36.0 + '@typescript-eslint/parser': ^8.39.1 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.36.0': - resolution: {integrity: sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==} + '@typescript-eslint/parser@8.39.1': + resolution: {integrity: sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/project-service@8.35.0': - resolution: {integrity: sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.36.0': - resolution: {integrity: sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==} + '@typescript-eslint/project-service@8.39.1': + resolution: {integrity: sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.35.0': - resolution: {integrity: sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==} + '@typescript-eslint/scope-manager@8.39.1': + resolution: {integrity: sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.36.0': - resolution: {integrity: sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.35.0': - resolution: {integrity: sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/tsconfig-utils@8.36.0': - resolution: {integrity: sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==} + '@typescript-eslint/tsconfig-utils@8.39.1': + resolution: {integrity: sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.36.0': - resolution: {integrity: sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==} + '@typescript-eslint/type-utils@8.39.1': + resolution: {integrity: sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/types@8.35.0': - resolution: {integrity: sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.36.0': - resolution: {integrity: sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.35.0': - resolution: {integrity: sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==} + '@typescript-eslint/types@8.39.1': + resolution: {integrity: sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.36.0': - resolution: {integrity: sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==} + '@typescript-eslint/typescript-estree@8.39.1': + resolution: {integrity: sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.35.0': - resolution: {integrity: sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==} + '@typescript-eslint/utils@8.39.1': + resolution: {integrity: sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.36.0': - resolution: {integrity: sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/visitor-keys@8.35.0': - resolution: {integrity: sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.36.0': - resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} + '@typescript-eslint/visitor-keys@8.39.1': + resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unrs/resolver-binding-android-arm-eabi@1.11.0': - resolution: {integrity: sha512-LRw5BW29sYj9NsQC6QoqeLVQhEa+BwVINYyMlcve+6stwdBsSt5UB7zw4UZB4+4PNqIVilHoMaPWCb/KhABHQw==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.11.0': - resolution: {integrity: sha512-zYX8D2zcWCAHqghA8tPjbp7LwjVXbIZP++mpU/Mrf5jUVlk3BWIxkeB8yYzZi5GpFSlqMcRZQxQqbMI0c2lASQ==} + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.11.0': - resolution: {integrity: sha512-YsYOT049hevAY/lTYD77GhRs885EXPeAfExG5KenqMJ417nYLS2N/kpRpYbABhFZBVQn+2uRPasTe4ypmYoo3w==} + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.11.0': - resolution: {integrity: sha512-PSjvk3OZf1aZImdGY5xj9ClFG3bC4gnSSYWrt+id0UAv+GwwVldhpMFjAga8SpMo2T1GjV9UKwM+QCsQCQmtdA==} + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.11.0': - resolution: {integrity: sha512-KC/iFaEN/wsTVYnHClyHh5RSYA9PpuGfqkFua45r4sweXpC0KHZ+BYY7ikfcGPt5w1lMpR1gneFzuqWLQxsRKg==} + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.0': - resolution: {integrity: sha512-CDh/0v8uot43cB4yKtDL9CVY8pbPnMV0dHyQCE4lFz6PW/+9tS0i9eqP5a91PAqEBVMqH1ycu+k8rP6wQU846w==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.0': - resolution: {integrity: sha512-+TE7epATDSnvwr3L/hNHX3wQ8KQYB+jSDTdywycg3qDqvavRP8/HX9qdq/rMcnaRDn4EOtallb3vL/5wCWGCkw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.11.0': - resolution: {integrity: sha512-VBAYGg3VahofpQ+L4k/ZO8TSICIbUKKTaMYOWHWfuYBFqPbSkArZZLezw3xd27fQkxX4BaLGb/RKnW0dH9Y/UA==} + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-arm64-musl@1.11.0': - resolution: {integrity: sha512-9IgGFUUb02J1hqdRAHXpZHIeUHRrbnGo6vrRbz0fREH7g+rzQy53/IBSyadZ/LG5iqMxukriNPu4hEMUn+uWEg==} + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.0': - resolution: {integrity: sha512-LR4iQ/LPjMfivpL2bQ9kmm3UnTas3U+umcCnq/CV7HAkukVdHxrDD1wwx74MIWbbgzQTLPYY7Ur2MnnvkYJCBQ==} + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.0': - resolution: {integrity: sha512-HCupFQwMrRhrOg7YHrobbB5ADg0Q8RNiuefqMHVsdhEy9lLyXm/CxsCXeLJdrg27NAPsCaMDtdlm8Z2X8x91Tg==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-musl@1.11.0': - resolution: {integrity: sha512-Ckxy76A5xgjWa4FNrzcKul5qFMWgP5JSQ5YKd0XakmWOddPLSkQT+uAvUpQNnFGNbgKzv90DyQlxPDYPQ4nd6A==} + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-s390x-gnu@1.11.0': - resolution: {integrity: sha512-HfO0PUCCRte2pMJmVyxPI+eqT7KuV3Fnvn2RPvMe5mOzb2BJKf4/Vth8sSt9cerQboMaTVpbxyYjjLBWIuI5BQ==} + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - '@unrs/resolver-binding-linux-x64-gnu@1.11.0': - resolution: {integrity: sha512-9PZdjP7tLOEjpXHS6+B/RNqtfVUyDEmaViPOuSqcbomLdkJnalt5RKQ1tr2m16+qAufV0aDkfhXtoO7DQos/jg==} + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-linux-x64-musl@1.11.0': - resolution: {integrity: sha512-qkE99ieiSKMnFJY/EfyGKVtNra52/k+lVF/PbO4EL5nU6AdvG4XhtJ+WHojAJP7ID9BNIra/yd75EHndewNRfA==} + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-wasm32-wasi@1.11.0': - resolution: {integrity: sha512-MjXek8UL9tIX34gymvQLecz2hMaQzOlaqYJJBomwm1gsvK2F7hF+YqJJ2tRyBDTv9EZJGMt4KlKkSD/gZWCOiw==} + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.11.0': - resolution: {integrity: sha512-9LT6zIGO7CHybiQSh7DnQGwFMZvVr0kUjah6qQfkH2ghucxPV6e71sUXJdSM4Ba0MaGE6DC/NwWf7mJmc3DAng==} + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.11.0': - resolution: {integrity: sha512-HYchBYOZ7WN266VjoGm20xFv5EonG/ODURRgwl9EZT7Bq1nLEs6VKJddzfFdXEAho0wfFlt8L/xIiE29Pmy1RA==} + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.11.0': - resolution: {integrity: sha512-+oLKLHw3I1UQo4MeHfoLYF+e6YBa8p5vYUw3Rgt7IDzCs+57vIZqQlIo62NDpYM0VG6BjWOwnzBczMvbtH8hag==} + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} cpu: [x64] os: [win32] '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} @@ -3220,14 +3211,14 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@xyflow/react@12.8.1': - resolution: {integrity: sha512-t5Rame4Gc/540VcOZd28yFe9Xd8lyjKUX+VTiyb1x4ykNXZH5zyDmsu+lj9je2O/jGBVb0pj1Vjcxrxyn+Xk2g==} + '@xyflow/react@12.8.3': + resolution: {integrity: sha512-8sdRZPMCzfhauF96krlUMPCKmi9cX64HsYG8qoVAAvTKDAqxXg7RSp/IhoXlzbI/lsRD1vAxeDBxvI/XqACa6g==} peerDependencies: react: '>=17' react-dom: '>=17' - '@xyflow/system@0.0.65': - resolution: {integrity: sha512-AliQPQeurQMoNlOdySnRoDQl9yDSA/1Lqi47Eo0m98lHcfrTdD9jK75H0tiGj+0qRC10SKNUXyMkT0KL0opg4g==} + '@xyflow/system@0.0.67': + resolution: {integrity: sha512-hYsmbj+8JDei0jmupBmxNLaeJEcf9kKmMl6IziGe02i0TOCsHwjIdP+qz+f4rI1/FR2CQiCZJrw4dkHOLC6tEQ==} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -3238,6 +3229,12 @@ packages: peerDependencies: acorn: ^8 + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3526,8 +3523,8 @@ packages: browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3543,10 +3540,6 @@ packages: builtin-status-codes@3.0.0: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3580,8 +3573,8 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -3638,8 +3631,8 @@ packages: '@chromatic-com/playwright': optional: true - chromatic@13.1.2: - resolution: {integrity: sha512-jgVptQabJHOnzmmvLjbtfutREkWGhDDk2gVqMH6N+V7z56oIy4Sd2/U7ZxNvnVFPinZQMSjSdUce4b6JIP64Dg==} + chromatic@13.1.3: + resolution: {integrity: sha512-aOZDwg1PsDe9/UhiXqS6EJPoCGK91hYbj3HaunV/0Ij492eWLkXIzku/e5cF1t7Ma7cAuGpCQDo0vGHg0UO91w==} hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 @@ -3760,11 +3753,11 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} - core-js-compat@3.44.0: - resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + core-js-compat@3.45.0: + resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} - core-js-pure@3.44.0: - resolution: {integrity: sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==} + core-js-pure@3.45.0: + resolution: {integrity: sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -4058,12 +4051,12 @@ packages: dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.2.0: - resolution: {integrity: sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==} + dotenv@17.2.1: + resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -4073,8 +4066,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.180: - resolution: {integrity: sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==} + electron-to-chromium@1.5.200: + resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -4105,8 +4098,8 @@ packages: endent@2.1.0: resolution: {integrity: sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==} - enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -4177,8 +4170,8 @@ packages: peerDependencies: esbuild: '>=0.12 <1' - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -4190,8 +4183,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@15.3.5: - resolution: {integrity: sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg==} + eslint-config-next@15.4.6: + resolution: {integrity: sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -4264,12 +4257,12 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-storybook@9.0.16: - resolution: {integrity: sha512-A9kJaYBGYswo11t9coo1rpY5i8qPJx9JX5/6YWK3L3zT9lCxJWkYFAed/1Jt92yk7EkOzLrwrIIjMj/+7erlgw==} + eslint-plugin-storybook@9.1.2: + resolution: {integrity: sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==} engines: {node: '>=20.0.0'} peerDependencies: eslint: '>=8' - storybook: ^9.0.16 + storybook: ^9.1.2 eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -4324,6 +4317,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -4464,8 +4460,8 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - framer-motion@12.23.0: - resolution: {integrity: sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==} + framer-motion@12.23.12: + resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -4482,12 +4478,12 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} - fs-monkey@1.0.6: - resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -4627,10 +4623,6 @@ packages: resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} engines: {node: '>= 0.10'} - hash-base@3.1.0: - resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} - engines: {node: '>=4'} - hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -4668,8 +4660,8 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - html-webpack-plugin@5.6.3: - resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} + html-webpack-plugin@5.6.4: + resolution: {integrity: sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==} engines: {node: '>=10.13.0'} peerDependencies: '@rspack/core': 0.x || 1.x @@ -4928,11 +4920,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isows@1.0.7: - resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} - peerDependencies: - ws: '*' - iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -4999,8 +4986,8 @@ packages: jsonc-parser@2.2.1: resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} jsonpath-plus@10.3.0: resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} @@ -5130,8 +5117,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.4: - resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + loupe@3.2.0: + resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -5142,8 +5129,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.525.0: - resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} + lucide-react@0.539.0: + resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5207,6 +5194,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5333,17 +5323,20 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - motion-dom@12.22.0: - resolution: {integrity: sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==} + motion-dom@12.23.12: + resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} - motion-utils@12.19.0: - resolution: {integrity: sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==} + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5379,8 +5372,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.0: - resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==} + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -5396,13 +5389,13 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.3.5: - resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} + next@15.4.6: + resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 + '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -5463,6 +5456,24 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuqs@2.4.3: + resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==} + peerDependencies: + '@remix-run/react': '>=2' + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^6 || ^7 + react-router-dom: ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + oas-kit-common@1.0.8: resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==} @@ -5546,8 +5557,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - orval@7.10.0: - resolution: {integrity: sha512-R1TlDDgK82dHfTXG0IuaIXHOrk6HQ1CuGejQQpQW9mBSCQA84AInp8U4Ovxw3upjMFNhghE8OlAQqD0ES8GgHQ==} + orval@7.11.2: + resolution: {integrity: sha512-Cjc/dgnQwAOkvymzvPpFqFc2nQwZ29E+ZFWUI8yKejleHaoFKIdwvkM/b1njtLEjePDcF0hyqXXCTz2wWaXLig==} hasBin: true os-browserify@0.3.0: @@ -5677,8 +5688,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} pify@2.3.0: @@ -5697,13 +5708,13 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - playwright-core@1.54.1: - resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + playwright-core@1.54.2: + resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==} engines: {node: '>=18'} hasBin: true - playwright@1.54.1: - resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + playwright@1.54.2: + resolution: {integrity: sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==} engines: {node: '>=18'} hasBin: true @@ -5959,8 +5970,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - react-day-picker@9.8.0: - resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==} + react-day-picker@9.8.1: + resolution: {integrity: sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==} engines: {node: '>=18'} peerDependencies: react: '>=16.8.0' @@ -5985,8 +5996,8 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - react-hook-form@7.60.0: - resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} + react-hook-form@7.62.0: + resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -6044,8 +6055,8 @@ packages: '@types/react': optional: true - react-shepherd@6.1.8: - resolution: {integrity: sha512-AA/ZqSbhkztCnRtNS5V9+V+lBJc1tjyYBGO6Gkjb41OX/jhGiFO0dJpfPnWYuHwAloYAXR0UuFq/lGqlXRWkrw==} + react-shepherd@6.1.9: + resolution: {integrity: sha512-kSFs7ER9+tDAQ9a80CGTaWHpuNf/6RNnnAqtPxFqZSt5NnlKi6T8/E93sYMPOibhvdtpG5pIZpeT3JI1+Ppqiw==} peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 @@ -6073,6 +6084,13 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react-window@1.8.11: + resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6216,8 +6234,8 @@ packages: ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} - rollup@4.35.0: - resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6307,15 +6325,16 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} hasBin: true shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - sharp@0.34.2: - resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -6330,8 +6349,8 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - shepherd.js@14.5.0: - resolution: {integrity: sha512-23yBjWnrEeaCHFVUukPNol/K0pdvq6NgyqxDeq1qfJuNhxTHpiAvqTB9ULUogndBcGxfkyTRud95PpUyZwGAGQ==} + shepherd.js@14.5.1: + resolution: {integrity: sha512-VuvPvLG1QjNOLP7AIm2HGyfmxEIz8QdskvWOHwUcxLDibYWjLRBmCWd8LSL5FlwhBW7D/GU+3gNVC/ASxAWdxg==} engines: {node: 18.* || >= 20} shimmer@1.2.1: @@ -6389,8 +6408,8 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - sonner@2.0.6: - resolution: {integrity: sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc @@ -6406,9 +6425,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -6431,8 +6450,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@9.0.16: - resolution: {integrity: sha512-DzjzeggdzlXKKBK1L9iqNKqqNpyfeaL1hxxeAOmqgeMezwy5d5mCJmjNcZEmx+prsRmvj1OWm4ZZAg6iP/wABg==} + storybook@9.1.2: + resolution: {integrity: sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -6446,10 +6465,6 @@ packages: stream-http@3.2.0: resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -6775,21 +6790,21 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedoc-plugin-markdown@4.6.4: - resolution: {integrity: sha512-AnbToFS1T1H+n40QbO2+i0wE6L+55rWnj7zxnM1r781+2gmhMF2dB6dzFpaylWLQYkbg4D1Y13sYnne/6qZwdw==} + typedoc-plugin-markdown@4.8.1: + resolution: {integrity: sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x - typedoc@0.28.5: - resolution: {integrity: sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==} + typedoc@0.28.10: + resolution: {integrity: sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true @@ -6800,8 +6815,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -6856,8 +6871,8 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} - unrs-resolver@1.11.0: - resolution: {integrity: sha512-uw3hCGO/RdAEAb4zgJ3C/v6KIAFFOtBoxR86b2Ejc5TnH7HrhTWJR2o0A9ullC3eWMegKQCw/arQ/JivywQzkg==} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} @@ -6938,8 +6953,8 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -6982,8 +6997,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.99.9: - resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} + webpack@5.101.1: + resolution: {integrity: sha512-rHY3vHXRbkSfhG6fH8zYQdth/BtDgXXuR2pHF++1f/EBkI8zkgM5XWfsC3BvOoW9pr1CvZ1qQCxhCEsbNgT50g==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -7066,8 +7081,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} hasBin: true @@ -7114,14 +7129,14 @@ packages: snapshots: - '@adobe/css-tools@4.4.3': {} + '@adobe/css-tools@4.4.4': {} '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@apidevtools/json-schema-ref-parser@11.7.2': dependencies: @@ -7144,7 +7159,7 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@asyncapi/specs@6.8.1': + '@asyncapi/specs@6.9.0': dependencies: '@types/json-schema': 7.0.15 @@ -7156,18 +7171,18 @@ snapshots: '@babel/compat-data@7.28.0': {} - '@babel/core@7.28.0': + '@babel/core@7.28.3': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.28.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -7176,49 +7191,49 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.0': + '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 + browserslist: 4.25.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.1 @@ -7231,55 +7246,55 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -7289,610 +7304,610 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-wrap-function@7.27.1': + '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 - '@babel/parser@7.28.0': + '@babel/parser@7.28.3': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-classes@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/traverse': 7.28.0 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/types': 7.28.0 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-regenerator@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-runtime@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.3) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.3) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.3) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.28.0(@babel/core@7.28.0)': + '@babel/preset-env@7.28.3(@babel/core@7.28.3)': dependencies: '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-regenerator': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) - core-js-compat: 3.44.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.3) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.3) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-classes': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-regenerator': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.3) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.3) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.3) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.3) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.3) + core-js-compat: 3.45.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 esutils: 2.0.3 - '@babel/preset-react@7.27.1(@babel/core@7.28.0)': + '@babel/preset-react@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + '@babel/preset-typescript@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.3': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 - '@babel/traverse@7.28.0': + '@babel/traverse@7.28.3': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.0 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 debug: 4.4.1 transitivePeerDependencies: - supports-color - '@babel/types@7.28.0': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -7910,13 +7925,13 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@chromatic-com/storybook@4.0.1(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@chromatic-com/storybook@4.1.0(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 - jsonfile: 6.1.0 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + jsonfile: 6.2.0 + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -7924,18 +7939,18 @@ snapshots: '@date-fns/tz@1.2.0': {} - '@emnapi/core@1.4.4': + '@emnapi/core@1.4.5': dependencies: - '@emnapi/wasi-threads': 1.0.3 + '@emnapi/wasi-threads': 1.0.4 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.4': + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.3': + '@emnapi/wasi-threads@1.0.4': dependencies: tslib: 2.8.1 optional: true @@ -7948,82 +7963,82 @@ snapshots: '@emotion/unitless@0.8.1': {} - '@esbuild/aix-ppc64@0.25.6': + '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/android-arm64@0.25.6': + '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/android-arm@0.25.6': + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-x64@0.25.6': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.25.6': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.25.6': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.25.6': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.25.6': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/linux-arm64@0.25.6': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-ia32@0.25.6': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-loong64@0.25.6': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.25.6': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-ppc64@0.25.6': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.25.6': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-s390x@0.25.6': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-x64@0.25.6': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/netbsd-arm64@0.25.6': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.25.6': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.25.6': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.25.6': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/openharmony-arm64@0.25.6': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/sunos-x64@0.25.6': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/win32-arm64@0.25.6': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/win32-ia32@0.25.6': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-x64@0.25.6': + '@esbuild/win32-x64@0.25.9': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': @@ -8053,35 +8068,35 @@ snapshots: '@faker-js/faker@9.9.0': {} - '@floating-ui/core@1.7.1': + '@floating-ui/core@1.7.3': dependencies: - '@floating-ui/utils': 0.2.9 + '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.1': + '@floating-ui/dom@1.7.3': dependencies: - '@floating-ui/core': 1.7.1 - '@floating-ui/utils': 0.2.9 + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.1 + '@floating-ui/dom': 1.7.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/utils@0.2.9': {} + '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.6.0': + '@gerrit0/mini-shiki@3.9.2': dependencies: - '@shikijs/engine-oniguruma': 3.6.0 - '@shikijs/langs': 3.6.0 - '@shikijs/themes': 3.6.0 - '@shikijs/types': 3.6.0 + '@shikijs/engine-oniguruma': 3.9.2 + '@shikijs/langs': 3.9.2 + '@shikijs/themes': 3.9.2 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 - '@hookform/resolvers@5.1.1(react-hook-form@7.60.0(react@18.3.1))': + '@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@18.3.1))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.60.0(react@18.3.1) + react-hook-form: 7.62.0(react@18.3.1) '@humanwhocodes/config-array@0.13.0': dependencies: @@ -8097,7 +8112,7 @@ snapshots: '@ibm-cloud/openapi-ruleset-utilities@1.9.0': {} - '@ibm-cloud/openapi-ruleset@1.31.1': + '@ibm-cloud/openapi-ruleset@1.31.2': dependencies: '@ibm-cloud/openapi-ruleset-utilities': 1.9.0 '@stoplight/spectral-formats': 1.8.2 @@ -8113,98 +8128,103 @@ snapshots: transitivePeerDependencies: - encoding - '@img/sharp-darwin-arm64@0.34.2': + '@img/sharp-darwin-arm64@0.34.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-arm64': 1.2.0 optional: true - '@img/sharp-darwin-x64@0.34.2': + '@img/sharp-darwin-x64@0.34.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': optional: true - '@img/sharp-libvips-darwin-arm64@1.1.0': + '@img/sharp-libvips-darwin-x64@1.2.0': optional: true - '@img/sharp-libvips-darwin-x64@1.1.0': + '@img/sharp-libvips-linux-arm64@1.2.0': optional: true - '@img/sharp-libvips-linux-arm64@1.1.0': + '@img/sharp-libvips-linux-arm@1.2.0': optional: true - '@img/sharp-libvips-linux-arm@1.1.0': + '@img/sharp-libvips-linux-ppc64@1.2.0': optional: true - '@img/sharp-libvips-linux-ppc64@1.1.0': + '@img/sharp-libvips-linux-s390x@1.2.0': optional: true - '@img/sharp-libvips-linux-s390x@1.1.0': + '@img/sharp-libvips-linux-x64@1.2.0': optional: true - '@img/sharp-libvips-linux-x64@1.1.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.1.0': + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 optional: true - '@img/sharp-linux-arm64@0.34.2': + '@img/sharp-linux-arm@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.2.0 optional: true - '@img/sharp-linux-arm@0.34.2': + '@img/sharp-linux-ppc64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 optional: true - '@img/sharp-linux-s390x@0.34.2': + '@img/sharp-linux-s390x@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 optional: true - '@img/sharp-linux-x64@0.34.2': + '@img/sharp-linux-x64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.2.0 optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': + '@img/sharp-linuxmusl-arm64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 optional: true - '@img/sharp-linuxmusl-x64@0.34.2': + '@img/sharp-linuxmusl-x64@0.34.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 optional: true - '@img/sharp-wasm32@0.34.2': + '@img/sharp-wasm32@0.34.3': dependencies: - '@emnapi/runtime': 1.4.4 + '@emnapi/runtime': 1.4.5 optional: true - '@img/sharp-win32-arm64@0.34.2': + '@img/sharp-win32-arm64@0.34.3': optional: true - '@img/sharp-win32-ia32@0.34.2': + '@img/sharp-win32-ia32@0.34.3': optional: true - '@img/sharp-win32-x64@0.34.2': + '@img/sharp-win32-x64@0.34.3': optional: true - '@inquirer/confirm@5.1.13(@types/node@24.0.14)': + '@inquirer/confirm@5.1.14(@types/node@24.2.1)': dependencies: - '@inquirer/core': 10.1.14(@types/node@24.0.14) - '@inquirer/type': 3.0.7(@types/node@24.0.14) + '@inquirer/core': 10.1.15(@types/node@24.2.1) + '@inquirer/type': 3.0.8(@types/node@24.2.1) optionalDependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 - '@inquirer/core@10.1.14(@types/node@24.0.14)': + '@inquirer/core@10.1.15(@types/node@24.2.1)': dependencies: - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@24.0.14) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.2.1) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -8212,13 +8232,13 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 - '@inquirer/figures@1.0.12': {} + '@inquirer/figures@1.0.13': {} - '@inquirer/type@3.0.7(@types/node@24.0.14)': + '@inquirer/type@3.0.8(@types/node@24.2.1)': optionalDependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 '@isaacs/cliui@8.0.2': dependencies: @@ -8229,39 +8249,24 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jridgewell/gen-mapping@0.3.12': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.29 - - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.10': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@jridgewell/trace-mapping@0.3.29': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jsdevtools/ono@7.1.3': {} @@ -8283,7 +8288,7 @@ snapshots: '@types/react': 18.3.17 react: 18.3.1 - '@mswjs/interceptors@0.39.2': + '@mswjs/interceptors@0.39.6': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -8292,48 +8297,48 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@0.2.11': + '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.4.4 - '@emnapi/runtime': 1.4.4 - '@tybys/wasm-util': 0.9.0 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 optional: true '@neoconfetti/react@1.0.0': {} - '@next/env@15.3.5': {} + '@next/env@15.4.6': {} - '@next/eslint-plugin-next@15.3.5': + '@next/eslint-plugin-next@15.4.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.3.5': + '@next/swc-darwin-arm64@15.4.6': optional: true - '@next/swc-darwin-x64@15.3.5': + '@next/swc-darwin-x64@15.4.6': optional: true - '@next/swc-linux-arm64-gnu@15.3.5': + '@next/swc-linux-arm64-gnu@15.4.6': optional: true - '@next/swc-linux-arm64-musl@15.3.5': + '@next/swc-linux-arm64-musl@15.4.6': optional: true - '@next/swc-linux-x64-gnu@15.3.5': + '@next/swc-linux-x64-gnu@15.4.6': optional: true - '@next/swc-linux-x64-musl@15.3.5': + '@next/swc-linux-x64-musl@15.4.6': optional: true - '@next/swc-win32-arm64-msvc@15.3.5': + '@next/swc-win32-arm64-msvc@15.4.6': optional: true - '@next/swc-win32-x64-msvc@15.3.5': + '@next/swc-win32-x64-msvc@15.4.6': optional: true - '@next/third-parties@15.3.5(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@next/third-parties@15.4.6(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: - next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 third-party-capital: 1.0.20 @@ -8380,7 +8385,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8389,7 +8394,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color @@ -8406,7 +8411,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8437,7 +8442,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8457,7 +8462,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8465,7 +8470,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8473,7 +8478,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8482,7 +8487,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8497,7 +8502,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8506,7 +8511,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8514,7 +8519,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -8523,7 +8528,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 '@types/mysql': 2.15.26 transitivePeerDependencies: - supports-color @@ -8533,7 +8538,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) '@types/pg': 8.6.1 '@types/pg-pool': 2.0.6 @@ -8545,7 +8550,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 transitivePeerDependencies: - supports-color @@ -8553,7 +8558,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/semantic-conventions': 1.36.0 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color @@ -8595,41 +8600,41 @@ snapshots: '@opentelemetry/semantic-conventions@1.28.0': {} - '@opentelemetry/semantic-conventions@1.34.0': {} + '@opentelemetry/semantic-conventions@1.36.0': {} '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@orval/angular@7.10.0(openapi-types@12.1.3)': + '@orval/angular@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/axios@7.10.0(openapi-types@12.1.3)': + '@orval/axios@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/core@7.10.0(openapi-types@12.1.3)': + '@orval/core@7.11.2(openapi-types@12.1.3)': dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@ibm-cloud/openapi-ruleset': 1.31.1 + '@ibm-cloud/openapi-ruleset': 1.31.2 acorn: 8.15.0 ajv: 8.17.1 chalk: 4.1.2 compare-versions: 6.1.1 debug: 4.4.1 - esbuild: 0.25.6 + esbuild: 0.25.9 esutils: 2.0.3 - fs-extra: 11.3.0 + fs-extra: 11.3.1 globby: 11.1.0 lodash.isempty: 4.4.0 lodash.uniq: 4.5.0 @@ -8643,64 +8648,65 @@ snapshots: - openapi-types - supports-color - '@orval/fetch@7.10.0(openapi-types@12.1.3)': + '@orval/fetch@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/hono@7.10.0(openapi-types@12.1.3)': + '@orval/hono@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) + '@orval/zod': 7.11.2(openapi-types@12.1.3) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mcp@7.10.0(openapi-types@12.1.3)': + '@orval/mcp@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) + '@orval/fetch': 7.11.2(openapi-types@12.1.3) + '@orval/zod': 7.11.2(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/mock@7.10.0(openapi-types@12.1.3)': + '@orval/mock@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - openapi3-ts: 4.2.2 + '@orval/core': 7.11.2(openapi-types@12.1.3) + openapi3-ts: 4.4.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/query@7.10.0(openapi-types@12.1.3)': + '@orval/query@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) + '@orval/fetch': 7.11.2(openapi-types@12.1.3) lodash.omitby: 4.6.0 transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/swr@7.10.0(openapi-types@12.1.3)': + '@orval/swr@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) + '@orval/fetch': 7.11.2(openapi-types@12.1.3) transitivePeerDependencies: - encoding - openapi-types - supports-color - '@orval/zod@7.10.0(openapi-types@12.1.3)': + '@orval/zod@7.11.2(openapi-types@12.1.3)': dependencies: - '@orval/core': 7.10.0(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) lodash.uniq: 4.5.0 transitivePeerDependencies: - encoding @@ -8715,26 +8721,26 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.54.1': + '@playwright/test@1.54.2': dependencies: - playwright: 1.54.1 + playwright: 1.54.2 - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.6))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.101.1(esbuild@0.25.9))': dependencies: ansi-html: 0.0.9 - core-js-pure: 3.44.0 + core-js-pure: 3.45.0 error-stack-parser: 2.1.4 html-entities: 2.6.0 loader-utils: 2.0.4 react-refresh: 0.14.2 schema-utils: 4.3.2 - source-map: 0.7.4 - webpack: 5.99.9(esbuild@0.25.6) + source-map: 0.7.6 + webpack: 5.101.1(esbuild@0.25.9) optionalDependencies: type-fest: 4.41.0 webpack-hot-middleware: 2.26.1 - '@prisma/instrumentation@6.10.1(@opentelemetry/api@1.9.0)': + '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) @@ -8743,14 +8749,14 @@ snapshots: '@radix-ui/number@1.1.1': {} - '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) react: 18.3.1 @@ -8781,12 +8787,12 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -8797,13 +8803,13 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -8831,11 +8837,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - '@radix-ui/react-context-menu@2.2.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) @@ -8851,17 +8857,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.17)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) @@ -8879,9 +8885,9 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -8892,13 +8898,13 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) react: 18.3.1 @@ -8907,7 +8913,7 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.17)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.17)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: @@ -8944,22 +8950,22 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.17)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.17)(react@18.3.1) aria-hidden: 1.2.6 @@ -8970,18 +8976,18 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.17)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) @@ -8993,9 +8999,9 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) @@ -9021,7 +9027,7 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -9040,15 +9046,15 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-radio-group@1.3.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -9058,9 +9064,9 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) @@ -9075,14 +9081,14 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.17)(react@18.3.1) @@ -9092,19 +9098,19 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.17)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) @@ -9137,9 +9143,9 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - '@radix-ui/react-switch@1.2.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9152,15 +9158,15 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-tabs@1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9168,15 +9174,15 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-toast@1.2.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) @@ -9188,16 +9194,16 @@ snapshots: '@types/react': 18.3.17 '@types/react-dom': 18.3.5(@types/react@18.3.17) - '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.2 + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.2.3(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1) @@ -9280,81 +9286,84 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)': + '@rollup/plugin-commonjs@28.0.1(rollup@4.46.2)': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.35.0) + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.4.6(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.3) is-reference: 1.2.1 magic-string: 0.30.17 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.35.0 + rollup: 4.46.2 - '@rollup/pluginutils@5.2.0(rollup@4.35.0)': + '@rollup/pluginutils@5.2.0(rollup@4.46.2)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.35.0 + rollup: 4.46.2 + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true - '@rollup/rollup-android-arm-eabi@4.35.0': + '@rollup/rollup-android-arm64@4.46.2': optional: true - '@rollup/rollup-android-arm64@4.35.0': + '@rollup/rollup-darwin-arm64@4.46.2': optional: true - '@rollup/rollup-darwin-arm64@4.35.0': + '@rollup/rollup-darwin-x64@4.46.2': optional: true - '@rollup/rollup-darwin-x64@4.35.0': + '@rollup/rollup-freebsd-arm64@4.46.2': optional: true - '@rollup/rollup-freebsd-arm64@4.35.0': + '@rollup/rollup-freebsd-x64@4.46.2': optional: true - '@rollup/rollup-freebsd-x64@4.35.0': + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': + '@rollup/rollup-linux-arm-musleabihf@4.46.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.35.0': + '@rollup/rollup-linux-arm64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.35.0': + '@rollup/rollup-linux-arm64-musl@4.46.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.35.0': + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': + '@rollup/rollup-linux-ppc64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': + '@rollup/rollup-linux-riscv64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.35.0': + '@rollup/rollup-linux-riscv64-musl@4.46.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.35.0': + '@rollup/rollup-linux-s390x-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.35.0': + '@rollup/rollup-linux-x64-gnu@4.46.2': optional: true - '@rollup/rollup-linux-x64-musl@4.35.0': + '@rollup/rollup-linux-x64-musl@4.46.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.35.0': + '@rollup/rollup-win32-arm64-msvc@4.46.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.35.0': + '@rollup/rollup-win32-ia32-msvc@4.46.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.35.0': + '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true '@rtsao/scc@1.1.0': {} @@ -9363,40 +9372,40 @@ snapshots: '@scarf/scarf@1.4.0': {} - '@sentry-internal/browser-utils@9.35.0': + '@sentry-internal/browser-utils@9.42.0': dependencies: - '@sentry/core': 9.35.0 + '@sentry/core': 9.42.0 - '@sentry-internal/feedback@9.35.0': + '@sentry-internal/feedback@9.42.0': dependencies: - '@sentry/core': 9.35.0 + '@sentry/core': 9.42.0 - '@sentry-internal/replay-canvas@9.35.0': + '@sentry-internal/replay-canvas@9.42.0': dependencies: - '@sentry-internal/replay': 9.35.0 - '@sentry/core': 9.35.0 + '@sentry-internal/replay': 9.42.0 + '@sentry/core': 9.42.0 - '@sentry-internal/replay@9.35.0': + '@sentry-internal/replay@9.42.0': dependencies: - '@sentry-internal/browser-utils': 9.35.0 - '@sentry/core': 9.35.0 + '@sentry-internal/browser-utils': 9.42.0 + '@sentry/core': 9.42.0 - '@sentry/babel-plugin-component-annotate@3.5.0': {} + '@sentry/babel-plugin-component-annotate@3.6.1': {} - '@sentry/browser@9.35.0': + '@sentry/browser@9.42.0': dependencies: - '@sentry-internal/browser-utils': 9.35.0 - '@sentry-internal/feedback': 9.35.0 - '@sentry-internal/replay': 9.35.0 - '@sentry-internal/replay-canvas': 9.35.0 - '@sentry/core': 9.35.0 + '@sentry-internal/browser-utils': 9.42.0 + '@sentry-internal/feedback': 9.42.0 + '@sentry-internal/replay': 9.42.0 + '@sentry-internal/replay-canvas': 9.42.0 + '@sentry/core': 9.42.0 - '@sentry/bundler-plugin-core@3.5.0': + '@sentry/bundler-plugin-core@3.6.1': dependencies: - '@babel/core': 7.28.0 - '@sentry/babel-plugin-component-annotate': 3.5.0 - '@sentry/cli': 2.42.2 - dotenv: 16.5.0 + '@babel/core': 7.28.3 + '@sentry/babel-plugin-component-annotate': 3.6.1 + '@sentry/cli': 2.52.0 + dotenv: 16.6.1 find-up: 5.0.0 glob: 9.3.5 magic-string: 0.30.8 @@ -9405,28 +9414,31 @@ snapshots: - encoding - supports-color - '@sentry/cli-darwin@2.42.2': + '@sentry/cli-darwin@2.52.0': + optional: true + + '@sentry/cli-linux-arm64@2.52.0': optional: true - '@sentry/cli-linux-arm64@2.42.2': + '@sentry/cli-linux-arm@2.52.0': optional: true - '@sentry/cli-linux-arm@2.42.2': + '@sentry/cli-linux-i686@2.52.0': optional: true - '@sentry/cli-linux-i686@2.42.2': + '@sentry/cli-linux-x64@2.52.0': optional: true - '@sentry/cli-linux-x64@2.42.2': + '@sentry/cli-win32-arm64@2.52.0': optional: true - '@sentry/cli-win32-i686@2.42.2': + '@sentry/cli-win32-i686@2.52.0': optional: true - '@sentry/cli-win32-x64@2.42.2': + '@sentry/cli-win32-x64@2.52.0': optional: true - '@sentry/cli@2.42.2': + '@sentry/cli@2.52.0': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0 @@ -9434,47 +9446,60 @@ snapshots: proxy-from-env: 1.1.0 which: 2.0.2 optionalDependencies: - '@sentry/cli-darwin': 2.42.2 - '@sentry/cli-linux-arm': 2.42.2 - '@sentry/cli-linux-arm64': 2.42.2 - '@sentry/cli-linux-i686': 2.42.2 - '@sentry/cli-linux-x64': 2.42.2 - '@sentry/cli-win32-i686': 2.42.2 - '@sentry/cli-win32-x64': 2.42.2 + '@sentry/cli-darwin': 2.52.0 + '@sentry/cli-linux-arm': 2.52.0 + '@sentry/cli-linux-arm64': 2.52.0 + '@sentry/cli-linux-i686': 2.52.0 + '@sentry/cli-linux-x64': 2.52.0 + '@sentry/cli-win32-arm64': 2.52.0 + '@sentry/cli-win32-i686': 2.52.0 + '@sentry/cli-win32-x64': 2.52.0 transitivePeerDependencies: - encoding - supports-color - '@sentry/core@9.35.0': {} + '@sentry/core@9.42.0': {} - '@sentry/nextjs@9.35.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(esbuild@0.25.6))': + '@sentry/nextjs@9.42.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.101.1(esbuild@0.25.9))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.34.0 - '@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0) - '@sentry-internal/browser-utils': 9.35.0 - '@sentry/core': 9.35.0 - '@sentry/node': 9.35.0 - '@sentry/opentelemetry': 9.35.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) - '@sentry/react': 9.35.0(react@18.3.1) - '@sentry/vercel-edge': 9.35.0 - '@sentry/webpack-plugin': 3.5.0(webpack@5.99.9(esbuild@0.25.6)) + '@opentelemetry/semantic-conventions': 1.36.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.46.2) + '@sentry-internal/browser-utils': 9.42.0 + '@sentry/core': 9.42.0 + '@sentry/node': 9.42.0 + '@sentry/opentelemetry': 9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/react': 9.42.0(react@18.3.1) + '@sentry/vercel-edge': 9.42.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) + '@sentry/webpack-plugin': 3.6.1(webpack@5.101.1(esbuild@0.25.9)) chalk: 3.0.0 - next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) resolve: 1.22.8 - rollup: 4.35.0 + rollup: 4.46.2 stacktrace-parser: 0.1.11 transitivePeerDependencies: - '@opentelemetry/context-async-hooks' - '@opentelemetry/core' - - '@opentelemetry/instrumentation' - '@opentelemetry/sdk-trace-base' - encoding - react - supports-color - webpack - '@sentry/node@9.35.0': + '@sentry/node-core@9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.42.0 + '@sentry/opentelemetry': 9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + import-in-the-middle: 1.14.2 + + '@sentry/node@9.42.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -9504,61 +9529,68 @@ snapshots: '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 - '@prisma/instrumentation': 6.10.1(@opentelemetry/api@1.9.0) - '@sentry/core': 9.35.0 - '@sentry/opentelemetry': 9.35.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@prisma/instrumentation': 6.11.1(@opentelemetry/api@1.9.0) + '@sentry/core': 9.42.0 + '@sentry/node-core': 9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/opentelemetry': 9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) import-in-the-middle: 1.14.2 minimatch: 9.0.5 transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@9.35.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)': + '@sentry/opentelemetry@9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 - '@sentry/core': 9.35.0 + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.42.0 - '@sentry/react@9.35.0(react@18.3.1)': + '@sentry/react@9.42.0(react@18.3.1)': dependencies: - '@sentry/browser': 9.35.0 - '@sentry/core': 9.35.0 + '@sentry/browser': 9.42.0 + '@sentry/core': 9.42.0 hoist-non-react-statics: 3.3.2 react: 18.3.1 - '@sentry/vercel-edge@9.35.0': + '@sentry/vercel-edge@9.42.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))': dependencies: '@opentelemetry/api': 1.9.0 - '@sentry/core': 9.35.0 + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.42.0 + '@sentry/opentelemetry': 9.42.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + transitivePeerDependencies: + - '@opentelemetry/context-async-hooks' + - '@opentelemetry/core' + - '@opentelemetry/sdk-trace-base' - '@sentry/webpack-plugin@3.5.0(webpack@5.99.9(esbuild@0.25.6))': + '@sentry/webpack-plugin@3.6.1(webpack@5.101.1(esbuild@0.25.9))': dependencies: - '@sentry/bundler-plugin-core': 3.5.0 + '@sentry/bundler-plugin-core': 3.6.1 unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) transitivePeerDependencies: - encoding - supports-color - '@shikijs/engine-oniguruma@3.6.0': + '@shikijs/engine-oniguruma@3.9.2': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.6.0': + '@shikijs/langs@3.9.2': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.9.2 - '@shikijs/themes@3.6.0': + '@shikijs/themes@3.9.2': dependencies: - '@shikijs/types': 3.6.0 + '@shikijs/types': 3.9.2 - '@shikijs/types@3.6.0': + '@shikijs/types@3.9.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -9584,7 +9616,7 @@ snapshots: dependencies: '@stoplight/json': 3.21.7 '@stoplight/path': 1.3.2 - '@stoplight/types': 13.6.0 + '@stoplight/types': 13.20.0 '@types/urijs': 1.19.25 dependency-graph: 0.11.0 fast-memoize: 2.5.2 @@ -9676,7 +9708,7 @@ snapshots: '@stoplight/spectral-rulesets@1.22.0': dependencies: - '@asyncapi/specs': 6.8.1 + '@asyncapi/specs': 6.9.0 '@stoplight/better-ajv-errors': 1.0.3(ajv@8.17.1) '@stoplight/json': 3.21.7 '@stoplight/spectral-core': 1.20.0 @@ -9730,56 +9762,56 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@storybook/addon-a11y@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-a11y@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.10.3 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) - '@storybook/addon-docs@9.0.16(@types/react@18.3.17)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-docs@9.1.2(@types/react@18.3.17)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.17)(react@18.3.1) - '@storybook/csf-plugin': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/csf-plugin': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-links@9.0.16(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-links@9.1.2(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) optionalDependencies: react: 18.3.1 - '@storybook/addon-onboarding@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/addon-onboarding@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) - '@storybook/builder-webpack5@9.0.16(esbuild@0.25.6)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)': + '@storybook/builder-webpack5@9.1.2(esbuild@0.25.9)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2)': dependencies: - '@storybook/core-webpack': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/core-webpack': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 - css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.6)) + css-loader: 6.11.0(webpack@5.101.1(esbuild@0.25.9)) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6)) - html-webpack-plugin: 5.6.3(webpack@5.99.9(esbuild@0.25.6)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9)) + html-webpack-plugin: 5.6.4(webpack@5.101.1(esbuild@0.25.9)) magic-string: 0.30.17 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) - style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.6)) - terser-webpack-plugin: 5.3.14(esbuild@0.25.6)(webpack@5.99.9(esbuild@0.25.6)) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) + style-loader: 3.3.4(webpack@5.101.1(esbuild@0.25.9)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.9)(webpack@5.101.1(esbuild@0.25.9)) ts-dedent: 2.2.0 - webpack: 5.99.9(esbuild@0.25.6) - webpack-dev-middleware: 6.1.3(webpack@5.99.9(esbuild@0.25.6)) + webpack: 5.101.1(esbuild@0.25.9) + webpack-dev-middleware: 6.1.3(webpack@5.101.1(esbuild@0.25.9)) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -9787,14 +9819,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/core-webpack@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/core-webpack@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) ts-dedent: 2.2.0 - '@storybook/csf-plugin@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/csf-plugin@9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -9804,48 +9836,48 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/nextjs@9.0.16(esbuild@0.25.6)(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.6))': - dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0) - '@babel/preset-env': 7.28.0(@babel/core@7.28.0) - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) - '@babel/runtime': 7.27.6 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.6)) - '@storybook/builder-webpack5': 9.0.16(esbuild@0.25.6)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) - '@storybook/preset-react-webpack': 9.0.16(esbuild@0.25.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) - '@storybook/react': 9.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) + '@storybook/nextjs@9.1.2(esbuild@0.25.9)(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.2)(webpack-hot-middleware@2.26.1)(webpack@5.101.1(esbuild@0.25.9))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.3) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.28.3) + '@babel/preset-env': 7.28.3(@babel/core@7.28.3) + '@babel/preset-react': 7.27.1(@babel/core@7.28.3) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) + '@babel/runtime': 7.28.3 + '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.101.1(esbuild@0.25.9)) + '@storybook/builder-webpack5': 9.1.2(esbuild@0.25.9)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2) + '@storybook/preset-react-webpack': 9.1.2(esbuild@0.25.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2) + '@storybook/react': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2) '@types/semver': 7.7.0 - babel-loader: 9.2.1(@babel/core@7.28.0)(webpack@5.99.9(esbuild@0.25.6)) - css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.6)) + babel-loader: 9.2.1(@babel/core@7.28.3)(webpack@5.101.1(esbuild@0.25.9)) + css-loader: 6.11.0(webpack@5.101.1(esbuild@0.25.9)) image-size: 2.0.2 loader-utils: 3.3.1 - next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9(esbuild@0.25.6)) + next: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-polyfill-webpack-plugin: 2.0.1(webpack@5.101.1(esbuild@0.25.9)) postcss: 8.5.6 - postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6)) + postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-refresh: 0.14.2 resolve-url-loader: 5.0.0 - sass-loader: 16.0.5(webpack@5.99.9(esbuild@0.25.6)) + sass-loader: 16.0.5(webpack@5.101.1(esbuild@0.25.9)) semver: 7.7.2 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) - style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.6)) - styled-jsx: 5.1.7(@babel/core@7.28.0)(react@18.3.1) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) + style-loader: 3.3.4(webpack@5.101.1(esbuild@0.25.9)) + styled-jsx: 5.1.7(@babel/core@7.28.3)(react@18.3.1) tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 optionalDependencies: - typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.6) + typescript: 5.9.2 + webpack: 5.101.1(esbuild@0.25.9) transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -9864,10 +9896,10 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@9.0.16(esbuild@0.25.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)': + '@storybook/preset-react-webpack@9.1.2(esbuild@0.25.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2)': dependencies: - '@storybook/core-webpack': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6)) + '@storybook/core-webpack': 9.1.2(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9)) '@types/semver': 7.7.0 find-up: 7.0.0 magic-string: 0.30.17 @@ -9876,11 +9908,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 semver: 7.7.2 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) tsconfig-paths: 4.2.0 - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - '@swc/core' - esbuild @@ -9888,37 +9920,37 @@ snapshots: - uglify-js - webpack-cli - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9))': dependencies: debug: 4.4.1 endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 micromatch: 4.0.8 - react-docgen-typescript: 2.4.0(typescript@5.8.3) + react-docgen-typescript: 2.4.0(typescript@5.9.2) tslib: 2.8.1 - typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.6) + typescript: 5.9.2 + webpack: 5.101.1(esbuild@0.25.9) transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@9.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + '@storybook/react-dom-shim@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) - '@storybook/react@9.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)': + '@storybook/react@9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 - '@supabase/auth-js@2.70.0': + '@supabase/auth-js@2.71.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -9934,65 +9966,62 @@ snapshots: dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/realtime-js@2.11.15': + '@supabase/realtime-js@2.15.1': dependencies: '@supabase/node-fetch': 2.6.15 '@types/phoenix': 1.6.6 '@types/ws': 8.18.1 - isows: 1.0.7(ws@8.18.3) ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@supabase/ssr@0.6.1(@supabase/supabase-js@2.50.3)': + '@supabase/ssr@0.6.1(@supabase/supabase-js@2.55.0)': dependencies: - '@supabase/supabase-js': 2.50.3 + '@supabase/supabase-js': 2.55.0 cookie: 1.0.2 - '@supabase/storage-js@2.7.1': + '@supabase/storage-js@2.11.0': dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/supabase-js@2.50.3': + '@supabase/supabase-js@2.55.0': dependencies: - '@supabase/auth-js': 2.70.0 + '@supabase/auth-js': 2.71.1 '@supabase/functions-js': 2.4.5 '@supabase/node-fetch': 2.6.15 '@supabase/postgrest-js': 1.19.4 - '@supabase/realtime-js': 2.11.15 - '@supabase/storage-js': 2.7.1 + '@supabase/realtime-js': 2.15.1 + '@supabase/storage-js': 2.11.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@tanstack/eslint-plugin-query@5.81.2(eslint@8.57.1)(typescript@5.8.3)': + '@tanstack/eslint-plugin-query@5.83.1(eslint@8.57.1)(typescript@5.9.2)': dependencies: - '@typescript-eslint/utils': 8.35.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.39.1(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript - '@tanstack/query-core@5.81.5': {} + '@tanstack/query-core@5.85.3': {} - '@tanstack/query-devtools@5.81.2': {} + '@tanstack/query-devtools@5.84.0': {} - '@tanstack/react-query-devtools@5.83.0(@tanstack/react-query@5.81.5(react@18.3.1))(react@18.3.1)': + '@tanstack/react-query-devtools@5.84.2(@tanstack/react-query@5.85.3(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/query-devtools': 5.81.2 - '@tanstack/react-query': 5.81.5(react@18.3.1) + '@tanstack/query-devtools': 5.84.0 + '@tanstack/react-query': 5.85.3(react@18.3.1) react: 18.3.1 - '@tanstack/react-query@5.81.5(react@18.3.1)': + '@tanstack/react-query@5.85.3(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.81.5 + '@tanstack/query-core': 5.85.3 react: 18.3.1 '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -10003,32 +10032,31 @@ snapshots: '@tanstack/table-core@8.21.3': {} - '@testing-library/dom@10.4.0': + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 '@types/aria-query': 5.0.4 aria-query: 5.3.0 - chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 + picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.6.3': + '@testing-library/jest-dom@6.7.0': dependencies: - '@adobe/css-tools': 4.4.3 + '@adobe/css-tools': 4.4.4 aria-query: 5.3.2 - chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - lodash: 4.17.21 + picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: - '@testing-library/dom': 10.4.0 + '@testing-library/dom': 10.4.1 - '@tybys/wasm-util@0.9.0': + '@tybys/wasm-util@0.10.0': dependencies: tslib: 2.8.1 optional: true @@ -10037,24 +10065,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 - '@types/babel__traverse@7.20.7': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 '@types/canvas-confetti@1.9.0': {} @@ -10064,7 +10092,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 '@types/cookie@0.6.0': {} @@ -10117,7 +10145,7 @@ snapshots: '@types/es-aggregate-error@1.0.6': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 '@types/eslint-scope@3.7.7': dependencies: @@ -10133,8 +10161,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/estree@1.0.6': {} - '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -10163,13 +10189,13 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 '@types/negotiator@0.6.4': {} - '@types/node@24.0.14': + '@types/node@24.2.1': dependencies: - undici-types: 7.8.0 + undici-types: 7.10.0 '@types/parse-json@4.0.2': {} @@ -10179,7 +10205,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -10195,6 +10221,10 @@ snapshots: dependencies: '@types/react': 18.3.17 + '@types/react-window@1.8.8': + dependencies: + '@types/react': 18.3.17 + '@types/react@18.3.17': dependencies: '@types/prop-types': 15.7.15 @@ -10212,7 +10242,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 '@types/tough-cookie@4.0.5': {} @@ -10224,211 +10254,160 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 - '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.36.0 - '@typescript-eslint/type-utils': 8.36.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.36.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.36.0 + '@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.39.1 + '@typescript-eslint/type-utils': 8.39.1(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.1(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.39.1 eslint: 8.57.1 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.36.0 - '@typescript-eslint/types': 8.36.0 - '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.36.0 + '@typescript-eslint/scope-manager': 8.39.1 + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.39.1 debug: 4.4.1 eslint: 8.57.1 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.39.1(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 + '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.9.2) + '@typescript-eslint/types': 8.39.1 debug: 4.4.1 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.36.0(typescript@5.8.3)': + '@typescript-eslint/scope-manager@8.39.1': dependencies: - '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) - '@typescript-eslint/types': 8.36.0 - debug: 4.4.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.35.0': - dependencies: - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/visitor-keys': 8.39.1 - '@typescript-eslint/scope-manager@8.36.0': + '@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 8.36.0 - '@typescript-eslint/visitor-keys': 8.36.0 + typescript: 5.9.2 - '@typescript-eslint/tsconfig-utils@8.35.0(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.39.1(eslint@8.57.1)(typescript@5.9.2)': dependencies: - typescript: 5.8.3 - - '@typescript-eslint/tsconfig-utils@8.36.0(typescript@5.8.3)': - dependencies: - typescript: 5.8.3 - - '@typescript-eslint/type-utils@8.36.0(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.36.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.1(eslint@8.57.1)(typescript@5.9.2) debug: 4.4.1 eslint: 8.57.1 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.35.0': {} - - '@typescript-eslint/types@8.36.0': {} - - '@typescript-eslint/typescript-estree@8.35.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/project-service': 8.35.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types@8.39.1': {} - '@typescript-eslint/typescript-estree@8.36.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.39.1(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.36.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) - '@typescript-eslint/types': 8.36.0 - '@typescript-eslint/visitor-keys': 8.36.0 + '@typescript-eslint/project-service': 8.39.1(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.9.2) + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/visitor-keys': 8.39.1 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/utils@8.39.1(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.39.1 + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.9.2) eslint: 8.57.1 - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.36.0(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/visitor-keys@8.39.1': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.36.0 - '@typescript-eslint/types': 8.36.0 - '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) - eslint: 8.57.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.35.0': - dependencies: - '@typescript-eslint/types': 8.35.0 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.36.0': - dependencies: - '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/types': 8.39.1 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.0': + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true - '@unrs/resolver-binding-android-arm64@1.11.0': + '@unrs/resolver-binding-android-arm64@1.11.1': optional: true - '@unrs/resolver-binding-darwin-arm64@1.11.0': + '@unrs/resolver-binding-darwin-arm64@1.11.1': optional: true - '@unrs/resolver-binding-darwin-x64@1.11.0': + '@unrs/resolver-binding-darwin-x64@1.11.1': optional: true - '@unrs/resolver-binding-freebsd-x64@1.11.0': + '@unrs/resolver-binding-freebsd-x64@1.11.1': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.0': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.0': + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.11.0': + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.11.0': + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.0': + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.0': + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.11.0': + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.11.0': + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.11.0': + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.11.0': + '@unrs/resolver-binding-linux-x64-musl@1.11.1': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.11.0': + '@unrs/resolver-binding-wasm32-wasi@1.11.1': dependencies: - '@napi-rs/wasm-runtime': 0.2.11 + '@napi-rs/wasm-runtime': 0.2.12 optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.11.0': + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.11.0': + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.11.0': + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true '@vitest/expect@3.2.4': @@ -10439,6 +10418,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -10450,7 +10437,7 @@ snapshots: '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - loupe: 3.1.4 + loupe: 3.2.0 tinyrainbow: 2.0.0 '@webassemblyjs/ast@1.14.1': @@ -10533,9 +10520,9 @@ snapshots: '@xtuc/long@4.2.2': {} - '@xyflow/react@12.8.1(@types/react@18.3.17)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@xyflow/react@12.8.3(@types/react@18.3.17)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@xyflow/system': 0.0.65 + '@xyflow/system': 0.0.67 classcat: 5.0.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -10544,7 +10531,7 @@ snapshots: - '@types/react' - immer - '@xyflow/system@0.0.65': + '@xyflow/system@0.0.67': dependencies: '@types/d3-drag': 3.0.7 '@types/d3-interpolate': 3.0.4 @@ -10564,6 +10551,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-import-phases@1.0.4(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -10765,45 +10756,45 @@ snapshots: axe-core: 4.10.3 mustache: 4.2.0 - axe-playwright@2.1.0(playwright@1.54.1): + axe-playwright@2.1.0(playwright@1.54.2): dependencies: '@types/junit-report-builder': 3.0.2 axe-core: 4.10.3 axe-html-reporter: 2.2.11(axe-core@4.10.3) junit-report-builder: 5.1.1 picocolors: 1.1.1 - playwright: 1.54.1 + playwright: 1.54.2 axobject-query@4.1.0: {} - babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.99.9(esbuild@0.25.6)): + babel-loader@9.2.1(@babel/core@7.28.3)(webpack@5.101.1(esbuild@0.25.9)): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): dependencies: '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.3): dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - core-js-compat: 3.44.0 + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) + core-js-compat: 3.45.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.3): dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) transitivePeerDependencies: - supports-color @@ -10889,12 +10880,12 @@ snapshots: dependencies: pako: 1.0.11 - browserslist@4.25.1: + browserslist@4.25.2: dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.180 + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.200 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) + update-browserslist-db: 1.1.3(browserslist@4.25.2) buffer-from@1.1.2: {} @@ -10907,10 +10898,6 @@ snapshots: builtin-status-codes@3.0.0: {} - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -10943,7 +10930,7 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001727: {} + caniuse-lite@1.0.30001735: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -10954,7 +10941,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.4 + loupe: 3.2.0 pathval: 2.0.1 chalk@3.0.0: @@ -10995,7 +10982,7 @@ snapshots: chromatic@12.2.0: {} - chromatic@13.1.2: {} + chromatic@13.1.3: {} chrome-trace-event@1.0.4: {} @@ -11031,7 +11018,7 @@ snapshots: cmdk@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -11098,11 +11085,11 @@ snapshots: cookie@1.0.2: {} - core-js-compat@3.44.0: + core-js-compat@3.45.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.25.2 - core-js-pure@3.44.0: {} + core-js-pure@3.45.0: {} core-util-is@1.0.3: {} @@ -11114,14 +11101,14 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@9.0.0(typescript@5.8.3): + cosmiconfig@9.0.0(typescript@5.9.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 create-ecdh@4.0.4: dependencies: @@ -11132,8 +11119,8 @@ snapshots: dependencies: cipher-base: 1.0.6 inherits: 2.0.4 - ripemd160: 2.0.2 - sha.js: 2.4.11 + ripemd160: 2.0.1 + sha.js: 2.4.12 create-hash@1.2.0: dependencies: @@ -11141,16 +11128,16 @@ snapshots: inherits: 2.0.4 md5.js: 1.3.5 ripemd160: 2.0.2 - sha.js: 2.4.11 + sha.js: 2.4.12 create-hmac@1.1.7: dependencies: cipher-base: 1.0.6 - create-hash: 1.2.0 + create-hash: 1.1.3 inherits: 2.0.4 - ripemd160: 2.0.2 + ripemd160: 2.0.1 safe-buffer: 5.2.1 - sha.js: 2.4.11 + sha.js: 2.4.12 cross-env@7.0.3: dependencies: @@ -11179,7 +11166,7 @@ snapshots: css-color-keywords@1.0.0: {} - css-loader@6.11.0(webpack@5.99.9(esbuild@0.25.6)): + css-loader@6.11.0(webpack@5.101.1(esbuild@0.25.9)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -11190,7 +11177,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) css-select@4.3.0: dependencies: @@ -11390,7 +11377,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 csstype: 3.1.3 dom-serializer@1.4.1: @@ -11418,9 +11405,9 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 - dotenv@16.5.0: {} + dotenv@16.6.1: {} - dotenv@17.2.0: {} + dotenv@17.2.1: {} dunder-proto@1.0.1: dependencies: @@ -11430,7 +11417,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.180: {} + electron-to-chromium@1.5.200: {} elliptic@6.6.1: dependencies: @@ -11466,7 +11453,7 @@ snapshots: fast-json-parse: 1.0.3 objectorarray: 1.0.5 - enhanced-resolve@5.18.2: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 @@ -11606,61 +11593,61 @@ snapshots: es6-promise@3.3.1: {} - esbuild-register@3.6.0(esbuild@0.25.6): + esbuild-register@3.6.0(esbuild@0.25.9): dependencies: debug: 4.4.1 - esbuild: 0.25.6 + esbuild: 0.25.9 transitivePeerDependencies: - supports-color - esbuild@0.25.6: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-next@15.3.5(eslint@8.57.1)(typescript@5.8.3): + eslint-config-next@15.4.6(eslint@8.57.1)(typescript@5.9.2): dependencies: - '@next/eslint-plugin-next': 15.3.5 + '@next/eslint-plugin-next': 15.4.6 '@rushstack/eslint-patch': 1.12.0 - '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.39.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 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.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(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) optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -11683,24 +11670,24 @@ snapshots: is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 - unrs-resolver: 1.11.0 + unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(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.36.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 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.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11711,7 +11698,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.36.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.1(eslint@8.57.1)(typescript@5.9.2))(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 @@ -11723,7 +11710,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.36.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.39.1(eslint@8.57.1)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -11774,11 +11761,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@9.0.16(eslint@8.57.1)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3): + eslint-plugin-storybook@9.1.2(eslint@8.57.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2))(typescript@5.9.2): dependencies: - '@typescript-eslint/utils': 8.36.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.39.1(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 - storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + storybook: 9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2) transitivePeerDependencies: - supports-color - typescript @@ -11864,6 +11851,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} event-target-shim@5.0.1: {} @@ -11931,9 +11922,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.4.6(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 file-entry-cache@6.0.1: dependencies: @@ -11996,7 +11987,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9)): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -12010,15 +12001,15 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.2 tapable: 2.2.2 - typescript: 5.8.3 - webpack: 5.99.9(esbuild@0.25.6) + typescript: 5.9.2 + webpack: 5.101.1(esbuild@0.25.9) forwarded-parse@2.1.2: {} - framer-motion@12.23.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + framer-motion@12.23.12(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - motion-dom: 12.22.0 - motion-utils: 12.19.0 + motion-dom: 12.23.12 + motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.2.2 @@ -12028,16 +12019,16 @@ snapshots: fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 - fs-extra@11.3.0: + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 - fs-monkey@1.0.6: {} + fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -12060,9 +12051,9 @@ snapshots: functions-have-names@1.2.3: {} - geist@1.4.2(next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.4.2(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: - next: 15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) gensync@1.0.0-beta.2: {} @@ -12188,12 +12179,6 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 - hash-base@3.1.0: - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.2 - safe-buffer: 5.2.1 - hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -12219,7 +12204,7 @@ snapshots: space-separated-tokens: 2.0.2 style-to-js: 1.1.17 unist-util-position: 5.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -12255,7 +12240,7 @@ snapshots: html-url-attributes@3.0.1: {} - html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.6)): + html-webpack-plugin@5.6.4(webpack@5.101.1(esbuild@0.25.9)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -12263,7 +12248,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) htmlparser2@6.1.0: dependencies: @@ -12499,10 +12484,6 @@ snapshots: isexe@2.0.0: {} - isows@1.0.7(ws@8.18.3): - dependencies: - ws: 8.18.3 - iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -12522,7 +12503,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.0.14 + '@types/node': 24.2.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -12558,7 +12539,7 @@ snapshots: jsonc-parser@2.2.1: {} - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -12683,7 +12664,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.4: {} + loupe@3.2.0: {} lower-case@2.0.2: dependencies: @@ -12695,7 +12676,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.525.0(react@18.3.1): + lucide-react@0.539.0(react@18.3.1): dependencies: react: 18.3.1 @@ -12705,11 +12686,11 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 magic-string@0.30.8: dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 make-dir@3.1.0: dependencies: @@ -12728,7 +12709,7 @@ snapshots: md5.js@1.3.5: dependencies: - hash-base: 3.1.0 + hash-base: 3.0.5 inherits: 2.0.4 safe-buffer: 5.2.1 @@ -12773,7 +12754,7 @@ snapshots: parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -12825,7 +12806,9 @@ snapshots: memfs@3.5.3: dependencies: - fs-monkey: 1.0.6 + fs-monkey: 1.1.0 + + memoize-one@5.2.1: {} merge-stream@2.0.0: {} @@ -13010,30 +12993,32 @@ snapshots: minipass@7.1.2: {} + mitt@3.0.1: {} + module-details-from-path@1.0.4: {} moment@2.30.1: {} - motion-dom@12.22.0: + motion-dom@12.23.12: dependencies: - motion-utils: 12.19.0 + motion-utils: 12.23.6 - motion-utils@12.19.0: {} + motion-utils@12.23.6: {} ms@2.1.3: {} - msw-storybook-addon@2.0.5(msw@2.10.4(@types/node@24.0.14)(typescript@5.8.3)): + msw-storybook-addon@2.0.5(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)): dependencies: is-node-process: 1.2.0 - msw: 2.10.4(@types/node@24.0.14)(typescript@5.8.3) + msw: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) - msw@2.10.4(@types/node@24.0.14)(typescript@5.8.3): + msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.13(@types/node@24.0.14) - '@mswjs/interceptors': 0.39.2 + '@inquirer/confirm': 5.1.14(@types/node@24.2.1) + '@mswjs/interceptors': 0.39.6 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 @@ -13048,7 +13033,7 @@ snapshots: type-fest: 4.41.0 yargs: 17.7.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - '@types/node' @@ -13064,7 +13049,7 @@ snapshots: nanoid@3.3.11: {} - napi-postinstall@0.3.0: {} + napi-postinstall@0.3.3: {} natural-compare@1.4.0: {} @@ -13075,29 +13060,27 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.3.5(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 15.3.5 - '@swc/counter': 0.1.3 + '@next/env': 15.4.6 '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001727 + caniuse-lite: 1.0.30001735 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.28.0)(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.28.3)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.3.5 - '@next/swc-darwin-x64': 15.3.5 - '@next/swc-linux-arm64-gnu': 15.3.5 - '@next/swc-linux-arm64-musl': 15.3.5 - '@next/swc-linux-x64-gnu': 15.3.5 - '@next/swc-linux-x64-musl': 15.3.5 - '@next/swc-win32-arm64-msvc': 15.3.5 - '@next/swc-win32-x64-msvc': 15.3.5 + '@next/swc-darwin-arm64': 15.4.6 + '@next/swc-darwin-x64': 15.4.6 + '@next/swc-linux-arm64-gnu': 15.4.6 + '@next/swc-linux-arm64-musl': 15.4.6 + '@next/swc-linux-x64-gnu': 15.4.6 + '@next/swc-linux-x64-musl': 15.4.6 + '@next/swc-win32-arm64-msvc': 15.4.6 + '@next/swc-win32-x64-msvc': 15.4.6 '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.54.1 - sharp: 0.34.2 + '@playwright/test': 1.54.2 + sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -13127,7 +13110,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9(esbuild@0.25.6)): + node-polyfill-webpack-plugin@2.0.1(webpack@5.101.1(esbuild@0.25.9)): dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -13154,7 +13137,7 @@ snapshots: url: 0.11.4 util: 0.12.5 vm-browserify: 1.1.2 - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) node-readfiles@0.2.0: dependencies: @@ -13172,6 +13155,13 @@ snapshots: dependencies: boolbase: 1.0.0 + nuqs@2.4.3(next@15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + mitt: 3.0.1 + react: 18.3.1 + optionalDependencies: + next: 15.4.6(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.54.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oas-kit-common@1.0.8: dependencies: fast-safe-stringify: 2.1.1 @@ -13272,11 +13262,11 @@ snapshots: openapi3-ts@4.2.2: dependencies: - yaml: 2.8.0 + yaml: 2.8.1 openapi3-ts@4.4.0: dependencies: - yaml: 2.8.0 + yaml: 2.8.1 optionator@0.9.4: dependencies: @@ -13287,19 +13277,19 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - orval@7.10.0(openapi-types@12.1.3): + orval@7.11.2(openapi-types@12.1.3): dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@orval/angular': 7.10.0(openapi-types@12.1.3) - '@orval/axios': 7.10.0(openapi-types@12.1.3) - '@orval/core': 7.10.0(openapi-types@12.1.3) - '@orval/fetch': 7.10.0(openapi-types@12.1.3) - '@orval/hono': 7.10.0(openapi-types@12.1.3) - '@orval/mcp': 7.10.0(openapi-types@12.1.3) - '@orval/mock': 7.10.0(openapi-types@12.1.3) - '@orval/query': 7.10.0(openapi-types@12.1.3) - '@orval/swr': 7.10.0(openapi-types@12.1.3) - '@orval/zod': 7.10.0(openapi-types@12.1.3) + '@orval/angular': 7.11.2(openapi-types@12.1.3) + '@orval/axios': 7.11.2(openapi-types@12.1.3) + '@orval/core': 7.11.2(openapi-types@12.1.3) + '@orval/fetch': 7.11.2(openapi-types@12.1.3) + '@orval/hono': 7.11.2(openapi-types@12.1.3) + '@orval/mcp': 7.11.2(openapi-types@12.1.3) + '@orval/mock': 7.11.2(openapi-types@12.1.3) + '@orval/query': 7.11.2(openapi-types@12.1.3) + '@orval/swr': 7.11.2(openapi-types@12.1.3) + '@orval/zod': 7.11.2(openapi-types@12.1.3) ajv: 8.17.1 cac: 6.7.14 chalk: 4.1.2 @@ -13307,14 +13297,14 @@ snapshots: enquirer: 2.4.1 execa: 5.1.1 find-up: 5.0.0 - fs-extra: 11.3.0 + fs-extra: 11.3.1 lodash.uniq: 4.5.0 openapi3-ts: 4.2.2 string-argv: 0.3.2 - tsconfck: 2.1.2(typescript@5.8.3) - typedoc: 0.28.5(typescript@5.8.3) - typedoc-plugin-markdown: 4.6.4(typedoc@0.28.5(typescript@5.8.3)) - typescript: 5.8.3 + tsconfck: 2.1.2(typescript@5.9.2) + typedoc: 0.28.10(typescript@5.9.2) + typedoc-plugin-markdown: 4.8.1(typedoc@0.28.10(typescript@5.9.2)) + typescript: 5.9.2 transitivePeerDependencies: - encoding - openapi-types @@ -13431,7 +13421,7 @@ snapshots: create-hmac: 1.1.7 ripemd160: 2.0.1 safe-buffer: 5.2.1 - sha.js: 2.4.11 + sha.js: 2.4.12 to-buffer: 1.2.1 pg-int8@1.0.1: {} @@ -13450,7 +13440,7 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} pify@2.3.0: {} @@ -13464,11 +13454,11 @@ snapshots: dependencies: find-up: 6.3.0 - playwright-core@1.54.1: {} + playwright-core@1.54.2: {} - playwright@1.54.1: + playwright@1.54.2: dependencies: - playwright-core: 1.54.1 + playwright-core: 1.54.2 optionalDependencies: fsevents: 2.3.2 @@ -13491,18 +13481,18 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 - yaml: 2.8.0 + yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 - postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.6)): + postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.1(esbuild@0.25.9)): dependencies: - cosmiconfig: 9.0.0(typescript@5.8.3) + cosmiconfig: 9.0.0(typescript@5.9.2) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) transitivePeerDependencies: - typescript @@ -13647,24 +13637,24 @@ snapshots: range-parser@1.2.1: {} - react-day-picker@9.8.0(react@18.3.1): + react-day-picker@9.8.1(react@18.3.1): dependencies: '@date-fns/tz': 1.2.0 date-fns: 4.1.0 date-fns-jalali: 4.1.0-0 react: 18.3.1 - react-docgen-typescript@2.4.0(typescript@5.8.3): + react-docgen-typescript@2.4.0(typescript@5.9.2): dependencies: - typescript: 5.8.3 + typescript: 5.9.2 react-docgen@7.1.1: dependencies: - '@babel/core': 7.28.0 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/core': 7.28.3 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 @@ -13686,7 +13676,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-components: 6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form@7.60.0(react@18.3.1): + react-hook-form@7.62.0(react@18.3.1): dependencies: react: 18.3.1 @@ -13749,12 +13739,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - react-shepherd@6.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + react-shepherd@6.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - shepherd.js: 14.5.0 - typescript: 5.8.3 + shepherd.js: 14.5.1 + typescript: 5.9.2 react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -13774,13 +13764,20 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.3 + memoize-one: 5.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -13981,29 +13978,30 @@ snapshots: hash-base: 3.0.5 inherits: 2.0.4 - rollup@4.35.0: + rollup@4.46.2: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.35.0 - '@rollup/rollup-android-arm64': 4.35.0 - '@rollup/rollup-darwin-arm64': 4.35.0 - '@rollup/rollup-darwin-x64': 4.35.0 - '@rollup/rollup-freebsd-arm64': 4.35.0 - '@rollup/rollup-freebsd-x64': 4.35.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.35.0 - '@rollup/rollup-linux-arm-musleabihf': 4.35.0 - '@rollup/rollup-linux-arm64-gnu': 4.35.0 - '@rollup/rollup-linux-arm64-musl': 4.35.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.35.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.35.0 - '@rollup/rollup-linux-riscv64-gnu': 4.35.0 - '@rollup/rollup-linux-s390x-gnu': 4.35.0 - '@rollup/rollup-linux-x64-gnu': 4.35.0 - '@rollup/rollup-linux-x64-musl': 4.35.0 - '@rollup/rollup-win32-arm64-msvc': 4.35.0 - '@rollup/rollup-win32-ia32-msvc': 4.35.0 - '@rollup/rollup-win32-x64-msvc': 4.35.0 + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 run-parallel@1.2.0: @@ -14039,11 +14037,11 @@ snapshots: safe-stable-stringify@1.1.1: {} - sass-loader@16.0.5(webpack@5.99.9(esbuild@0.25.6)): + sass-loader@16.0.5(webpack@5.101.1(esbuild@0.25.9)): dependencies: neo-async: 2.6.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) scheduler@0.23.2: dependencies: @@ -14094,40 +14092,42 @@ snapshots: setimmediate@1.0.5: {} - sha.js@2.4.11: + sha.js@2.4.12: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + to-buffer: 1.2.1 shallowequal@1.1.0: {} - sharp@0.34.2: + sharp@0.34.3: dependencies: color: 4.2.3 detect-libc: 2.0.4 semver: 7.7.2 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.2 - '@img/sharp-darwin-x64': 0.34.2 - '@img/sharp-libvips-darwin-arm64': 1.1.0 - '@img/sharp-libvips-darwin-x64': 1.1.0 - '@img/sharp-libvips-linux-arm': 1.1.0 - '@img/sharp-libvips-linux-arm64': 1.1.0 - '@img/sharp-libvips-linux-ppc64': 1.1.0 - '@img/sharp-libvips-linux-s390x': 1.1.0 - '@img/sharp-libvips-linux-x64': 1.1.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 - '@img/sharp-libvips-linuxmusl-x64': 1.1.0 - '@img/sharp-linux-arm': 0.34.2 - '@img/sharp-linux-arm64': 0.34.2 - '@img/sharp-linux-s390x': 0.34.2 - '@img/sharp-linux-x64': 0.34.2 - '@img/sharp-linuxmusl-arm64': 0.34.2 - '@img/sharp-linuxmusl-x64': 0.34.2 - '@img/sharp-wasm32': 0.34.2 - '@img/sharp-win32-arm64': 0.34.2 - '@img/sharp-win32-ia32': 0.34.2 - '@img/sharp-win32-x64': 0.34.2 + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 optional: true shebang-command@2.0.0: @@ -14138,9 +14138,9 @@ snapshots: shell-quote@1.8.3: {} - shepherd.js@14.5.0: + shepherd.js@14.5.1: dependencies: - '@floating-ui/dom': 1.7.1 + '@floating-ui/dom': 1.7.3 '@scarf/scarf': 1.4.0 deepmerge-ts: 7.1.5 @@ -14215,7 +14215,7 @@ snapshots: slash@3.0.0: {} - sonner@2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14229,7 +14229,7 @@ snapshots: source-map@0.6.1: {} - source-map@0.7.4: {} + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} @@ -14248,16 +14248,17 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2): + storybook@9.1.2(@testing-library/dom@10.4.1)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(prettier@3.6.2): dependencies: '@storybook/global': 5.0.0 - '@testing-library/jest-dom': 6.6.3 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@testing-library/jest-dom': 6.7.0 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 - esbuild: 0.25.6 - esbuild-register: 3.6.0(esbuild@0.25.6) + esbuild: 0.25.9 + esbuild-register: 3.6.0(esbuild@0.25.9) recast: 0.23.11 semver: 7.7.2 ws: 8.18.3 @@ -14266,8 +14267,10 @@ snapshots: transitivePeerDependencies: - '@testing-library/dom' - bufferutil + - msw - supports-color - utf-8-validate + - vite stream-browserify@3.0.0: dependencies: @@ -14281,8 +14284,6 @@ snapshots: readable-stream: 3.6.2 xtend: 4.0.2 - streamsearch@1.1.0: {} - strict-event-emitter@0.5.1: {} string-argv@0.3.2: {} @@ -14384,9 +14385,9 @@ snapshots: strip-json-comments@3.1.1: {} - style-loader@3.3.4(webpack@5.99.9(esbuild@0.25.6)): + style-loader@3.3.4(webpack@5.101.1(esbuild@0.25.9)): dependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) style-to-js@1.1.17: dependencies: @@ -14410,25 +14411,25 @@ snapshots: stylis: 4.3.2 tslib: 2.6.2 - styled-jsx@5.1.6(@babel/core@7.28.0)(react@18.3.1): + styled-jsx@5.1.6(@babel/core@7.28.3)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 - styled-jsx@5.1.7(@babel/core@7.28.0)(react@18.3.1): + styled-jsx@5.1.7(@babel/core@7.28.3)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.28.3 stylis@4.3.2: {} sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -14497,20 +14498,20 @@ snapshots: tapable@2.2.2: {} - terser-webpack-plugin@5.3.14(esbuild@0.25.6)(webpack@5.99.9(esbuild@0.25.6)): + terser-webpack-plugin@5.3.14(esbuild@0.25.9)(webpack@5.101.1(esbuild@0.25.9)): dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) optionalDependencies: - esbuild: 0.25.6 + esbuild: 0.25.9 terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.10 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -14535,8 +14536,8 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 tinyrainbow@2.0.0: {} @@ -14567,22 +14568,22 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: - typescript: 5.8.3 + typescript: 5.9.2 ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} - tsconfck@2.1.2(typescript@5.8.3): + tsconfck@2.1.2(typescript@5.9.2): optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.2 + enhanced-resolve: 5.18.3 tapable: 2.2.2 tsconfig-paths: 4.2.0 @@ -14654,20 +14655,20 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedoc-plugin-markdown@4.6.4(typedoc@0.28.5(typescript@5.8.3)): + typedoc-plugin-markdown@4.8.1(typedoc@0.28.10(typescript@5.9.2)): dependencies: - typedoc: 0.28.5(typescript@5.8.3) + typedoc: 0.28.10(typescript@5.9.2) - typedoc@0.28.5(typescript@5.8.3): + typedoc@0.28.10(typescript@5.9.2): dependencies: - '@gerrit0/mini-shiki': 3.6.0 + '@gerrit0/mini-shiki': 3.9.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.8.3 - yaml: 2.8.0 + typescript: 5.9.2 + yaml: 2.8.1 - typescript@5.8.3: {} + typescript@5.9.2: {} uc.micro@2.1.0: {} @@ -14678,7 +14679,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.8.0: {} + undici-types@7.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -14742,33 +14743,33 @@ snapshots: acorn: 8.15.0 webpack-virtual-modules: 0.6.2 - unrs-resolver@1.11.0: + unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.0 + napi-postinstall: 0.3.3 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.0 - '@unrs/resolver-binding-android-arm64': 1.11.0 - '@unrs/resolver-binding-darwin-arm64': 1.11.0 - '@unrs/resolver-binding-darwin-x64': 1.11.0 - '@unrs/resolver-binding-freebsd-x64': 1.11.0 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.0 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.0 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.0 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.0 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.0 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.0 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.0 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.0 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.0 - '@unrs/resolver-binding-linux-x64-musl': 1.11.0 - '@unrs/resolver-binding-wasm32-wasi': 1.11.0 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.0 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.0 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.0 - - update-browserslist-db@1.1.3(browserslist@4.25.1): - dependencies: - browserslist: 4.25.1 + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -14831,14 +14832,14 @@ snapshots: vaul@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@types/react-dom' - vfile-message@4.0.2: + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 @@ -14846,7 +14847,7 @@ snapshots: vfile@6.0.3: dependencies: '@types/unist': 3.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 victory-vendor@36.9.2: dependencies: @@ -14878,7 +14879,7 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-dev-middleware@6.1.3(webpack@5.99.9(esbuild@0.25.6)): + webpack-dev-middleware@6.1.3(webpack@5.101.1(esbuild@0.25.9)): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -14886,7 +14887,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.6) + webpack: 5.101.1(esbuild@0.25.9) webpack-hot-middleware@2.26.1: dependencies: @@ -14900,7 +14901,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.99.9(esbuild@0.25.6): + webpack@5.101.1(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -14909,9 +14910,10 @@ snapshots: '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 - browserslist: 4.25.1 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.2 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.2 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -14923,7 +14925,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(esbuild@0.25.6)(webpack@5.99.9(esbuild@0.25.6)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.9)(webpack@5.101.1(esbuild@0.25.9)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -15015,7 +15017,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.8.1: {} yargs-parser@21.1.1: {} diff --git a/autogpt_platform/frontend/scripts/generate-api-queries.ts b/autogpt_platform/frontend/scripts/generate-api-queries.ts new file mode 100644 index 000000000000..b9e8ec25bd39 --- /dev/null +++ b/autogpt_platform/frontend/scripts/generate-api-queries.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +import { getAgptServerBaseUrl } from "@/lib/env-config"; +import { execSync } from "child_process"; +import * as path from "path"; +import * as fs from "fs"; + +function fetchOpenApiSpec(): void { + const args = process.argv.slice(2); + const forceFlag = args.includes("--force"); + + const baseUrl = getAgptServerBaseUrl(); + const openApiUrl = `${baseUrl}/openapi.json`; + const outputPath = path.join( + __dirname, + "..", + "src", + "app", + "api", + "openapi.json", + ); + + console.log(`Output path: ${outputPath}`); + console.log(`Force flag: ${forceFlag}`); + + // Check if local file exists + const localFileExists = fs.existsSync(outputPath); + + if (!forceFlag && localFileExists) { + console.log("✅ Using existing local OpenAPI spec file"); + console.log("💡 Use --force flag to fetch from server"); + return; + } + + if (!localFileExists) { + console.log("📄 No local OpenAPI spec found, fetching from server..."); + } else { + console.log( + "🔄 Force flag detected, fetching fresh OpenAPI spec from server...", + ); + } + + console.log(`Fetching OpenAPI spec from: ${openApiUrl}`); + + try { + // Fetch the OpenAPI spec + execSync(`curl "${openApiUrl}" > "${outputPath}"`, { stdio: "inherit" }); + + // Format with prettier + execSync(`prettier --write "${outputPath}"`, { stdio: "inherit" }); + + console.log("✅ OpenAPI spec fetched and formatted successfully"); + } catch (error) { + console.error("❌ Failed to fetch OpenAPI spec:", error); + process.exit(1); + } +} + +if (require.main === module) { + fetchOpenApiSpec(); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts b/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts index ea4a194acd44..f6d8ad40db1c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts @@ -14,12 +14,7 @@ export async function addDollars(formData: FormData) { comments: formData.get("comments") as string, }; const api = new BackendApi(); - const resp = await api.addUserCredits( - data.user_id, - data.amount, - data.comments, - ); - console.log(resp); + await api.addUserCredits(data.user_id, data.amount, data.comments); revalidatePath("/admin/spending"); } diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/spending/page.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/spending/page.tsx index ddb60f3f018a..2b741102d10e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/spending/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/spending/page.tsx @@ -29,6 +29,7 @@ function SpendingDashboard({ Loading submissions... } diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/auth-code-error/page.tsx b/autogpt_platform/frontend/src/app/(platform)/auth/auth-code-error/page.tsx index 797cad062846..d87cac27228a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/auth-code-error/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/auth/auth-code-error/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { isServerSide } from "@/lib/utils/is-server-side"; import { useEffect, useState } from "react"; export default function AuthErrorPage() { @@ -9,7 +10,7 @@ export default function AuthErrorPage() { useEffect(() => { // This code only runs on the client side - if (typeof window !== "undefined") { + if (!isServerSide()) { const hash = window.location.hash.substring(1); // Remove the leading '#' const params = new URLSearchParams(hash); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/AiBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/AiBlock.tsx new file mode 100644 index 000000000000..530adac5bc56 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/AiBlock.tsx @@ -0,0 +1,63 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Plus } from "lucide-react"; +import { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + title?: string; + description?: string; + ai_name?: string; +} + +export const AiBlock: React.FC = ({ + title, + description, + className, + ai_name, + ...rest +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Block.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Block.tsx new file mode 100644 index 000000000000..c05efc6dae31 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Block.tsx @@ -0,0 +1,77 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { beautifyString, cn } from "@/lib/utils"; +import { Plus } from "lucide-react"; +import React, { ButtonHTMLAttributes } from "react";import { highlightText } from "./helpers"; +; + +interface Props extends ButtonHTMLAttributes { + title?: string; + description?: string; + highlightedText?: string; +} + +interface BlockComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + +export const Block: BlockComponent = ({ + title, + description, + highlightedText, + className, + ...rest +}) => { + return ( + + ); +}; + +const BlockSkeleton = () => { + return ( + +
+ + +
+ + + ); +}; + +Block.Skeleton = BlockSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/BlockMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/BlockMenu.tsx new file mode 100644 index 000000000000..338a1de7835b --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/BlockMenu.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ToyBrick } from "lucide-react"; +import { BlockMenuContent } from "../BlockMenuContent/BlockMenuContent"; +import { ControlPanelButton } from "../ControlPanelButton"; +import { useBlockMenu } from "./useBlockMenu"; + +interface BlockMenuProps { + pinBlocksPopover: boolean; + blockMenuSelected: "save" | "block" | ""; + setBlockMenuSelected: React.Dispatch< + React.SetStateAction<"" | "save" | "block"> + >; +} + +export const BlockMenu: React.FC = ({ + pinBlocksPopover, + blockMenuSelected, + setBlockMenuSelected, +}) => { + const {open, onOpen} = useBlockMenu({pinBlocksPopover, setBlockMenuSelected}); + return ( + + + + {/* Need to find phosphor icon alternative for this lucide icon */} + + + + + + + + + ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/useBlockMenu.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/useBlockMenu.ts new file mode 100644 index 000000000000..0c67743c3303 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenu/useBlockMenu.ts @@ -0,0 +1,23 @@ +import { useState } from "react"; + +interface useBlockMenuProps { + pinBlocksPopover: boolean; + setBlockMenuSelected: React.Dispatch< + React.SetStateAction<"" | "save" | "block"> + >; +} + +export const useBlockMenu = ({pinBlocksPopover, setBlockMenuSelected}: useBlockMenuProps) => { + const [open, setOpen] = useState(false); + const onOpen = (newOpen: boolean) => { + if (!pinBlocksPopover) { + setOpen(newOpen); + setBlockMenuSelected(newOpen ? "block" : ""); + } + }; + + return { + open, + onOpen, + }; +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenuContent/BlockMenuContent.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenuContent/BlockMenuContent.tsx new file mode 100644 index 000000000000..88483a27ffff --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/BlockMenuContent/BlockMenuContent.tsx @@ -0,0 +1,10 @@ +"use client"; +import React from "react"; + +export const BlockMenuContent = () => { + return ( +
+ This is the block menu content +
+ ); +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/ControlPanelButton.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/ControlPanelButton.tsx new file mode 100644 index 000000000000..523bad014230 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/ControlPanelButton.tsx @@ -0,0 +1,35 @@ +// BLOCK MENU TODO: We need a disable state in this, currently it's not in design. + +import { cn } from "@/lib/utils"; +import React from "react"; + +interface Props extends React.HTMLAttributes { + selected?: boolean; + children?: React.ReactNode; // For icon purpose + disabled?: boolean; +} + +export const ControlPanelButton: React.FC = ({ + selected = false, + children, + disabled, + className, + ...rest +}) => { + return ( + // Using div instead of button, because it's only for design purposes. We are using this to give design to PopoverTrigger. +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/FilterChip.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/FilterChip.tsx new file mode 100644 index 000000000000..9099ec9c94c6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/FilterChip.tsx @@ -0,0 +1,54 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { X } from "lucide-react"; +import React, { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + selected?: boolean; + number?: number; + name?: string; +} + +export const FilterChip: React.FC = ({ + selected = false, + number, + name, + className, + ...rest +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Integration.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Integration.tsx new file mode 100644 index 000000000000..0ace646ecfe6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/Integration.tsx @@ -0,0 +1,88 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { beautifyString, cn } from "@/lib/utils"; +import Image from "next/image"; +import React, { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + title?: string; + description?: string; + icon_url?: string; + number_of_blocks?: number; +} + +interface IntegrationComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + +export const Integration: IntegrationComponent = ({ + title, + icon_url, + description, + className, + number_of_blocks, + ...rest +}) => { + return ( + + ); +}; + +const IntegrationSkeleton: React.FC<{ className?: string }> = ({ + className, +}) => { + return ( + + +
+
+ + +
+ +
+
+ ); +}; + +Integration.Skeleton = IntegrationSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntegrationChip.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntegrationChip.tsx new file mode 100644 index 000000000000..2a1caeb56b35 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntegrationChip.tsx @@ -0,0 +1,60 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { beautifyString, cn } from "@/lib/utils"; +import Image from "next/image"; +import React, { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + name?: string; + icon_url?: string; +} + +interface IntegrationChipComponent extends React.FC { + Skeleton: React.FC; +} + +export const IntegrationChip: IntegrationChipComponent = ({ + icon_url, + name, + className, + ...rest +}) => { + return ( + + ); +}; + +const IntegrationChipSkeleton: React.FC = () => { + return ( + + + + + ); +}; + +IntegrationChip.Skeleton = IntegrationChipSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntergrationBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntergrationBlock.tsx new file mode 100644 index 000000000000..9b38bf2a3552 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/IntergrationBlock.tsx @@ -0,0 +1,99 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { beautifyString, cn } from "@/lib/utils"; +import { Plus } from "lucide-react"; +import Image from "next/image"; +import React, { ButtonHTMLAttributes } from "react"; +import { highlightText } from "./helpers"; + +interface Props extends ButtonHTMLAttributes { + title?: string; + description?: string; + icon_url?: string; + highlightedText?: string; +} + +interface IntegrationBlockComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + + + +export const IntegrationBlock: IntegrationBlockComponent = ({ + title, + icon_url, + description, + className, + highlightedText, + ...rest +}) => { + return ( + + ); +}; + +const IntegrationBlockSkeleton = ({ className }: { className?: string }) => { + return ( + + +
+ + +
+ + + ); +}; + +IntegrationBlock.Skeleton = IntegrationBlockSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MarketplaceAgentBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MarketplaceAgentBlock.tsx new file mode 100644 index 000000000000..3358a8b08dd5 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MarketplaceAgentBlock.tsx @@ -0,0 +1,135 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { ExternalLink, Loader2, Plus } from "lucide-react"; +import Image from "next/image"; +import React, { ButtonHTMLAttributes } from "react"; +import Link from "next/link"; +import { highlightText } from "./helpers"; + +interface Props extends ButtonHTMLAttributes { + title?: string; + creator_name?: string; + number_of_runs?: number; + image_url?: string; + highlightedText?: string; + slug: string; + loading: boolean; +} + +interface MarketplaceAgentBlockComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + +export const MarketplaceAgentBlock: MarketplaceAgentBlockComponent = ({ + title, + image_url, + creator_name, + number_of_runs, + className, + loading, + highlightedText, + slug, + ...rest +}) => { + return ( + + ); +}; + +const MarketplaceAgentBlockSkeleton: React.FC<{ className?: string }> = ({ + className, +}) => { + return ( + + +
+ +
+ + + +
+
+ + + ); +}; + +MarketplaceAgentBlock.Skeleton = MarketplaceAgentBlockSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MenuItem.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MenuItem.tsx new file mode 100644 index 000000000000..b52f7296f3d2 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/MenuItem.tsx @@ -0,0 +1,40 @@ +// BLOCK MENU TODO: We need to add a better hover state to it; currently it's not in the design either. + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import React, { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + selected?: boolean; + number?: number; + name?: string; +} + +export const MenuItem: React.FC = ({ + selected = false, + number, + name, + className, + ...rest +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/NewControlPanel.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/NewControlPanel.tsx new file mode 100644 index 000000000000..bd969033acd2 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/NewControlPanel.tsx @@ -0,0 +1,110 @@ +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import React, { useMemo } from "react"; +import { BlockMenu } from "../BlockMenu/BlockMenu"; +import { useNewControlPanel } from "./useNewControlPanel"; +import { NewSaveControl } from "../SaveControl/NewSaveControl"; +import { GraphExecutionID } from "@/lib/autogpt-server-api"; +import { history } from "@/components/history"; +import { ControlPanelButton } from "../ControlPanelButton"; +import { ArrowUUpLeftIcon, ArrowUUpRightIcon } from "@phosphor-icons/react"; + +export type Control = { + icon: React.ReactNode; + label: string; + disabled?: boolean; + onClick: () => void; +}; + +interface ControlPanelProps { + className?: string; + flowExecutionID: GraphExecutionID | undefined; + visualizeBeads: "no" | "static" | "animate"; + pinSavePopover: boolean; + pinBlocksPopover: boolean; +} + +export const NewControlPanel = ({ + flowExecutionID, + visualizeBeads, + pinSavePopover, + pinBlocksPopover, + className, +}: ControlPanelProps) => { + const { + blockMenuSelected, + setBlockMenuSelected, + agentDescription, + setAgentDescription, + saveAgent, + agentName, + setAgentName, + savedAgent, + isSaving, + isRunning, + isStopping, + } = useNewControlPanel({ flowExecutionID, visualizeBeads }); + + const controls: Control[] = useMemo( + () => [ + { + label: "Undo", + icon: , + onClick: history.undo, + disabled: !history.canUndo(), + }, + { + label: "Redo", + icon: , + onClick: history.redo, + disabled: !history.canRedo(), + }, + ], + [] + ); + + return ( +
+
+ + + {controls.map((control, index) => ( + control.onClick()} + data-id={`control-button-${index}`} + data-testid={`blocks-control-${control.label.toLowerCase()}-button`} + disabled={control.disabled || false} + className="rounded-none" + > + {control.icon} + + ))} + + +
+
+ ); +}; + +export default NewControlPanel; diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/useNewControlPanel.ts b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/useNewControlPanel.ts new file mode 100644 index 000000000000..79ecb44237fe --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NewControlPanel/useNewControlPanel.ts @@ -0,0 +1,35 @@ +import useAgentGraph from "@/hooks/useAgentGraph"; +import { GraphExecutionID, GraphID } from "@/lib/autogpt-server-api"; +import { useSearchParams } from "next/navigation"; +import { useState } from "react"; + +export interface NewControlPanelProps { + flowExecutionID: GraphExecutionID | undefined; + visualizeBeads: "no" | "static" | "animate"; +} + +export const useNewControlPanel = ({flowExecutionID, visualizeBeads}: NewControlPanelProps) => { + const [blockMenuSelected, setBlockMenuSelected] = useState< + "save" | "block" | "" + >(""); + const query = useSearchParams(); + const _graphVersion = query.get("flowVersion"); + const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined; + + const flowID = query.get("flowID") as GraphID | null ?? undefined; + const {agentDescription, setAgentDescription, saveAgent, agentName, setAgentName, savedAgent, isSaving, isRunning, isStopping} = useAgentGraph(flowID, graphVersion, flowExecutionID, visualizeBeads !== "no") + + return { + blockMenuSelected, + setBlockMenuSelected, + agentDescription, + setAgentDescription, + saveAgent, + agentName, + setAgentName, + savedAgent, + isSaving, + isRunning, + isStopping, + } +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NoSearchResult.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NoSearchResult.tsx new file mode 100644 index 000000000000..a6e14a9b85ba --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/NoSearchResult.tsx @@ -0,0 +1,17 @@ +import { SmileySadIcon } from "@phosphor-icons/react"; + +export const NoSearchResult = () => { + return ( +
+ +
+

+ No match found +

+

+ Try adjusting your search terms +

+
+
+ ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SaveControl/NewSaveControl.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SaveControl/NewSaveControl.tsx new file mode 100644 index 000000000000..5801b0f76dd2 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SaveControl/NewSaveControl.tsx @@ -0,0 +1,158 @@ +import React, { useCallback, useEffect } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { GraphMeta } from "@/lib/autogpt-server-api"; +import { Label } from "@/components/ui/label"; +import { IconSave } from "@/components/ui/icons"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { ControlPanelButton } from "../ControlPanelButton"; + +interface SaveControlProps { + agentMeta: GraphMeta | null; + agentName: string; + agentDescription: string; + canSave: boolean; + onSave: () => void; + onNameChange: (name: string) => void; + onDescriptionChange: (description: string) => void; + pinSavePopover: boolean; + + blockMenuSelected: "save" | "block" | ""; + setBlockMenuSelected: React.Dispatch< + React.SetStateAction<"" | "save" | "block"> + >; +} + +export const NewSaveControl = ({ + agentMeta, + canSave, + onSave, + agentName, + onNameChange, + agentDescription, + onDescriptionChange, + blockMenuSelected, + setBlockMenuSelected, + pinSavePopover, +}: SaveControlProps) => { + + const handleSave = useCallback(() => { + onSave(); + }, [onSave]); + + const { toast } = useToast(); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key === "s") { + event.preventDefault(); + handleSave(); + toast({ + duration: 2000, + title: "All changes saved successfully!", + }); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handleSave, toast]); + + return ( + open || setBlockMenuSelected("")} + > + + { + setBlockMenuSelected("save"); + }} + className="rounded-none" + > + {/* Need to find phosphor icon alternative for this lucide icon */} + + + + + + + +
+ + onNameChange(e.target.value)} + data-id="save-control-name-input" + data-testid="save-control-name-input" + maxLength={100} + /> + + onDescriptionChange(e.target.value)} + data-id="save-control-description-input" + data-testid="save-control-description-input" + maxLength={500} + /> + {agentMeta?.version && ( + <> + + + + )} +
+
+ + + +
+
+
+ ); +}; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SearchHistoryChip.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SearchHistoryChip.tsx new file mode 100644 index 000000000000..8ad488874888 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/SearchHistoryChip.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { ArrowUpRight } from "lucide-react"; +import React, { ButtonHTMLAttributes } from "react"; + +interface Props extends ButtonHTMLAttributes { + content?: string; +} + +interface SearchHistoryChipComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + +export const SearchHistoryChip: SearchHistoryChipComponent = ({ + content, + className, + ...rest +}) => { + return ( + + ); +}; + +const SearchHistoryChipSkeleton: React.FC<{ className?: string }> = ({ + className, +}) => { + return ( + + ); +}; + +SearchHistoryChip.Skeleton = SearchHistoryChipSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/UGCAgentBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/UGCAgentBlock.tsx new file mode 100644 index 000000000000..2dd3296a11f1 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/UGCAgentBlock.tsx @@ -0,0 +1,117 @@ +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { Plus } from "lucide-react"; +import Image from "next/image"; +import React, { ButtonHTMLAttributes } from "react"; +import { highlightText } from "./helpers"; +import { formatTimeAgo } from "@/lib/utils/time"; + +interface Props extends ButtonHTMLAttributes { + title?: string; + edited_time?: Date; + version?: number; + image_url?: string; + highlightedText?: string; +} + +interface UGCAgentBlockComponent extends React.FC { + Skeleton: React.FC<{ className?: string }>; +} + +export const UGCAgentBlock: UGCAgentBlockComponent = ({ + title, + image_url, + edited_time = new Date(), + version, + className, + highlightedText, + ...rest +}) => { + return ( + + ); +}; + +const UGCAgentBlockSkeleton: React.FC<{ className?: string }> = ({ + className, +}) => { + return ( + + +
+ +
+ + +
+
+ + + ); +}; + +UGCAgentBlock.Skeleton = UGCAgentBlockSkeleton; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/helpers.tsx new file mode 100644 index 000000000000..8643b4650ad3 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewBlockMenu/helpers.tsx @@ -0,0 +1,22 @@ +export const highlightText = ( + text: string | undefined, + highlight: string | undefined, + ) => { + if (!text || !highlight) return text; + + function escapeRegExp(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + const escaped = escapeRegExp(highlight); + const parts = text.split(new RegExp(`(${escaped})`, "gi")); + return parts.map((part, i) => + part.toLowerCase() === highlight?.toLowerCase() ? ( + + {part} + + ) : ( + part + ), + ); + }; \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/AgentRunsView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/AgentRunsView.tsx new file mode 100644 index 000000000000..499423397a1e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/AgentRunsView.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs"; +import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; +import { useAgentRunsView } from "./useAgentRunsView"; +import { AgentRunsLoading } from "./components/AgentRunsLoading"; +import { Button } from "@/components/atoms/Button/Button"; +import { Plus } from "@phosphor-icons/react"; + +export function AgentRunsView() { + const { response, ready, error, agentId } = useAgentRunsView(); + + // Handle loading state + if (!ready) { + return ; + } + + // Handle errors - check for query error first, then response errors + if (error || (response && response.status !== 200)) { + return ( + window.location.reload()} + /> + ); + } + + // Handle missing data + if (!response?.data) { + return ( + window.location.reload()} + /> + ); + } + + const agent = response.data; + + return ( +
+ {/* Left Sidebar - 30% */} +
+ +
+ + {/* Main Content - 70% */} +
+ + {/* Main content will go here */} +
Main content area
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/AgentRunsLoading.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/AgentRunsLoading.tsx new file mode 100644 index 000000000000..38e5cf16ef8e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/AgentRunsLoading.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function AgentRunsLoading() { + return ( +
+
+ {/* Left Sidebar */} +
+ + + +
+ + {/* Main Content */} +
+ + + + +
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/useAgentRunsView.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/useAgentRunsView.ts new file mode 100644 index 000000000000..24ae3a38328c --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/useAgentRunsView.ts @@ -0,0 +1,15 @@ +import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library"; +import { useParams } from "next/navigation"; + +export function useAgentRunsView() { + const { id } = useParams(); + const agentId = id as string; + const { data: response, isSuccess, error } = useGetV2GetLibraryAgent(agentId); + + return { + agentId: id, + ready: isSuccess, + error, + response, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx new file mode 100644 index 000000000000..7d13ea7546c5 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/OldAgentLibraryView.tsx @@ -0,0 +1,616 @@ +"use client"; +import { useParams, useRouter } from "next/navigation"; +import { useQueryState } from "nuqs"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { + Graph, + GraphExecution, + GraphExecutionID, + GraphExecutionMeta, + GraphID, + LibraryAgent, + LibraryAgentID, + LibraryAgentPreset, + LibraryAgentPresetID, + Schedule, + ScheduleID, +} from "@/lib/autogpt-server-api"; +import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import { exportAsJSONFile } from "@/lib/utils"; + +import DeleteConfirmDialog from "@/components/agptui/delete-confirm-dialog"; +import type { ButtonAction } from "@/components/agptui/types"; +import { useOnboarding } from "@/components/onboarding/onboarding-provider"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import LoadingBox, { LoadingSpinner } from "@/components/ui/loading"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { AgentRunDetailsView } from "./components/agent-run-details-view"; +import { AgentRunDraftView } from "./components/agent-run-draft-view"; +import { useAgentRunsInfinite } from "../use-agent-runs"; +import { AgentRunsSelectorList } from "./components/agent-runs-selector-list"; +import { AgentScheduleDetailsView } from "./components/agent-schedule-details-view"; + +export function OldAgentLibraryView() { + const { id: agentID }: { id: LibraryAgentID } = useParams(); + const [executionId, setExecutionId] = useQueryState("executionId"); + const { toast } = useToast(); + const router = useRouter(); + const api = useBackendAPI(); + + // ============================ STATE ============================= + + const [graph, setGraph] = useState(null); // Graph version corresponding to LibraryAgent + const [agent, setAgent] = useState(null); + const agentRunsQuery = useAgentRunsInfinite(graph?.id); // only runs once graph.id is known + const agentRuns = agentRunsQuery.agentRuns; + const [agentPresets, setAgentPresets] = useState([]); + const [schedules, setSchedules] = useState([]); + const [selectedView, selectView] = useState< + | { type: "run"; id?: GraphExecutionID } + | { type: "preset"; id: LibraryAgentPresetID } + | { type: "schedule"; id: ScheduleID } + >({ type: "run" }); + const [selectedRun, setSelectedRun] = useState< + GraphExecution | GraphExecutionMeta | null + >(null); + const selectedSchedule = + selectedView.type == "schedule" + ? schedules.find((s) => s.id == selectedView.id) + : null; + const [isFirstLoad, setIsFirstLoad] = useState(true); + const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] = + useState(false); + const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] = + useState(null); + const [confirmingDeleteAgentPreset, setConfirmingDeleteAgentPreset] = + useState(null); + const { + state: onboardingState, + updateState: updateOnboardingState, + incrementRuns, + } = useOnboarding(); + const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false); + + // Set page title with agent name + useEffect(() => { + if (agent) { + document.title = `${agent.name} - Library - AutoGPT Platform`; + } + }, [agent]); + + const openRunDraftView = useCallback(() => { + selectView({ type: "run" }); + }, []); + + const selectRun = useCallback((id: GraphExecutionID) => { + selectView({ type: "run", id }); + }, []); + + const selectPreset = useCallback((id: LibraryAgentPresetID) => { + selectView({ type: "preset", id }); + }, []); + + const selectSchedule = useCallback((id: ScheduleID) => { + selectView({ type: "schedule", id }); + }, []); + + const graphVersions = useRef>({}); + const loadingGraphVersions = useRef>>({}); + const getGraphVersion = useCallback( + async (graphID: GraphID, version: number) => { + if (version in graphVersions.current) + return graphVersions.current[version]; + if (version in loadingGraphVersions.current) + return loadingGraphVersions.current[version]; + + const pendingGraph = api.getGraph(graphID, version).then((graph) => { + graphVersions.current[version] = graph; + return graph; + }); + // Cache promise as well to avoid duplicate requests + loadingGraphVersions.current[version] = pendingGraph; + return pendingGraph; + }, + [api, graphVersions, loadingGraphVersions], + ); + + // Reward user for viewing results of their onboarding agent + useEffect(() => { + if ( + !onboardingState || + !selectedRun || + onboardingState.completedSteps.includes("GET_RESULTS") + ) + return; + + if (selectedRun.id === onboardingState.onboardingAgentExecutionId) { + updateOnboardingState({ + completedSteps: [...onboardingState.completedSteps, "GET_RESULTS"], + }); + } + }, [selectedRun, onboardingState, updateOnboardingState]); + + const lastRefresh = useRef(0); + const refreshPageData = useCallback(() => { + if (Date.now() - lastRefresh.current < 2e3) return; // 2 second debounce + lastRefresh.current = Date.now(); + + api.getLibraryAgent(agentID).then((agent) => { + setAgent(agent); + + getGraphVersion(agent.graph_id, agent.graph_version).then( + (_graph) => + (graph && graph.version == _graph.version) || setGraph(_graph), + ); + Promise.all([ + agentRunsQuery.refetchRuns(), + api.listLibraryAgentPresets({ + graph_id: agent.graph_id, + page_size: 100, + }), + ]).then(([runsQueryResult, presets]) => { + setAgentPresets(presets.presets); + + const newestAgentRunsResponse = runsQueryResult.data?.pages[0]; + if (!newestAgentRunsResponse || newestAgentRunsResponse.status != 200) + return; + const newestAgentRuns = newestAgentRunsResponse.data.executions; + // Preload the corresponding graph versions for the latest 10 runs + new Set( + newestAgentRuns.slice(0, 10).map((run) => run.graph_version), + ).forEach((version) => getGraphVersion(agent.graph_id, version)); + }); + }); + }, [api, agentID, getGraphVersion, graph]); + + // On first load: select the latest run + useEffect(() => { + // Only for first load or first execution + if (selectedView.id || !isFirstLoad) return; + if (agentRuns.length == 0 && agentPresets.length == 0) return; + + setIsFirstLoad(false); + if (agentRuns.length > 0) { + // select latest run + const latestRun = agentRuns.reduce((latest, current) => { + if (latest.started_at && !current.started_at) return current; + else if (!latest.started_at) return latest; + return latest.started_at > current.started_at ? latest : current; + }, agentRuns[0]); + selectRun(latestRun.id as GraphExecutionID); + } else { + // select top preset + const latestPreset = agentPresets.toSorted( + (a, b) => b.updated_at.getTime() - a.updated_at.getTime(), + )[0]; + selectPreset(latestPreset.id); + } + }, [ + isFirstLoad, + selectedView.id, + agentRuns, + agentPresets, + selectRun, + selectPreset, + ]); + + useEffect(() => { + if (executionId) { + selectRun(executionId as GraphExecutionID); + setExecutionId(null); + } + }, [executionId, selectRun, setExecutionId]); + + // Initial load + useEffect(() => { + refreshPageData(); + + // Show a toast when the WebSocket connection disconnects + let connectionToast: ReturnType | null = null; + const cancelDisconnectHandler = api.onWebSocketDisconnect(() => { + connectionToast ??= toast({ + title: "Connection to server was lost", + variant: "destructive", + description: ( +
+ Trying to reconnect... + +
+ ), + duration: Infinity, + dismissable: true, + }); + }); + const cancelConnectHandler = api.onWebSocketConnect(() => { + if (connectionToast) + connectionToast.update({ + id: connectionToast.id, + title: "✅ Connection re-established", + variant: "default", + description: ( +
+ Refreshing data... + +
+ ), + duration: 2000, + dismissable: true, + }); + connectionToast = null; + }); + return () => { + cancelDisconnectHandler(); + cancelConnectHandler(); + }; + }, []); + + // Subscribe to WebSocket updates for agent runs + useEffect(() => { + if (!agent?.graph_id) return; + + return api.onWebSocketConnect(() => { + refreshPageData(); // Sync up on (re)connect + + // Subscribe to all executions for this agent + api.subscribeToGraphExecutions(agent.graph_id); + }); + }, [api, agent?.graph_id, refreshPageData]); + + // Handle execution updates + useEffect(() => { + const detachExecUpdateHandler = api.onWebSocketMessage( + "graph_execution_event", + (data) => { + if (data.graph_id != agent?.graph_id) return; + + if (data.status == "COMPLETED") { + incrementRuns(); + } + + agentRunsQuery.upsertAgentRun(data); + }, + ); + + return () => { + detachExecUpdateHandler(); + }; + }, [api, agent?.graph_id, selectedView.id, incrementRuns]); + + // Pre-load selectedRun based on selectedView + useEffect(() => { + if (selectedView.type != "run" || !selectedView.id) return; + + const newSelectedRun = agentRuns.find((run) => run.id == selectedView.id); + if (selectedView.id !== selectedRun?.id) { + // Pull partial data from "cache" while waiting for the rest to load + setSelectedRun(newSelectedRun ?? null); + } + }, [api, selectedView, agentRuns, selectedRun?.id]); + + // Load selectedRun based on selectedView; refresh on agent refresh + useEffect(() => { + if (selectedView.type != "run" || !selectedView.id || !agent) return; + + api + .getGraphExecutionInfo(agent.graph_id, selectedView.id) + .then(async (run) => { + // Ensure corresponding graph version is available before rendering I/O + await getGraphVersion(run.graph_id, run.graph_version); + setSelectedRun(run); + }); + }, [api, selectedView, agent, getGraphVersion]); + + const fetchSchedules = useCallback(async () => { + if (!agent) return; + + setSchedules(await api.listGraphExecutionSchedules(agent.graph_id)); + }, [api, agent?.graph_id]); + + useEffect(() => { + fetchSchedules(); + }, [fetchSchedules]); + + // =========================== ACTIONS ============================ + + const deleteRun = useCallback( + async (run: GraphExecutionMeta) => { + if (run.status == "RUNNING" || run.status == "QUEUED") { + await api.stopGraphExecution(run.graph_id, run.id); + } + await api.deleteGraphExecution(run.id); + + setConfirmingDeleteAgentRun(null); + if (selectedView.type == "run" && selectedView.id == run.id) { + openRunDraftView(); + } + agentRunsQuery.removeAgentRun(run.id); + }, + [api, selectedView, openRunDraftView], + ); + + const deletePreset = useCallback( + async (presetID: LibraryAgentPresetID) => { + await api.deleteLibraryAgentPreset(presetID); + + setConfirmingDeleteAgentPreset(null); + if (selectedView.type == "preset" && selectedView.id == presetID) { + openRunDraftView(); + } + setAgentPresets((presets) => presets.filter((p) => p.id !== presetID)); + }, + [api, selectedView, openRunDraftView], + ); + + const deleteSchedule = useCallback( + async (scheduleID: ScheduleID) => { + const removedSchedule = + await api.deleteGraphExecutionSchedule(scheduleID); + + setSchedules((schedules) => { + const newSchedules = schedules.filter( + (s) => s.id !== removedSchedule.id, + ); + if ( + selectedView.type == "schedule" && + selectedView.id == removedSchedule.id + ) { + if (newSchedules.length > 0) { + // Select next schedule if available + selectSchedule(newSchedules[0].id); + } else { + // Reset to draft view if current schedule was deleted + openRunDraftView(); + } + } + return newSchedules; + }); + openRunDraftView(); + }, + [schedules, api], + ); + + const downloadGraph = useCallback( + async () => + agent && + // Export sanitized graph from backend + api + .getGraph(agent.graph_id, agent.graph_version, true) + .then((graph) => + exportAsJSONFile(graph, `${graph.name}_v${graph.version}.json`), + ), + [api, agent], + ); + + const copyAgent = useCallback(async () => { + setCopyAgentDialogOpen(false); + api + .forkLibraryAgent(agentID) + .then((newAgent) => { + router.push(`/library/agents/${newAgent.id}`); + }) + .catch((error) => { + console.error("Error copying agent:", error); + toast({ + title: "Error copying agent", + description: `An error occurred while copying the agent: ${error.message}`, + variant: "destructive", + }); + }); + }, [agentID, api, router, toast]); + + const agentActions: ButtonAction[] = useMemo( + () => [ + { + label: "Customize agent", + href: `/build?flowID=${agent?.graph_id}&flowVersion=${agent?.graph_version}`, + disabled: !agent?.can_access_graph, + }, + { label: "Export agent to file", callback: downloadGraph }, + ...(!agent?.can_access_graph + ? [ + { + label: "Edit a copy", + callback: () => setCopyAgentDialogOpen(true), + }, + ] + : []), + { + label: "Delete agent", + callback: () => setAgentDeleteDialogOpen(true), + }, + ], + [agent, downloadGraph], + ); + + const runGraph = + graphVersions.current[selectedRun?.graph_version ?? 0] ?? graph; + + const onCreateSchedule = useCallback( + (schedule: Schedule) => { + setSchedules((prev) => [...prev, schedule]); + selectSchedule(schedule.id); + }, + [selectView], + ); + + const onCreatePreset = useCallback( + (preset: LibraryAgentPreset) => { + setAgentPresets((prev) => [...prev, preset]); + selectPreset(preset.id); + }, + [selectPreset], + ); + + const onUpdatePreset = useCallback( + (updated: LibraryAgentPreset) => { + setAgentPresets((prev) => + prev.map((p) => (p.id === updated.id ? updated : p)), + ); + selectPreset(updated.id); + }, + [selectPreset], + ); + + if (!agent || !graph) { + return ; + } + + return ( +
+ {/* Sidebar w/ list of runs */} + {/* TODO: render this below header in sm and md layouts */} + + +
+ {/* Header */} +
+

+ { + agent.name /* TODO: use dynamic/custom run title - https://github.com/Significant-Gravitas/AutoGPT/issues/9184 */ + } +

+
+ + {/* Run / Schedule views */} + {(selectedView.type == "run" && selectedView.id ? ( + selectedRun && runGraph ? ( + setConfirmingDeleteAgentRun(selectedRun)} + /> + ) : null + ) : selectedView.type == "run" ? ( + /* Draft new runs / Create new presets */ + + ) : selectedView.type == "preset" ? ( + /* Edit & update presets */ + preset.id == selectedView.id)! + } + onRun={selectRun} + onCreateSchedule={onCreateSchedule} + onUpdatePreset={onUpdatePreset} + doDeletePreset={setConfirmingDeleteAgentPreset} + agentActions={agentActions} + /> + ) : selectedView.type == "schedule" ? ( + selectedSchedule && + graph && ( + + ) + ) : null) || } + + + agent && + api.deleteLibraryAgent(agent.id).then(() => router.push("/library")) + } + /> + + !open && setConfirmingDeleteAgentRun(null)} + onDoDelete={() => + confirmingDeleteAgentRun && deleteRun(confirmingDeleteAgentRun) + } + /> + !open && setConfirmingDeleteAgentPreset(null)} + onDoDelete={() => + confirmingDeleteAgentPreset && + deletePreset(confirmingDeleteAgentPreset) + } + /> + {/* Copy agent confirmation dialog */} + + + + You're making an editable copy + + The original Marketplace agent stays the same and cannot be + edited. We'll save a new version of this agent to your + Library. From there, you can customize it however you'd + like by clicking "Customize agent" — this will open + the builder where you can see and modify the inner workings. + + + + + + + + +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx similarity index 81% rename from autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx rename to autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx index e97dfd3d4ba0..d4b5f98ab4f6 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-run-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx @@ -15,9 +15,19 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import ActionButtonGroup from "@/components/agptui/action-button-group"; import type { ButtonAction } from "@/components/agptui/types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { IconRefresh, IconSquare } from "@/components/ui/icons"; +import { + IconRefresh, + IconSquare, + IconCircleAlert, +} from "@/components/ui/icons"; import { Input } from "@/components/ui/input"; import LoadingBox from "@/components/ui/loading"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { useToastOnFail } from "@/components/molecules/Toast/use-toast"; import { @@ -26,7 +36,7 @@ import { } from "@/components/agents/agent-run-status-chip"; import useCredits from "@/hooks/useCredits"; -export default function AgentRunDetailsView({ +export function AgentRunDetailsView({ agent, graph, run, @@ -180,6 +190,7 @@ export default function AgentRunDetailsView({ ), callback: runAgain, + dataTestId: "run-again-button", }, ] : []), @@ -224,9 +235,49 @@ export default function AgentRunDetailsView({ ))} + {run.status === "FAILED" && ( +
+

+ Error:{" "} + {run.stats?.error || + "The execution failed due to an internal error. You can re-run the agent to retry."} +

+
+ )} + {/* Smart Agent Execution Summary */} + {run.stats?.activity_status && ( + + + + Smart Agent Execution Summary + + + + + + +

+ This is an AI-generated summary and may not be + completely accurate. It provides a conversational + overview of what the agent accomplished during + execution. +

+
+
+
+
+
+ +

+ {run.stats.activity_status} +

+
+
+ )} + {agentRunOutputs !== null && ( diff --git a/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx similarity index 99% rename from autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx rename to autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx index 3c56b0da6562..2f18d1d645ea 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-run-draft-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx @@ -30,7 +30,7 @@ import { useToastOnFail, } from "@/components/molecules/Toast/use-toast"; -export default function AgentRunDraftView({ +export function AgentRunDraftView({ graph, agentPreset, triggerSetupInfo, @@ -509,7 +509,7 @@ export default function AgentRunDraftView({ return (
-
+
Input diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx new file mode 100644 index 000000000000..0645d22496ce --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-runs-selector-list.tsx @@ -0,0 +1,208 @@ +"use client"; +import { Plus } from "lucide-react"; +import React, { useEffect, useState } from "react"; + +import { + GraphExecutionID, + GraphExecutionMeta, + LibraryAgent, + LibraryAgentPreset, + LibraryAgentPresetID, + Schedule, + ScheduleID, +} from "@/lib/autogpt-server-api"; +import { cn } from "@/lib/utils"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/atoms/Button/Button"; +import LoadingBox from "@/components/ui/loading"; +import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; +import { Separator } from "@/components/ui/separator"; + +import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip"; +import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card"; +import { AgentRunsQuery } from "../../use-agent-runs"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface AgentRunsSelectorListProps { + agent: LibraryAgent; + agentRunsQuery: AgentRunsQuery; + agentPresets: LibraryAgentPreset[]; + schedules: Schedule[]; + selectedView: { type: "run" | "preset" | "schedule"; id?: string }; + allowDraftNewRun?: boolean; + onSelectRun: (id: GraphExecutionID) => void; + onSelectPreset: (preset: LibraryAgentPresetID) => void; + onSelectSchedule: (id: ScheduleID) => void; + onSelectDraftNewRun: () => void; + doDeleteRun: (id: GraphExecutionMeta) => void; + doDeletePreset: (id: LibraryAgentPresetID) => void; + doDeleteSchedule: (id: ScheduleID) => void; + className?: string; +} + +export function AgentRunsSelectorList({ + agent, + agentRunsQuery: { + agentRuns, + agentRunsLoading, + hasMoreRuns, + fetchMoreRuns, + isFetchingMoreRuns, + }, + agentPresets, + schedules, + selectedView, + allowDraftNewRun = true, + onSelectRun, + onSelectPreset, + onSelectSchedule, + onSelectDraftNewRun, + doDeleteRun, + doDeletePreset, + doDeleteSchedule, + className, +}: AgentRunsSelectorListProps): React.ReactElement { + const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">( + "runs", + ); + + useEffect(() => { + if (selectedView.type === "schedule") { + setActiveListTab("scheduled"); + } else { + setActiveListTab("runs"); + } + }, [selectedView]); + + const listItemClasses = "h-28 w-72 lg:w-full lg:h-32"; + + return ( + + ); +} diff --git a/autogpt_platform/frontend/src/components/agents/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx similarity index 89% rename from autogpt_platform/frontend/src/components/agents/agent-schedule-details-view.tsx rename to autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx index 152b140d18a8..3ce4e455ada1 100644 --- a/autogpt_platform/frontend/src/components/agents/agent-schedule-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx @@ -18,9 +18,11 @@ import { Input } from "@/components/ui/input"; import LoadingBox from "@/components/ui/loading"; import { useToastOnFail } from "@/components/molecules/Toast/use-toast"; import { humanizeCronExpression } from "@/lib/cron-expression-utils"; +import { formatScheduleTime } from "@/lib/timezone-utils"; +import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth"; import { PlayIcon } from "lucide-react"; -export default function AgentScheduleDetailsView({ +export function AgentScheduleDetailsView({ graph, schedule, agentActions, @@ -39,6 +41,10 @@ export default function AgentScheduleDetailsView({ const toastOnFail = useToastOnFail(); + // Get user's timezone for displaying schedule times + const { data: timezoneData } = useGetV1GetUserTimezone(); + const userTimezone = timezoneData?.data?.timezone || "UTC"; + const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => { return [ { @@ -49,14 +55,14 @@ export default function AgentScheduleDetailsView({ }, { label: "Schedule", - value: humanizeCronExpression(schedule.cron), + value: humanizeCronExpression(schedule.cron, userTimezone), }, { label: "Next run", - value: schedule.next_run_time.toLocaleString(), + value: formatScheduleTime(schedule.next_run_time, userTimezone), }, ]; - }, [schedule, selectedRunStatus]); + }, [schedule, selectedRunStatus, userTimezone]); const agentRunInputs: Record< string, diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/use-agent-runs.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/use-agent-runs.ts new file mode 100644 index 000000000000..506744496f38 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/use-agent-runs.ts @@ -0,0 +1,205 @@ +import { + getV1ListGraphExecutionsResponse, + getV1ListGraphExecutionsResponse200, + useGetV1ListGraphExecutionsInfinite, +} from "@/app/api/__generated__/endpoints/graphs/graphs"; +import { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated"; +import { getQueryClient } from "@/lib/react-query/queryClient"; +import { + GraphExecutionMeta as LegacyGraphExecutionMeta, + GraphID, + GraphExecutionID, +} from "@/lib/autogpt-server-api"; +import { GraphExecutionMeta as RawGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta"; + +export type GraphExecutionMeta = Omit< + RawGraphExecutionMeta, + "id" | "user_id" | "graph_id" | "preset_id" | "stats" +> & + Pick< + LegacyGraphExecutionMeta, + "id" | "user_id" | "graph_id" | "preset_id" | "stats" + >; + +/** Hook to fetch runs for a specific graph, with support for infinite scroll. + * + * @param graphID - The ID of the graph to fetch agent runs for. This parameter is + * optional in the sense that the hook doesn't run unless it is passed. + * This way, it can be used in components where the graph ID is not + * immediately available. + */ +export const useAgentRunsInfinite = (graphID?: GraphID) => { + const queryClient = getQueryClient(); + const { + data: queryResults, + refetch: refetchRuns, + isPending: agentRunsLoading, + isRefetching: agentRunsReloading, + hasNextPage: hasMoreRuns, + fetchNextPage: fetchMoreRuns, + isFetchingNextPage: isFetchingMoreRuns, + queryKey, + } = useGetV1ListGraphExecutionsInfinite( + graphID!, + { page: 1, page_size: 20 }, + { + query: { + getNextPageParam: (lastPage) => { + const pagination = (lastPage.data as GraphExecutionsPaginated) + .pagination; + const hasMore = + pagination.current_page * pagination.page_size < + pagination.total_items; + + return hasMore ? pagination.current_page + 1 : undefined; + }, + + // Prevent query from running if graphID is not available (yet) + ...(!graphID + ? { + enabled: false, + queryFn: () => + // Fake empty response if graphID is not available (yet) + Promise.resolve({ + status: 200, + data: { + executions: [], + pagination: { + current_page: 1, + page_size: 20, + total_items: 0, + total_pages: 0, + }, + }, + headers: new Headers(), + } satisfies getV1ListGraphExecutionsResponse), + } + : {}), + }, + }, + queryClient, + ); + + const agentRuns = + queryResults?.pages.flatMap((page) => { + const response = page.data as GraphExecutionsPaginated; + return response.executions; + }) ?? []; + + const agentRunCount = queryResults?.pages[-1] + ? (queryResults.pages[-1].data as GraphExecutionsPaginated).pagination + .total_items + : 0; + + const upsertAgentRun = (newAgentRun: GraphExecutionMeta) => { + queryClient.setQueryData( + queryKey, + (currentQueryData: typeof queryResults) => { + if (!currentQueryData?.pages) return currentQueryData; + + const exists = currentQueryData.pages.some((page) => { + if (page.status !== 200) return false; + + const response = page.data; + return response.executions.some((run) => run.id === newAgentRun.id); + }); + if (exists) { + // If the run already exists, we update it + return { + ...currentQueryData, + pages: currentQueryData.pages.map((page) => { + if (page.status !== 200) return page; + const response = page.data; + const executions = response.executions; + + const index = executions.findIndex( + (run) => run.id === newAgentRun.id, + ); + if (index === -1) return page; + + const newExecutions = [...executions]; + newExecutions[index] = newAgentRun; + + return { + ...page, + data: { + ...response, + executions: newExecutions, + }, + } satisfies getV1ListGraphExecutionsResponse; + }), + }; + } + + // If the run does not exist, we add it to the first page + const page = currentQueryData + .pages[0] as getV1ListGraphExecutionsResponse200 & { + headers: Headers; + }; + const updatedExecutions = [newAgentRun, ...page.data.executions]; + const updatedPage = { + ...page, + data: { + ...page.data, + executions: updatedExecutions, + }, + } satisfies getV1ListGraphExecutionsResponse; + const updatedPages = [updatedPage, ...currentQueryData.pages.slice(1)]; + return { + ...currentQueryData, + pages: updatedPages, + }; + }, + ); + }; + + const removeAgentRun = (runID: GraphExecutionID) => { + queryClient.setQueryData( + [queryKey, { page: 1, page_size: 20 }], + (currentQueryData: typeof queryResults) => { + if (!currentQueryData?.pages) return currentQueryData; + + let found = false; + return { + ...currentQueryData, + pages: currentQueryData.pages.map((page) => { + const response = page.data as GraphExecutionsPaginated; + const filteredExecutions = response.executions.filter( + (run) => run.id !== runID, + ); + if (filteredExecutions.length < response.executions.length) { + found = true; + } + + return { + ...page, + data: { + ...response, + executions: filteredExecutions, + pagination: { + ...response.pagination, + total_items: + response.pagination.total_items - (found ? 1 : 0), + }, + }, + }; + }), + }; + }, + ); + }; + + return { + agentRuns: agentRuns as GraphExecutionMeta[], + refetchRuns, + agentRunCount, + agentRunsLoading: agentRunsLoading || agentRunsReloading, + hasMoreRuns, + fetchMoreRuns, + isFetchingMoreRuns, + upsertAgentRun, + removeAgentRun, + }; +}; + +export type AgentRunsQuery = ReturnType; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/loading.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/loading.tsx index b0be672e03c2..a2d436066641 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/loading.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/loading.tsx @@ -1,21 +1,3 @@ -import AgentFlowListSkeleton from "@/components/monitor/skeletons/AgentFlowListSkeleton"; -import React from "react"; -import FlowRunsListSkeleton from "@/components/monitor/skeletons/FlowRunsListSkeleton"; -import FlowRunsStatusSkeleton from "@/components/monitor/skeletons/FlowRunsStatusSkeleton"; +import { AgentRunsLoading } from "./components/AgentRunsView/components/AgentRunsLoading"; -export default function MonitorLoadingSkeleton() { - return ( -
-
- {/* Agents Section */} - - - {/* Runs Section */} - - - {/* Stats Section */} - -
-
- ); -} +export default AgentRunsLoading; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx index a4d686edc631..015b6ac63fe9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/page.tsx @@ -1,610 +1,16 @@ "use client"; -import { useParams, useRouter } from "next/navigation"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { - Graph, - GraphExecution, - GraphExecutionID, - GraphExecutionMeta, - GraphID, - LibraryAgent, - LibraryAgentID, - LibraryAgentPreset, - LibraryAgentPresetID, - Schedule, - ScheduleID, -} from "@/lib/autogpt-server-api"; -import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import { exportAsJSONFile } from "@/lib/utils"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import AgentRunDetailsView from "@/components/agents/agent-run-details-view"; -import AgentRunDraftView from "@/components/agents/agent-run-draft-view"; -import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list"; -import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view"; -import DeleteConfirmDialog from "@/components/agptui/delete-confirm-dialog"; -import type { ButtonAction } from "@/components/agptui/types"; -import { useOnboarding } from "@/components/onboarding/onboarding-provider"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import LoadingBox, { LoadingSpinner } from "@/components/ui/loading"; -import { useToast } from "@/components/molecules/Toast/use-toast"; +import { OldAgentLibraryView } from "./components/OldAgentLibraryView/OldAgentLibraryView"; +import { AgentRunsView } from "./components/AgentRunsView/AgentRunsView"; -export default function AgentRunsPage(): React.ReactElement { - const { id: agentID }: { id: LibraryAgentID } = useParams(); - const { toast } = useToast(); - const router = useRouter(); - const api = useBackendAPI(); +export default function AgentLibraryPage() { + const isNewAgentRunsEnabled = useGetFlag(Flag.NEW_AGENT_RUNS); - // ============================ STATE ============================= - - const [graph, setGraph] = useState(null); // Graph version corresponding to LibraryAgent - const [agent, setAgent] = useState(null); - const [agentRuns, setAgentRuns] = useState([]); - const [agentPresets, setAgentPresets] = useState([]); - const [schedules, setSchedules] = useState([]); - const [selectedView, selectView] = useState< - | { type: "run"; id?: GraphExecutionID } - | { type: "preset"; id: LibraryAgentPresetID } - | { type: "schedule"; id: ScheduleID } - >({ type: "run" }); - const [selectedRun, setSelectedRun] = useState< - GraphExecution | GraphExecutionMeta | null - >(null); - const selectedSchedule = - selectedView.type == "schedule" - ? schedules.find((s) => s.id == selectedView.id) - : null; - const [isFirstLoad, setIsFirstLoad] = useState(true); - const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] = - useState(false); - const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] = - useState(null); - const [confirmingDeleteAgentPreset, setConfirmingDeleteAgentPreset] = - useState(null); - const { - state: onboardingState, - updateState: updateOnboardingState, - incrementRuns, - } = useOnboarding(); - const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false); - - // Set page title with agent name - useEffect(() => { - if (agent) { - document.title = `${agent.name} - Library - AutoGPT Platform`; - } - }, [agent]); - - const openRunDraftView = useCallback(() => { - selectView({ type: "run" }); - }, []); - - const selectRun = useCallback((id: GraphExecutionID) => { - selectView({ type: "run", id }); - }, []); - - const selectPreset = useCallback((id: LibraryAgentPresetID) => { - selectView({ type: "preset", id }); - }, []); - - const selectSchedule = useCallback((id: ScheduleID) => { - selectView({ type: "schedule", id }); - }, []); - - const graphVersions = useRef>({}); - const loadingGraphVersions = useRef>>({}); - const getGraphVersion = useCallback( - async (graphID: GraphID, version: number) => { - if (version in graphVersions.current) - return graphVersions.current[version]; - if (version in loadingGraphVersions.current) - return loadingGraphVersions.current[version]; - - const pendingGraph = api.getGraph(graphID, version).then((graph) => { - graphVersions.current[version] = graph; - return graph; - }); - // Cache promise as well to avoid duplicate requests - loadingGraphVersions.current[version] = pendingGraph; - return pendingGraph; - }, - [api, graphVersions, loadingGraphVersions], - ); - - // Reward user for viewing results of their onboarding agent - useEffect(() => { - if ( - !onboardingState || - !selectedRun || - onboardingState.completedSteps.includes("GET_RESULTS") - ) - return; - - if (selectedRun.id === onboardingState.onboardingAgentExecutionId) { - updateOnboardingState({ - completedSteps: [...onboardingState.completedSteps, "GET_RESULTS"], - }); - } - }, [selectedRun, onboardingState, updateOnboardingState]); - - const lastRefresh = useRef(0); - const refreshPageData = useCallback(() => { - if (Date.now() - lastRefresh.current < 2e3) return; // 2 second debounce - lastRefresh.current = Date.now(); - - api.getLibraryAgent(agentID).then((agent) => { - setAgent(agent); - - getGraphVersion(agent.graph_id, agent.graph_version).then( - (_graph) => - (graph && graph.version == _graph.version) || setGraph(_graph), - ); - Promise.all([ - api.getGraphExecutions(agent.graph_id), - api.listLibraryAgentPresets({ - graph_id: agent.graph_id, - page_size: 100, - }), - ]).then(([runs, presets]) => { - setAgentRuns(runs); - setAgentPresets(presets.presets); - - // Preload the corresponding graph versions for the latest 10 runs - new Set(runs.slice(0, 10).map((run) => run.graph_version)).forEach( - (version) => getGraphVersion(agent.graph_id, version), - ); - }); - }); - }, [api, agentID, getGraphVersion, graph]); - - // On first load: select the latest run - useEffect(() => { - // Only for first load or first execution - if (selectedView.id || !isFirstLoad) return; - if (agentRuns.length == 0 && agentPresets.length == 0) return; - - setIsFirstLoad(false); - if (agentRuns.length > 0) { - // select latest run - const latestRun = agentRuns.reduce((latest, current) => { - if (latest.started_at && !current.started_at) return current; - else if (!latest.started_at) return latest; - return latest.started_at > current.started_at ? latest : current; - }, agentRuns[0]); - selectRun(latestRun.id); - } else { - // select top preset - const latestPreset = agentPresets.toSorted( - (a, b) => b.updated_at.getTime() - a.updated_at.getTime(), - )[0]; - selectPreset(latestPreset.id); - } - }, [ - isFirstLoad, - selectedView.id, - agentRuns, - agentPresets, - selectRun, - selectPreset, - ]); - - // Initial load - useEffect(() => { - refreshPageData(); - - // Show a toast when the WebSocket connection disconnects - let connectionToast: ReturnType | null = null; - const cancelDisconnectHandler = api.onWebSocketDisconnect(() => { - connectionToast ??= toast({ - title: "Connection to server was lost", - variant: "destructive", - description: ( -
- Trying to reconnect... - -
- ), - duration: Infinity, // show until connection is re-established - dismissable: false, - }); - }); - const cancelConnectHandler = api.onWebSocketConnect(() => { - if (connectionToast) - connectionToast.update({ - id: connectionToast.id, - title: "✅ Connection re-established", - variant: "default", - description: ( -
- Refreshing data... - -
- ), - duration: 2000, - dismissable: true, - }); - connectionToast = null; - }); - return () => { - cancelDisconnectHandler(); - cancelConnectHandler(); - }; - }, []); - - // Subscribe to WebSocket updates for agent runs - useEffect(() => { - if (!agent?.graph_id) return; - - return api.onWebSocketConnect(() => { - refreshPageData(); // Sync up on (re)connect - - // Subscribe to all executions for this agent - api.subscribeToGraphExecutions(agent.graph_id); - }); - }, [api, agent?.graph_id, refreshPageData]); - - // Handle execution updates - useEffect(() => { - const detachExecUpdateHandler = api.onWebSocketMessage( - "graph_execution_event", - (data) => { - if (data.graph_id != agent?.graph_id) return; - - if (data.status == "COMPLETED") { - incrementRuns(); - } - - setAgentRuns((prev) => { - const index = prev.findIndex((run) => run.id === data.id); - if (index === -1) { - return [...prev, data]; - } - const newRuns = [...prev]; - newRuns[index] = { ...newRuns[index], ...data }; - return newRuns; - }); - if (data.id === selectedView.id) { - setSelectedRun((prev) => ({ ...prev, ...data })); - } - }, - ); - - return () => { - detachExecUpdateHandler(); - }; - }, [api, agent?.graph_id, selectedView.id, incrementRuns]); - - // Pre-load selectedRun based on selectedView - useEffect(() => { - if (selectedView.type != "run" || !selectedView.id) return; - - const newSelectedRun = agentRuns.find((run) => run.id == selectedView.id); - if (selectedView.id !== selectedRun?.id) { - // Pull partial data from "cache" while waiting for the rest to load - setSelectedRun(newSelectedRun ?? null); - } - }, [api, selectedView, agentRuns, selectedRun?.id]); - - // Load selectedRun based on selectedView; refresh on agent refresh - useEffect(() => { - if (selectedView.type != "run" || !selectedView.id || !agent) return; - - api - .getGraphExecutionInfo(agent.graph_id, selectedView.id) - .then(async (run) => { - // Ensure corresponding graph version is available before rendering I/O - await getGraphVersion(run.graph_id, run.graph_version); - setSelectedRun(run); - }); - }, [api, selectedView, agent, getGraphVersion]); - - const fetchSchedules = useCallback(async () => { - if (!agent) return; - - setSchedules(await api.listGraphExecutionSchedules(agent.graph_id)); - }, [api, agent?.graph_id]); - - useEffect(() => { - fetchSchedules(); - }, [fetchSchedules]); - - // =========================== ACTIONS ============================ - - const deleteRun = useCallback( - async (run: GraphExecutionMeta) => { - if (run.status == "RUNNING" || run.status == "QUEUED") { - await api.stopGraphExecution(run.graph_id, run.id); - } - await api.deleteGraphExecution(run.id); - - setConfirmingDeleteAgentRun(null); - if (selectedView.type == "run" && selectedView.id == run.id) { - openRunDraftView(); - } - setAgentRuns((runs) => runs.filter((r) => r.id !== run.id)); - }, - [api, selectedView, openRunDraftView], - ); - - const deletePreset = useCallback( - async (presetID: LibraryAgentPresetID) => { - await api.deleteLibraryAgentPreset(presetID); - - setConfirmingDeleteAgentPreset(null); - if (selectedView.type == "preset" && selectedView.id == presetID) { - openRunDraftView(); - } - setAgentPresets((presets) => presets.filter((p) => p.id !== presetID)); - }, - [api, selectedView, openRunDraftView], - ); - - const deleteSchedule = useCallback( - async (scheduleID: ScheduleID) => { - const removedSchedule = - await api.deleteGraphExecutionSchedule(scheduleID); - - setSchedules((schedules) => { - const newSchedules = schedules.filter( - (s) => s.id !== removedSchedule.id, - ); - if ( - selectedView.type == "schedule" && - selectedView.id == removedSchedule.id - ) { - if (newSchedules.length > 0) { - // Select next schedule if available - selectSchedule(newSchedules[0].id); - } else { - // Reset to draft view if current schedule was deleted - openRunDraftView(); - } - } - return newSchedules; - }); - openRunDraftView(); - }, - [schedules, api], - ); - - const downloadGraph = useCallback( - async () => - agent && - // Export sanitized graph from backend - api - .getGraph(agent.graph_id, agent.graph_version, true) - .then((graph) => - exportAsJSONFile(graph, `${graph.name}_v${graph.version}.json`), - ), - [api, agent], - ); - - const copyAgent = useCallback(async () => { - setCopyAgentDialogOpen(false); - api - .forkLibraryAgent(agentID) - .then((newAgent) => { - router.push(`/library/agents/${newAgent.id}`); - }) - .catch((error) => { - console.error("Error copying agent:", error); - toast({ - title: "Error copying agent", - description: `An error occurred while copying the agent: ${error.message}`, - variant: "destructive", - }); - }); - }, [agentID, api, router, toast]); - - const agentActions: ButtonAction[] = useMemo( - () => [ - { - label: "Customize agent", - href: `/build?flowID=${agent?.graph_id}&flowVersion=${agent?.graph_version}`, - disabled: !agent?.can_access_graph, - }, - { label: "Export agent to file", callback: downloadGraph }, - ...(!agent?.can_access_graph - ? [ - { - label: "Edit a copy", - callback: () => setCopyAgentDialogOpen(true), - }, - ] - : []), - { - label: "Delete agent", - callback: () => setAgentDeleteDialogOpen(true), - }, - ], - [agent, downloadGraph], - ); - - const runGraph = - graphVersions.current[selectedRun?.graph_version ?? 0] ?? graph; - - const onCreateSchedule = useCallback( - (schedule: Schedule) => { - setSchedules((prev) => [...prev, schedule]); - selectSchedule(schedule.id); - }, - [selectView], - ); - - const onCreatePreset = useCallback( - (preset: LibraryAgentPreset) => { - setAgentPresets((prev) => [...prev, preset]); - selectPreset(preset.id); - }, - [selectPreset], - ); - - const onUpdatePreset = useCallback( - (updated: LibraryAgentPreset) => { - setAgentPresets((prev) => - prev.map((p) => (p.id === updated.id ? updated : p)), - ); - selectPreset(updated.id); - }, - [selectPreset], - ); - - if (!agent || !graph) { - return ; + if (isNewAgentRunsEnabled) { + return ; } - return ( -
- {/* Sidebar w/ list of runs */} - {/* TODO: render this below header in sm and md layouts */} - - -
- {/* Header */} -
-

- { - agent.name /* TODO: use dynamic/custom run title - https://github.com/Significant-Gravitas/AutoGPT/issues/9184 */ - } -

-
- - {/* Run / Schedule views */} - {(selectedView.type == "run" && selectedView.id ? ( - selectedRun && runGraph ? ( - setConfirmingDeleteAgentRun(selectedRun)} - /> - ) : null - ) : selectedView.type == "run" ? ( - /* Draft new runs / Create new presets */ - - ) : selectedView.type == "preset" ? ( - /* Edit & update presets */ - preset.id == selectedView.id)! - } - onRun={selectRun} - onCreateSchedule={onCreateSchedule} - onUpdatePreset={onUpdatePreset} - doDeletePreset={setConfirmingDeleteAgentPreset} - agentActions={agentActions} - /> - ) : selectedView.type == "schedule" ? ( - selectedSchedule && - graph && ( - - ) - ) : null) || } - - - agent && - api.deleteLibraryAgent(agent.id).then(() => router.push("/library")) - } - /> - - !open && setConfirmingDeleteAgentRun(null)} - onDoDelete={() => - confirmingDeleteAgentRun && deleteRun(confirmingDeleteAgentRun) - } - /> - !open && setConfirmingDeleteAgentPreset(null)} - onDoDelete={() => - confirmingDeleteAgentPreset && - deletePreset(confirmingDeleteAgentPreset) - } - /> - {/* Copy agent confirmation dialog */} - - - - You're making an editable copy - - The original Marketplace agent stays the same and cannot be - edited. We'll save a new version of this agent to your - Library. From there, you can customize it however you'd - like by clicking "Customize agent" — this will open - the builder where you can see and modify the inner workings. - - - - - - - - -
-
- ); + return ; } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx index bb76233b6aa5..be1ebe1aefa0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryActionSubHeader/LibraryActionSubHeader.tsx @@ -15,7 +15,10 @@ export default function LibraryActionSubHeader({ My agents - + {agentCount} agents
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx index 7f2c120963bc..1b3926a6e19b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/LibraryAgentList.tsx @@ -1,6 +1,7 @@ "use client"; import LibraryActionSubHeader from "../LibraryActionSubHeader/LibraryActionSubHeader"; import LibraryAgentCard from "../LibraryAgentCard/LibraryAgentCard"; +import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; import { useLibraryAgentList } from "./useLibraryAgentList"; export default function LibraryAgentList() { @@ -8,8 +9,9 @@ export default function LibraryAgentList() { agentLoading, agentCount, allAgents: agents, + hasNextPage, isFetchingNextPage, - isSearching, + fetchNextPage, } = useLibraryAgentList(); const LoadingSpinner = () => ( @@ -18,7 +20,6 @@ export default function LibraryAgentList() { return ( <> - {/* TODO: We need a new endpoint on backend that returns total number of agents */}
{agentLoading ? ( @@ -26,18 +27,18 @@ export default function LibraryAgentList() {
) : ( - <> + } + >
{agents.map((agent) => ( ))}
- {(isFetchingNextPage || isSearching) && ( -
- -
- )} - +
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts index 0fec814af915..fe402af6d594 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts @@ -1,7 +1,5 @@ import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library"; import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse"; -import { useScrollThreshold } from "@/hooks/useScrollThreshold"; -import { useCallback } from "react"; import { useLibraryPageContext } from "../state-provider"; export const useLibraryAgentList = () => { @@ -12,7 +10,6 @@ export const useLibraryAgentList = () => { hasNextPage, isFetchingNextPage, isLoading: agentLoading, - isFetching, } = useGetV2ListLibraryAgentsInfinite( { page: 1, @@ -34,38 +31,22 @@ export const useLibraryAgentList = () => { }, ); - const handleInfiniteScroll = useCallback( - (scrollY: number) => { - if (!hasNextPage || isFetchingNextPage) return; - - const { scrollHeight, clientHeight } = document.documentElement; - const SCROLL_THRESHOLD = 20; - - if (scrollY + clientHeight >= scrollHeight - SCROLL_THRESHOLD) { - fetchNextPage(); - } - }, - [hasNextPage, isFetchingNextPage, fetchNextPage], - ); - - useScrollThreshold(handleInfiniteScroll, 50); - const allAgents = - agents?.pages.flatMap((page) => { - const data = page.data as LibraryAgentResponse; - return data.agents; + agents?.pages?.flatMap((page) => { + const response = page.data as LibraryAgentResponse; + return response.agents; }) ?? []; - const agentCount = agents?.pages[0] + const agentCount = agents?.pages?.[0] ? (agents.pages[0].data as LibraryAgentResponse).pagination.total_items : 0; return { allAgents, agentLoading, - isFetchingNextPage, hasNextPage, agentCount, - isSearching: isFetching && !isFetchingNextPage, + isFetchingNextPage, + fetchNextPage, }; }; diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx index 099f036052b1..4a1422b5ea3b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx @@ -8,6 +8,7 @@ export default function LibrarySearchBar(): React.ReactNode { useLibrarySearchbar(); return (
inputRef.current?.focus()} className="relative z-[21] mx-auto flex h-[50px] w-full max-w-[500px] flex-1 cursor-pointer items-center rounded-[45px] bg-[#EDEDED] px-[24px] py-[10px]" > @@ -23,6 +24,7 @@ export default function LibrarySearchBar(): React.ReactNode { onChange={handleSearchInput} className="flex-1 border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none focus:ring-0" type="text" + data-testid="library-textbox" placeholder="Search agents" /> diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx index d1d973d29568..f86b26f5f505 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx @@ -14,7 +14,7 @@ import { useLibrarySortMenu } from "./useLibrarySortMenu"; export default function LibrarySortMenu(): React.ReactNode { const { handleSortChange } = useLibrarySortMenu(); return ( -
+
sort by setSearchQuery(e.target.value)} + placeholder={placeholder} + className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`} + data-testid="store-search-input" + /> + + ); +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/useSearchBar.ts b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/useSearchBar.ts new file mode 100644 index 000000000000..e6d6b4837f11 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/SearchBar/useSearchBar.ts @@ -0,0 +1,24 @@ +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export const useSearchbar = () => { + const router = useRouter(); + + const [searchQuery, setSearchQuery] = useState(""); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + console.log(searchQuery); + + if (searchQuery.trim()) { + const encodedTerm = encodeURIComponent(searchQuery); + router.push(`/marketplace/search?searchTerm=${encodedTerm}`); + } + }; + + return { + handleSubmit, + setSearchQuery, + searchQuery, + }; +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx new file mode 100644 index 000000000000..6b515f01bdb5 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/StoreCard/StoreCard.tsx @@ -0,0 +1,120 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import Image from "next/image"; +import { StarRatingIcons } from "@/components/ui/icons"; + +interface StoreCardProps { + agentName: string; + agentImage: string; + description: string; + runs: number; + rating: number; + onClick: () => void; + avatarSrc: string; + hideAvatar?: boolean; + creatorName?: string; +} + +export const StoreCard: React.FC = ({ + agentName, + agentImage, + description, + runs, + rating, + onClick, + avatarSrc, + hideAvatar = false, + creatorName, +}) => { + const handleClick = () => { + onClick(); + }; + + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + handleClick(); + } + }} + > + {/* First Section: Image with Avatar */} +
+ {agentImage && ( + {`${agentName} + )} + {!hideAvatar && ( +
+ + {avatarSrc && ( + + )} + + {(creatorName || agentName).charAt(0)} + + +
+ )} +
+ +
+ {/* Second Section: Agent Name and Creator Name */} +
+

+ {agentName} +

+ {!hideAvatar && creatorName && ( +

+ by {creatorName} +

+ )} +
+ + {/* Third Section: Description */} +
+

+ {description} +

+
+ +
+ {/* Spacer to push stats to bottom */} + + {/* Fourth Section: Stats Row - aligned to bottom */} +
+
+
+ {runs.toLocaleString()} runs +
+
+ + {rating.toFixed(1)} + +
+ {StarRatingIcons(rating)} +
+
+
+
+
+
+ ); +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx index 484022f15136..7d8158b12322 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx @@ -1,102 +1,55 @@ -import BackendAPI from "@/lib/autogpt-server-api"; -import { AgentsSection } from "@/components/agptui/composite/AgentsSection"; -import { BreadCrumbs } from "@/components/agptui/BreadCrumbs"; +import { getQueryClient } from "@/lib/react-query/queryClient"; +import { + getV2GetCreatorDetails, + prefetchGetV2GetCreatorDetailsQuery, + prefetchGetV2ListStoreAgentsQuery, +} from "@/app/api/__generated__/endpoints/store/store"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { MainCreatorPage } from "../../components/MainCreatorPage/MainCreatorPage"; import { Metadata } from "next"; -import { CreatorInfoCard } from "@/components/agptui/CreatorInfoCard"; -import { CreatorLinks } from "@/components/agptui/CreatorLinks"; -import { Separator } from "@/components/ui/separator"; +import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails"; -// Force dynamic rendering to avoid static generation issues with cookies export const dynamic = "force-dynamic"; -type MarketplaceCreatorPageParams = { creator: string }; +export interface MarketplaceCreatorPageParams { + creator: string; +} export async function generateMetadata({ params: _params, }: { params: Promise; }): Promise { - const api = new BackendAPI(); const params = await _params; - const creator = await api.getStoreCreator(params.creator.toLowerCase()); + const { data: creator } = await getV2GetCreatorDetails( + params.creator.toLowerCase(), + ); return { - title: `${creator.name} - AutoGPT Store`, - description: creator.description, + title: `${(creator as CreatorDetails).name} - AutoGPT Store`, + description: (creator as CreatorDetails).description, }; } -// export async function generateStaticParams() { -// const api = new BackendAPI(); -// const creators = await api.getStoreCreators({ featured: true }); -// return creators.creators.map((creator) => ({ -// creator: creator.username, -// })); -// } - export default async function Page({ params: _params, }: { params: Promise; }) { - const api = new BackendAPI(); - const params = await _params; + const queryClient = getQueryClient(); - try { - const creator = await api.getStoreCreator(params.creator); - const creatorAgents = await api.getStoreAgents({ creator: params.creator }); - - return ( -
-
- - -
-
- -
-
-

- About -

-
- {creator.description} -
+ const params = await _params; - -
-
-
- - -
-
-
- ); - } catch { - return ( -
-
Creator not found
-
- ); - } + await Promise.all([ + prefetchGetV2ListStoreAgentsQuery(queryClient, { + creator: params.creator, + }), + prefetchGetV2GetCreatorDetailsQuery(queryClient, params.creator), + ]); + + return ( + + + + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx index c743e3f69009..7186ec54cd09 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx @@ -1,21 +1,12 @@ -import React from "react"; -import { HeroSection } from "@/components/agptui/composite/HeroSection"; -import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection"; -import { - AgentsSection, - Agent, -} from "@/components/agptui/composite/AgentsSection"; -import { BecomeACreator } from "@/components/agptui/BecomeACreator"; -import { - FeaturedCreators, - FeaturedCreator, -} from "@/components/agptui/composite/FeaturedCreators"; -import { Separator } from "@/components/ui/separator"; import { Metadata } from "next"; +import { + prefetchGetV2ListStoreAgentsQuery, + prefetchGetV2ListStoreCreatorsQuery, +} from "@/app/api/__generated__/endpoints/store/store"; +import { getQueryClient } from "@/lib/react-query/queryClient"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { MainMarkeplacePage } from "./components/MainMarketplacePage/MainMarketplacePage"; -import { getMarketplaceData } from "./actions"; - -// Force dynamic rendering to avoid static generation issues with cookies export const dynamic = "force-dynamic"; // FIX: Correct metadata @@ -63,31 +54,24 @@ export const metadata: Metadata = { }; export default async function MarketplacePage(): Promise { - const { featuredAgents, topAgents, featuredCreators } = - await getMarketplaceData(); + const queryClient = getQueryClient(); + + await Promise.all([ + prefetchGetV2ListStoreAgentsQuery(queryClient, { + featured: true, + }), + prefetchGetV2ListStoreAgentsQuery(queryClient, { + sorted_by: "runs", + }), + prefetchGetV2ListStoreCreatorsQuery(queryClient, { + featured: true, + sorted_by: "num_agents", + }), + ]); return ( -
-
- - - {/* 100px margin because our featured sections button are placed 40px below the container */} - - - - - - -
-
+ + + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/APIKeySection.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/APIKeySection.tsx index a9f98d66d099..61a2dcf26081 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/APIKeySection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/APIKeySection.tsx @@ -44,9 +44,9 @@ export function APIKeysSection() { {apiKeys.map((key) => ( - + {key.name} - +
{`${key.prefix}******************${key.postfix}`}
@@ -76,7 +76,11 @@ export function APIKeysSection() { - diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/AgentTable.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/AgentTable.tsx index 81d3896f49c3..f5a25fc6bca0 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/AgentTable.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/AgentTable.tsx @@ -2,61 +2,45 @@ import * as React from "react"; import { AgentTableCard } from "../AgentTableCard/AgentTableCard"; -import { StoreSubmissionRequest } from "@/app/api/__generated__/models/storeSubmissionRequest"; -import { useAgentTable } from "./useAgentTable"; +import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission"; import { AgentTableRow, AgentTableRowProps, } from "../AgentTableRow/AgentTableRow"; +import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest"; export interface AgentTableProps { agents: Omit< AgentTableRowProps, | "setSelectedAgents" | "selectedAgents" - | "onEditSubmission" + | "onViewSubmission" | "onDeleteSubmission" + | "onEditSubmission" >[]; - onEditSubmission: (submission: StoreSubmissionRequest) => void; + onViewSubmission: (submission: StoreSubmission) => void; onDeleteSubmission: (submission_id: string) => void; + onEditSubmission: ( + submission: StoreSubmissionEditRequest & { + store_listing_version_id: string | undefined; + agent_id: string; + }, + ) => void; } export const AgentTable: React.FC = ({ agents, - onEditSubmission, + onViewSubmission, onDeleteSubmission, + onEditSubmission, }) => { - const { selectedAgents, handleSelectAll, setSelectedAgents } = useAgentTable({ - agents, - }); - return ( -
+
{/* Table header - Hide on mobile */}
-
-
- 0 - } - onChange={handleSelectAll} - /> - -
-
-
+
Agent info
@@ -85,15 +69,14 @@ export const AgentTable: React.FC = ({
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/useAgentTable.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/useAgentTable.ts deleted file mode 100644 index 13d6fa851869..000000000000 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTable/useAgentTable.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useState } from "react"; -import { AgentTableRowProps } from "../AgentTableRow/AgentTableRow"; - -interface useAgentTableProps { - agents: Omit< - AgentTableRowProps, - | "setSelectedAgents" - | "selectedAgents" - | "onEditSubmission" - | "onDeleteSubmission" - >[]; -} - -export const useAgentTable = ({ agents }: useAgentTableProps) => { - const [selectedAgents, setSelectedAgents] = useState>(new Set()); - - const handleSelectAll = (e: React.ChangeEvent) => { - if (e.target.checked) { - setSelectedAgents(new Set(agents.map((agent) => agent.agent_id))); - } else { - setSelectedAgents(new Set()); - } - }; - return { selectedAgents, handleSelectAll, setSelectedAgents }; -}; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableCard/AgentTableCard.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableCard/AgentTableCard.tsx index d2aa90048683..1c078eea7dbe 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableCard/AgentTableCard.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableCard/AgentTableCard.tsx @@ -2,8 +2,9 @@ import Image from "next/image"; import { IconStarFilled, IconMore } from "@/components/ui/icons"; -import { StoreSubmissionRequest } from "@/app/api/__generated__/models/storeSubmissionRequest"; -import { Status, StatusType } from "@/components/agptui/Status"; +import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission"; +import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus"; +import { Status } from "@/components/agptui/Status"; export interface AgentTableCardProps { agent_id: string; @@ -12,12 +13,12 @@ export interface AgentTableCardProps { sub_heading: string; description: string; imageSrc: string[]; - dateSubmitted: string; - status: StatusType; + dateSubmitted: Date; + status: SubmissionStatus; runs: number; rating: number; id: number; - onEditSubmission: (submission: StoreSubmissionRequest) => void; + onViewSubmission: (submission: StoreSubmission) => void; } export const AgentTableCard = ({ @@ -31,10 +32,10 @@ export const AgentTableCard = ({ status, runs, rating, - onEditSubmission, + onViewSubmission, }: AgentTableCardProps) => { - const onEdit = () => { - onEditSubmission({ + const onView = () => { + onViewSubmission({ agent_id, agent_version, slug: "", @@ -42,14 +43,17 @@ export const AgentTableCard = ({ sub_heading, description, image_urls: imageSrc, - categories: [], + date_submitted: dateSubmitted, + status: status, + runs, + rating, }); }; return (
-
+
{agentName}
+ + - - - Edit - + {canEdit ? ( + + + Edit + + ) : ( + + + View + + )} - + Delete diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/useAgentTableRow.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/useAgentTableRow.ts index 1cb0f2c520a6..7014eec198e3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/useAgentTableRow.ts +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/useAgentTableRow.ts @@ -1,36 +1,52 @@ -import { StoreSubmissionRequest } from "@/app/api/__generated__/models/storeSubmissionRequest"; +import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission"; +import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest"; +import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus"; interface useAgentTableRowProps { id: number; - onEditSubmission: (submission: StoreSubmissionRequest) => void; + onViewSubmission: (submission: StoreSubmission) => void; onDeleteSubmission: (submission_id: string) => void; + onEditSubmission: ( + submission: StoreSubmissionEditRequest & { + store_listing_version_id: string | undefined; + agent_id: string; + }, + ) => void; agent_id: string; agent_version: number; agentName: string; sub_heading: string; description: string; imageSrc: string[]; - selectedAgents: Set; - setSelectedAgents: React.Dispatch>>; + dateSubmitted: Date; + status: SubmissionStatus; + runs: number; + rating: number; + video_url?: string; + categories?: string[]; + store_listing_version_id?: string; } export const useAgentTableRow = ({ - id, - onEditSubmission, + onViewSubmission, onDeleteSubmission, + onEditSubmission, agent_id, agent_version, agentName, sub_heading, description, imageSrc, - selectedAgents, - setSelectedAgents, + dateSubmitted, + status, + runs, + rating, + video_url, + categories, + store_listing_version_id, }: useAgentTableRowProps) => { - const checkboxId = `agent-${id}-checkbox`; - - const handleEdit = () => { - onEditSubmission({ + const handleView = () => { + onViewSubmission({ agent_id, agent_version, slug: "", @@ -38,22 +54,33 @@ export const useAgentTableRow = ({ sub_heading, description, image_urls: imageSrc, - categories: [], - } satisfies StoreSubmissionRequest); + date_submitted: dateSubmitted, + status: status, + runs, + rating, + video_url, + categories, + store_listing_version_id, + } satisfies StoreSubmission); }; - const handleDelete = () => { - onDeleteSubmission(agent_id); + const handleEdit = () => { + onEditSubmission({ + name: agentName, + sub_heading, + description, + image_urls: imageSrc, + video_url, + categories, + changes_summary: "Update Submission", + store_listing_version_id, + agent_id, + }); }; - const handleCheckboxChange = () => { - if (selectedAgents.has(agent_id)) { - selectedAgents.delete(agent_id); - } else { - selectedAgents.add(agent_id); - } - setSelectedAgents(new Set(selectedAgents)); + const handleDelete = () => { + onDeleteSubmission(agent_id); }; - return { checkboxId, handleEdit, handleDelete, handleCheckboxChange }; + return { handleView, handleDelete, handleEdit }; }; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/MainDashboardPage.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/MainDashboardPage.tsx index b72bc66084df..9b10ecf0883b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/MainDashboardPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/MainDashboardPage.tsx @@ -1,56 +1,65 @@ -import { PublishAgentPopout } from "@/components/agptui/composite/PublishAgentPopout"; -import { Button } from "@/components/ui/button"; import { useMainDashboardPage } from "./useMainDashboardPage"; import { Separator } from "@/components/ui/separator"; import { AgentTable } from "../AgentTable/AgentTable"; -import { StatusType } from "@/components/agptui/Status"; +import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal"; +import { EditAgentModal } from "@/components/contextual/EditAgentModal/EditAgentModal"; +import { Button } from "@/components/atoms/Button/Button"; +import { EmptySubmissions } from "./components/EmptySubmissions"; +import { SubmissionLoadError } from "./components/SumbmissionLoadError"; +import { SubmissionsLoading } from "./components/SubmissionsLoading"; +import { Text } from "@/components/atoms/Text/Text"; export const MainDashboardPage = () => { const { - onOpenPopout, onDeleteSubmission, + onViewSubmission, onEditSubmission, + onEditSuccess, + onEditClose, + onOpenSubmitModal, + onPublishStateChange, + publishState, + editState, + // API data submissions, isLoading, - openPopout, - submissionData, - popoutStep, + error, } = useMainDashboardPage(); - if (isLoading) { - return "Loading...."; - } - return (
{/* Header Section */}
-

+ Agent dashboard -

+
-

+ Submit a New Agent -

-

+ + Select from the list of agents you currently have, or upload from your local machine. -

+
- Submit agent } - openPopout={openPopout} - inputStep={popoutStep} - submissionData={submissionData} />
@@ -58,34 +67,53 @@ export const MainDashboardPage = () => { {/* Agents Section */}
-

+ Your uploaded agents -

- {submissions && ( + + + {error ? ( + + ) : isLoading ? ( + + ) : submissions && submissions.submissions.length > 0 ? ( ({ - id: index, - agent_id: submission.agent_id, - agent_version: submission.agent_version, - sub_heading: submission.sub_heading, - date_submitted: submission.date_submitted, - agentName: submission.name, - description: submission.description, - imageSrc: submission.image_urls || [""], - dateSubmitted: new Date( - submission.date_submitted, - ).toLocaleDateString(), - status: submission.status.toLowerCase() as StatusType, - runs: submission.runs, - rating: submission.rating, - })) || [] - } - onEditSubmission={onEditSubmission} + agents={submissions.submissions.map((submission, index) => ({ + id: index, + agent_id: submission.agent_id, + agent_version: submission.agent_version, + sub_heading: submission.sub_heading, + agentName: submission.name, + description: submission.description, + imageSrc: submission.image_urls || [""], + dateSubmitted: submission.date_submitted, + status: submission.status, + runs: submission.runs, + rating: submission.rating, + video_url: submission.video_url || undefined, + categories: submission.categories, + slug: submission.slug, + store_listing_version_id: + submission.store_listing_version_id || undefined, + }))} + onViewSubmission={onViewSubmission} onDeleteSubmission={onDeleteSubmission} + onEditSubmission={onEditSubmission} /> + ) : ( + )}
+ +
); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/EmptySubmissions.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/EmptySubmissions.tsx new file mode 100644 index 000000000000..d0841f01382e --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/EmptySubmissions.tsx @@ -0,0 +1,30 @@ +import { Tray } from "@phosphor-icons/react/dist/ssr"; +import { Text } from "@/components/atoms/Text/Text"; + +export function EmptySubmissions() { + return ( +
+
+
+ +
+
+ + No agents submitted yet + + + You haven't submitted any agents to the store yet. +
+ Click “Submit agent” above to get started. +
+
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SubmissionsLoading.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SubmissionsLoading.tsx new file mode 100644 index 000000000000..68b2d47b04ee --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SubmissionsLoading.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export function SubmissionsLoading() { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ + + +
+
+ + +
+
+
+ ))} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SumbmissionLoadError.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SumbmissionLoadError.tsx new file mode 100644 index 000000000000..70184b7c5aa3 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/components/SumbmissionLoadError.tsx @@ -0,0 +1,36 @@ +import { Tray } from "@phosphor-icons/react"; +import { Text } from "@/components/atoms/Text/Text"; +import { Button } from "@/components/atoms/Button/Button"; + +export function SubmissionLoadError() { + return ( +
+
+
+ +
+
+ + Failed to load agents + + + Something went wrong while loading your submitted agents. + +
+ +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/useMainDashboardPage.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/useMainDashboardPage.ts index 46340ba27f79..02eef96a5183 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/useMainDashboardPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/MainDashboardPage/useMainDashboardPage.ts @@ -3,22 +3,47 @@ import { useDeleteV2DeleteStoreSubmission, useGetV2ListMySubmissions, } from "@/app/api/__generated__/endpoints/store/store"; -import { StoreSubmissionRequest } from "@/app/api/__generated__/models/storeSubmissionRequest"; +import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission"; +import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest"; import { StoreSubmissionsResponse } from "@/app/api/__generated__/models/storeSubmissionsResponse"; import { getQueryClient } from "@/lib/react-query/queryClient"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useState } from "react"; +import * as Sentry from "@sentry/nextjs"; + +type PublishStep = "select" | "info" | "review"; + +type PublishState = { + isOpen: boolean; + step: PublishStep; + submissionData: StoreSubmission | null; +}; + +type EditState = { + isOpen: boolean; + submission: + | (StoreSubmissionEditRequest & { + store_listing_version_id: string | undefined; + agent_id: string; + }) + | null; +}; export const useMainDashboardPage = () => { const queryClient = getQueryClient(); const { user } = useSupabase(); - const [openPopout, setOpenPopout] = useState(false); - const [submissionData, setSubmissionData] = - useState(); - const [popoutStep, setPopoutStep] = useState<"select" | "info" | "review">( - "info", - ); + + const [publishState, setPublishState] = useState({ + isOpen: false, + step: "select", + submissionData: null, + }); + + const [editState, setEditState] = useState({ + isOpen: false, + submission: null, + }); const { mutateAsync: deleteSubmission } = useDeleteV2DeleteStoreSubmission({ mutation: { @@ -30,22 +55,62 @@ export const useMainDashboardPage = () => { }, }); - const { data: submissions, isLoading } = useGetV2ListMySubmissions( - undefined, - { - query: { - select: (x) => { - return x.data as StoreSubmissionsResponse; - }, - enabled: !!user, + const { + data: submissions, + isSuccess, + error, + } = useGetV2ListMySubmissions(undefined, { + query: { + select: (x) => { + return x.data as StoreSubmissionsResponse; }, + enabled: !!user, + }, + }); + + const onViewSubmission = (submission: StoreSubmission) => { + setPublishState({ + isOpen: true, + step: "review", + submissionData: submission, + }); + }; + + const onEditSubmission = ( + submission: StoreSubmissionEditRequest & { + store_listing_version_id: string | undefined; + agent_id: string; }, - ); + ) => { + setEditState({ + isOpen: true, + submission, + }); + }; + + const onEditSuccess = async (submission: StoreSubmission) => { + try { + if (!submission.store_listing_version_id) { + Sentry.captureException( + new Error("No store listing version ID found for submission"), + ); + return; + } - const onEditSubmission = (submission: StoreSubmissionRequest) => { - setSubmissionData(submission); - setPopoutStep("review"); - setOpenPopout(true); + setEditState({ + isOpen: false, + submission: null, + }); + } catch (error) { + Sentry.captureException(error); + } + }; + + const onEditClose = () => { + setEditState({ + isOpen: false, + submission: null, + }); }; const onDeleteSubmission = async (submission_id: string) => { @@ -54,19 +119,32 @@ export const useMainDashboardPage = () => { }); }; - const onOpenPopout = () => { - setPopoutStep("select"); - setOpenPopout(true); + const onOpenSubmitModal = () => { + // Always reset to clean state when opening for new submission + setPublishState({ + isOpen: true, + step: "select", + submissionData: null, + }); + }; + + const onPublishStateChange = (newState: PublishState) => { + setPublishState(newState); }; return { - onOpenPopout, + onOpenSubmitModal, + onPublishStateChange, onDeleteSubmission, + onViewSubmission, onEditSubmission, + onEditSuccess, + onEditClose, + publishState, + editState, + // API data submissions, - isLoading, - openPopout, - submissionData, - popoutStep, + isLoading: !isSuccess, + error, }; }; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts deleted file mode 100644 index 97c9d8b15324..000000000000 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; -import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api/types"; -import { - postV1UpdateNotificationPreferences, - postV1UpdateUserEmail, -} from "@/app/api/__generated__/endpoints/auth/auth"; - -export async function updateSettings(formData: FormData) { - const supabase = await getServerSupabase(); - const { - data: { user }, - } = await supabase.auth.getUser(); - - // Handle auth-related updates - const password = formData.get("password") as string; - const email = formData.get("email") as string; - - if (password) { - const { error: passwordError } = await supabase.auth.updateUser({ - password, - }); - - if (passwordError) { - throw new Error(`${passwordError.message}`); - } - } - - if (email !== user?.email) { - const { error: emailError } = await supabase.auth.updateUser({ - email, - }); - await postV1UpdateUserEmail(email); - - if (emailError) { - throw new Error(`${emailError.message}`); - } - } - - try { - const preferences: NotificationPreferenceDTO = { - email: user?.email || "", - preferences: { - AGENT_RUN: formData.get("notifyOnAgentRun") === "true", - ZERO_BALANCE: formData.get("notifyOnZeroBalance") === "true", - LOW_BALANCE: formData.get("notifyOnLowBalance") === "true", - BLOCK_EXECUTION_FAILED: - formData.get("notifyOnBlockExecutionFailed") === "true", - CONTINUOUS_AGENT_ERROR: - formData.get("notifyOnContinuousAgentError") === "true", - DAILY_SUMMARY: formData.get("notifyOnDailySummary") === "true", - WEEKLY_SUMMARY: formData.get("notifyOnWeeklySummary") === "true", - MONTHLY_SUMMARY: formData.get("notifyOnMonthlySummary") === "true", - }, - daily_limit: 0, - }; - await postV1UpdateNotificationPreferences(preferences); - } catch (error) { - console.error(error); - throw new Error(`Failed to update preferences: ${error}`); - } - - revalidatePath("/profile/settings"); - return { success: true }; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx index 13734ad8deb2..89f4e4683448 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx @@ -1,316 +1,30 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; import { User } from "@supabase/supabase-js"; -import { useSettingsForm } from "./useSettingsForm"; +import { EmailForm } from "./components/EmailForm/EmailForm"; +import { NotificationForm } from "./components/NotificationForm/NotificationForm"; +import { TimezoneForm } from "./components/TimezoneForm/TimezoneForm"; -export const SettingsForm = ({ - preferences, - user, -}: { +type SettingsFormProps = { preferences: NotificationPreference; user: User; -}) => { - const { form, onSubmit, onCancel } = useSettingsForm({ - preferences, - user, - }); + timezone?: string; +}; +export function SettingsForm({ + preferences, + user, + timezone, +}: SettingsFormProps) { return ( -
- - {/* Account Settings Section */} -
- ( - - Email - - - - - - )} - /> - - ( - - New Password - - - - - - )} - /> - - ( - - Confirm New Password - - - - - - )} - /> -
- - - - {/* Notifications Section */} -
-

Notifications

- - {/* Agent Notifications */} -
-

- Agent Notifications -

- ( - -
- - Agent Run Notifications - - - Receive notifications when an agent starts or completes a - run - -
- - - -
- )} - /> - - ( - -
- - Block Execution Failures - - - Get notified when a block execution fails during agent - runs - -
- - - -
- )} - /> - - ( - -
- - Continuous Agent Errors - - - Receive alerts when an agent encounters repeated errors - -
- - - -
- )} - /> -
- - {/* Balance Notifications */} -
-

- Balance Notifications -

- ( - -
- - Zero Balance Alert - - - Get notified when your account balance reaches zero - -
- - - -
- )} - /> - - ( - -
- - Low Balance Warning - - - Receive warnings when your balance is running low - -
- - - -
- )} - /> -
- - {/* Summary Reports */} -
-

- Summary Reports -

- ( - -
- Daily Summary - - Receive a daily summary of your account activity - -
- - - -
- )} - /> - - ( - -
- Weekly Summary - - Get a weekly overview of your account performance - -
- - - -
- )} - /> - - ( - -
- Monthly Summary - - Receive a comprehensive monthly report of your account - -
- - - -
- )} - /> -
-
- - {/* Form Actions */} -
- - -
- - +
+ + + + + +
); -}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/EmailForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/EmailForm.tsx new file mode 100644 index 000000000000..fda46b41cc54 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/EmailForm.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/atoms/Input/Input"; +import { Text } from "@/components/atoms/Text/Text"; +import { Button } from "@/components/atoms/Button/Button"; +import { User } from "@supabase/supabase-js"; +import { useEmailForm } from "./useEmailForm"; + +type EmailFormProps = { + user: User; +}; + +export function EmailForm({ user }: EmailFormProps) { + const { form, onSubmit, isLoading, currentEmail } = useEmailForm({ user }); + + const hasError = Object.keys(form.formState.errors).length > 0; + const isSameEmail = form.watch("email") === currentEmail; + + return ( +
+ + Security & Access + +
+ + ( + + + + + + )} + /> +
+ + +
+ + +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/actions.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/actions.ts new file mode 100644 index 000000000000..95ffb9139f7b --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/actions.ts @@ -0,0 +1,10 @@ +import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; + +export async function updateSupabaseUserEmail(email: string) { + const supabase = await getServerSupabase(); + const { data, error } = await supabase.auth.updateUser({ + email, + }); + + return { data, error }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/useEmailForm.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/useEmailForm.ts new file mode 100644 index 000000000000..b6a6d1074b4c --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/useEmailForm.ts @@ -0,0 +1,92 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { User } from "@supabase/supabase-js"; +import { usePostV1UpdateUserEmail } from "@/app/api/__generated__/endpoints/auth/auth"; + +const emailFormSchema = z.object({ + email: z + .string() + .min(1, "Email is required") + .email("Please enter a valid email address"), +}); + +function createEmailDefaultValues(user: { email?: string }) { + return { + email: user.email || "", + }; +} + +async function updateUserEmailAPI(email: string) { + const response = await fetch("/api/auth/user", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update email"); + } + + return response.json(); +} + +export function useEmailForm({ user }: { user: User }) { + const { toast } = useToast(); + const defaultValues = createEmailDefaultValues(user); + const currentEmail = user.email; + + const form = useForm>({ + resolver: zodResolver(emailFormSchema), + defaultValues, + mode: "onSubmit", + }); + + const updateEmailMutation = usePostV1UpdateUserEmail({ + mutation: { + onError: (error) => { + toast({ + title: "Error updating email", + description: + error instanceof Error ? error.message : "Failed to update email", + variant: "destructive", + }); + }, + }, + }); + + async function onSubmit(values: z.infer) { + try { + if (values.email !== user.email) { + await Promise.all([ + updateUserEmailAPI(values.email), + updateEmailMutation.mutateAsync({ data: values.email }), + ]); + + toast({ + title: "Successfully updated email", + }); + } + } catch (error) { + toast({ + title: "Error updating email", + description: + error instanceof Error ? error.message : "Something went wrong", + variant: "destructive", + }); + } + } + + return { + form, + onSubmit, + isLoading: updateEmailMutation.isPending, + currentEmail, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx new file mode 100644 index 000000000000..f0f4d01ab715 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx @@ -0,0 +1,314 @@ +"use client"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; +import { Text } from "@/components/atoms/Text/Text"; +import { Button } from "@/components/atoms/Button/Button"; +import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; +import { User } from "@supabase/supabase-js"; +import { useNotificationForm } from "./useNotificationForm"; + +type NotificationFormProps = { + preferences: NotificationPreference; + user: User; +}; + +export function NotificationForm({ preferences, user }: NotificationFormProps) { + const { form, onSubmit, onCancel, isLoading } = useNotificationForm({ + preferences, + user, + }); + + return ( +
+ + Notifications + +
+ + {/* Agent Notifications */} +
+ + Agent Notifications + + ( + +
+ + Agent Run Notifications + + + Receive notifications when an agent starts or completes a + run + +
+ + + +
+ )} + /> + + ( + +
+ + Block Execution Failures + + + Get notified when a block execution fails during agent + runs + +
+ + + +
+ )} + /> + + ( + +
+ + Continuous Agent Errors + + + Receive alerts when an agent encounters repeated errors + +
+ + + +
+ )} + /> +
+ + {/* Store Notifications */} +
+ + Store Notifications + + ( + +
+ + Agent Approved + + + Get notified when your submitted agent is approved for the + store + +
+ + + +
+ )} + /> + + ( + +
+ + Agent Rejected + + + Receive notifications when your agent submission needs + updates + +
+ + + +
+ )} + /> +
+ + {/* Balance Notifications */} +
+ + Balance Notifications + + ( + +
+ + Zero Balance Alert + + + Get notified when your account balance reaches zero + +
+ + + +
+ )} + /> + + ( + +
+ + Low Balance Warning + + + Receive warnings when your balance is running low + +
+ + + +
+ )} + /> +
+ + {/* Summary Reports */} +
+ + Summary reports + + ( + +
+ + Daily Summary + + + Receive a daily summary of your account activity + +
+ + + +
+ )} + /> + + ( + +
+ + Weekly Summary + + + Get a weekly overview of your account performance + +
+ + + +
+ )} + /> + + ( + +
+ + Monthly Summary + + + Receive a comprehensive monthly report of your account + +
+ + + +
+ )} + /> +
+ + {/* Form Actions */} +
+ + +
+
+ +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/useNotificationForm.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/useNotificationForm.ts new file mode 100644 index 000000000000..5e998344ec04 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/useNotificationForm.ts @@ -0,0 +1,120 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; +import { User } from "@supabase/supabase-js"; +import { usePostV1UpdateNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth"; +import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api/types"; + +const notificationFormSchema = z.object({ + notifyOnAgentRun: z.boolean(), + notifyOnZeroBalance: z.boolean(), + notifyOnLowBalance: z.boolean(), + notifyOnBlockExecutionFailed: z.boolean(), + notifyOnContinuousAgentError: z.boolean(), + notifyOnDailySummary: z.boolean(), + notifyOnWeeklySummary: z.boolean(), + notifyOnMonthlySummary: z.boolean(), + notifyOnAgentApproved: z.boolean(), + notifyOnAgentRejected: z.boolean(), +}); + +function createNotificationDefaultValues(preferences: { + preferences?: Record; +}) { + return { + notifyOnAgentRun: preferences.preferences?.AGENT_RUN, + notifyOnZeroBalance: preferences.preferences?.ZERO_BALANCE, + notifyOnLowBalance: preferences.preferences?.LOW_BALANCE, + notifyOnBlockExecutionFailed: + preferences.preferences?.BLOCK_EXECUTION_FAILED, + notifyOnContinuousAgentError: + preferences.preferences?.CONTINUOUS_AGENT_ERROR, + notifyOnDailySummary: preferences.preferences?.DAILY_SUMMARY, + notifyOnWeeklySummary: preferences.preferences?.WEEKLY_SUMMARY, + notifyOnMonthlySummary: preferences.preferences?.MONTHLY_SUMMARY, + notifyOnAgentApproved: preferences.preferences?.AGENT_APPROVED, + notifyOnAgentRejected: preferences.preferences?.AGENT_REJECTED, + }; +} + +export function useNotificationForm({ + preferences, + user, +}: { + preferences: NotificationPreference; + user: User; +}) { + const { toast } = useToast(); + const defaultValues = createNotificationDefaultValues(preferences); + + const form = useForm>({ + resolver: zodResolver(notificationFormSchema), + defaultValues, + }); + + const updateNotificationsMutation = usePostV1UpdateNotificationPreferences({ + mutation: { + onError: (error) => { + toast({ + title: "Error updating notifications", + description: + error instanceof Error + ? error.message + : "Failed to update notification preferences", + variant: "destructive", + }); + }, + }, + }); + + async function onSubmit(values: z.infer) { + try { + const notificationPreferences: NotificationPreferenceDTO = { + email: user.email || "", + preferences: { + AGENT_RUN: values.notifyOnAgentRun, + ZERO_BALANCE: values.notifyOnZeroBalance, + LOW_BALANCE: values.notifyOnLowBalance, + BLOCK_EXECUTION_FAILED: values.notifyOnBlockExecutionFailed, + CONTINUOUS_AGENT_ERROR: values.notifyOnContinuousAgentError, + DAILY_SUMMARY: values.notifyOnDailySummary, + WEEKLY_SUMMARY: values.notifyOnWeeklySummary, + MONTHLY_SUMMARY: values.notifyOnMonthlySummary, + AGENT_APPROVED: values.notifyOnAgentApproved, + AGENT_REJECTED: values.notifyOnAgentRejected, + }, + daily_limit: 0, + }; + + await updateNotificationsMutation.mutateAsync({ + data: notificationPreferences, + }); + + toast({ + title: "Successfully updated notification preferences", + }); + } catch (error) { + toast({ + title: "Error updating notifications", + description: + error instanceof Error ? error.message : "Something went wrong", + variant: "destructive", + }); + } + } + + function onCancel() { + form.reset(defaultValues); + } + + return { + form, + onSubmit, + onCancel, + isLoading: updateNotificationsMutation.isPending, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/TimezoneForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/TimezoneForm.tsx new file mode 100644 index 000000000000..c04955c9f327 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/TimezoneForm.tsx @@ -0,0 +1,126 @@ +"use client"; + +import * as React from "react"; +import { useTimezoneForm } from "./useTimezoneForm"; +import { User } from "@supabase/supabase-js"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/atoms/Button/Button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +type TimezoneFormProps = { + user: User; + currentTimezone?: string; +}; + +// Common timezones list - can be expanded later +const TIMEZONES = [ + { value: "UTC", label: "UTC (Coordinated Universal Time)" }, + { value: "America/New_York", label: "Eastern Time (US & Canada)" }, + { value: "America/Chicago", label: "Central Time (US & Canada)" }, + { value: "America/Denver", label: "Mountain Time (US & Canada)" }, + { value: "America/Los_Angeles", label: "Pacific Time (US & Canada)" }, + { value: "America/Phoenix", label: "Arizona (US)" }, + { value: "America/Anchorage", label: "Alaska (US)" }, + { value: "Pacific/Honolulu", label: "Hawaii (US)" }, + { value: "Europe/London", label: "London (UK)" }, + { value: "Europe/Paris", label: "Paris (France)" }, + { value: "Europe/Berlin", label: "Berlin (Germany)" }, + { value: "Europe/Moscow", label: "Moscow (Russia)" }, + { value: "Asia/Dubai", label: "Dubai (UAE)" }, + { value: "Asia/Kolkata", label: "India Standard Time" }, + { value: "Asia/Shanghai", label: "China Standard Time" }, + { value: "Asia/Tokyo", label: "Tokyo (Japan)" }, + { value: "Asia/Seoul", label: "Seoul (South Korea)" }, + { value: "Asia/Singapore", label: "Singapore" }, + { value: "Australia/Sydney", label: "Sydney (Australia)" }, + { value: "Australia/Melbourne", label: "Melbourne (Australia)" }, + { value: "Pacific/Auckland", label: "Auckland (New Zealand)" }, + { value: "America/Toronto", label: "Toronto (Canada)" }, + { value: "America/Vancouver", label: "Vancouver (Canada)" }, + { value: "America/Mexico_City", label: "Mexico City (Mexico)" }, + { value: "America/Sao_Paulo", label: "São Paulo (Brazil)" }, + { value: "America/Buenos_Aires", label: "Buenos Aires (Argentina)" }, + { value: "Africa/Cairo", label: "Cairo (Egypt)" }, + { value: "Africa/Johannesburg", label: "Johannesburg (South Africa)" }, +]; + +export function TimezoneForm({ + user, + currentTimezone = "not-set", +}: TimezoneFormProps) { + // If timezone is not set, try to detect it from the browser + const effectiveTimezone = React.useMemo(() => { + if (currentTimezone === "not-set") { + // Try to get browser timezone as a suggestion + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + } catch { + return "UTC"; + } + } + return currentTimezone; + }, [currentTimezone]); + + const { form, onSubmit, isLoading } = useTimezoneForm({ + user, + currentTimezone: effectiveTimezone, + }); + + return ( + + + Timezone + + +
+ + ( + + Select your timezone + + + + )} + /> + + + +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/useTimezoneForm.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/useTimezoneForm.ts new file mode 100644 index 000000000000..0089a60260db --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/TimezoneForm/useTimezoneForm.ts @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { User } from "@supabase/supabase-js"; +import { + usePostV1UpdateUserTimezone, + getGetV1GetUserTimezoneQueryKey, +} from "@/app/api/__generated__/endpoints/auth/auth"; +import { useQueryClient } from "@tanstack/react-query"; + +const formSchema = z.object({ + timezone: z.string().min(1, "Please select a timezone"), +}); + +type FormData = z.infer; + +type UseTimezoneFormProps = { + user: User; + currentTimezone: string; +}; + +export const useTimezoneForm = ({ currentTimezone }: UseTimezoneFormProps) => { + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + timezone: currentTimezone, + }, + }); + + const updateTimezone = usePostV1UpdateUserTimezone(); + + const onSubmit = async (data: FormData) => { + setIsLoading(true); + try { + await updateTimezone.mutateAsync({ + data: { timezone: data.timezone } as any, + }); + + // Invalidate the timezone query to refetch the updated value + await queryClient.invalidateQueries({ + queryKey: getGetV1GetUserTimezoneQueryKey(), + }); + + toast({ + title: "Success", + description: "Your timezone has been updated successfully.", + variant: "success", + }); + } catch { + toast({ + title: "Error", + description: "Failed to update timezone. Please try again.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + return { + form, + onSubmit, + isLoading, + }; +}; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/helper.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/helper.ts deleted file mode 100644 index ad04713af2ae..000000000000 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/helper.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { z } from "zod"; - -export const formSchema = z - .object({ - email: z.string().email(), - password: z - .string() - .optional() - .refine((val) => { - if (val) return val.length >= 12; - return true; - }, "String must contain at least 12 character(s)"), - confirmPassword: z.string().optional(), - notifyOnAgentRun: z.boolean(), - notifyOnZeroBalance: z.boolean(), - notifyOnLowBalance: z.boolean(), - notifyOnBlockExecutionFailed: z.boolean(), - notifyOnContinuousAgentError: z.boolean(), - notifyOnDailySummary: z.boolean(), - notifyOnWeeklySummary: z.boolean(), - notifyOnMonthlySummary: z.boolean(), - }) - .refine((data) => { - if (data.password || data.confirmPassword) { - return data.password === data.confirmPassword; - } - return true; - }); - -export const createDefaultValues = ( - user: { email?: string }, - preferences: { preferences?: Record }, -) => { - const defaultValues = { - email: user.email || "", - password: "", - confirmPassword: "", - notifyOnAgentRun: preferences.preferences?.AGENT_RUN, - notifyOnZeroBalance: preferences.preferences?.ZERO_BALANCE, - notifyOnLowBalance: preferences.preferences?.LOW_BALANCE, - notifyOnBlockExecutionFailed: - preferences.preferences?.BLOCK_EXECUTION_FAILED, - notifyOnContinuousAgentError: - preferences.preferences?.CONTINUOUS_AGENT_ERROR, - notifyOnDailySummary: preferences.preferences?.DAILY_SUMMARY, - notifyOnWeeklySummary: preferences.preferences?.WEEKLY_SUMMARY, - notifyOnMonthlySummary: preferences.preferences?.MONTHLY_SUMMARY, - }; - - return defaultValues; -}; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx deleted file mode 100644 index 3b6361bb3bff..000000000000 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; -import { useForm } from "react-hook-form"; -import { createDefaultValues, formSchema } from "./helper"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { updateSettings } from "../../actions"; -import { useToast } from "@/components/molecules/Toast/use-toast"; -import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; -import { User } from "@supabase/supabase-js"; - -export const useSettingsForm = ({ - preferences, - user, -}: { - preferences: NotificationPreference; - user: User; -}) => { - const { toast } = useToast(); - const defaultValues = createDefaultValues(user, preferences); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues, - }); - - async function onSubmit(values: z.infer) { - try { - const formData = new FormData(); - - Object.entries(values).forEach(([key, value]) => { - if (key !== "confirmPassword") { - formData.append(key, value.toString()); - } - }); - - await updateSettings(formData); - - toast({ - title: "Successfully updated settings", - }); - } catch (error) { - toast({ - title: "Error", - description: - error instanceof Error ? error.message : "Something went wrong", - variant: "destructive", - }); - throw error; - } - } - - function onCancel() { - form.reset(defaultValues); - } - - return { form, onSubmit, onCancel }; -}; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx index 8152999b5cb3..b738e08a3402 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx @@ -1,16 +1,21 @@ "use client"; -import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth"; +import { + useGetV1GetNotificationPreferences, + useGetV1GetUserTimezone, +} from "@/app/api/__generated__/endpoints/auth/auth"; import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { useTimezoneDetection } from "@/hooks/useTimezoneDetection"; import * as React from "react"; import SettingsLoading from "./loading"; import { redirect } from "next/navigation"; +import { Text } from "@/components/atoms/Text/Text"; export default function SettingsPage() { const { data: preferences, - isError, - isLoading, + isError: preferencesError, + isLoading: preferencesLoading, } = useGetV1GetNotificationPreferences({ query: { select: (res) => { @@ -19,9 +24,24 @@ export default function SettingsPage() { }, }); + const { data: timezoneData, isLoading: timezoneLoading } = + useGetV1GetUserTimezone({ + query: { + select: (res) => { + return res.data; + }, + }, + }); + const { user, isUserLoading } = useSupabase(); - if (isLoading || isUserLoading) { + // Auto-detect timezone if it's not set + const timezone = timezoneData?.timezone + ? String(timezoneData.timezone) + : "not-set"; + useTimezoneDetection(timezone); + + if (preferencesLoading || isUserLoading || timezoneLoading) { return ; } @@ -29,19 +49,19 @@ export default function SettingsPage() { redirect("/login"); } - if (isError || !preferences || !preferences.preferences) { + if (preferencesError || !preferences || !preferences.preferences) { return "Errror..."; // TODO: Will use a Error reusable components from Block Menu redesign } return (
-
-

My account

-

+

+ My account + Manage your account settings and preferences. -

+
- +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx index 77d82d6d6cb2..cb48d512f711 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx @@ -79,7 +79,6 @@ export default function SignupPage() { control={form.control} name="password" render={({ field }) => { - console.log(field); return ( {/* Turnstile CAPTCHA Component */} - {!turnstile.verified ? ( + {isCloudEnv && !turnstile.verified ? ( ) { setIsLoading(true); - if (!turnstile.verified && !isVercelPreview) { + if (isCloudEnv && !turnstile.verified && !isVercelPreview) { toast({ title: "Please complete the CAPTCHA challenge.", variant: "default", diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/admin/admin.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/admin/admin.ts deleted file mode 100644 index 716e4e8c0c52..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/admin/admin.ts +++ /dev/null @@ -1,1018 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { AddUserCreditsResponse } from "../../models/addUserCreditsResponse"; - -import type { BodyPostV2AddCreditsToUser } from "../../models/bodyPostV2AddCreditsToUser"; - -import type { GetV2GetAdminListingsHistoryParams } from "../../models/getV2GetAdminListingsHistoryParams"; - -import type { GetV2GetAllUsersHistoryParams } from "../../models/getV2GetAllUsersHistoryParams"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { ReviewSubmissionRequest } from "../../models/reviewSubmissionRequest"; - -import type { StoreListingsWithVersionsResponse } from "../../models/storeListingsWithVersionsResponse"; - -import type { StoreSubmission } from "../../models/storeSubmission"; - -import type { UserHistoryResponse } from "../../models/userHistoryResponse"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Get store listings with their version history for admins. - -This provides a consolidated view of listings with their versions, -allowing for an expandable UI in the admin dashboard. - -Args: - status: Filter by submission status (PENDING, APPROVED, REJECTED) - search: Search by name, description, or user email - page: Page number for pagination - page_size: Number of items per page - -Returns: - StoreListingsWithVersionsResponse with listings and their versions - * @summary Get Admin Listings History - */ -export type getV2GetAdminListingsHistoryResponse200 = { - data: StoreListingsWithVersionsResponse; - status: 200; -}; - -export type getV2GetAdminListingsHistoryResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetAdminListingsHistoryResponseComposite = - | getV2GetAdminListingsHistoryResponse200 - | getV2GetAdminListingsHistoryResponse422; - -export type getV2GetAdminListingsHistoryResponse = - getV2GetAdminListingsHistoryResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetAdminListingsHistoryUrl = ( - params?: GetV2GetAdminListingsHistoryParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/store/admin/listings?${stringifiedParams}` - : `/api/store/admin/listings`; -}; - -export const getV2GetAdminListingsHistory = async ( - params?: GetV2GetAdminListingsHistoryParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetAdminListingsHistoryUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetAdminListingsHistoryQueryKey = ( - params?: GetV2GetAdminListingsHistoryParams, -) => { - return [`/api/store/admin/listings`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2GetAdminListingsHistoryQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAdminListingsHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2GetAdminListingsHistoryQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetAdminListingsHistory(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetAdminListingsHistoryQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetAdminListingsHistoryQueryError = HTTPValidationError; - -export function useGetV2GetAdminListingsHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2GetAdminListingsHistoryParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAdminListingsHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAdminListingsHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAdminListingsHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAdminListingsHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Admin Listings History - */ - -export function useGetV2GetAdminListingsHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAdminListingsHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetAdminListingsHistoryQueryOptions( - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Admin Listings History - */ -export const prefetchGetV2GetAdminListingsHistoryQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2GetAdminListingsHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetAdminListingsHistoryQueryOptions( - params, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Review a store listing submission. - -Args: - store_listing_version_id: ID of the submission to review - request: Review details including approval status and comments - user: Authenticated admin user performing the review - -Returns: - StoreSubmission with updated review information - * @summary Review Store Submission - */ -export type postV2ReviewStoreSubmissionResponse200 = { - data: StoreSubmission; - status: 200; -}; - -export type postV2ReviewStoreSubmissionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2ReviewStoreSubmissionResponseComposite = - | postV2ReviewStoreSubmissionResponse200 - | postV2ReviewStoreSubmissionResponse422; - -export type postV2ReviewStoreSubmissionResponse = - postV2ReviewStoreSubmissionResponseComposite & { - headers: Headers; - }; - -export const getPostV2ReviewStoreSubmissionUrl = ( - storeListingVersionId: string, -) => { - return `/api/store/admin/submissions/${storeListingVersionId}/review`; -}; - -export const postV2ReviewStoreSubmission = async ( - storeListingVersionId: string, - reviewSubmissionRequest: ReviewSubmissionRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2ReviewStoreSubmissionUrl(storeListingVersionId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(reviewSubmissionRequest), - }, - ); -}; - -export const getPostV2ReviewStoreSubmissionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { storeListingVersionId: string; data: ReviewSubmissionRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { storeListingVersionId: string; data: ReviewSubmissionRequest }, - TContext -> => { - const mutationKey = ["postV2ReviewStoreSubmission"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { storeListingVersionId: string; data: ReviewSubmissionRequest } - > = (props) => { - const { storeListingVersionId, data } = props ?? {}; - - return postV2ReviewStoreSubmission( - storeListingVersionId, - data, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2ReviewStoreSubmissionMutationResult = NonNullable< - Awaited> ->; -export type PostV2ReviewStoreSubmissionMutationBody = ReviewSubmissionRequest; -export type PostV2ReviewStoreSubmissionMutationError = HTTPValidationError; - -/** - * @summary Review Store Submission - */ -export const usePostV2ReviewStoreSubmission = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { storeListingVersionId: string; data: ReviewSubmissionRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { storeListingVersionId: string; data: ReviewSubmissionRequest }, - TContext -> => { - const mutationOptions = - getPostV2ReviewStoreSubmissionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Download the agent file by streaming its content. - -Args: - store_listing_version_id (str): The ID of the agent to download - -Returns: - StreamingResponse: A streaming response containing the agent's graph data. - -Raises: - HTTPException: If the agent is not found or an unexpected error occurs. - * @summary Admin Download Agent File - */ -export type getV2AdminDownloadAgentFileResponse200 = { - data: unknown; - status: 200; -}; - -export type getV2AdminDownloadAgentFileResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2AdminDownloadAgentFileResponseComposite = - | getV2AdminDownloadAgentFileResponse200 - | getV2AdminDownloadAgentFileResponse422; - -export type getV2AdminDownloadAgentFileResponse = - getV2AdminDownloadAgentFileResponseComposite & { - headers: Headers; - }; - -export const getGetV2AdminDownloadAgentFileUrl = ( - storeListingVersionId: string, -) => { - return `/api/store/admin/submissions/download/${storeListingVersionId}`; -}; - -export const getV2AdminDownloadAgentFile = async ( - storeListingVersionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2AdminDownloadAgentFileUrl(storeListingVersionId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2AdminDownloadAgentFileQueryKey = ( - storeListingVersionId: string, -) => { - return [ - `/api/store/admin/submissions/download/${storeListingVersionId}`, - ] as const; -}; - -export const getGetV2AdminDownloadAgentFileQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2AdminDownloadAgentFileQueryKey(storeListingVersionId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2AdminDownloadAgentFile(storeListingVersionId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!storeListingVersionId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2AdminDownloadAgentFileQueryResult = NonNullable< - Awaited> ->; -export type GetV2AdminDownloadAgentFileQueryError = HTTPValidationError; - -export function useGetV2AdminDownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2AdminDownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2AdminDownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Admin Download Agent File - */ - -export function useGetV2AdminDownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2AdminDownloadAgentFileQueryOptions( - storeListingVersionId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Admin Download Agent File - */ -export const prefetchGetV2AdminDownloadAgentFileQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2AdminDownloadAgentFileQueryOptions( - storeListingVersionId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Add Credits to User - */ -export type postV2AddCreditsToUserResponse200 = { - data: AddUserCreditsResponse; - status: 200; -}; - -export type postV2AddCreditsToUserResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2AddCreditsToUserResponseComposite = - | postV2AddCreditsToUserResponse200 - | postV2AddCreditsToUserResponse422; - -export type postV2AddCreditsToUserResponse = - postV2AddCreditsToUserResponseComposite & { - headers: Headers; - }; - -export const getPostV2AddCreditsToUserUrl = () => { - return `/api/credits/admin/add_credits`; -}; - -export const postV2AddCreditsToUser = async ( - bodyPostV2AddCreditsToUser: BodyPostV2AddCreditsToUser, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2AddCreditsToUserUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV2AddCreditsToUser), - }, - ); -}; - -export const getPostV2AddCreditsToUserMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddCreditsToUser }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddCreditsToUser }, - TContext -> => { - const mutationKey = ["postV2AddCreditsToUser"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: BodyPostV2AddCreditsToUser } - > = (props) => { - const { data } = props ?? {}; - - return postV2AddCreditsToUser(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2AddCreditsToUserMutationResult = NonNullable< - Awaited> ->; -export type PostV2AddCreditsToUserMutationBody = BodyPostV2AddCreditsToUser; -export type PostV2AddCreditsToUserMutationError = HTTPValidationError; - -/** - * @summary Add Credits to User - */ -export const usePostV2AddCreditsToUser = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddCreditsToUser }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: BodyPostV2AddCreditsToUser }, - TContext -> => { - const mutationOptions = getPostV2AddCreditsToUserMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get All Users History - */ -export type getV2GetAllUsersHistoryResponse200 = { - data: UserHistoryResponse; - status: 200; -}; - -export type getV2GetAllUsersHistoryResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetAllUsersHistoryResponseComposite = - | getV2GetAllUsersHistoryResponse200 - | getV2GetAllUsersHistoryResponse422; - -export type getV2GetAllUsersHistoryResponse = - getV2GetAllUsersHistoryResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetAllUsersHistoryUrl = ( - params?: GetV2GetAllUsersHistoryParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/credits/admin/users_history?${stringifiedParams}` - : `/api/credits/admin/users_history`; -}; - -export const getV2GetAllUsersHistory = async ( - params?: GetV2GetAllUsersHistoryParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetAllUsersHistoryUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetAllUsersHistoryQueryKey = ( - params?: GetV2GetAllUsersHistoryParams, -) => { - return [ - `/api/credits/admin/users_history`, - ...(params ? [params] : []), - ] as const; -}; - -export const getGetV2GetAllUsersHistoryQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAllUsersHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2GetAllUsersHistoryQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetAllUsersHistory(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetAllUsersHistoryQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetAllUsersHistoryQueryError = HTTPValidationError; - -export function useGetV2GetAllUsersHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2GetAllUsersHistoryParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAllUsersHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAllUsersHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAllUsersHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAllUsersHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get All Users History - */ - -export function useGetV2GetAllUsersHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2GetAllUsersHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetAllUsersHistoryQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get All Users History - */ -export const prefetchGetV2GetAllUsersHistoryQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2GetAllUsersHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetAllUsersHistoryQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/analytics/analytics.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/analytics/analytics.ts deleted file mode 100644 index a297a7b0b74c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/analytics/analytics.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation } from "@tanstack/react-query"; -import type { - MutationFunction, - QueryClient, - UseMutationOptions, - UseMutationResult, -} from "@tanstack/react-query"; - -import type { BodyPostV1LogRawAnalytics } from "../../models/bodyPostV1LogRawAnalytics"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { LogRawMetricRequest } from "../../models/logRawMetricRequest"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Log Raw Metric - */ -export type postV1LogRawMetricResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1LogRawMetricResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1LogRawMetricResponseComposite = - | postV1LogRawMetricResponse200 - | postV1LogRawMetricResponse422; - -export type postV1LogRawMetricResponse = postV1LogRawMetricResponseComposite & { - headers: Headers; -}; - -export const getPostV1LogRawMetricUrl = () => { - return `/api/analytics/log_raw_metric`; -}; - -export const postV1LogRawMetric = async ( - logRawMetricRequest: LogRawMetricRequest, - options?: RequestInit, -): Promise => { - return customMutator(getPostV1LogRawMetricUrl(), { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(logRawMetricRequest), - }); -}; - -export const getPostV1LogRawMetricMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: LogRawMetricRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: LogRawMetricRequest }, - TContext -> => { - const mutationKey = ["postV1LogRawMetric"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: LogRawMetricRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV1LogRawMetric(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1LogRawMetricMutationResult = NonNullable< - Awaited> ->; -export type PostV1LogRawMetricMutationBody = LogRawMetricRequest; -export type PostV1LogRawMetricMutationError = HTTPValidationError; - -/** - * @summary Log Raw Metric - */ -export const usePostV1LogRawMetric = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: LogRawMetricRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: LogRawMetricRequest }, - TContext -> => { - const mutationOptions = getPostV1LogRawMetricMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Log Raw Analytics - */ -export type postV1LogRawAnalyticsResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1LogRawAnalyticsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1LogRawAnalyticsResponseComposite = - | postV1LogRawAnalyticsResponse200 - | postV1LogRawAnalyticsResponse422; - -export type postV1LogRawAnalyticsResponse = - postV1LogRawAnalyticsResponseComposite & { - headers: Headers; - }; - -export const getPostV1LogRawAnalyticsUrl = () => { - return `/api/analytics/log_raw_analytics`; -}; - -export const postV1LogRawAnalytics = async ( - bodyPostV1LogRawAnalytics: BodyPostV1LogRawAnalytics, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1LogRawAnalyticsUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV1LogRawAnalytics), - }, - ); -}; - -export const getPostV1LogRawAnalyticsMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV1LogRawAnalytics }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV1LogRawAnalytics }, - TContext -> => { - const mutationKey = ["postV1LogRawAnalytics"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: BodyPostV1LogRawAnalytics } - > = (props) => { - const { data } = props ?? {}; - - return postV1LogRawAnalytics(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1LogRawAnalyticsMutationResult = NonNullable< - Awaited> ->; -export type PostV1LogRawAnalyticsMutationBody = BodyPostV1LogRawAnalytics; -export type PostV1LogRawAnalyticsMutationError = HTTPValidationError; - -/** - * @summary Log Raw Analytics - */ -export const usePostV1LogRawAnalytics = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV1LogRawAnalytics }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: BodyPostV1LogRawAnalytics }, - TContext -> => { - const mutationOptions = getPostV1LogRawAnalyticsMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/api-keys/api-keys.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/api-keys/api-keys.ts deleted file mode 100644 index 3dc942a020a7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/api-keys/api-keys.ts +++ /dev/null @@ -1,910 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { APIKeyWithoutHash } from "../../models/aPIKeyWithoutHash"; - -import type { CreateAPIKeyRequest } from "../../models/createAPIKeyRequest"; - -import type { CreateAPIKeyResponse } from "../../models/createAPIKeyResponse"; - -import type { GetV1ListUserApiKeys200 } from "../../models/getV1ListUserApiKeys200"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { UpdatePermissionsRequest } from "../../models/updatePermissionsRequest"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * List all API keys for the user - * @summary List user API keys - */ -export type getV1ListUserApiKeysResponse200 = { - data: GetV1ListUserApiKeys200; - status: 200; -}; - -export type getV1ListUserApiKeysResponseComposite = - getV1ListUserApiKeysResponse200; - -export type getV1ListUserApiKeysResponse = - getV1ListUserApiKeysResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListUserApiKeysUrl = () => { - return `/api/api-keys`; -}; - -export const getV1ListUserApiKeys = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListUserApiKeysUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListUserApiKeysQueryKey = () => { - return [`/api/api-keys`] as const; -}; - -export const getGetV1ListUserApiKeysQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1ListUserApiKeysQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ListUserApiKeys({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListUserApiKeysQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListUserApiKeysQueryError = unknown; - -export function useGetV1ListUserApiKeys< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListUserApiKeys< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListUserApiKeys< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List user API keys - */ - -export function useGetV1ListUserApiKeys< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListUserApiKeysQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List user API keys - */ -export const prefetchGetV1ListUserApiKeysQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListUserApiKeysQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Create a new API key - * @summary Create new API key - */ -export type postV1CreateNewApiKeyResponse200 = { - data: CreateAPIKeyResponse; - status: 200; -}; - -export type postV1CreateNewApiKeyResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1CreateNewApiKeyResponseComposite = - | postV1CreateNewApiKeyResponse200 - | postV1CreateNewApiKeyResponse422; - -export type postV1CreateNewApiKeyResponse = - postV1CreateNewApiKeyResponseComposite & { - headers: Headers; - }; - -export const getPostV1CreateNewApiKeyUrl = () => { - return `/api/api-keys`; -}; - -export const postV1CreateNewApiKey = async ( - createAPIKeyRequest: CreateAPIKeyRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1CreateNewApiKeyUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(createAPIKeyRequest), - }, - ); -}; - -export const getPostV1CreateNewApiKeyMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: CreateAPIKeyRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: CreateAPIKeyRequest }, - TContext -> => { - const mutationKey = ["postV1CreateNewApiKey"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: CreateAPIKeyRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV1CreateNewApiKey(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1CreateNewApiKeyMutationResult = NonNullable< - Awaited> ->; -export type PostV1CreateNewApiKeyMutationBody = CreateAPIKeyRequest; -export type PostV1CreateNewApiKeyMutationError = HTTPValidationError; - -/** - * @summary Create new API key - */ -export const usePostV1CreateNewApiKey = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: CreateAPIKeyRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: CreateAPIKeyRequest }, - TContext -> => { - const mutationOptions = getPostV1CreateNewApiKeyMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Get a specific API key - * @summary Get specific API key - */ -export type getV1GetSpecificApiKeyResponse200 = { - data: APIKeyWithoutHash; - status: 200; -}; - -export type getV1GetSpecificApiKeyResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetSpecificApiKeyResponseComposite = - | getV1GetSpecificApiKeyResponse200 - | getV1GetSpecificApiKeyResponse422; - -export type getV1GetSpecificApiKeyResponse = - getV1GetSpecificApiKeyResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetSpecificApiKeyUrl = (keyId: string) => { - return `/api/api-keys/${keyId}`; -}; - -export const getV1GetSpecificApiKey = async ( - keyId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetSpecificApiKeyUrl(keyId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetSpecificApiKeyQueryKey = (keyId: string) => { - return [`/api/api-keys/${keyId}`] as const; -}; - -export const getGetV1GetSpecificApiKeyQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - keyId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetSpecificApiKeyQueryKey(keyId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetSpecificApiKey(keyId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!keyId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetSpecificApiKeyQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetSpecificApiKeyQueryError = HTTPValidationError; - -export function useGetV1GetSpecificApiKey< - TData = Awaited>, - TError = HTTPValidationError, ->( - keyId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetSpecificApiKey< - TData = Awaited>, - TError = HTTPValidationError, ->( - keyId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetSpecificApiKey< - TData = Awaited>, - TError = HTTPValidationError, ->( - keyId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get specific API key - */ - -export function useGetV1GetSpecificApiKey< - TData = Awaited>, - TError = HTTPValidationError, ->( - keyId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetSpecificApiKeyQueryOptions(keyId, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get specific API key - */ -export const prefetchGetV1GetSpecificApiKeyQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - keyId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetSpecificApiKeyQueryOptions(keyId, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Revoke an API key - * @summary Revoke API key - */ -export type deleteV1RevokeApiKeyResponse200 = { - data: APIKeyWithoutHash; - status: 200; -}; - -export type deleteV1RevokeApiKeyResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV1RevokeApiKeyResponseComposite = - | deleteV1RevokeApiKeyResponse200 - | deleteV1RevokeApiKeyResponse422; - -export type deleteV1RevokeApiKeyResponse = - deleteV1RevokeApiKeyResponseComposite & { - headers: Headers; - }; - -export const getDeleteV1RevokeApiKeyUrl = (keyId: string) => { - return `/api/api-keys/${keyId}`; -}; - -export const deleteV1RevokeApiKey = async ( - keyId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV1RevokeApiKeyUrl(keyId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV1RevokeApiKeyMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext -> => { - const mutationKey = ["deleteV1RevokeApiKey"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { keyId: string } - > = (props) => { - const { keyId } = props ?? {}; - - return deleteV1RevokeApiKey(keyId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV1RevokeApiKeyMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV1RevokeApiKeyMutationError = HTTPValidationError; - -/** - * @summary Revoke API key - */ -export const useDeleteV1RevokeApiKey = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { keyId: string }, - TContext -> => { - const mutationOptions = getDeleteV1RevokeApiKeyMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Suspend an API key - * @summary Suspend API key - */ -export type postV1SuspendApiKeyResponse200 = { - data: APIKeyWithoutHash; - status: 200; -}; - -export type postV1SuspendApiKeyResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1SuspendApiKeyResponseComposite = - | postV1SuspendApiKeyResponse200 - | postV1SuspendApiKeyResponse422; - -export type postV1SuspendApiKeyResponse = - postV1SuspendApiKeyResponseComposite & { - headers: Headers; - }; - -export const getPostV1SuspendApiKeyUrl = (keyId: string) => { - return `/api/api-keys/${keyId}/suspend`; -}; - -export const postV1SuspendApiKey = async ( - keyId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1SuspendApiKeyUrl(keyId), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1SuspendApiKeyMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext -> => { - const mutationKey = ["postV1SuspendApiKey"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { keyId: string } - > = (props) => { - const { keyId } = props ?? {}; - - return postV1SuspendApiKey(keyId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1SuspendApiKeyMutationResult = NonNullable< - Awaited> ->; - -export type PostV1SuspendApiKeyMutationError = HTTPValidationError; - -/** - * @summary Suspend API key - */ -export const usePostV1SuspendApiKey = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { keyId: string }, - TContext -> => { - const mutationOptions = getPostV1SuspendApiKeyMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Update API key permissions - * @summary Update key permissions - */ -export type putV1UpdateKeyPermissionsResponse200 = { - data: APIKeyWithoutHash; - status: 200; -}; - -export type putV1UpdateKeyPermissionsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type putV1UpdateKeyPermissionsResponseComposite = - | putV1UpdateKeyPermissionsResponse200 - | putV1UpdateKeyPermissionsResponse422; - -export type putV1UpdateKeyPermissionsResponse = - putV1UpdateKeyPermissionsResponseComposite & { - headers: Headers; - }; - -export const getPutV1UpdateKeyPermissionsUrl = (keyId: string) => { - return `/api/api-keys/${keyId}/permissions`; -}; - -export const putV1UpdateKeyPermissions = async ( - keyId: string, - updatePermissionsRequest: UpdatePermissionsRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPutV1UpdateKeyPermissionsUrl(keyId), - { - ...options, - method: "PUT", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(updatePermissionsRequest), - }, - ); -}; - -export const getPutV1UpdateKeyPermissionsMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string; data: UpdatePermissionsRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { keyId: string; data: UpdatePermissionsRequest }, - TContext -> => { - const mutationKey = ["putV1UpdateKeyPermissions"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { keyId: string; data: UpdatePermissionsRequest } - > = (props) => { - const { keyId, data } = props ?? {}; - - return putV1UpdateKeyPermissions(keyId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PutV1UpdateKeyPermissionsMutationResult = NonNullable< - Awaited> ->; -export type PutV1UpdateKeyPermissionsMutationBody = UpdatePermissionsRequest; -export type PutV1UpdateKeyPermissionsMutationError = HTTPValidationError; - -/** - * @summary Update key permissions - */ -export const usePutV1UpdateKeyPermissions = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { keyId: string; data: UpdatePermissionsRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { keyId: string; data: UpdatePermissionsRequest }, - TContext -> => { - const mutationOptions = getPutV1UpdateKeyPermissionsMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/auth/auth.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/auth/auth.ts deleted file mode 100644 index 1bb7d62d0ef2..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/auth/auth.ts +++ /dev/null @@ -1,561 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { NotificationPreference } from "../../models/notificationPreference"; - -import type { NotificationPreferenceDTO } from "../../models/notificationPreferenceDTO"; - -import type { PostV1UpdateUserEmail200 } from "../../models/postV1UpdateUserEmail200"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Get or create user - */ -export type postV1GetOrCreateUserResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1GetOrCreateUserResponseComposite = - postV1GetOrCreateUserResponse200; - -export type postV1GetOrCreateUserResponse = - postV1GetOrCreateUserResponseComposite & { - headers: Headers; - }; - -export const getPostV1GetOrCreateUserUrl = () => { - return `/api/auth/user`; -}; - -export const postV1GetOrCreateUser = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1GetOrCreateUserUrl(), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1GetOrCreateUserMutationOptions = < - TError = unknown, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - void, - TContext -> => { - const mutationKey = ["postV1GetOrCreateUser"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - void - > = () => { - return postV1GetOrCreateUser(requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1GetOrCreateUserMutationResult = NonNullable< - Awaited> ->; - -export type PostV1GetOrCreateUserMutationError = unknown; - -/** - * @summary Get or create user - */ -export const usePostV1GetOrCreateUser = ( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - void, - TContext -> => { - const mutationOptions = getPostV1GetOrCreateUserMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Update user email - */ -export type postV1UpdateUserEmailResponse200 = { - data: PostV1UpdateUserEmail200; - status: 200; -}; - -export type postV1UpdateUserEmailResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1UpdateUserEmailResponseComposite = - | postV1UpdateUserEmailResponse200 - | postV1UpdateUserEmailResponse422; - -export type postV1UpdateUserEmailResponse = - postV1UpdateUserEmailResponseComposite & { - headers: Headers; - }; - -export const getPostV1UpdateUserEmailUrl = () => { - return `/api/auth/user/email`; -}; - -export const postV1UpdateUserEmail = async ( - postV1UpdateUserEmailBody: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1UpdateUserEmailUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV1UpdateUserEmailBody), - }, - ); -}; - -export const getPostV1UpdateUserEmailMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: string }, - TContext -> => { - const mutationKey = ["postV1UpdateUserEmail"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: string } - > = (props) => { - const { data } = props ?? {}; - - return postV1UpdateUserEmail(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1UpdateUserEmailMutationResult = NonNullable< - Awaited> ->; -export type PostV1UpdateUserEmailMutationBody = string; -export type PostV1UpdateUserEmailMutationError = HTTPValidationError; - -/** - * @summary Update user email - */ -export const usePostV1UpdateUserEmail = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: string }, - TContext -> => { - const mutationOptions = getPostV1UpdateUserEmailMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get notification preferences - */ -export type getV1GetNotificationPreferencesResponse200 = { - data: NotificationPreference; - status: 200; -}; - -export type getV1GetNotificationPreferencesResponseComposite = - getV1GetNotificationPreferencesResponse200; - -export type getV1GetNotificationPreferencesResponse = - getV1GetNotificationPreferencesResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetNotificationPreferencesUrl = () => { - return `/api/auth/user/preferences`; -}; - -export const getV1GetNotificationPreferences = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetNotificationPreferencesUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetNotificationPreferencesQueryKey = () => { - return [`/api/auth/user/preferences`] as const; -}; - -export const getGetV1GetNotificationPreferencesQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetNotificationPreferencesQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetNotificationPreferences({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetNotificationPreferencesQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetNotificationPreferencesQueryError = unknown; - -export function useGetV1GetNotificationPreferences< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetNotificationPreferences< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetNotificationPreferences< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get notification preferences - */ - -export function useGetV1GetNotificationPreferences< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetNotificationPreferencesQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get notification preferences - */ -export const prefetchGetV1GetNotificationPreferencesQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetNotificationPreferencesQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Update notification preferences - */ -export type postV1UpdateNotificationPreferencesResponse200 = { - data: NotificationPreference; - status: 200; -}; - -export type postV1UpdateNotificationPreferencesResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1UpdateNotificationPreferencesResponseComposite = - | postV1UpdateNotificationPreferencesResponse200 - | postV1UpdateNotificationPreferencesResponse422; - -export type postV1UpdateNotificationPreferencesResponse = - postV1UpdateNotificationPreferencesResponseComposite & { - headers: Headers; - }; - -export const getPostV1UpdateNotificationPreferencesUrl = () => { - return `/api/auth/user/preferences`; -}; - -export const postV1UpdateNotificationPreferences = async ( - notificationPreferenceDTO: NotificationPreferenceDTO, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1UpdateNotificationPreferencesUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(notificationPreferenceDTO), - }, - ); -}; - -export const getPostV1UpdateNotificationPreferencesMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: NotificationPreferenceDTO }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: NotificationPreferenceDTO }, - TContext -> => { - const mutationKey = ["postV1UpdateNotificationPreferences"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: NotificationPreferenceDTO } - > = (props) => { - const { data } = props ?? {}; - - return postV1UpdateNotificationPreferences(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1UpdateNotificationPreferencesMutationResult = NonNullable< - Awaited> ->; -export type PostV1UpdateNotificationPreferencesMutationBody = - NotificationPreferenceDTO; -export type PostV1UpdateNotificationPreferencesMutationError = - HTTPValidationError; - -/** - * @summary Update notification preferences - */ -export const usePostV1UpdateNotificationPreferences = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: NotificationPreferenceDTO }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: NotificationPreferenceDTO }, - TContext -> => { - const mutationOptions = - getPostV1UpdateNotificationPreferencesMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/blocks/blocks.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/blocks/blocks.ts deleted file mode 100644 index 5de872000e9d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/blocks/blocks.ts +++ /dev/null @@ -1,348 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { GetV1ListAvailableBlocks200Item } from "../../models/getV1ListAvailableBlocks200Item"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { PostV1ExecuteGraphBlock200 } from "../../models/postV1ExecuteGraphBlock200"; - -import type { PostV1ExecuteGraphBlockBody } from "../../models/postV1ExecuteGraphBlockBody"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary List available blocks - */ -export type getV1ListAvailableBlocksResponse200 = { - data: GetV1ListAvailableBlocks200Item[]; - status: 200; -}; - -export type getV1ListAvailableBlocksResponseComposite = - getV1ListAvailableBlocksResponse200; - -export type getV1ListAvailableBlocksResponse = - getV1ListAvailableBlocksResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListAvailableBlocksUrl = () => { - return `/api/blocks`; -}; - -export const getV1ListAvailableBlocks = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListAvailableBlocksUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListAvailableBlocksQueryKey = () => { - return [`/api/blocks`] as const; -}; - -export const getGetV1ListAvailableBlocksQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1ListAvailableBlocksQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ListAvailableBlocks({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListAvailableBlocksQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListAvailableBlocksQueryError = unknown; - -export function useGetV1ListAvailableBlocks< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListAvailableBlocks< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListAvailableBlocks< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List available blocks - */ - -export function useGetV1ListAvailableBlocks< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListAvailableBlocksQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List available blocks - */ -export const prefetchGetV1ListAvailableBlocksQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListAvailableBlocksQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Execute graph block - */ -export type postV1ExecuteGraphBlockResponse200 = { - data: PostV1ExecuteGraphBlock200; - status: 200; -}; - -export type postV1ExecuteGraphBlockResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1ExecuteGraphBlockResponseComposite = - | postV1ExecuteGraphBlockResponse200 - | postV1ExecuteGraphBlockResponse422; - -export type postV1ExecuteGraphBlockResponse = - postV1ExecuteGraphBlockResponseComposite & { - headers: Headers; - }; - -export const getPostV1ExecuteGraphBlockUrl = (blockId: string) => { - return `/api/blocks/${blockId}/execute`; -}; - -export const postV1ExecuteGraphBlock = async ( - blockId: string, - postV1ExecuteGraphBlockBody: PostV1ExecuteGraphBlockBody, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1ExecuteGraphBlockUrl(blockId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV1ExecuteGraphBlockBody), - }, - ); -}; - -export const getPostV1ExecuteGraphBlockMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { blockId: string; data: PostV1ExecuteGraphBlockBody }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { blockId: string; data: PostV1ExecuteGraphBlockBody }, - TContext -> => { - const mutationKey = ["postV1ExecuteGraphBlock"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { blockId: string; data: PostV1ExecuteGraphBlockBody } - > = (props) => { - const { blockId, data } = props ?? {}; - - return postV1ExecuteGraphBlock(blockId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1ExecuteGraphBlockMutationResult = NonNullable< - Awaited> ->; -export type PostV1ExecuteGraphBlockMutationBody = PostV1ExecuteGraphBlockBody; -export type PostV1ExecuteGraphBlockMutationError = HTTPValidationError; - -/** - * @summary Execute graph block - */ -export const usePostV1ExecuteGraphBlock = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { blockId: string; data: PostV1ExecuteGraphBlockBody }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { blockId: string; data: PostV1ExecuteGraphBlockBody }, - TContext -> => { - const mutationOptions = getPostV1ExecuteGraphBlockMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/credits/credits.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/credits/credits.ts deleted file mode 100644 index 2607388104e1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/credits/credits.ts +++ /dev/null @@ -1,1611 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { AutoTopUpConfig } from "../../models/autoTopUpConfig"; - -import type { GetV1GetCreditHistoryParams } from "../../models/getV1GetCreditHistoryParams"; - -import type { GetV1GetUserCredits200 } from "../../models/getV1GetUserCredits200"; - -import type { GetV1ManagePaymentMethods200 } from "../../models/getV1ManagePaymentMethods200"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { PostV1RefundCreditTransactionBody } from "../../models/postV1RefundCreditTransactionBody"; - -import type { RefundRequest } from "../../models/refundRequest"; - -import type { RequestTopUp } from "../../models/requestTopUp"; - -import type { TransactionHistory } from "../../models/transactionHistory"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Get user credits - */ -export type getV1GetUserCreditsResponse200 = { - data: GetV1GetUserCredits200; - status: 200; -}; - -export type getV1GetUserCreditsResponseComposite = - getV1GetUserCreditsResponse200; - -export type getV1GetUserCreditsResponse = - getV1GetUserCreditsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetUserCreditsUrl = () => { - return `/api/credits`; -}; - -export const getV1GetUserCredits = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetUserCreditsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetUserCreditsQueryKey = () => { - return [`/api/credits`] as const; -}; - -export const getGetV1GetUserCreditsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1GetUserCreditsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetUserCredits({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetUserCreditsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetUserCreditsQueryError = unknown; - -export function useGetV1GetUserCredits< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetUserCredits< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetUserCredits< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get user credits - */ - -export function useGetV1GetUserCredits< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetUserCreditsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get user credits - */ -export const prefetchGetV1GetUserCreditsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetUserCreditsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Request credit top up - */ -export type postV1RequestCreditTopUpResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1RequestCreditTopUpResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1RequestCreditTopUpResponseComposite = - | postV1RequestCreditTopUpResponse200 - | postV1RequestCreditTopUpResponse422; - -export type postV1RequestCreditTopUpResponse = - postV1RequestCreditTopUpResponseComposite & { - headers: Headers; - }; - -export const getPostV1RequestCreditTopUpUrl = () => { - return `/api/credits`; -}; - -export const postV1RequestCreditTopUp = async ( - requestTopUp: RequestTopUp, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1RequestCreditTopUpUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(requestTopUp), - }, - ); -}; - -export const getPostV1RequestCreditTopUpMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: RequestTopUp }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: RequestTopUp }, - TContext -> => { - const mutationKey = ["postV1RequestCreditTopUp"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: RequestTopUp } - > = (props) => { - const { data } = props ?? {}; - - return postV1RequestCreditTopUp(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1RequestCreditTopUpMutationResult = NonNullable< - Awaited> ->; -export type PostV1RequestCreditTopUpMutationBody = RequestTopUp; -export type PostV1RequestCreditTopUpMutationError = HTTPValidationError; - -/** - * @summary Request credit top up - */ -export const usePostV1RequestCreditTopUp = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: RequestTopUp }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: RequestTopUp }, - TContext -> => { - const mutationOptions = getPostV1RequestCreditTopUpMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Fulfill checkout session - */ -export type patchV1FulfillCheckoutSessionResponse200 = { - data: unknown; - status: 200; -}; - -export type patchV1FulfillCheckoutSessionResponseComposite = - patchV1FulfillCheckoutSessionResponse200; - -export type patchV1FulfillCheckoutSessionResponse = - patchV1FulfillCheckoutSessionResponseComposite & { - headers: Headers; - }; - -export const getPatchV1FulfillCheckoutSessionUrl = () => { - return `/api/credits`; -}; - -export const patchV1FulfillCheckoutSession = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getPatchV1FulfillCheckoutSessionUrl(), - { - ...options, - method: "PATCH", - }, - ); -}; - -export const getPatchV1FulfillCheckoutSessionMutationOptions = < - TError = unknown, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - void, - TContext -> => { - const mutationKey = ["patchV1FulfillCheckoutSession"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - void - > = () => { - return patchV1FulfillCheckoutSession(requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PatchV1FulfillCheckoutSessionMutationResult = NonNullable< - Awaited> ->; - -export type PatchV1FulfillCheckoutSessionMutationError = unknown; - -/** - * @summary Fulfill checkout session - */ -export const usePatchV1FulfillCheckoutSession = < - TError = unknown, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - void, - TContext -> => { - const mutationOptions = - getPatchV1FulfillCheckoutSessionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Refund credit transaction - */ -export type postV1RefundCreditTransactionResponse200 = { - data: number; - status: 200; -}; - -export type postV1RefundCreditTransactionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1RefundCreditTransactionResponseComposite = - | postV1RefundCreditTransactionResponse200 - | postV1RefundCreditTransactionResponse422; - -export type postV1RefundCreditTransactionResponse = - postV1RefundCreditTransactionResponseComposite & { - headers: Headers; - }; - -export const getPostV1RefundCreditTransactionUrl = (transactionKey: string) => { - return `/api/credits/${transactionKey}/refund`; -}; - -export const postV1RefundCreditTransaction = async ( - transactionKey: string, - postV1RefundCreditTransactionBody: PostV1RefundCreditTransactionBody, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1RefundCreditTransactionUrl(transactionKey), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV1RefundCreditTransactionBody), - }, - ); -}; - -export const getPostV1RefundCreditTransactionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { transactionKey: string; data: PostV1RefundCreditTransactionBody }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { transactionKey: string; data: PostV1RefundCreditTransactionBody }, - TContext -> => { - const mutationKey = ["postV1RefundCreditTransaction"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { transactionKey: string; data: PostV1RefundCreditTransactionBody } - > = (props) => { - const { transactionKey, data } = props ?? {}; - - return postV1RefundCreditTransaction(transactionKey, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1RefundCreditTransactionMutationResult = NonNullable< - Awaited> ->; -export type PostV1RefundCreditTransactionMutationBody = - PostV1RefundCreditTransactionBody; -export type PostV1RefundCreditTransactionMutationError = HTTPValidationError; - -/** - * @summary Refund credit transaction - */ -export const usePostV1RefundCreditTransaction = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { transactionKey: string; data: PostV1RefundCreditTransactionBody }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { transactionKey: string; data: PostV1RefundCreditTransactionBody }, - TContext -> => { - const mutationOptions = - getPostV1RefundCreditTransactionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get auto top up - */ -export type getV1GetAutoTopUpResponse200 = { - data: AutoTopUpConfig; - status: 200; -}; - -export type getV1GetAutoTopUpResponseComposite = getV1GetAutoTopUpResponse200; - -export type getV1GetAutoTopUpResponse = getV1GetAutoTopUpResponseComposite & { - headers: Headers; -}; - -export const getGetV1GetAutoTopUpUrl = () => { - return `/api/credits/auto-top-up`; -}; - -export const getV1GetAutoTopUp = async ( - options?: RequestInit, -): Promise => { - return customMutator(getGetV1GetAutoTopUpUrl(), { - ...options, - method: "GET", - }); -}; - -export const getGetV1GetAutoTopUpQueryKey = () => { - return [`/api/credits/auto-top-up`] as const; -}; - -export const getGetV1GetAutoTopUpQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1GetAutoTopUpQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetAutoTopUp({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetAutoTopUpQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetAutoTopUpQueryError = unknown; - -export function useGetV1GetAutoTopUp< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAutoTopUp< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAutoTopUp< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get auto top up - */ - -export function useGetV1GetAutoTopUp< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetAutoTopUpQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get auto top up - */ -export const prefetchGetV1GetAutoTopUpQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetAutoTopUpQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Configure auto top up - */ -export type postV1ConfigureAutoTopUpResponse200 = { - data: string; - status: 200; -}; - -export type postV1ConfigureAutoTopUpResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1ConfigureAutoTopUpResponseComposite = - | postV1ConfigureAutoTopUpResponse200 - | postV1ConfigureAutoTopUpResponse422; - -export type postV1ConfigureAutoTopUpResponse = - postV1ConfigureAutoTopUpResponseComposite & { - headers: Headers; - }; - -export const getPostV1ConfigureAutoTopUpUrl = () => { - return `/api/credits/auto-top-up`; -}; - -export const postV1ConfigureAutoTopUp = async ( - autoTopUpConfig: AutoTopUpConfig, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1ConfigureAutoTopUpUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(autoTopUpConfig), - }, - ); -}; - -export const getPostV1ConfigureAutoTopUpMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AutoTopUpConfig }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: AutoTopUpConfig }, - TContext -> => { - const mutationKey = ["postV1ConfigureAutoTopUp"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: AutoTopUpConfig } - > = (props) => { - const { data } = props ?? {}; - - return postV1ConfigureAutoTopUp(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1ConfigureAutoTopUpMutationResult = NonNullable< - Awaited> ->; -export type PostV1ConfigureAutoTopUpMutationBody = AutoTopUpConfig; -export type PostV1ConfigureAutoTopUpMutationError = HTTPValidationError; - -/** - * @summary Configure auto top up - */ -export const usePostV1ConfigureAutoTopUp = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: AutoTopUpConfig }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: AutoTopUpConfig }, - TContext -> => { - const mutationOptions = getPostV1ConfigureAutoTopUpMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Handle Stripe webhooks - */ -export type postV1HandleStripeWebhooksResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1HandleStripeWebhooksResponseComposite = - postV1HandleStripeWebhooksResponse200; - -export type postV1HandleStripeWebhooksResponse = - postV1HandleStripeWebhooksResponseComposite & { - headers: Headers; - }; - -export const getPostV1HandleStripeWebhooksUrl = () => { - return `/api/credits/stripe_webhook`; -}; - -export const postV1HandleStripeWebhooks = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1HandleStripeWebhooksUrl(), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1HandleStripeWebhooksMutationOptions = < - TError = unknown, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - void, - TContext -> => { - const mutationKey = ["postV1HandleStripeWebhooks"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - void - > = () => { - return postV1HandleStripeWebhooks(requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1HandleStripeWebhooksMutationResult = NonNullable< - Awaited> ->; - -export type PostV1HandleStripeWebhooksMutationError = unknown; - -/** - * @summary Handle Stripe webhooks - */ -export const usePostV1HandleStripeWebhooks = < - TError = unknown, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - void, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - void, - TContext -> => { - const mutationOptions = getPostV1HandleStripeWebhooksMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Manage payment methods - */ -export type getV1ManagePaymentMethodsResponse200 = { - data: GetV1ManagePaymentMethods200; - status: 200; -}; - -export type getV1ManagePaymentMethodsResponseComposite = - getV1ManagePaymentMethodsResponse200; - -export type getV1ManagePaymentMethodsResponse = - getV1ManagePaymentMethodsResponseComposite & { - headers: Headers; - }; - -export const getGetV1ManagePaymentMethodsUrl = () => { - return `/api/credits/manage`; -}; - -export const getV1ManagePaymentMethods = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ManagePaymentMethodsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ManagePaymentMethodsQueryKey = () => { - return [`/api/credits/manage`] as const; -}; - -export const getGetV1ManagePaymentMethodsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1ManagePaymentMethodsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ManagePaymentMethods({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ManagePaymentMethodsQueryResult = NonNullable< - Awaited> ->; -export type GetV1ManagePaymentMethodsQueryError = unknown; - -export function useGetV1ManagePaymentMethods< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ManagePaymentMethods< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ManagePaymentMethods< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Manage payment methods - */ - -export function useGetV1ManagePaymentMethods< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ManagePaymentMethodsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Manage payment methods - */ -export const prefetchGetV1ManagePaymentMethodsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ManagePaymentMethodsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get credit history - */ -export type getV1GetCreditHistoryResponse200 = { - data: TransactionHistory; - status: 200; -}; - -export type getV1GetCreditHistoryResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetCreditHistoryResponseComposite = - | getV1GetCreditHistoryResponse200 - | getV1GetCreditHistoryResponse422; - -export type getV1GetCreditHistoryResponse = - getV1GetCreditHistoryResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetCreditHistoryUrl = ( - params?: GetV1GetCreditHistoryParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/credits/transactions?${stringifiedParams}` - : `/api/credits/transactions`; -}; - -export const getV1GetCreditHistory = async ( - params?: GetV1GetCreditHistoryParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetCreditHistoryUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetCreditHistoryQueryKey = ( - params?: GetV1GetCreditHistoryParams, -) => { - return [`/api/credits/transactions`, ...(params ? [params] : [])] as const; -}; - -export const getGetV1GetCreditHistoryQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV1GetCreditHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetCreditHistoryQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetCreditHistory(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetCreditHistoryQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetCreditHistoryQueryError = HTTPValidationError; - -export function useGetV1GetCreditHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV1GetCreditHistoryParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetCreditHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV1GetCreditHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetCreditHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV1GetCreditHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get credit history - */ - -export function useGetV1GetCreditHistory< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV1GetCreditHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetCreditHistoryQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get credit history - */ -export const prefetchGetV1GetCreditHistoryQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV1GetCreditHistoryParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetCreditHistoryQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get refund requests - */ -export type getV1GetRefundRequestsResponse200 = { - data: RefundRequest[]; - status: 200; -}; - -export type getV1GetRefundRequestsResponseComposite = - getV1GetRefundRequestsResponse200; - -export type getV1GetRefundRequestsResponse = - getV1GetRefundRequestsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetRefundRequestsUrl = () => { - return `/api/credits/refunds`; -}; - -export const getV1GetRefundRequests = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetRefundRequestsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetRefundRequestsQueryKey = () => { - return [`/api/credits/refunds`] as const; -}; - -export const getGetV1GetRefundRequestsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetRefundRequestsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetRefundRequests({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetRefundRequestsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetRefundRequestsQueryError = unknown; - -export function useGetV1GetRefundRequests< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetRefundRequests< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetRefundRequests< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get refund requests - */ - -export function useGetV1GetRefundRequests< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetRefundRequestsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get refund requests - */ -export const prefetchGetV1GetRefundRequestsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetRefundRequestsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/email/email.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/email/email.ts deleted file mode 100644 index fd0d22ee0bfa..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/email/email.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation } from "@tanstack/react-query"; -import type { - MutationFunction, - QueryClient, - UseMutationOptions, - UseMutationResult, -} from "@tanstack/react-query"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { PostV1HandlePostmarkEmailWebhooksBody } from "../../models/postV1HandlePostmarkEmailWebhooksBody"; - -import type { PostV1OneClickEmailUnsubscribeParams } from "../../models/postV1OneClickEmailUnsubscribeParams"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary One Click Email Unsubscribe - */ -export type postV1OneClickEmailUnsubscribeResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1OneClickEmailUnsubscribeResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1OneClickEmailUnsubscribeResponseComposite = - | postV1OneClickEmailUnsubscribeResponse200 - | postV1OneClickEmailUnsubscribeResponse422; - -export type postV1OneClickEmailUnsubscribeResponse = - postV1OneClickEmailUnsubscribeResponseComposite & { - headers: Headers; - }; - -export const getPostV1OneClickEmailUnsubscribeUrl = ( - params: PostV1OneClickEmailUnsubscribeParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/email/unsubscribe?${stringifiedParams}` - : `/api/email/unsubscribe`; -}; - -export const postV1OneClickEmailUnsubscribe = async ( - params: PostV1OneClickEmailUnsubscribeParams, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1OneClickEmailUnsubscribeUrl(params), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1OneClickEmailUnsubscribeMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { params: PostV1OneClickEmailUnsubscribeParams }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { params: PostV1OneClickEmailUnsubscribeParams }, - TContext -> => { - const mutationKey = ["postV1OneClickEmailUnsubscribe"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { params: PostV1OneClickEmailUnsubscribeParams } - > = (props) => { - const { params } = props ?? {}; - - return postV1OneClickEmailUnsubscribe(params, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1OneClickEmailUnsubscribeMutationResult = NonNullable< - Awaited> ->; - -export type PostV1OneClickEmailUnsubscribeMutationError = HTTPValidationError; - -/** - * @summary One Click Email Unsubscribe - */ -export const usePostV1OneClickEmailUnsubscribe = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { params: PostV1OneClickEmailUnsubscribeParams }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { params: PostV1OneClickEmailUnsubscribeParams }, - TContext -> => { - const mutationOptions = - getPostV1OneClickEmailUnsubscribeMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Handle Postmark Email Webhooks - */ -export type postV1HandlePostmarkEmailWebhooksResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1HandlePostmarkEmailWebhooksResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1HandlePostmarkEmailWebhooksResponseComposite = - | postV1HandlePostmarkEmailWebhooksResponse200 - | postV1HandlePostmarkEmailWebhooksResponse422; - -export type postV1HandlePostmarkEmailWebhooksResponse = - postV1HandlePostmarkEmailWebhooksResponseComposite & { - headers: Headers; - }; - -export const getPostV1HandlePostmarkEmailWebhooksUrl = () => { - return `/api/email/`; -}; - -export const postV1HandlePostmarkEmailWebhooks = async ( - postV1HandlePostmarkEmailWebhooksBody: PostV1HandlePostmarkEmailWebhooksBody, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1HandlePostmarkEmailWebhooksUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV1HandlePostmarkEmailWebhooksBody), - }, - ); -}; - -export const getPostV1HandlePostmarkEmailWebhooksMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: PostV1HandlePostmarkEmailWebhooksBody }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: PostV1HandlePostmarkEmailWebhooksBody }, - TContext -> => { - const mutationKey = ["postV1HandlePostmarkEmailWebhooks"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: PostV1HandlePostmarkEmailWebhooksBody } - > = (props) => { - const { data } = props ?? {}; - - return postV1HandlePostmarkEmailWebhooks(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1HandlePostmarkEmailWebhooksMutationResult = NonNullable< - Awaited> ->; -export type PostV1HandlePostmarkEmailWebhooksMutationBody = - PostV1HandlePostmarkEmailWebhooksBody; -export type PostV1HandlePostmarkEmailWebhooksMutationError = - HTTPValidationError; - -/** - * @summary Handle Postmark Email Webhooks - */ -export const usePostV1HandlePostmarkEmailWebhooks = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: PostV1HandlePostmarkEmailWebhooksBody }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: PostV1HandlePostmarkEmailWebhooksBody }, - TContext -> => { - const mutationOptions = - getPostV1HandlePostmarkEmailWebhooksMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/graphs/graphs.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/graphs/graphs.ts deleted file mode 100644 index 65f786fc3844..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/graphs/graphs.ts +++ /dev/null @@ -1,2511 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { BodyPostV1ExecuteGraphAgent } from "../../models/bodyPostV1ExecuteGraphAgent"; - -import type { CreateGraph } from "../../models/createGraph"; - -import type { DeleteGraphResponse } from "../../models/deleteGraphResponse"; - -import type { ExecuteGraphResponse } from "../../models/executeGraphResponse"; - -import type { GetV1GetExecutionDetails200 } from "../../models/getV1GetExecutionDetails200"; - -import type { GetV1GetGraphVersionParams } from "../../models/getV1GetGraphVersionParams"; - -import type { GetV1GetSpecificGraphParams } from "../../models/getV1GetSpecificGraphParams"; - -import type { Graph } from "../../models/graph"; - -import type { GraphExecutionMeta } from "../../models/graphExecutionMeta"; - -import type { GraphMeta } from "../../models/graphMeta"; - -import type { GraphModel } from "../../models/graphModel"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { PostV1ExecuteGraphAgentParams } from "../../models/postV1ExecuteGraphAgentParams"; - -import type { PostV1StopGraphExecution200 } from "../../models/postV1StopGraphExecution200"; - -import type { SetGraphActiveVersion } from "../../models/setGraphActiveVersion"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary List user graphs - */ -export type getV1ListUserGraphsResponse200 = { - data: GraphMeta[]; - status: 200; -}; - -export type getV1ListUserGraphsResponseComposite = - getV1ListUserGraphsResponse200; - -export type getV1ListUserGraphsResponse = - getV1ListUserGraphsResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListUserGraphsUrl = () => { - return `/api/graphs`; -}; - -export const getV1ListUserGraphs = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListUserGraphsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListUserGraphsQueryKey = () => { - return [`/api/graphs`] as const; -}; - -export const getGetV1ListUserGraphsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1ListUserGraphsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ListUserGraphs({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListUserGraphsQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListUserGraphsQueryError = unknown; - -export function useGetV1ListUserGraphs< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListUserGraphs< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListUserGraphs< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List user graphs - */ - -export function useGetV1ListUserGraphs< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListUserGraphsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List user graphs - */ -export const prefetchGetV1ListUserGraphsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListUserGraphsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Create new graph - */ -export type postV1CreateNewGraphResponse200 = { - data: GraphModel; - status: 200; -}; - -export type postV1CreateNewGraphResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1CreateNewGraphResponseComposite = - | postV1CreateNewGraphResponse200 - | postV1CreateNewGraphResponse422; - -export type postV1CreateNewGraphResponse = - postV1CreateNewGraphResponseComposite & { - headers: Headers; - }; - -export const getPostV1CreateNewGraphUrl = () => { - return `/api/graphs`; -}; - -export const postV1CreateNewGraph = async ( - createGraph: CreateGraph, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1CreateNewGraphUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(createGraph), - }, - ); -}; - -export const getPostV1CreateNewGraphMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: CreateGraph }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: CreateGraph }, - TContext -> => { - const mutationKey = ["postV1CreateNewGraph"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: CreateGraph } - > = (props) => { - const { data } = props ?? {}; - - return postV1CreateNewGraph(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1CreateNewGraphMutationResult = NonNullable< - Awaited> ->; -export type PostV1CreateNewGraphMutationBody = CreateGraph; -export type PostV1CreateNewGraphMutationError = HTTPValidationError; - -/** - * @summary Create new graph - */ -export const usePostV1CreateNewGraph = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: CreateGraph }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: CreateGraph }, - TContext -> => { - const mutationOptions = getPostV1CreateNewGraphMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get graph version - */ -export type getV1GetGraphVersionResponse200 = { - data: GraphModel; - status: 200; -}; - -export type getV1GetGraphVersionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetGraphVersionResponseComposite = - | getV1GetGraphVersionResponse200 - | getV1GetGraphVersionResponse422; - -export type getV1GetGraphVersionResponse = - getV1GetGraphVersionResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetGraphVersionUrl = ( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/graphs/${graphId}/versions/${version}?${stringifiedParams}` - : `/api/graphs/${graphId}/versions/${version}`; -}; - -export const getV1GetGraphVersion = async ( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetGraphVersionUrl(graphId, version, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetGraphVersionQueryKey = ( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, -) => { - return [ - `/api/graphs/${graphId}/versions/${version}`, - ...(params ? [params] : []), - ] as const; -}; - -export const getGetV1GetGraphVersionQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV1GetGraphVersionQueryKey(graphId, version, params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetGraphVersion(graphId, version, params, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!(graphId && version), - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetGraphVersionQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetGraphVersionQueryError = HTTPValidationError; - -export function useGetV1GetGraphVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - version: number | null, - params: undefined | GetV1GetGraphVersionParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetGraphVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetGraphVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get graph version - */ - -export function useGetV1GetGraphVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetGraphVersionQueryOptions( - graphId, - version, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get graph version - */ -export const prefetchGetV1GetGraphVersionQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - version: number | null, - params?: GetV1GetGraphVersionParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetGraphVersionQueryOptions( - graphId, - version, - params, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get specific graph - */ -export type getV1GetSpecificGraphResponse200 = { - data: GraphModel; - status: 200; -}; - -export type getV1GetSpecificGraphResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetSpecificGraphResponseComposite = - | getV1GetSpecificGraphResponse200 - | getV1GetSpecificGraphResponse422; - -export type getV1GetSpecificGraphResponse = - getV1GetSpecificGraphResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetSpecificGraphUrl = ( - graphId: string, - params?: GetV1GetSpecificGraphParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/graphs/${graphId}?${stringifiedParams}` - : `/api/graphs/${graphId}`; -}; - -export const getV1GetSpecificGraph = async ( - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetSpecificGraphUrl(graphId, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetSpecificGraphQueryKey = ( - graphId: string, - params?: GetV1GetSpecificGraphParams, -) => { - return [`/api/graphs/${graphId}`, ...(params ? [params] : [])] as const; -}; - -export const getGetV1GetSpecificGraphQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetSpecificGraphQueryKey(graphId, params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetSpecificGraph(graphId, params, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!graphId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetSpecificGraphQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetSpecificGraphQueryError = HTTPValidationError; - -export function useGetV1GetSpecificGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params: undefined | GetV1GetSpecificGraphParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetSpecificGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetSpecificGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get specific graph - */ - -export function useGetV1GetSpecificGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetSpecificGraphQueryOptions( - graphId, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get specific graph - */ -export const prefetchGetV1GetSpecificGraphQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - params?: GetV1GetSpecificGraphParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetSpecificGraphQueryOptions( - graphId, - params, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Delete graph permanently - */ -export type deleteV1DeleteGraphPermanentlyResponse200 = { - data: DeleteGraphResponse; - status: 200; -}; - -export type deleteV1DeleteGraphPermanentlyResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV1DeleteGraphPermanentlyResponseComposite = - | deleteV1DeleteGraphPermanentlyResponse200 - | deleteV1DeleteGraphPermanentlyResponse422; - -export type deleteV1DeleteGraphPermanentlyResponse = - deleteV1DeleteGraphPermanentlyResponseComposite & { - headers: Headers; - }; - -export const getDeleteV1DeleteGraphPermanentlyUrl = (graphId: string) => { - return `/api/graphs/${graphId}`; -}; - -export const deleteV1DeleteGraphPermanently = async ( - graphId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV1DeleteGraphPermanentlyUrl(graphId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV1DeleteGraphPermanentlyMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphId: string }, - TContext -> => { - const mutationKey = ["deleteV1DeleteGraphPermanently"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphId: string } - > = (props) => { - const { graphId } = props ?? {}; - - return deleteV1DeleteGraphPermanently(graphId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV1DeleteGraphPermanentlyMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV1DeleteGraphPermanentlyMutationError = HTTPValidationError; - -/** - * @summary Delete graph permanently - */ -export const useDeleteV1DeleteGraphPermanently = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphId: string }, - TContext -> => { - const mutationOptions = - getDeleteV1DeleteGraphPermanentlyMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Update graph version - */ -export type putV1UpdateGraphVersionResponse200 = { - data: GraphModel; - status: 200; -}; - -export type putV1UpdateGraphVersionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type putV1UpdateGraphVersionResponseComposite = - | putV1UpdateGraphVersionResponse200 - | putV1UpdateGraphVersionResponse422; - -export type putV1UpdateGraphVersionResponse = - putV1UpdateGraphVersionResponseComposite & { - headers: Headers; - }; - -export const getPutV1UpdateGraphVersionUrl = (graphId: string) => { - return `/api/graphs/${graphId}`; -}; - -export const putV1UpdateGraphVersion = async ( - graphId: string, - graph: Graph, - options?: RequestInit, -): Promise => { - return customMutator( - getPutV1UpdateGraphVersionUrl(graphId), - { - ...options, - method: "PUT", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(graph), - }, - ); -}; - -export const getPutV1UpdateGraphVersionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: Graph }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: Graph }, - TContext -> => { - const mutationKey = ["putV1UpdateGraphVersion"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphId: string; data: Graph } - > = (props) => { - const { graphId, data } = props ?? {}; - - return putV1UpdateGraphVersion(graphId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PutV1UpdateGraphVersionMutationResult = NonNullable< - Awaited> ->; -export type PutV1UpdateGraphVersionMutationBody = Graph; -export type PutV1UpdateGraphVersionMutationError = HTTPValidationError; - -/** - * @summary Update graph version - */ -export const usePutV1UpdateGraphVersion = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: Graph }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphId: string; data: Graph }, - TContext -> => { - const mutationOptions = getPutV1UpdateGraphVersionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get all graph versions - */ -export type getV1GetAllGraphVersionsResponse200 = { - data: GraphModel[]; - status: 200; -}; - -export type getV1GetAllGraphVersionsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetAllGraphVersionsResponseComposite = - | getV1GetAllGraphVersionsResponse200 - | getV1GetAllGraphVersionsResponse422; - -export type getV1GetAllGraphVersionsResponse = - getV1GetAllGraphVersionsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetAllGraphVersionsUrl = (graphId: string) => { - return `/api/graphs/${graphId}/versions`; -}; - -export const getV1GetAllGraphVersions = async ( - graphId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetAllGraphVersionsUrl(graphId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetAllGraphVersionsQueryKey = (graphId: string) => { - return [`/api/graphs/${graphId}/versions`] as const; -}; - -export const getGetV1GetAllGraphVersionsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetAllGraphVersionsQueryKey(graphId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetAllGraphVersions(graphId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!graphId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetAllGraphVersionsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetAllGraphVersionsQueryError = HTTPValidationError; - -export function useGetV1GetAllGraphVersions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAllGraphVersions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAllGraphVersions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get all graph versions - */ - -export function useGetV1GetAllGraphVersions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetAllGraphVersionsQueryOptions( - graphId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get all graph versions - */ -export const prefetchGetV1GetAllGraphVersionsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetAllGraphVersionsQueryOptions( - graphId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Set active graph version - */ -export type putV1SetActiveGraphVersionResponse200 = { - data: unknown; - status: 200; -}; - -export type putV1SetActiveGraphVersionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type putV1SetActiveGraphVersionResponseComposite = - | putV1SetActiveGraphVersionResponse200 - | putV1SetActiveGraphVersionResponse422; - -export type putV1SetActiveGraphVersionResponse = - putV1SetActiveGraphVersionResponseComposite & { - headers: Headers; - }; - -export const getPutV1SetActiveGraphVersionUrl = (graphId: string) => { - return `/api/graphs/${graphId}/versions/active`; -}; - -export const putV1SetActiveGraphVersion = async ( - graphId: string, - setGraphActiveVersion: SetGraphActiveVersion, - options?: RequestInit, -): Promise => { - return customMutator( - getPutV1SetActiveGraphVersionUrl(graphId), - { - ...options, - method: "PUT", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(setGraphActiveVersion), - }, - ); -}; - -export const getPutV1SetActiveGraphVersionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: SetGraphActiveVersion }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: SetGraphActiveVersion }, - TContext -> => { - const mutationKey = ["putV1SetActiveGraphVersion"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphId: string; data: SetGraphActiveVersion } - > = (props) => { - const { graphId, data } = props ?? {}; - - return putV1SetActiveGraphVersion(graphId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PutV1SetActiveGraphVersionMutationResult = NonNullable< - Awaited> ->; -export type PutV1SetActiveGraphVersionMutationBody = SetGraphActiveVersion; -export type PutV1SetActiveGraphVersionMutationError = HTTPValidationError; - -/** - * @summary Set active graph version - */ -export const usePutV1SetActiveGraphVersion = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: SetGraphActiveVersion }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphId: string; data: SetGraphActiveVersion }, - TContext -> => { - const mutationOptions = getPutV1SetActiveGraphVersionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Execute graph agent - */ -export type postV1ExecuteGraphAgentResponse200 = { - data: ExecuteGraphResponse; - status: 200; -}; - -export type postV1ExecuteGraphAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1ExecuteGraphAgentResponseComposite = - | postV1ExecuteGraphAgentResponse200 - | postV1ExecuteGraphAgentResponse422; - -export type postV1ExecuteGraphAgentResponse = - postV1ExecuteGraphAgentResponseComposite & { - headers: Headers; - }; - -export const getPostV1ExecuteGraphAgentUrl = ( - graphId: string, - graphVersion: number | null, - params?: PostV1ExecuteGraphAgentParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/graphs/${graphId}/execute/${graphVersion}?${stringifiedParams}` - : `/api/graphs/${graphId}/execute/${graphVersion}`; -}; - -export const postV1ExecuteGraphAgent = async ( - graphId: string, - graphVersion: number | null, - bodyPostV1ExecuteGraphAgent: BodyPostV1ExecuteGraphAgent, - params?: PostV1ExecuteGraphAgentParams, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1ExecuteGraphAgentUrl(graphId, graphVersion, params), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV1ExecuteGraphAgent), - }, - ); -}; - -export const getPostV1ExecuteGraphAgentMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - graphId: string; - graphVersion: number | null; - data: BodyPostV1ExecuteGraphAgent; - params?: PostV1ExecuteGraphAgentParams; - }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { - graphId: string; - graphVersion: number | null; - data: BodyPostV1ExecuteGraphAgent; - params?: PostV1ExecuteGraphAgentParams; - }, - TContext -> => { - const mutationKey = ["postV1ExecuteGraphAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { - graphId: string; - graphVersion: number | null; - data: BodyPostV1ExecuteGraphAgent; - params?: PostV1ExecuteGraphAgentParams; - } - > = (props) => { - const { graphId, graphVersion, data, params } = props ?? {}; - - return postV1ExecuteGraphAgent( - graphId, - graphVersion, - data, - params, - requestOptions, - ); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1ExecuteGraphAgentMutationResult = NonNullable< - Awaited> ->; -export type PostV1ExecuteGraphAgentMutationBody = BodyPostV1ExecuteGraphAgent; -export type PostV1ExecuteGraphAgentMutationError = HTTPValidationError; - -/** - * @summary Execute graph agent - */ -export const usePostV1ExecuteGraphAgent = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - graphId: string; - graphVersion: number | null; - data: BodyPostV1ExecuteGraphAgent; - params?: PostV1ExecuteGraphAgentParams; - }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { - graphId: string; - graphVersion: number | null; - data: BodyPostV1ExecuteGraphAgent; - params?: PostV1ExecuteGraphAgentParams; - }, - TContext -> => { - const mutationOptions = getPostV1ExecuteGraphAgentMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Stop graph execution - */ -export type postV1StopGraphExecutionResponse200 = { - data: PostV1StopGraphExecution200; - status: 200; -}; - -export type postV1StopGraphExecutionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1StopGraphExecutionResponseComposite = - | postV1StopGraphExecutionResponse200 - | postV1StopGraphExecutionResponse422; - -export type postV1StopGraphExecutionResponse = - postV1StopGraphExecutionResponseComposite & { - headers: Headers; - }; - -export const getPostV1StopGraphExecutionUrl = ( - graphId: string, - graphExecId: string, -) => { - return `/api/graphs/${graphId}/executions/${graphExecId}/stop`; -}; - -export const postV1StopGraphExecution = async ( - graphId: string, - graphExecId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1StopGraphExecutionUrl(graphId, graphExecId), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1StopGraphExecutionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; graphExecId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphId: string; graphExecId: string }, - TContext -> => { - const mutationKey = ["postV1StopGraphExecution"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphId: string; graphExecId: string } - > = (props) => { - const { graphId, graphExecId } = props ?? {}; - - return postV1StopGraphExecution(graphId, graphExecId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1StopGraphExecutionMutationResult = NonNullable< - Awaited> ->; - -export type PostV1StopGraphExecutionMutationError = HTTPValidationError; - -/** - * @summary Stop graph execution - */ -export const usePostV1StopGraphExecution = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; graphExecId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphId: string; graphExecId: string }, - TContext -> => { - const mutationOptions = getPostV1StopGraphExecutionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get all executions - */ -export type getV1GetAllExecutionsResponse200 = { - data: GraphExecutionMeta[]; - status: 200; -}; - -export type getV1GetAllExecutionsResponseComposite = - getV1GetAllExecutionsResponse200; - -export type getV1GetAllExecutionsResponse = - getV1GetAllExecutionsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetAllExecutionsUrl = () => { - return `/api/executions`; -}; - -export const getV1GetAllExecutions = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetAllExecutionsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetAllExecutionsQueryKey = () => { - return [`/api/executions`] as const; -}; - -export const getGetV1GetAllExecutionsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1GetAllExecutionsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetAllExecutions({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetAllExecutionsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetAllExecutionsQueryError = unknown; - -export function useGetV1GetAllExecutions< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAllExecutions< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetAllExecutions< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get all executions - */ - -export function useGetV1GetAllExecutions< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetAllExecutionsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get all executions - */ -export const prefetchGetV1GetAllExecutionsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetAllExecutionsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get graph executions - */ -export type getV1GetGraphExecutionsResponse200 = { - data: GraphExecutionMeta[]; - status: 200; -}; - -export type getV1GetGraphExecutionsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetGraphExecutionsResponseComposite = - | getV1GetGraphExecutionsResponse200 - | getV1GetGraphExecutionsResponse422; - -export type getV1GetGraphExecutionsResponse = - getV1GetGraphExecutionsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetGraphExecutionsUrl = (graphId: string) => { - return `/api/graphs/${graphId}/executions`; -}; - -export const getV1GetGraphExecutions = async ( - graphId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetGraphExecutionsUrl(graphId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetGraphExecutionsQueryKey = (graphId: string) => { - return [`/api/graphs/${graphId}/executions`] as const; -}; - -export const getGetV1GetGraphExecutionsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetGraphExecutionsQueryKey(graphId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetGraphExecutions(graphId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!graphId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetGraphExecutionsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetGraphExecutionsQueryError = HTTPValidationError; - -export function useGetV1GetGraphExecutions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetGraphExecutions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetGraphExecutions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get graph executions - */ - -export function useGetV1GetGraphExecutions< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetGraphExecutionsQueryOptions(graphId, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get graph executions - */ -export const prefetchGetV1GetGraphExecutionsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetGraphExecutionsQueryOptions(graphId, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get execution details - */ -export type getV1GetExecutionDetailsResponse200 = { - data: GetV1GetExecutionDetails200; - status: 200; -}; - -export type getV1GetExecutionDetailsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetExecutionDetailsResponseComposite = - | getV1GetExecutionDetailsResponse200 - | getV1GetExecutionDetailsResponse422; - -export type getV1GetExecutionDetailsResponse = - getV1GetExecutionDetailsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetExecutionDetailsUrl = ( - graphId: string, - graphExecId: string, -) => { - return `/api/graphs/${graphId}/executions/${graphExecId}`; -}; - -export const getV1GetExecutionDetails = async ( - graphId: string, - graphExecId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetExecutionDetailsUrl(graphId, graphExecId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetExecutionDetailsQueryKey = ( - graphId: string, - graphExecId: string, -) => { - return [`/api/graphs/${graphId}/executions/${graphExecId}`] as const; -}; - -export const getGetV1GetExecutionDetailsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - graphExecId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV1GetExecutionDetailsQueryKey(graphId, graphExecId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetExecutionDetails(graphId, graphExecId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!(graphId && graphExecId), - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetExecutionDetailsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetExecutionDetailsQueryError = HTTPValidationError; - -export function useGetV1GetExecutionDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - graphExecId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetExecutionDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - graphExecId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetExecutionDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - graphExecId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get execution details - */ - -export function useGetV1GetExecutionDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - graphExecId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetExecutionDetailsQueryOptions( - graphId, - graphExecId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get execution details - */ -export const prefetchGetV1GetExecutionDetailsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - graphExecId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetExecutionDetailsQueryOptions( - graphId, - graphExecId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Delete graph execution - */ -export type deleteV1DeleteGraphExecutionResponse204 = { - data: void; - status: 204; -}; - -export type deleteV1DeleteGraphExecutionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV1DeleteGraphExecutionResponseComposite = - | deleteV1DeleteGraphExecutionResponse204 - | deleteV1DeleteGraphExecutionResponse422; - -export type deleteV1DeleteGraphExecutionResponse = - deleteV1DeleteGraphExecutionResponseComposite & { - headers: Headers; - }; - -export const getDeleteV1DeleteGraphExecutionUrl = (graphExecId: string) => { - return `/api/executions/${graphExecId}`; -}; - -export const deleteV1DeleteGraphExecution = async ( - graphExecId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV1DeleteGraphExecutionUrl(graphExecId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV1DeleteGraphExecutionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphExecId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphExecId: string }, - TContext -> => { - const mutationKey = ["deleteV1DeleteGraphExecution"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphExecId: string } - > = (props) => { - const { graphExecId } = props ?? {}; - - return deleteV1DeleteGraphExecution(graphExecId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV1DeleteGraphExecutionMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV1DeleteGraphExecutionMutationError = HTTPValidationError; - -/** - * @summary Delete graph execution - */ -export const useDeleteV1DeleteGraphExecution = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphExecId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphExecId: string }, - TContext -> => { - const mutationOptions = - getDeleteV1DeleteGraphExecutionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/health/health.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/health/health.ts deleted file mode 100644 index 8bfd4898fcae..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/health/health.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Health - */ -export type getHealthHealthResponse200 = { - data: unknown; - status: 200; -}; - -export type getHealthHealthResponseComposite = getHealthHealthResponse200; - -export type getHealthHealthResponse = getHealthHealthResponseComposite & { - headers: Headers; -}; - -export const getGetHealthHealthUrl = () => { - return `/health`; -}; - -export const getHealthHealth = async ( - options?: RequestInit, -): Promise => { - return customMutator(getGetHealthHealthUrl(), { - ...options, - method: "GET", - }); -}; - -export const getGetHealthHealthQueryKey = () => { - return [`/health`] as const; -}; - -export const getGetHealthHealthQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetHealthHealthQueryKey(); - - const queryFn: QueryFunction>> = ({ - signal, - }) => getHealthHealth({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetHealthHealthQueryResult = NonNullable< - Awaited> ->; -export type GetHealthHealthQueryError = unknown; - -export function useGetHealthHealth< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetHealthHealth< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetHealthHealth< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Health - */ - -export function useGetHealthHealth< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetHealthHealthQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Health - */ -export const prefetchGetHealthHealthQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetHealthHealthQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/integrations/integrations.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/integrations/integrations.ts deleted file mode 100644 index 9525a6a90e51..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/integrations/integrations.ts +++ /dev/null @@ -1,2347 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { BodyPostV1Callback } from "../../models/bodyPostV1Callback"; - -import type { CredentialsMetaResponse } from "../../models/credentialsMetaResponse"; - -import type { DeleteV1DeleteCredentials200 } from "../../models/deleteV1DeleteCredentials200"; - -import type { DeleteV1DeleteCredentialsParams } from "../../models/deleteV1DeleteCredentialsParams"; - -import type { GetV1GetCredential200 } from "../../models/getV1GetCredential200"; - -import type { GetV1LoginParams } from "../../models/getV1LoginParams"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { LoginResponse } from "../../models/loginResponse"; - -import type { PostV1CreateCredentials201 } from "../../models/postV1CreateCredentials201"; - -import type { PostV1CreateCredentialsBody } from "../../models/postV1CreateCredentialsBody"; - -import type { ProviderConstants } from "../../models/providerConstants"; - -import type { ProviderEnumResponse } from "../../models/providerEnumResponse"; - -import type { ProviderNamesResponse } from "../../models/providerNamesResponse"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Login - */ -export type getV1LoginResponse200 = { - data: LoginResponse; - status: 200; -}; - -export type getV1LoginResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1LoginResponseComposite = - | getV1LoginResponse200 - | getV1LoginResponse422; - -export type getV1LoginResponse = getV1LoginResponseComposite & { - headers: Headers; -}; - -export const getGetV1LoginUrl = ( - provider: string, - params?: GetV1LoginParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/integrations/${provider}/login?${stringifiedParams}` - : `/api/integrations/${provider}/login`; -}; - -export const getV1Login = async ( - provider: string, - params?: GetV1LoginParams, - options?: RequestInit, -): Promise => { - return customMutator(getGetV1LoginUrl(provider, params), { - ...options, - method: "GET", - }); -}; - -export const getGetV1LoginQueryKey = ( - provider: string, - params?: GetV1LoginParams, -) => { - return [ - `/api/integrations/${provider}/login`, - ...(params ? [params] : []), - ] as const; -}; - -export const getGetV1LoginQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - params?: GetV1LoginParams, - options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1LoginQueryKey(provider, params); - - const queryFn: QueryFunction>> = ({ - signal, - }) => getV1Login(provider, params, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!provider, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1LoginQueryResult = NonNullable< - Awaited> ->; -export type GetV1LoginQueryError = HTTPValidationError; - -export function useGetV1Login< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - params: undefined | GetV1LoginParams, - options: { - query: Partial< - UseQueryOptions>, TError, TData> - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1Login< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - params?: GetV1LoginParams, - options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1Login< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - params?: GetV1LoginParams, - options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Login - */ - -export function useGetV1Login< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - params?: GetV1LoginParams, - options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1LoginQueryOptions(provider, params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Login - */ -export const prefetchGetV1LoginQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - provider: string, - params?: GetV1LoginParams, - options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1LoginQueryOptions(provider, params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Callback - */ -export type postV1CallbackResponse200 = { - data: CredentialsMetaResponse; - status: 200; -}; - -export type postV1CallbackResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1CallbackResponseComposite = - | postV1CallbackResponse200 - | postV1CallbackResponse422; - -export type postV1CallbackResponse = postV1CallbackResponseComposite & { - headers: Headers; -}; - -export const getPostV1CallbackUrl = (provider: string) => { - return `/api/integrations/${provider}/callback`; -}; - -export const postV1Callback = async ( - provider: string, - bodyPostV1Callback: BodyPostV1Callback, - options?: RequestInit, -): Promise => { - return customMutator(getPostV1CallbackUrl(provider), { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV1Callback), - }); -}; - -export const getPostV1CallbackMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; data: BodyPostV1Callback }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { provider: string; data: BodyPostV1Callback }, - TContext -> => { - const mutationKey = ["postV1Callback"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { provider: string; data: BodyPostV1Callback } - > = (props) => { - const { provider, data } = props ?? {}; - - return postV1Callback(provider, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1CallbackMutationResult = NonNullable< - Awaited> ->; -export type PostV1CallbackMutationBody = BodyPostV1Callback; -export type PostV1CallbackMutationError = HTTPValidationError; - -/** - * @summary Callback - */ -export const usePostV1Callback = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; data: BodyPostV1Callback }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { provider: string; data: BodyPostV1Callback }, - TContext -> => { - const mutationOptions = getPostV1CallbackMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary List Credentials - */ -export type getV1ListCredentialsResponse200 = { - data: CredentialsMetaResponse[]; - status: 200; -}; - -export type getV1ListCredentialsResponseComposite = - getV1ListCredentialsResponse200; - -export type getV1ListCredentialsResponse = - getV1ListCredentialsResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListCredentialsUrl = () => { - return `/api/integrations/credentials`; -}; - -export const getV1ListCredentials = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListCredentialsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListCredentialsQueryKey = () => { - return [`/api/integrations/credentials`] as const; -}; - -export const getGetV1ListCredentialsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1ListCredentialsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ListCredentials({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListCredentialsQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListCredentialsQueryError = unknown; - -export function useGetV1ListCredentials< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListCredentials< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListCredentials< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Credentials - */ - -export function useGetV1ListCredentials< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListCredentialsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List Credentials - */ -export const prefetchGetV1ListCredentialsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListCredentialsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary List Credentials By Provider - */ -export type getV1ListCredentialsByProviderResponse200 = { - data: CredentialsMetaResponse[]; - status: 200; -}; - -export type getV1ListCredentialsByProviderResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1ListCredentialsByProviderResponseComposite = - | getV1ListCredentialsByProviderResponse200 - | getV1ListCredentialsByProviderResponse422; - -export type getV1ListCredentialsByProviderResponse = - getV1ListCredentialsByProviderResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListCredentialsByProviderUrl = (provider: string) => { - return `/api/integrations/${provider}/credentials`; -}; - -export const getV1ListCredentialsByProvider = async ( - provider: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListCredentialsByProviderUrl(provider), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListCredentialsByProviderQueryKey = (provider: string) => { - return [`/api/integrations/${provider}/credentials`] as const; -}; - -export const getGetV1ListCredentialsByProviderQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV1ListCredentialsByProviderQueryKey(provider); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1ListCredentialsByProvider(provider, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!provider, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListCredentialsByProviderQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListCredentialsByProviderQueryError = HTTPValidationError; - -export function useGetV1ListCredentialsByProvider< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListCredentialsByProvider< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListCredentialsByProvider< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Credentials By Provider - */ - -export function useGetV1ListCredentialsByProvider< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListCredentialsByProviderQueryOptions( - provider, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List Credentials By Provider - */ -export const prefetchGetV1ListCredentialsByProviderQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - provider: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListCredentialsByProviderQueryOptions( - provider, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Create Credentials - */ -export type postV1CreateCredentialsResponse201 = { - data: PostV1CreateCredentials201; - status: 201; -}; - -export type postV1CreateCredentialsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1CreateCredentialsResponseComposite = - | postV1CreateCredentialsResponse201 - | postV1CreateCredentialsResponse422; - -export type postV1CreateCredentialsResponse = - postV1CreateCredentialsResponseComposite & { - headers: Headers; - }; - -export const getPostV1CreateCredentialsUrl = (provider: string) => { - return `/api/integrations/${provider}/credentials`; -}; - -export const postV1CreateCredentials = async ( - provider: string, - postV1CreateCredentialsBody: PostV1CreateCredentialsBody, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1CreateCredentialsUrl(provider), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV1CreateCredentialsBody), - }, - ); -}; - -export const getPostV1CreateCredentialsMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; data: PostV1CreateCredentialsBody }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { provider: string; data: PostV1CreateCredentialsBody }, - TContext -> => { - const mutationKey = ["postV1CreateCredentials"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { provider: string; data: PostV1CreateCredentialsBody } - > = (props) => { - const { provider, data } = props ?? {}; - - return postV1CreateCredentials(provider, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1CreateCredentialsMutationResult = NonNullable< - Awaited> ->; -export type PostV1CreateCredentialsMutationBody = PostV1CreateCredentialsBody; -export type PostV1CreateCredentialsMutationError = HTTPValidationError; - -/** - * @summary Create Credentials - */ -export const usePostV1CreateCredentials = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; data: PostV1CreateCredentialsBody }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { provider: string; data: PostV1CreateCredentialsBody }, - TContext -> => { - const mutationOptions = getPostV1CreateCredentialsMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get Credential - */ -export type getV1GetCredentialResponse200 = { - data: GetV1GetCredential200; - status: 200; -}; - -export type getV1GetCredentialResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1GetCredentialResponseComposite = - | getV1GetCredentialResponse200 - | getV1GetCredentialResponse422; - -export type getV1GetCredentialResponse = getV1GetCredentialResponseComposite & { - headers: Headers; -}; - -export const getGetV1GetCredentialUrl = (provider: string, credId: string) => { - return `/api/integrations/${provider}/credentials/${credId}`; -}; - -export const getV1GetCredential = async ( - provider: string, - credId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetCredentialUrl(provider, credId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetCredentialQueryKey = ( - provider: string, - credId: string, -) => { - return [`/api/integrations/${provider}/credentials/${credId}`] as const; -}; - -export const getGetV1GetCredentialQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - credId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetCredentialQueryKey(provider, credId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetCredential(provider, credId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!(provider && credId), - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetCredentialQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetCredentialQueryError = HTTPValidationError; - -export function useGetV1GetCredential< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - credId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetCredential< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - credId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetCredential< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - credId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Credential - */ - -export function useGetV1GetCredential< - TData = Awaited>, - TError = HTTPValidationError, ->( - provider: string, - credId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetCredentialQueryOptions( - provider, - credId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Credential - */ -export const prefetchGetV1GetCredentialQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - provider: string, - credId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetCredentialQueryOptions( - provider, - credId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Delete Credentials - */ -export type deleteV1DeleteCredentialsResponse200 = { - data: DeleteV1DeleteCredentials200; - status: 200; -}; - -export type deleteV1DeleteCredentialsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV1DeleteCredentialsResponseComposite = - | deleteV1DeleteCredentialsResponse200 - | deleteV1DeleteCredentialsResponse422; - -export type deleteV1DeleteCredentialsResponse = - deleteV1DeleteCredentialsResponseComposite & { - headers: Headers; - }; - -export const getDeleteV1DeleteCredentialsUrl = ( - provider: string, - credId: string, - params?: DeleteV1DeleteCredentialsParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/integrations/${provider}/credentials/${credId}?${stringifiedParams}` - : `/api/integrations/${provider}/credentials/${credId}`; -}; - -export const deleteV1DeleteCredentials = async ( - provider: string, - credId: string, - params?: DeleteV1DeleteCredentialsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV1DeleteCredentialsUrl(provider, credId, params), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV1DeleteCredentialsMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - provider: string; - credId: string; - params?: DeleteV1DeleteCredentialsParams; - }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { - provider: string; - credId: string; - params?: DeleteV1DeleteCredentialsParams; - }, - TContext -> => { - const mutationKey = ["deleteV1DeleteCredentials"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { - provider: string; - credId: string; - params?: DeleteV1DeleteCredentialsParams; - } - > = (props) => { - const { provider, credId, params } = props ?? {}; - - return deleteV1DeleteCredentials(provider, credId, params, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV1DeleteCredentialsMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV1DeleteCredentialsMutationError = HTTPValidationError; - -/** - * @summary Delete Credentials - */ -export const useDeleteV1DeleteCredentials = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { - provider: string; - credId: string; - params?: DeleteV1DeleteCredentialsParams; - }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { - provider: string; - credId: string; - params?: DeleteV1DeleteCredentialsParams; - }, - TContext -> => { - const mutationOptions = getDeleteV1DeleteCredentialsMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Webhook Ingress Generic - */ -export type postV1WebhookIngressGenericResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1WebhookIngressGenericResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1WebhookIngressGenericResponseComposite = - | postV1WebhookIngressGenericResponse200 - | postV1WebhookIngressGenericResponse422; - -export type postV1WebhookIngressGenericResponse = - postV1WebhookIngressGenericResponseComposite & { - headers: Headers; - }; - -export const getPostV1WebhookIngressGenericUrl = ( - provider: string, - webhookId: string, -) => { - return `/api/integrations/${provider}/webhooks/${webhookId}/ingress`; -}; - -export const postV1WebhookIngressGeneric = async ( - provider: string, - webhookId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1WebhookIngressGenericUrl(provider, webhookId), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1WebhookIngressGenericMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; webhookId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { provider: string; webhookId: string }, - TContext -> => { - const mutationKey = ["postV1WebhookIngressGeneric"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { provider: string; webhookId: string } - > = (props) => { - const { provider, webhookId } = props ?? {}; - - return postV1WebhookIngressGeneric(provider, webhookId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1WebhookIngressGenericMutationResult = NonNullable< - Awaited> ->; - -export type PostV1WebhookIngressGenericMutationError = HTTPValidationError; - -/** - * @summary Webhook Ingress Generic - */ -export const usePostV1WebhookIngressGeneric = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { provider: string; webhookId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { provider: string; webhookId: string }, - TContext -> => { - const mutationOptions = - getPostV1WebhookIngressGenericMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Webhook Ping - */ -export type postV1WebhookPingResponse200 = { - data: unknown; - status: 200; -}; - -export type postV1WebhookPingResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1WebhookPingResponseComposite = - | postV1WebhookPingResponse200 - | postV1WebhookPingResponse422; - -export type postV1WebhookPingResponse = postV1WebhookPingResponseComposite & { - headers: Headers; -}; - -export const getPostV1WebhookPingUrl = (webhookId: string) => { - return `/api/integrations/webhooks/${webhookId}/ping`; -}; - -export const postV1WebhookPing = async ( - webhookId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1WebhookPingUrl(webhookId), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV1WebhookPingMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { webhookId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { webhookId: string }, - TContext -> => { - const mutationKey = ["postV1WebhookPing"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { webhookId: string } - > = (props) => { - const { webhookId } = props ?? {}; - - return postV1WebhookPing(webhookId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1WebhookPingMutationResult = NonNullable< - Awaited> ->; - -export type PostV1WebhookPingMutationError = HTTPValidationError; - -/** - * @summary Webhook Ping - */ -export const usePostV1WebhookPing = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { webhookId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { webhookId: string }, - TContext -> => { - const mutationOptions = getPostV1WebhookPingMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Get a list of all available provider names. - -Returns both statically defined providers (from ProviderName enum) -and dynamically registered providers (from SDK decorators). - -Note: The complete list of provider names is also available as a constant -in the generated TypeScript client via PROVIDER_NAMES. - * @summary List Providers - */ -export type getV1ListProvidersResponse200 = { - data: string[]; - status: 200; -}; - -export type getV1ListProvidersResponseComposite = getV1ListProvidersResponse200; - -export type getV1ListProvidersResponse = getV1ListProvidersResponseComposite & { - headers: Headers; -}; - -export const getGetV1ListProvidersUrl = () => { - return `/api/integrations/providers`; -}; - -export const getV1ListProviders = async ( - options?: RequestInit, -): Promise => { - return customMutator(getGetV1ListProvidersUrl(), { - ...options, - method: "GET", - }); -}; - -export const getGetV1ListProvidersQueryKey = () => { - return [`/api/integrations/providers`] as const; -}; - -export const getGetV1ListProvidersQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1ListProvidersQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1ListProviders({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListProvidersQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListProvidersQueryError = unknown; - -export function useGetV1ListProviders< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListProviders< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListProviders< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Providers - */ - -export function useGetV1ListProviders< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListProvidersQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List Providers - */ -export const prefetchGetV1ListProvidersQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListProvidersQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get all provider names in a structured format. - -This endpoint is specifically designed to expose the provider names -in the OpenAPI schema so that code generators like Orval can create -appropriate TypeScript constants. - * @summary Get Provider Names - */ -export type getV1GetProviderNamesResponse200 = { - data: ProviderNamesResponse; - status: 200; -}; - -export type getV1GetProviderNamesResponseComposite = - getV1GetProviderNamesResponse200; - -export type getV1GetProviderNamesResponse = - getV1GetProviderNamesResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetProviderNamesUrl = () => { - return `/api/integrations/providers/names`; -}; - -export const getV1GetProviderNames = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetProviderNamesUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetProviderNamesQueryKey = () => { - return [`/api/integrations/providers/names`] as const; -}; - -export const getGetV1GetProviderNamesQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV1GetProviderNamesQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetProviderNames({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetProviderNamesQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetProviderNamesQueryError = unknown; - -export function useGetV1GetProviderNames< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderNames< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderNames< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Provider Names - */ - -export function useGetV1GetProviderNames< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetProviderNamesQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Provider Names - */ -export const prefetchGetV1GetProviderNamesQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetProviderNamesQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get provider names as constants. - -This endpoint returns a model with provider names as constants, -specifically designed for OpenAPI code generation tools to create -TypeScript constants. - * @summary Get Provider Constants - */ -export type getV1GetProviderConstantsResponse200 = { - data: ProviderConstants; - status: 200; -}; - -export type getV1GetProviderConstantsResponseComposite = - getV1GetProviderConstantsResponse200; - -export type getV1GetProviderConstantsResponse = - getV1GetProviderConstantsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetProviderConstantsUrl = () => { - return `/api/integrations/providers/constants`; -}; - -export const getV1GetProviderConstants = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetProviderConstantsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetProviderConstantsQueryKey = () => { - return [`/api/integrations/providers/constants`] as const; -}; - -export const getGetV1GetProviderConstantsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetProviderConstantsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetProviderConstants({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetProviderConstantsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetProviderConstantsQueryError = unknown; - -export function useGetV1GetProviderConstants< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderConstants< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderConstants< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Provider Constants - */ - -export function useGetV1GetProviderConstants< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetProviderConstantsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Provider Constants - */ -export const prefetchGetV1GetProviderConstantsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetProviderConstantsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Example endpoint that uses the CompleteProviderNames enum. - -This endpoint exists to ensure that the CompleteProviderNames enum is included -in the OpenAPI schema, which will cause Orval to generate it as a -TypeScript enum/constant. - * @summary Get Provider Enum Example - */ -export type getV1GetProviderEnumExampleResponse200 = { - data: ProviderEnumResponse; - status: 200; -}; - -export type getV1GetProviderEnumExampleResponseComposite = - getV1GetProviderEnumExampleResponse200; - -export type getV1GetProviderEnumExampleResponse = - getV1GetProviderEnumExampleResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetProviderEnumExampleUrl = () => { - return `/api/integrations/providers/enum-example`; -}; - -export const getV1GetProviderEnumExample = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetProviderEnumExampleUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetProviderEnumExampleQueryKey = () => { - return [`/api/integrations/providers/enum-example`] as const; -}; - -export const getGetV1GetProviderEnumExampleQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetProviderEnumExampleQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1GetProviderEnumExample({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetProviderEnumExampleQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetProviderEnumExampleQueryError = unknown; - -export function useGetV1GetProviderEnumExample< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderEnumExample< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetProviderEnumExample< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Provider Enum Example - */ - -export function useGetV1GetProviderEnumExample< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetProviderEnumExampleQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Provider Enum Example - */ -export const prefetchGetV1GetProviderEnumExampleQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetProviderEnumExampleQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/library/library.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/library/library.ts deleted file mode 100644 index 338f10d27ddb..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/library/library.ts +++ /dev/null @@ -1,1737 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseInfiniteQueryResult, - DefinedUseQueryResult, - InfiniteData, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseInfiniteQueryOptions, - UseInfiniteQueryResult, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { BodyPostV2AddMarketplaceAgent } from "../../models/bodyPostV2AddMarketplaceAgent"; - -import type { GetV2GetAgentByStoreId200 } from "../../models/getV2GetAgentByStoreId200"; - -import type { GetV2GetLibraryAgentByGraphIdParams } from "../../models/getV2GetLibraryAgentByGraphIdParams"; - -import type { GetV2ListLibraryAgentsParams } from "../../models/getV2ListLibraryAgentsParams"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { LibraryAgent } from "../../models/libraryAgent"; - -import type { LibraryAgentResponse } from "../../models/libraryAgentResponse"; - -import type { LibraryAgentUpdateRequest } from "../../models/libraryAgentUpdateRequest"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Get all agents in the user's library (both created and saved). - -Args: - user_id: ID of the authenticated user. - search_term: Optional search term to filter agents by name/description. - filter_by: List of filters to apply (favorites, created by user). - sort_by: List of sorting criteria (created date, updated date). - page: Page number to retrieve. - page_size: Number of agents per page. - -Returns: - A LibraryAgentResponse containing agents and pagination metadata. - -Raises: - HTTPException: If a server/database error occurs. - * @summary List Library Agents - */ -export type getV2ListLibraryAgentsResponse200 = { - data: LibraryAgentResponse; - status: 200; -}; - -export type getV2ListLibraryAgentsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2ListLibraryAgentsResponseComposite = - | getV2ListLibraryAgentsResponse200 - | getV2ListLibraryAgentsResponse422; - -export type getV2ListLibraryAgentsResponse = - getV2ListLibraryAgentsResponseComposite & { - headers: Headers; - }; - -export const getGetV2ListLibraryAgentsUrl = ( - params?: GetV2ListLibraryAgentsParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/library/agents?${stringifiedParams}` - : `/api/library/agents`; -}; - -export const getV2ListLibraryAgents = async ( - params?: GetV2ListLibraryAgentsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2ListLibraryAgentsUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2ListLibraryAgentsQueryKey = ( - params?: GetV2ListLibraryAgentsParams, -) => { - return [`/api/library/agents`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2ListLibraryAgentsInfiniteQueryOptions = < - TData = InfiniteData< - Awaited>, - GetV2ListLibraryAgentsParams["page"] - >, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListLibraryAgentsQueryKey(params); - - const queryFn: QueryFunction< - Awaited>, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > = ({ signal, pageParam }) => - getV2ListLibraryAgents( - { ...params, page: pageParam || params?.["page"] }, - { signal, ...requestOptions }, - ); - - return { queryKey, queryFn, ...queryOptions } as UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > & { queryKey: DataTag }; -}; - -export type GetV2ListLibraryAgentsInfiniteQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListLibraryAgentsInfiniteQueryError = HTTPValidationError; - -export function useGetV2ListLibraryAgentsInfinite< - TData = InfiniteData< - Awaited>, - GetV2ListLibraryAgentsParams["page"] - >, - TError = HTTPValidationError, ->( - params: undefined | GetV2ListLibraryAgentsParams, - options: { - query: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited>, - QueryKey - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseInfiniteQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListLibraryAgentsInfinite< - TData = InfiniteData< - Awaited>, - GetV2ListLibraryAgentsParams["page"] - >, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited>, - QueryKey - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseInfiniteQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListLibraryAgentsInfinite< - TData = InfiniteData< - Awaited>, - GetV2ListLibraryAgentsParams["page"] - >, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseInfiniteQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Library Agents - */ - -export function useGetV2ListLibraryAgentsInfinite< - TData = InfiniteData< - Awaited>, - GetV2ListLibraryAgentsParams["page"] - >, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseInfiniteQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListLibraryAgentsInfiniteQueryOptions( - params, - options, - ); - - const query = useInfiniteQuery( - queryOptions, - queryClient, - ) as UseInfiniteQueryResult & { - queryKey: DataTag; - }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List Library Agents - */ -export const prefetchGetV2ListLibraryAgentsInfiniteQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseInfiniteQueryOptions< - Awaited>, - TError, - TData, - QueryKey, - GetV2ListLibraryAgentsParams["page"] - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListLibraryAgentsInfiniteQueryOptions( - params, - options, - ); - - await queryClient.prefetchInfiniteQuery(queryOptions); - - return queryClient; -}; - -export const getGetV2ListLibraryAgentsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListLibraryAgentsQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2ListLibraryAgents(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2ListLibraryAgentsQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListLibraryAgentsQueryError = HTTPValidationError; - -export function useGetV2ListLibraryAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2ListLibraryAgentsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListLibraryAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListLibraryAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List Library Agents - */ - -export function useGetV2ListLibraryAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListLibraryAgentsQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List Library Agents - */ -export const prefetchGetV2ListLibraryAgentsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2ListLibraryAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListLibraryAgentsQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Add an agent from the marketplace to the user's library. - -Args: - store_listing_version_id: ID of the store listing version to add. - user_id: ID of the authenticated user. - -Returns: - library_model.LibraryAgent: Agent added to the library - -Raises: - HTTPException(404): If the listing version is not found. - HTTPException(500): If a server/database error occurs. - * @summary Add Marketplace Agent - */ -export type postV2AddMarketplaceAgentResponse201 = { - data: LibraryAgent; - status: 201; -}; - -export type postV2AddMarketplaceAgentResponse404 = { - data: void; - status: 404; -}; - -export type postV2AddMarketplaceAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2AddMarketplaceAgentResponseComposite = - | postV2AddMarketplaceAgentResponse201 - | postV2AddMarketplaceAgentResponse404 - | postV2AddMarketplaceAgentResponse422; - -export type postV2AddMarketplaceAgentResponse = - postV2AddMarketplaceAgentResponseComposite & { - headers: Headers; - }; - -export const getPostV2AddMarketplaceAgentUrl = () => { - return `/api/library/agents`; -}; - -export const postV2AddMarketplaceAgent = async ( - bodyPostV2AddMarketplaceAgent: BodyPostV2AddMarketplaceAgent, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2AddMarketplaceAgentUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV2AddMarketplaceAgent), - }, - ); -}; - -export const getPostV2AddMarketplaceAgentMutationOptions = < - TError = void | HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddMarketplaceAgent }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddMarketplaceAgent }, - TContext -> => { - const mutationKey = ["postV2AddMarketplaceAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: BodyPostV2AddMarketplaceAgent } - > = (props) => { - const { data } = props ?? {}; - - return postV2AddMarketplaceAgent(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2AddMarketplaceAgentMutationResult = NonNullable< - Awaited> ->; -export type PostV2AddMarketplaceAgentMutationBody = - BodyPostV2AddMarketplaceAgent; -export type PostV2AddMarketplaceAgentMutationError = void | HTTPValidationError; - -/** - * @summary Add Marketplace Agent - */ -export const usePostV2AddMarketplaceAgent = < - TError = void | HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2AddMarketplaceAgent }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: BodyPostV2AddMarketplaceAgent }, - TContext -> => { - const mutationOptions = getPostV2AddMarketplaceAgentMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get Library Agent - */ -export type getV2GetLibraryAgentResponse200 = { - data: LibraryAgent; - status: 200; -}; - -export type getV2GetLibraryAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetLibraryAgentResponseComposite = - | getV2GetLibraryAgentResponse200 - | getV2GetLibraryAgentResponse422; - -export type getV2GetLibraryAgentResponse = - getV2GetLibraryAgentResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetLibraryAgentUrl = (libraryAgentId: string) => { - return `/api/library/agents/${libraryAgentId}`; -}; - -export const getV2GetLibraryAgent = async ( - libraryAgentId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetLibraryAgentUrl(libraryAgentId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetLibraryAgentQueryKey = (libraryAgentId: string) => { - return [`/api/library/agents/${libraryAgentId}`] as const; -}; - -export const getGetV2GetLibraryAgentQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - libraryAgentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2GetLibraryAgentQueryKey(libraryAgentId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetLibraryAgent(libraryAgentId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!libraryAgentId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetLibraryAgentQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetLibraryAgentQueryError = HTTPValidationError; - -export function useGetV2GetLibraryAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - libraryAgentId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetLibraryAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - libraryAgentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetLibraryAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - libraryAgentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Library Agent - */ - -export function useGetV2GetLibraryAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - libraryAgentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetLibraryAgentQueryOptions( - libraryAgentId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Library Agent - */ -export const prefetchGetV2GetLibraryAgentQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - libraryAgentId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetLibraryAgentQueryOptions( - libraryAgentId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Update the library agent with the given fields. - -Args: - library_agent_id: ID of the library agent to update. - payload: Fields to update (auto_update_version, is_favorite, etc.). - user_id: ID of the authenticated user. - -Raises: - HTTPException(500): If a server/database error occurs. - * @summary Update Library Agent - */ -export type patchV2UpdateLibraryAgentResponse200 = { - data: LibraryAgent; - status: 200; -}; - -export type patchV2UpdateLibraryAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type patchV2UpdateLibraryAgentResponse500 = { - data: void; - status: 500; -}; - -export type patchV2UpdateLibraryAgentResponseComposite = - | patchV2UpdateLibraryAgentResponse200 - | patchV2UpdateLibraryAgentResponse422 - | patchV2UpdateLibraryAgentResponse500; - -export type patchV2UpdateLibraryAgentResponse = - patchV2UpdateLibraryAgentResponseComposite & { - headers: Headers; - }; - -export const getPatchV2UpdateLibraryAgentUrl = (libraryAgentId: string) => { - return `/api/library/agents/${libraryAgentId}`; -}; - -export const patchV2UpdateLibraryAgent = async ( - libraryAgentId: string, - libraryAgentUpdateRequest: LibraryAgentUpdateRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPatchV2UpdateLibraryAgentUrl(libraryAgentId), - { - ...options, - method: "PATCH", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(libraryAgentUpdateRequest), - }, - ); -}; - -export const getPatchV2UpdateLibraryAgentMutationOptions = < - TError = HTTPValidationError | void, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string; data: LibraryAgentUpdateRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string; data: LibraryAgentUpdateRequest }, - TContext -> => { - const mutationKey = ["patchV2UpdateLibraryAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { libraryAgentId: string; data: LibraryAgentUpdateRequest } - > = (props) => { - const { libraryAgentId, data } = props ?? {}; - - return patchV2UpdateLibraryAgent(libraryAgentId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PatchV2UpdateLibraryAgentMutationResult = NonNullable< - Awaited> ->; -export type PatchV2UpdateLibraryAgentMutationBody = LibraryAgentUpdateRequest; -export type PatchV2UpdateLibraryAgentMutationError = HTTPValidationError | void; - -/** - * @summary Update Library Agent - */ -export const usePatchV2UpdateLibraryAgent = < - TError = HTTPValidationError | void, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string; data: LibraryAgentUpdateRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { libraryAgentId: string; data: LibraryAgentUpdateRequest }, - TContext -> => { - const mutationOptions = getPatchV2UpdateLibraryAgentMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Soft-delete the specified library agent. - -Args: - library_agent_id: ID of the library agent to delete. - user_id: ID of the authenticated user. - -Returns: - 204 No Content if successful. - -Raises: - HTTPException(404): If the agent does not exist. - HTTPException(500): If a server/database error occurs. - * @summary Delete Library Agent - */ -export type deleteV2DeleteLibraryAgentResponse200 = { - data: unknown; - status: 200; -}; - -export type deleteV2DeleteLibraryAgentResponse204 = { - data: void; - status: 204; -}; - -export type deleteV2DeleteLibraryAgentResponse404 = { - data: void; - status: 404; -}; - -export type deleteV2DeleteLibraryAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV2DeleteLibraryAgentResponseComposite = - | deleteV2DeleteLibraryAgentResponse200 - | deleteV2DeleteLibraryAgentResponse204 - | deleteV2DeleteLibraryAgentResponse404 - | deleteV2DeleteLibraryAgentResponse422; - -export type deleteV2DeleteLibraryAgentResponse = - deleteV2DeleteLibraryAgentResponseComposite & { - headers: Headers; - }; - -export const getDeleteV2DeleteLibraryAgentUrl = (libraryAgentId: string) => { - return `/api/library/agents/${libraryAgentId}`; -}; - -export const deleteV2DeleteLibraryAgent = async ( - libraryAgentId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV2DeleteLibraryAgentUrl(libraryAgentId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV2DeleteLibraryAgentMutationOptions = < - TError = void | HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext -> => { - const mutationKey = ["deleteV2DeleteLibraryAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { libraryAgentId: string } - > = (props) => { - const { libraryAgentId } = props ?? {}; - - return deleteV2DeleteLibraryAgent(libraryAgentId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV2DeleteLibraryAgentMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV2DeleteLibraryAgentMutationError = - void | HTTPValidationError; - -/** - * @summary Delete Library Agent - */ -export const useDeleteV2DeleteLibraryAgent = < - TError = void | HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { libraryAgentId: string }, - TContext -> => { - const mutationOptions = getDeleteV2DeleteLibraryAgentMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get Library Agent By Graph Id - */ -export type getV2GetLibraryAgentByGraphIdResponse200 = { - data: LibraryAgent; - status: 200; -}; - -export type getV2GetLibraryAgentByGraphIdResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetLibraryAgentByGraphIdResponseComposite = - | getV2GetLibraryAgentByGraphIdResponse200 - | getV2GetLibraryAgentByGraphIdResponse422; - -export type getV2GetLibraryAgentByGraphIdResponse = - getV2GetLibraryAgentByGraphIdResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetLibraryAgentByGraphIdUrl = ( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/library/agents/by-graph/${graphId}?${stringifiedParams}` - : `/api/library/agents/by-graph/${graphId}`; -}; - -export const getV2GetLibraryAgentByGraphId = async ( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetLibraryAgentByGraphIdUrl(graphId, params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetLibraryAgentByGraphIdQueryKey = ( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, -) => { - return [ - `/api/library/agents/by-graph/${graphId}`, - ...(params ? [params] : []), - ] as const; -}; - -export const getGetV2GetLibraryAgentByGraphIdQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2GetLibraryAgentByGraphIdQueryKey(graphId, params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetLibraryAgentByGraphId(graphId, params, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!graphId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetLibraryAgentByGraphIdQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetLibraryAgentByGraphIdQueryError = HTTPValidationError; - -export function useGetV2GetLibraryAgentByGraphId< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params: undefined | GetV2GetLibraryAgentByGraphIdParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetLibraryAgentByGraphId< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetLibraryAgentByGraphId< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Library Agent By Graph Id - */ - -export function useGetV2GetLibraryAgentByGraphId< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetLibraryAgentByGraphIdQueryOptions( - graphId, - params, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Library Agent By Graph Id - */ -export const prefetchGetV2GetLibraryAgentByGraphIdQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - params?: GetV2GetLibraryAgentByGraphIdParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetLibraryAgentByGraphIdQueryOptions( - graphId, - params, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get Library Agent from Store Listing Version ID. - * @summary Get Agent By Store ID - */ -export type getV2GetAgentByStoreIdResponse200 = { - data: GetV2GetAgentByStoreId200; - status: 200; -}; - -export type getV2GetAgentByStoreIdResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetAgentByStoreIdResponseComposite = - | getV2GetAgentByStoreIdResponse200 - | getV2GetAgentByStoreIdResponse422; - -export type getV2GetAgentByStoreIdResponse = - getV2GetAgentByStoreIdResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetAgentByStoreIdUrl = (storeListingVersionId: string) => { - return `/api/library/agents/marketplace/${storeListingVersionId}`; -}; - -export const getV2GetAgentByStoreId = async ( - storeListingVersionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetAgentByStoreIdUrl(storeListingVersionId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetAgentByStoreIdQueryKey = ( - storeListingVersionId: string, -) => { - return [`/api/library/agents/marketplace/${storeListingVersionId}`] as const; -}; - -export const getGetV2GetAgentByStoreIdQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2GetAgentByStoreIdQueryKey(storeListingVersionId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetAgentByStoreId(storeListingVersionId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!storeListingVersionId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetAgentByStoreIdQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetAgentByStoreIdQueryError = HTTPValidationError; - -export function useGetV2GetAgentByStoreId< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentByStoreId< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentByStoreId< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get Agent By Store ID - */ - -export function useGetV2GetAgentByStoreId< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetAgentByStoreIdQueryOptions( - storeListingVersionId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get Agent By Store ID - */ -export const prefetchGetV2GetAgentByStoreIdQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetAgentByStoreIdQueryOptions( - storeListingVersionId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Fork Library Agent - */ -export type postV2ForkLibraryAgentResponse200 = { - data: LibraryAgent; - status: 200; -}; - -export type postV2ForkLibraryAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2ForkLibraryAgentResponseComposite = - | postV2ForkLibraryAgentResponse200 - | postV2ForkLibraryAgentResponse422; - -export type postV2ForkLibraryAgentResponse = - postV2ForkLibraryAgentResponseComposite & { - headers: Headers; - }; - -export const getPostV2ForkLibraryAgentUrl = (libraryAgentId: string) => { - return `/api/library/agents/${libraryAgentId}/fork`; -}; - -export const postV2ForkLibraryAgent = async ( - libraryAgentId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2ForkLibraryAgentUrl(libraryAgentId), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV2ForkLibraryAgentMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext -> => { - const mutationKey = ["postV2ForkLibraryAgent"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { libraryAgentId: string } - > = (props) => { - const { libraryAgentId } = props ?? {}; - - return postV2ForkLibraryAgent(libraryAgentId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2ForkLibraryAgentMutationResult = NonNullable< - Awaited> ->; - -export type PostV2ForkLibraryAgentMutationError = HTTPValidationError; - -/** - * @summary Fork Library Agent - */ -export const usePostV2ForkLibraryAgent = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { libraryAgentId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { libraryAgentId: string }, - TContext -> => { - const mutationOptions = getPostV2ForkLibraryAgentMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/onboarding/onboarding.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/onboarding/onboarding.ts deleted file mode 100644 index 9be4d49de32d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/onboarding/onboarding.ts +++ /dev/null @@ -1,744 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { UserOnboardingUpdate } from "../../models/userOnboardingUpdate"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Get onboarding status - */ -export type getV1GetOnboardingStatusResponse200 = { - data: unknown; - status: 200; -}; - -export type getV1GetOnboardingStatusResponseComposite = - getV1GetOnboardingStatusResponse200; - -export type getV1GetOnboardingStatusResponse = - getV1GetOnboardingStatusResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetOnboardingStatusUrl = () => { - return `/api/onboarding`; -}; - -export const getV1GetOnboardingStatus = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetOnboardingStatusUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetOnboardingStatusQueryKey = () => { - return [`/api/onboarding`] as const; -}; - -export const getGetV1GetOnboardingStatusQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetOnboardingStatusQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetOnboardingStatus({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetOnboardingStatusQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetOnboardingStatusQueryError = unknown; - -export function useGetV1GetOnboardingStatus< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetOnboardingStatus< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetOnboardingStatus< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get onboarding status - */ - -export function useGetV1GetOnboardingStatus< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetOnboardingStatusQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get onboarding status - */ -export const prefetchGetV1GetOnboardingStatusQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetOnboardingStatusQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Update onboarding progress - */ -export type patchV1UpdateOnboardingProgressResponse200 = { - data: unknown; - status: 200; -}; - -export type patchV1UpdateOnboardingProgressResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type patchV1UpdateOnboardingProgressResponseComposite = - | patchV1UpdateOnboardingProgressResponse200 - | patchV1UpdateOnboardingProgressResponse422; - -export type patchV1UpdateOnboardingProgressResponse = - patchV1UpdateOnboardingProgressResponseComposite & { - headers: Headers; - }; - -export const getPatchV1UpdateOnboardingProgressUrl = () => { - return `/api/onboarding`; -}; - -export const patchV1UpdateOnboardingProgress = async ( - userOnboardingUpdate: UserOnboardingUpdate, - options?: RequestInit, -): Promise => { - return customMutator( - getPatchV1UpdateOnboardingProgressUrl(), - { - ...options, - method: "PATCH", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(userOnboardingUpdate), - }, - ); -}; - -export const getPatchV1UpdateOnboardingProgressMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: UserOnboardingUpdate }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: UserOnboardingUpdate }, - TContext -> => { - const mutationKey = ["patchV1UpdateOnboardingProgress"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: UserOnboardingUpdate } - > = (props) => { - const { data } = props ?? {}; - - return patchV1UpdateOnboardingProgress(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PatchV1UpdateOnboardingProgressMutationResult = NonNullable< - Awaited> ->; -export type PatchV1UpdateOnboardingProgressMutationBody = UserOnboardingUpdate; -export type PatchV1UpdateOnboardingProgressMutationError = HTTPValidationError; - -/** - * @summary Update onboarding progress - */ -export const usePatchV1UpdateOnboardingProgress = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: UserOnboardingUpdate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: UserOnboardingUpdate }, - TContext -> => { - const mutationOptions = - getPatchV1UpdateOnboardingProgressMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary Get recommended agents - */ -export type getV1GetRecommendedAgentsResponse200 = { - data: unknown; - status: 200; -}; - -export type getV1GetRecommendedAgentsResponseComposite = - getV1GetRecommendedAgentsResponse200; - -export type getV1GetRecommendedAgentsResponse = - getV1GetRecommendedAgentsResponseComposite & { - headers: Headers; - }; - -export const getGetV1GetRecommendedAgentsUrl = () => { - return `/api/onboarding/agents`; -}; - -export const getV1GetRecommendedAgents = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1GetRecommendedAgentsUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1GetRecommendedAgentsQueryKey = () => { - return [`/api/onboarding/agents`] as const; -}; - -export const getGetV1GetRecommendedAgentsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1GetRecommendedAgentsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV1GetRecommendedAgents({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1GetRecommendedAgentsQueryResult = NonNullable< - Awaited> ->; -export type GetV1GetRecommendedAgentsQueryError = unknown; - -export function useGetV1GetRecommendedAgents< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetRecommendedAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1GetRecommendedAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get recommended agents - */ - -export function useGetV1GetRecommendedAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1GetRecommendedAgentsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get recommended agents - */ -export const prefetchGetV1GetRecommendedAgentsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1GetRecommendedAgentsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Check onboarding enabled - */ -export type getV1CheckOnboardingEnabledResponse200 = { - data: unknown; - status: 200; -}; - -export type getV1CheckOnboardingEnabledResponseComposite = - getV1CheckOnboardingEnabledResponse200; - -export type getV1CheckOnboardingEnabledResponse = - getV1CheckOnboardingEnabledResponseComposite & { - headers: Headers; - }; - -export const getGetV1CheckOnboardingEnabledUrl = () => { - return `/api/onboarding/enabled`; -}; - -export const getV1CheckOnboardingEnabled = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1CheckOnboardingEnabledUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1CheckOnboardingEnabledQueryKey = () => { - return [`/api/onboarding/enabled`] as const; -}; - -export const getGetV1CheckOnboardingEnabledQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1CheckOnboardingEnabledQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1CheckOnboardingEnabled({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1CheckOnboardingEnabledQueryResult = NonNullable< - Awaited> ->; -export type GetV1CheckOnboardingEnabledQueryError = unknown; - -export function useGetV1CheckOnboardingEnabled< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1CheckOnboardingEnabled< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1CheckOnboardingEnabled< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Check onboarding enabled - */ - -export function useGetV1CheckOnboardingEnabled< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1CheckOnboardingEnabledQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Check onboarding enabled - */ -export const prefetchGetV1CheckOnboardingEnabledQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1CheckOnboardingEnabledQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/otto/otto.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/otto/otto.ts deleted file mode 100644 index 9096ff2be826..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/otto/otto.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation } from "@tanstack/react-query"; -import type { - MutationFunction, - QueryClient, - UseMutationOptions, - UseMutationResult, -} from "@tanstack/react-query"; - -import type { ApiResponse } from "../../models/apiResponse"; - -import type { ChatRequest } from "../../models/chatRequest"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Proxy requests to Otto API while adding necessary security headers and logging. -Requires an authenticated user. - * @summary Proxy Otto Chat Request - */ -export type postV2ProxyOttoChatRequestResponse200 = { - data: ApiResponse; - status: 200; -}; - -export type postV2ProxyOttoChatRequestResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2ProxyOttoChatRequestResponseComposite = - | postV2ProxyOttoChatRequestResponse200 - | postV2ProxyOttoChatRequestResponse422; - -export type postV2ProxyOttoChatRequestResponse = - postV2ProxyOttoChatRequestResponseComposite & { - headers: Headers; - }; - -export const getPostV2ProxyOttoChatRequestUrl = () => { - return `/api/otto/ask`; -}; - -export const postV2ProxyOttoChatRequest = async ( - chatRequest: ChatRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2ProxyOttoChatRequestUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(chatRequest), - }, - ); -}; - -export const getPostV2ProxyOttoChatRequestMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: ChatRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: ChatRequest }, - TContext -> => { - const mutationKey = ["postV2ProxyOttoChatRequest"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: ChatRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV2ProxyOttoChatRequest(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2ProxyOttoChatRequestMutationResult = NonNullable< - Awaited> ->; -export type PostV2ProxyOttoChatRequestMutationBody = ChatRequest; -export type PostV2ProxyOttoChatRequestMutationError = HTTPValidationError; - -/** - * @summary Proxy Otto Chat Request - */ -export const usePostV2ProxyOttoChatRequest = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: ChatRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: ChatRequest }, - TContext -> => { - const mutationOptions = getPostV2ProxyOttoChatRequestMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/presets/presets.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/presets/presets.ts deleted file mode 100644 index e0555bc41249..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/presets/presets.ts +++ /dev/null @@ -1,1064 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { BodyPostV2ExecuteAPreset } from "../../models/bodyPostV2ExecuteAPreset"; - -import type { GetV2ListPresetsParams } from "../../models/getV2ListPresetsParams"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { LibraryAgentPreset } from "../../models/libraryAgentPreset"; - -import type { LibraryAgentPresetResponse } from "../../models/libraryAgentPresetResponse"; - -import type { LibraryAgentPresetUpdatable } from "../../models/libraryAgentPresetUpdatable"; - -import type { PostV2CreateANewPresetBody } from "../../models/postV2CreateANewPresetBody"; - -import type { PostV2ExecuteAPreset200 } from "../../models/postV2ExecuteAPreset200"; - -import type { TriggeredPresetSetupRequest } from "../../models/triggeredPresetSetupRequest"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Retrieve a paginated list of presets for the current user. - * @summary List presets - */ -export type getV2ListPresetsResponse200 = { - data: LibraryAgentPresetResponse; - status: 200; -}; - -export type getV2ListPresetsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2ListPresetsResponseComposite = - | getV2ListPresetsResponse200 - | getV2ListPresetsResponse422; - -export type getV2ListPresetsResponse = getV2ListPresetsResponseComposite & { - headers: Headers; -}; - -export const getGetV2ListPresetsUrl = (params: GetV2ListPresetsParams) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/library/presets?${stringifiedParams}` - : `/api/library/presets`; -}; - -export const getV2ListPresets = async ( - params: GetV2ListPresetsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2ListPresetsUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2ListPresetsQueryKey = (params: GetV2ListPresetsParams) => { - return [`/api/library/presets`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2ListPresetsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params: GetV2ListPresetsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListPresetsQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV2ListPresets(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2ListPresetsQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListPresetsQueryError = HTTPValidationError; - -export function useGetV2ListPresets< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: GetV2ListPresetsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListPresets< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: GetV2ListPresetsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListPresets< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: GetV2ListPresetsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List presets - */ - -export function useGetV2ListPresets< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: GetV2ListPresetsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListPresetsQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List presets - */ -export const prefetchGetV2ListPresetsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params: GetV2ListPresetsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListPresetsQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Create a new preset for the current user. - * @summary Create a new preset - */ -export type postV2CreateANewPresetResponse200 = { - data: LibraryAgentPreset; - status: 200; -}; - -export type postV2CreateANewPresetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2CreateANewPresetResponseComposite = - | postV2CreateANewPresetResponse200 - | postV2CreateANewPresetResponse422; - -export type postV2CreateANewPresetResponse = - postV2CreateANewPresetResponseComposite & { - headers: Headers; - }; - -export const getPostV2CreateANewPresetUrl = () => { - return `/api/library/presets`; -}; - -export const postV2CreateANewPreset = async ( - postV2CreateANewPresetBody: PostV2CreateANewPresetBody, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2CreateANewPresetUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(postV2CreateANewPresetBody), - }, - ); -}; - -export const getPostV2CreateANewPresetMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: PostV2CreateANewPresetBody }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: PostV2CreateANewPresetBody }, - TContext -> => { - const mutationKey = ["postV2CreateANewPreset"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: PostV2CreateANewPresetBody } - > = (props) => { - const { data } = props ?? {}; - - return postV2CreateANewPreset(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2CreateANewPresetMutationResult = NonNullable< - Awaited> ->; -export type PostV2CreateANewPresetMutationBody = PostV2CreateANewPresetBody; -export type PostV2CreateANewPresetMutationError = HTTPValidationError; - -/** - * @summary Create a new preset - */ -export const usePostV2CreateANewPreset = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: PostV2CreateANewPresetBody }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: PostV2CreateANewPresetBody }, - TContext -> => { - const mutationOptions = getPostV2CreateANewPresetMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Retrieve details for a specific preset by its ID. - * @summary Get a specific preset - */ -export type getV2GetASpecificPresetResponse200 = { - data: LibraryAgentPreset; - status: 200; -}; - -export type getV2GetASpecificPresetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetASpecificPresetResponseComposite = - | getV2GetASpecificPresetResponse200 - | getV2GetASpecificPresetResponse422; - -export type getV2GetASpecificPresetResponse = - getV2GetASpecificPresetResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetASpecificPresetUrl = (presetId: string) => { - return `/api/library/presets/${presetId}`; -}; - -export const getV2GetASpecificPreset = async ( - presetId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetASpecificPresetUrl(presetId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetASpecificPresetQueryKey = (presetId: string) => { - return [`/api/library/presets/${presetId}`] as const; -}; - -export const getGetV2GetASpecificPresetQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - presetId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2GetASpecificPresetQueryKey(presetId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetASpecificPreset(presetId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!presetId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetASpecificPresetQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetASpecificPresetQueryError = HTTPValidationError; - -export function useGetV2GetASpecificPreset< - TData = Awaited>, - TError = HTTPValidationError, ->( - presetId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetASpecificPreset< - TData = Awaited>, - TError = HTTPValidationError, ->( - presetId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetASpecificPreset< - TData = Awaited>, - TError = HTTPValidationError, ->( - presetId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get a specific preset - */ - -export function useGetV2GetASpecificPreset< - TData = Awaited>, - TError = HTTPValidationError, ->( - presetId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetASpecificPresetQueryOptions( - presetId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get a specific preset - */ -export const prefetchGetV2GetASpecificPresetQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - presetId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetASpecificPresetQueryOptions( - presetId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Update an existing preset by its ID. - * @summary Update an existing preset - */ -export type patchV2UpdateAnExistingPresetResponse200 = { - data: LibraryAgentPreset; - status: 200; -}; - -export type patchV2UpdateAnExistingPresetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type patchV2UpdateAnExistingPresetResponseComposite = - | patchV2UpdateAnExistingPresetResponse200 - | patchV2UpdateAnExistingPresetResponse422; - -export type patchV2UpdateAnExistingPresetResponse = - patchV2UpdateAnExistingPresetResponseComposite & { - headers: Headers; - }; - -export const getPatchV2UpdateAnExistingPresetUrl = (presetId: string) => { - return `/api/library/presets/${presetId}`; -}; - -export const patchV2UpdateAnExistingPreset = async ( - presetId: string, - libraryAgentPresetUpdatable: LibraryAgentPresetUpdatable, - options?: RequestInit, -): Promise => { - return customMutator( - getPatchV2UpdateAnExistingPresetUrl(presetId), - { - ...options, - method: "PATCH", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(libraryAgentPresetUpdatable), - }, - ); -}; - -export const getPatchV2UpdateAnExistingPresetMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: LibraryAgentPresetUpdatable }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: LibraryAgentPresetUpdatable }, - TContext -> => { - const mutationKey = ["patchV2UpdateAnExistingPreset"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { presetId: string; data: LibraryAgentPresetUpdatable } - > = (props) => { - const { presetId, data } = props ?? {}; - - return patchV2UpdateAnExistingPreset(presetId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PatchV2UpdateAnExistingPresetMutationResult = NonNullable< - Awaited> ->; -export type PatchV2UpdateAnExistingPresetMutationBody = - LibraryAgentPresetUpdatable; -export type PatchV2UpdateAnExistingPresetMutationError = HTTPValidationError; - -/** - * @summary Update an existing preset - */ -export const usePatchV2UpdateAnExistingPreset = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: LibraryAgentPresetUpdatable }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { presetId: string; data: LibraryAgentPresetUpdatable }, - TContext -> => { - const mutationOptions = - getPatchV2UpdateAnExistingPresetMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Delete an existing preset by its ID. - * @summary Delete a preset - */ -export type deleteV2DeleteAPresetResponse204 = { - data: void; - status: 204; -}; - -export type deleteV2DeleteAPresetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV2DeleteAPresetResponseComposite = - | deleteV2DeleteAPresetResponse204 - | deleteV2DeleteAPresetResponse422; - -export type deleteV2DeleteAPresetResponse = - deleteV2DeleteAPresetResponseComposite & { - headers: Headers; - }; - -export const getDeleteV2DeleteAPresetUrl = (presetId: string) => { - return `/api/library/presets/${presetId}`; -}; - -export const deleteV2DeleteAPreset = async ( - presetId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV2DeleteAPresetUrl(presetId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV2DeleteAPresetMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { presetId: string }, - TContext -> => { - const mutationKey = ["deleteV2DeleteAPreset"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { presetId: string } - > = (props) => { - const { presetId } = props ?? {}; - - return deleteV2DeleteAPreset(presetId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV2DeleteAPresetMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV2DeleteAPresetMutationError = HTTPValidationError; - -/** - * @summary Delete a preset - */ -export const useDeleteV2DeleteAPreset = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { presetId: string }, - TContext -> => { - const mutationOptions = getDeleteV2DeleteAPresetMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Sets up a webhook-triggered `LibraryAgentPreset` for a `LibraryAgent`. -Returns the correspondingly created `LibraryAgentPreset` with `webhook_id` set. - * @summary Setup Trigger - */ -export type postV2SetupTriggerResponse200 = { - data: LibraryAgentPreset; - status: 200; -}; - -export type postV2SetupTriggerResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2SetupTriggerResponseComposite = - | postV2SetupTriggerResponse200 - | postV2SetupTriggerResponse422; - -export type postV2SetupTriggerResponse = postV2SetupTriggerResponseComposite & { - headers: Headers; -}; - -export const getPostV2SetupTriggerUrl = () => { - return `/api/library/presets/setup-trigger`; -}; - -export const postV2SetupTrigger = async ( - triggeredPresetSetupRequest: TriggeredPresetSetupRequest, - options?: RequestInit, -): Promise => { - return customMutator(getPostV2SetupTriggerUrl(), { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(triggeredPresetSetupRequest), - }); -}; - -export const getPostV2SetupTriggerMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: TriggeredPresetSetupRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: TriggeredPresetSetupRequest }, - TContext -> => { - const mutationKey = ["postV2SetupTrigger"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: TriggeredPresetSetupRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV2SetupTrigger(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2SetupTriggerMutationResult = NonNullable< - Awaited> ->; -export type PostV2SetupTriggerMutationBody = TriggeredPresetSetupRequest; -export type PostV2SetupTriggerMutationError = HTTPValidationError; - -/** - * @summary Setup Trigger - */ -export const usePostV2SetupTrigger = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: TriggeredPresetSetupRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: TriggeredPresetSetupRequest }, - TContext -> => { - const mutationOptions = getPostV2SetupTriggerMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Execute a preset with the given graph and node input for the current user. - * @summary Execute a preset - */ -export type postV2ExecuteAPresetResponse200 = { - data: PostV2ExecuteAPreset200; - status: 200; -}; - -export type postV2ExecuteAPresetResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2ExecuteAPresetResponseComposite = - | postV2ExecuteAPresetResponse200 - | postV2ExecuteAPresetResponse422; - -export type postV2ExecuteAPresetResponse = - postV2ExecuteAPresetResponseComposite & { - headers: Headers; - }; - -export const getPostV2ExecuteAPresetUrl = (presetId: string) => { - return `/api/library/presets/${presetId}/execute`; -}; - -export const postV2ExecuteAPreset = async ( - presetId: string, - bodyPostV2ExecuteAPreset: BodyPostV2ExecuteAPreset, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2ExecuteAPresetUrl(presetId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(bodyPostV2ExecuteAPreset), - }, - ); -}; - -export const getPostV2ExecuteAPresetMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: BodyPostV2ExecuteAPreset }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: BodyPostV2ExecuteAPreset }, - TContext -> => { - const mutationKey = ["postV2ExecuteAPreset"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { presetId: string; data: BodyPostV2ExecuteAPreset } - > = (props) => { - const { presetId, data } = props ?? {}; - - return postV2ExecuteAPreset(presetId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2ExecuteAPresetMutationResult = NonNullable< - Awaited> ->; -export type PostV2ExecuteAPresetMutationBody = BodyPostV2ExecuteAPreset; -export type PostV2ExecuteAPresetMutationError = HTTPValidationError; - -/** - * @summary Execute a preset - */ -export const usePostV2ExecuteAPreset = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { presetId: string; data: BodyPostV2ExecuteAPreset }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { presetId: string; data: BodyPostV2ExecuteAPreset }, - TContext -> => { - const mutationOptions = getPostV2ExecuteAPresetMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/schedules/schedules.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/schedules/schedules.ts deleted file mode 100644 index 18e79df4af15..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/schedules/schedules.ts +++ /dev/null @@ -1,697 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { DeleteV1DeleteExecutionSchedule200 } from "../../models/deleteV1DeleteExecutionSchedule200"; - -import type { GraphExecutionJobInfo } from "../../models/graphExecutionJobInfo"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { ScheduleCreationRequest } from "../../models/scheduleCreationRequest"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * @summary Create execution schedule - */ -export type postV1CreateExecutionScheduleResponse200 = { - data: GraphExecutionJobInfo; - status: 200; -}; - -export type postV1CreateExecutionScheduleResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV1CreateExecutionScheduleResponseComposite = - | postV1CreateExecutionScheduleResponse200 - | postV1CreateExecutionScheduleResponse422; - -export type postV1CreateExecutionScheduleResponse = - postV1CreateExecutionScheduleResponseComposite & { - headers: Headers; - }; - -export const getPostV1CreateExecutionScheduleUrl = (graphId: string) => { - return `/api/graphs/${graphId}/schedules`; -}; - -export const postV1CreateExecutionSchedule = async ( - graphId: string, - scheduleCreationRequest: ScheduleCreationRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV1CreateExecutionScheduleUrl(graphId), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(scheduleCreationRequest), - }, - ); -}; - -export const getPostV1CreateExecutionScheduleMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: ScheduleCreationRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: ScheduleCreationRequest }, - TContext -> => { - const mutationKey = ["postV1CreateExecutionSchedule"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { graphId: string; data: ScheduleCreationRequest } - > = (props) => { - const { graphId, data } = props ?? {}; - - return postV1CreateExecutionSchedule(graphId, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV1CreateExecutionScheduleMutationResult = NonNullable< - Awaited> ->; -export type PostV1CreateExecutionScheduleMutationBody = ScheduleCreationRequest; -export type PostV1CreateExecutionScheduleMutationError = HTTPValidationError; - -/** - * @summary Create execution schedule - */ -export const usePostV1CreateExecutionSchedule = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { graphId: string; data: ScheduleCreationRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { graphId: string; data: ScheduleCreationRequest }, - TContext -> => { - const mutationOptions = - getPostV1CreateExecutionScheduleMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * @summary List execution schedules for a graph - */ -export type getV1ListExecutionSchedulesForAGraphResponse200 = { - data: GraphExecutionJobInfo[]; - status: 200; -}; - -export type getV1ListExecutionSchedulesForAGraphResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV1ListExecutionSchedulesForAGraphResponseComposite = - | getV1ListExecutionSchedulesForAGraphResponse200 - | getV1ListExecutionSchedulesForAGraphResponse422; - -export type getV1ListExecutionSchedulesForAGraphResponse = - getV1ListExecutionSchedulesForAGraphResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListExecutionSchedulesForAGraphUrl = (graphId: string) => { - return `/api/graphs/${graphId}/schedules`; -}; - -export const getV1ListExecutionSchedulesForAGraph = async ( - graphId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListExecutionSchedulesForAGraphUrl(graphId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListExecutionSchedulesForAGraphQueryKey = ( - graphId: string, -) => { - return [`/api/graphs/${graphId}/schedules`] as const; -}; - -export const getGetV1ListExecutionSchedulesForAGraphQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV1ListExecutionSchedulesForAGraphQueryKey(graphId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1ListExecutionSchedulesForAGraph(graphId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!graphId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListExecutionSchedulesForAGraphQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListExecutionSchedulesForAGraphQueryError = - HTTPValidationError; - -export function useGetV1ListExecutionSchedulesForAGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListExecutionSchedulesForAGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListExecutionSchedulesForAGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List execution schedules for a graph - */ - -export function useGetV1ListExecutionSchedulesForAGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV1ListExecutionSchedulesForAGraphQueryOptions( - graphId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List execution schedules for a graph - */ -export const prefetchGetV1ListExecutionSchedulesForAGraphQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - graphId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV1ListExecutionSchedulesForAGraphQueryOptions( - graphId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary List execution schedules for a user - */ -export type getV1ListExecutionSchedulesForAUserResponse200 = { - data: GraphExecutionJobInfo[]; - status: 200; -}; - -export type getV1ListExecutionSchedulesForAUserResponseComposite = - getV1ListExecutionSchedulesForAUserResponse200; - -export type getV1ListExecutionSchedulesForAUserResponse = - getV1ListExecutionSchedulesForAUserResponseComposite & { - headers: Headers; - }; - -export const getGetV1ListExecutionSchedulesForAUserUrl = () => { - return `/api/schedules`; -}; - -export const getV1ListExecutionSchedulesForAUser = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV1ListExecutionSchedulesForAUserUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV1ListExecutionSchedulesForAUserQueryKey = () => { - return [`/api/schedules`] as const; -}; - -export const getGetV1ListExecutionSchedulesForAUserQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV1ListExecutionSchedulesForAUserQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV1ListExecutionSchedulesForAUser({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV1ListExecutionSchedulesForAUserQueryResult = NonNullable< - Awaited> ->; -export type GetV1ListExecutionSchedulesForAUserQueryError = unknown; - -export function useGetV1ListExecutionSchedulesForAUser< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListExecutionSchedulesForAUser< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV1ListExecutionSchedulesForAUser< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List execution schedules for a user - */ - -export function useGetV1ListExecutionSchedulesForAUser< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = - getGetV1ListExecutionSchedulesForAUserQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List execution schedules for a user - */ -export const prefetchGetV1ListExecutionSchedulesForAUserQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = - getGetV1ListExecutionSchedulesForAUserQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Delete execution schedule - */ -export type deleteV1DeleteExecutionScheduleResponse200 = { - data: DeleteV1DeleteExecutionSchedule200; - status: 200; -}; - -export type deleteV1DeleteExecutionScheduleResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV1DeleteExecutionScheduleResponseComposite = - | deleteV1DeleteExecutionScheduleResponse200 - | deleteV1DeleteExecutionScheduleResponse422; - -export type deleteV1DeleteExecutionScheduleResponse = - deleteV1DeleteExecutionScheduleResponseComposite & { - headers: Headers; - }; - -export const getDeleteV1DeleteExecutionScheduleUrl = (scheduleId: string) => { - return `/api/schedules/${scheduleId}`; -}; - -export const deleteV1DeleteExecutionSchedule = async ( - scheduleId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV1DeleteExecutionScheduleUrl(scheduleId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV1DeleteExecutionScheduleMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { scheduleId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { scheduleId: string }, - TContext -> => { - const mutationKey = ["deleteV1DeleteExecutionSchedule"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { scheduleId: string } - > = (props) => { - const { scheduleId } = props ?? {}; - - return deleteV1DeleteExecutionSchedule(scheduleId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV1DeleteExecutionScheduleMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV1DeleteExecutionScheduleMutationError = HTTPValidationError; - -/** - * @summary Delete execution schedule - */ -export const useDeleteV1DeleteExecutionSchedule = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { scheduleId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { scheduleId: string }, - TContext -> => { - const mutationOptions = - getDeleteV1DeleteExecutionScheduleMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts deleted file mode 100644 index a6b379665ffe..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts +++ /dev/null @@ -1,3123 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DataTag, - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryClient, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; - -import type { BodyPostV2UploadSubmissionMedia } from "../../models/bodyPostV2UploadSubmissionMedia"; - -import type { CreatorDetails } from "../../models/creatorDetails"; - -import type { CreatorsResponse } from "../../models/creatorsResponse"; - -import type { GetV2ListMySubmissionsParams } from "../../models/getV2ListMySubmissionsParams"; - -import type { GetV2ListStoreAgentsParams } from "../../models/getV2ListStoreAgentsParams"; - -import type { GetV2ListStoreCreatorsParams } from "../../models/getV2ListStoreCreatorsParams"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { MyAgentsResponse } from "../../models/myAgentsResponse"; - -import type { PostV2GenerateSubmissionImageParams } from "../../models/postV2GenerateSubmissionImageParams"; - -import type { Profile } from "../../models/profile"; - -import type { ProfileDetails } from "../../models/profileDetails"; - -import type { StoreAgentDetails } from "../../models/storeAgentDetails"; - -import type { StoreAgentsResponse } from "../../models/storeAgentsResponse"; - -import type { StoreReview } from "../../models/storeReview"; - -import type { StoreReviewCreate } from "../../models/storeReviewCreate"; - -import type { StoreSubmission } from "../../models/storeSubmission"; - -import type { StoreSubmissionRequest } from "../../models/storeSubmissionRequest"; - -import type { StoreSubmissionsResponse } from "../../models/storeSubmissionsResponse"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Get the profile details for the authenticated user. - * @summary Get user profile - */ -export type getV2GetUserProfileResponse200 = { - data: ProfileDetails; - status: 200; -}; - -export type getV2GetUserProfileResponseComposite = - getV2GetUserProfileResponse200; - -export type getV2GetUserProfileResponse = - getV2GetUserProfileResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetUserProfileUrl = () => { - return `/api/store/profile`; -}; - -export const getV2GetUserProfile = async ( - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetUserProfileUrl(), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetUserProfileQueryKey = () => { - return [`/api/store/profile`] as const; -}; - -export const getGetV2GetUserProfileQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV2GetUserProfileQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV2GetUserProfile({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetUserProfileQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetUserProfileQueryError = unknown; - -export function useGetV2GetUserProfile< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetUserProfile< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetUserProfile< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get user profile - */ - -export function useGetV2GetUserProfile< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetUserProfileQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get user profile - */ -export const prefetchGetV2GetUserProfileQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetUserProfileQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Update the store profile for the authenticated user. - -Args: - profile (Profile): The updated profile details - user_id (str): ID of the authenticated user - -Returns: - CreatorDetails: The updated profile - -Raises: - HTTPException: If there is an error updating the profile - * @summary Update user profile - */ -export type postV2UpdateUserProfileResponse200 = { - data: CreatorDetails; - status: 200; -}; - -export type postV2UpdateUserProfileResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2UpdateUserProfileResponseComposite = - | postV2UpdateUserProfileResponse200 - | postV2UpdateUserProfileResponse422; - -export type postV2UpdateUserProfileResponse = - postV2UpdateUserProfileResponseComposite & { - headers: Headers; - }; - -export const getPostV2UpdateUserProfileUrl = () => { - return `/api/store/profile`; -}; - -export const postV2UpdateUserProfile = async ( - profile: Profile, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2UpdateUserProfileUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(profile), - }, - ); -}; - -export const getPostV2UpdateUserProfileMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: Profile }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: Profile }, - TContext -> => { - const mutationKey = ["postV2UpdateUserProfile"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: Profile } - > = (props) => { - const { data } = props ?? {}; - - return postV2UpdateUserProfile(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2UpdateUserProfileMutationResult = NonNullable< - Awaited> ->; -export type PostV2UpdateUserProfileMutationBody = Profile; -export type PostV2UpdateUserProfileMutationError = HTTPValidationError; - -/** - * @summary Update user profile - */ -export const usePostV2UpdateUserProfile = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: Profile }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: Profile }, - TContext -> => { - const mutationOptions = getPostV2UpdateUserProfileMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Get a paginated list of agents from the store with optional filtering and sorting. - -Args: - featured (bool, optional): Filter to only show featured agents. Defaults to False. - creator (str | None, optional): Filter agents by creator username. Defaults to None. - sorted_by (str | None, optional): Sort agents by "runs" or "rating". Defaults to None. - search_query (str | None, optional): Search agents by name, subheading and description. Defaults to None. - category (str | None, optional): Filter agents by category. Defaults to None. - page (int, optional): Page number for pagination. Defaults to 1. - page_size (int, optional): Number of agents per page. Defaults to 20. - -Returns: - StoreAgentsResponse: Paginated list of agents matching the filters - -Raises: - HTTPException: If page or page_size are less than 1 - -Used for: -- Home Page Featured Agents -- Home Page Top Agents -- Search Results -- Agent Details - Other Agents By Creator -- Agent Details - Similar Agents -- Creator Details - Agents By Creator - * @summary List store agents - */ -export type getV2ListStoreAgentsResponse200 = { - data: StoreAgentsResponse; - status: 200; -}; - -export type getV2ListStoreAgentsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2ListStoreAgentsResponseComposite = - | getV2ListStoreAgentsResponse200 - | getV2ListStoreAgentsResponse422; - -export type getV2ListStoreAgentsResponse = - getV2ListStoreAgentsResponseComposite & { - headers: Headers; - }; - -export const getGetV2ListStoreAgentsUrl = ( - params?: GetV2ListStoreAgentsParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/store/agents?${stringifiedParams}` - : `/api/store/agents`; -}; - -export const getV2ListStoreAgents = async ( - params?: GetV2ListStoreAgentsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2ListStoreAgentsUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2ListStoreAgentsQueryKey = ( - params?: GetV2ListStoreAgentsParams, -) => { - return [`/api/store/agents`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2ListStoreAgentsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListStoreAgentsQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2ListStoreAgents(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2ListStoreAgentsQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListStoreAgentsQueryError = HTTPValidationError; - -export function useGetV2ListStoreAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2ListStoreAgentsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListStoreAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListStoreAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List store agents - */ - -export function useGetV2ListStoreAgents< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListStoreAgentsQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List store agents - */ -export const prefetchGetV2ListStoreAgentsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2ListStoreAgentsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListStoreAgentsQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * This is only used on the AgentDetails Page - -It returns the store listing agents details. - * @summary Get specific agent - */ -export type getV2GetSpecificAgentResponse200 = { - data: StoreAgentDetails; - status: 200; -}; - -export type getV2GetSpecificAgentResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetSpecificAgentResponseComposite = - | getV2GetSpecificAgentResponse200 - | getV2GetSpecificAgentResponse422; - -export type getV2GetSpecificAgentResponse = - getV2GetSpecificAgentResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetSpecificAgentUrl = ( - username: string, - agentName: string, -) => { - return `/api/store/agents/${username}/${agentName}`; -}; - -export const getV2GetSpecificAgent = async ( - username: string, - agentName: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetSpecificAgentUrl(username, agentName), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetSpecificAgentQueryKey = ( - username: string, - agentName: string, -) => { - return [`/api/store/agents/${username}/${agentName}`] as const; -}; - -export const getGetV2GetSpecificAgentQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - agentName: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2GetSpecificAgentQueryKey(username, agentName); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetSpecificAgent(username, agentName, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!(username && agentName), - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetSpecificAgentQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetSpecificAgentQueryError = HTTPValidationError; - -export function useGetV2GetSpecificAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - agentName: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetSpecificAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - agentName: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetSpecificAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - agentName: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get specific agent - */ - -export function useGetV2GetSpecificAgent< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - agentName: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetSpecificAgentQueryOptions( - username, - agentName, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get specific agent - */ -export const prefetchGetV2GetSpecificAgentQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - username: string, - agentName: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetSpecificAgentQueryOptions( - username, - agentName, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get Agent Graph from Store Listing Version ID. - * @summary Get agent graph - */ -export type getV2GetAgentGraphResponse200 = { - data: unknown; - status: 200; -}; - -export type getV2GetAgentGraphResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetAgentGraphResponseComposite = - | getV2GetAgentGraphResponse200 - | getV2GetAgentGraphResponse422; - -export type getV2GetAgentGraphResponse = getV2GetAgentGraphResponseComposite & { - headers: Headers; -}; - -export const getGetV2GetAgentGraphUrl = (storeListingVersionId: string) => { - return `/api/store/graph/${storeListingVersionId}`; -}; - -export const getV2GetAgentGraph = async ( - storeListingVersionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetAgentGraphUrl(storeListingVersionId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetAgentGraphQueryKey = ( - storeListingVersionId: string, -) => { - return [`/api/store/graph/${storeListingVersionId}`] as const; -}; - -export const getGetV2GetAgentGraphQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2GetAgentGraphQueryKey(storeListingVersionId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetAgentGraph(storeListingVersionId, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!storeListingVersionId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetAgentGraphQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetAgentGraphQueryError = HTTPValidationError; - -export function useGetV2GetAgentGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get agent graph - */ - -export function useGetV2GetAgentGraph< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetAgentGraphQueryOptions( - storeListingVersionId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get agent graph - */ -export const prefetchGetV2GetAgentGraphQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetAgentGraphQueryOptions( - storeListingVersionId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get Store Agent Details from Store Listing Version ID. - * @summary Get agent by version - */ -export type getV2GetAgentByVersionResponse200 = { - data: StoreAgentDetails; - status: 200; -}; - -export type getV2GetAgentByVersionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetAgentByVersionResponseComposite = - | getV2GetAgentByVersionResponse200 - | getV2GetAgentByVersionResponse422; - -export type getV2GetAgentByVersionResponse = - getV2GetAgentByVersionResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetAgentByVersionUrl = (storeListingVersionId: string) => { - return `/api/store/agents/${storeListingVersionId}`; -}; - -export const getV2GetAgentByVersion = async ( - storeListingVersionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetAgentByVersionUrl(storeListingVersionId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetAgentByVersionQueryKey = ( - storeListingVersionId: string, -) => { - return [`/api/store/agents/${storeListingVersionId}`] as const; -}; - -export const getGetV2GetAgentByVersionQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2GetAgentByVersionQueryKey(storeListingVersionId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetAgentByVersion(storeListingVersionId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!storeListingVersionId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetAgentByVersionQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetAgentByVersionQueryError = HTTPValidationError; - -export function useGetV2GetAgentByVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentByVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetAgentByVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get agent by version - */ - -export function useGetV2GetAgentByVersion< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetAgentByVersionQueryOptions( - storeListingVersionId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get agent by version - */ -export const prefetchGetV2GetAgentByVersionQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetAgentByVersionQueryOptions( - storeListingVersionId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Create a review for a store agent. - -Args: - username: Creator's username - agent_name: Name/slug of the agent - review: Review details including score and optional comments - user_id: ID of authenticated user creating the review - -Returns: - The created review - * @summary Create agent review - */ -export type postV2CreateAgentReviewResponse200 = { - data: StoreReview; - status: 200; -}; - -export type postV2CreateAgentReviewResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2CreateAgentReviewResponseComposite = - | postV2CreateAgentReviewResponse200 - | postV2CreateAgentReviewResponse422; - -export type postV2CreateAgentReviewResponse = - postV2CreateAgentReviewResponseComposite & { - headers: Headers; - }; - -export const getPostV2CreateAgentReviewUrl = ( - username: string, - agentName: string, -) => { - return `/api/store/agents/${username}/${agentName}/review`; -}; - -export const postV2CreateAgentReview = async ( - username: string, - agentName: string, - storeReviewCreate: StoreReviewCreate, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2CreateAgentReviewUrl(username, agentName), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(storeReviewCreate), - }, - ); -}; - -export const getPostV2CreateAgentReviewMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { username: string; agentName: string; data: StoreReviewCreate }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { username: string; agentName: string; data: StoreReviewCreate }, - TContext -> => { - const mutationKey = ["postV2CreateAgentReview"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { username: string; agentName: string; data: StoreReviewCreate } - > = (props) => { - const { username, agentName, data } = props ?? {}; - - return postV2CreateAgentReview(username, agentName, data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2CreateAgentReviewMutationResult = NonNullable< - Awaited> ->; -export type PostV2CreateAgentReviewMutationBody = StoreReviewCreate; -export type PostV2CreateAgentReviewMutationError = HTTPValidationError; - -/** - * @summary Create agent review - */ -export const usePostV2CreateAgentReview = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { username: string; agentName: string; data: StoreReviewCreate }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { username: string; agentName: string; data: StoreReviewCreate }, - TContext -> => { - const mutationOptions = getPostV2CreateAgentReviewMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * This is needed for: -- Home Page Featured Creators -- Search Results Page - ---- - -To support this functionality we need: -- featured: bool - to limit the list to just featured agents -- search_query: str - vector search based on the creators profile description. -- sorted_by: [agent_rating, agent_runs] - - * @summary List store creators - */ -export type getV2ListStoreCreatorsResponse200 = { - data: CreatorsResponse; - status: 200; -}; - -export type getV2ListStoreCreatorsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2ListStoreCreatorsResponseComposite = - | getV2ListStoreCreatorsResponse200 - | getV2ListStoreCreatorsResponse422; - -export type getV2ListStoreCreatorsResponse = - getV2ListStoreCreatorsResponseComposite & { - headers: Headers; - }; - -export const getGetV2ListStoreCreatorsUrl = ( - params?: GetV2ListStoreCreatorsParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/store/creators?${stringifiedParams}` - : `/api/store/creators`; -}; - -export const getV2ListStoreCreators = async ( - params?: GetV2ListStoreCreatorsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2ListStoreCreatorsUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2ListStoreCreatorsQueryKey = ( - params?: GetV2ListStoreCreatorsParams, -) => { - return [`/api/store/creators`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2ListStoreCreatorsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreCreatorsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListStoreCreatorsQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2ListStoreCreators(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2ListStoreCreatorsQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListStoreCreatorsQueryError = HTTPValidationError; - -export function useGetV2ListStoreCreators< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2ListStoreCreatorsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListStoreCreators< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreCreatorsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListStoreCreators< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreCreatorsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List store creators - */ - -export function useGetV2ListStoreCreators< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListStoreCreatorsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListStoreCreatorsQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List store creators - */ -export const prefetchGetV2ListStoreCreatorsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2ListStoreCreatorsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListStoreCreatorsQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Get the details of a creator -- Creator Details Page - * @summary Get creator details - */ -export type getV2GetCreatorDetailsResponse200 = { - data: CreatorDetails; - status: 200; -}; - -export type getV2GetCreatorDetailsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2GetCreatorDetailsResponseComposite = - | getV2GetCreatorDetailsResponse200 - | getV2GetCreatorDetailsResponse422; - -export type getV2GetCreatorDetailsResponse = - getV2GetCreatorDetailsResponseComposite & { - headers: Headers; - }; - -export const getGetV2GetCreatorDetailsUrl = (username: string) => { - return `/api/store/creator/${username}`; -}; - -export const getV2GetCreatorDetails = async ( - username: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2GetCreatorDetailsUrl(username), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2GetCreatorDetailsQueryKey = (username: string) => { - return [`/api/store/creator/${username}`] as const; -}; - -export const getGetV2GetCreatorDetailsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2GetCreatorDetailsQueryKey(username); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2GetCreatorDetails(username, { signal, ...requestOptions }); - - return { - queryKey, - queryFn, - enabled: !!username, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetCreatorDetailsQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetCreatorDetailsQueryError = HTTPValidationError; - -export function useGetV2GetCreatorDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetCreatorDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetCreatorDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get creator details - */ - -export function useGetV2GetCreatorDetails< - TData = Awaited>, - TError = HTTPValidationError, ->( - username: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetCreatorDetailsQueryOptions(username, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get creator details - */ -export const prefetchGetV2GetCreatorDetailsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - username: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetCreatorDetailsQueryOptions(username, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * @summary Get my agents - */ -export type getV2GetMyAgentsResponse200 = { - data: MyAgentsResponse; - status: 200; -}; - -export type getV2GetMyAgentsResponseComposite = getV2GetMyAgentsResponse200; - -export type getV2GetMyAgentsResponse = getV2GetMyAgentsResponseComposite & { - headers: Headers; -}; - -export const getGetV2GetMyAgentsUrl = () => { - return `/api/store/myagents`; -}; - -export const getV2GetMyAgents = async ( - options?: RequestInit, -): Promise => { - return customMutator(getGetV2GetMyAgentsUrl(), { - ...options, - method: "GET", - }); -}; - -export const getGetV2GetMyAgentsQueryKey = () => { - return [`/api/store/myagents`] as const; -}; - -export const getGetV2GetMyAgentsQueryOptions = < - TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; -}) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => getV2GetMyAgents({ signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2GetMyAgentsQueryResult = NonNullable< - Awaited> ->; -export type GetV2GetMyAgentsQueryError = unknown; - -export function useGetV2GetMyAgents< - TData = Awaited>, - TError = unknown, ->( - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetMyAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2GetMyAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Get my agents - */ - -export function useGetV2GetMyAgents< - TData = Awaited>, - TError = unknown, ->( - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2GetMyAgentsQueryOptions(options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Get my agents - */ -export const prefetchGetV2GetMyAgentsQuery = async < - TData = Awaited>, - TError = unknown, ->( - queryClient: QueryClient, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2GetMyAgentsQueryOptions(options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Delete a store listing submission. - -Args: - user_id (str): ID of the authenticated user - submission_id (str): ID of the submission to be deleted - -Returns: - bool: True if the submission was successfully deleted, False otherwise - * @summary Delete store submission - */ -export type deleteV2DeleteStoreSubmissionResponse200 = { - data: boolean; - status: 200; -}; - -export type deleteV2DeleteStoreSubmissionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type deleteV2DeleteStoreSubmissionResponseComposite = - | deleteV2DeleteStoreSubmissionResponse200 - | deleteV2DeleteStoreSubmissionResponse422; - -export type deleteV2DeleteStoreSubmissionResponse = - deleteV2DeleteStoreSubmissionResponseComposite & { - headers: Headers; - }; - -export const getDeleteV2DeleteStoreSubmissionUrl = (submissionId: string) => { - return `/api/store/submissions/${submissionId}`; -}; - -export const deleteV2DeleteStoreSubmission = async ( - submissionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getDeleteV2DeleteStoreSubmissionUrl(submissionId), - { - ...options, - method: "DELETE", - }, - ); -}; - -export const getDeleteV2DeleteStoreSubmissionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { submissionId: string }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { submissionId: string }, - TContext -> => { - const mutationKey = ["deleteV2DeleteStoreSubmission"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { submissionId: string } - > = (props) => { - const { submissionId } = props ?? {}; - - return deleteV2DeleteStoreSubmission(submissionId, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type DeleteV2DeleteStoreSubmissionMutationResult = NonNullable< - Awaited> ->; - -export type DeleteV2DeleteStoreSubmissionMutationError = HTTPValidationError; - -/** - * @summary Delete store submission - */ -export const useDeleteV2DeleteStoreSubmission = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { submissionId: string }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { submissionId: string }, - TContext -> => { - const mutationOptions = - getDeleteV2DeleteStoreSubmissionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Get a paginated list of store submissions for the authenticated user. - -Args: - user_id (str): ID of the authenticated user - page (int, optional): Page number for pagination. Defaults to 1. - page_size (int, optional): Number of submissions per page. Defaults to 20. - -Returns: - StoreListingsResponse: Paginated list of store submissions - -Raises: - HTTPException: If page or page_size are less than 1 - * @summary List my submissions - */ -export type getV2ListMySubmissionsResponse200 = { - data: StoreSubmissionsResponse; - status: 200; -}; - -export type getV2ListMySubmissionsResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2ListMySubmissionsResponseComposite = - | getV2ListMySubmissionsResponse200 - | getV2ListMySubmissionsResponse422; - -export type getV2ListMySubmissionsResponse = - getV2ListMySubmissionsResponseComposite & { - headers: Headers; - }; - -export const getGetV2ListMySubmissionsUrl = ( - params?: GetV2ListMySubmissionsParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/store/submissions?${stringifiedParams}` - : `/api/store/submissions`; -}; - -export const getV2ListMySubmissions = async ( - params?: GetV2ListMySubmissionsParams, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2ListMySubmissionsUrl(params), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2ListMySubmissionsQueryKey = ( - params?: GetV2ListMySubmissionsParams, -) => { - return [`/api/store/submissions`, ...(params ? [params] : [])] as const; -}; - -export const getGetV2ListMySubmissionsQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListMySubmissionsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? getGetV2ListMySubmissionsQueryKey(params); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2ListMySubmissions(params, { signal, ...requestOptions }); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2ListMySubmissionsQueryResult = NonNullable< - Awaited> ->; -export type GetV2ListMySubmissionsQueryError = HTTPValidationError; - -export function useGetV2ListMySubmissions< - TData = Awaited>, - TError = HTTPValidationError, ->( - params: undefined | GetV2ListMySubmissionsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListMySubmissions< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListMySubmissionsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2ListMySubmissions< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListMySubmissionsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary List my submissions - */ - -export function useGetV2ListMySubmissions< - TData = Awaited>, - TError = HTTPValidationError, ->( - params?: GetV2ListMySubmissionsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2ListMySubmissionsQueryOptions(params, options); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary List my submissions - */ -export const prefetchGetV2ListMySubmissionsQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - params?: GetV2ListMySubmissionsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2ListMySubmissionsQueryOptions(params, options); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; - -/** - * Create a new store listing submission. - -Args: - submission_request (StoreSubmissionRequest): The submission details - user_id (str): ID of the authenticated user submitting the listing - -Returns: - StoreSubmission: The created store submission - -Raises: - HTTPException: If there is an error creating the submission - * @summary Create store submission - */ -export type postV2CreateStoreSubmissionResponse200 = { - data: StoreSubmission; - status: 200; -}; - -export type postV2CreateStoreSubmissionResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2CreateStoreSubmissionResponseComposite = - | postV2CreateStoreSubmissionResponse200 - | postV2CreateStoreSubmissionResponse422; - -export type postV2CreateStoreSubmissionResponse = - postV2CreateStoreSubmissionResponseComposite & { - headers: Headers; - }; - -export const getPostV2CreateStoreSubmissionUrl = () => { - return `/api/store/submissions`; -}; - -export const postV2CreateStoreSubmission = async ( - storeSubmissionRequest: StoreSubmissionRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2CreateStoreSubmissionUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(storeSubmissionRequest), - }, - ); -}; - -export const getPostV2CreateStoreSubmissionMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: StoreSubmissionRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: StoreSubmissionRequest }, - TContext -> => { - const mutationKey = ["postV2CreateStoreSubmission"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: StoreSubmissionRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV2CreateStoreSubmission(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2CreateStoreSubmissionMutationResult = NonNullable< - Awaited> ->; -export type PostV2CreateStoreSubmissionMutationBody = StoreSubmissionRequest; -export type PostV2CreateStoreSubmissionMutationError = HTTPValidationError; - -/** - * @summary Create store submission - */ -export const usePostV2CreateStoreSubmission = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: StoreSubmissionRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: StoreSubmissionRequest }, - TContext -> => { - const mutationOptions = - getPostV2CreateStoreSubmissionMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Upload media (images/videos) for a store listing submission. - -Args: - file (UploadFile): The media file to upload - user_id (str): ID of the authenticated user uploading the media - -Returns: - str: URL of the uploaded media file - -Raises: - HTTPException: If there is an error uploading the media - * @summary Upload submission media - */ -export type postV2UploadSubmissionMediaResponse200 = { - data: unknown; - status: 200; -}; - -export type postV2UploadSubmissionMediaResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2UploadSubmissionMediaResponseComposite = - | postV2UploadSubmissionMediaResponse200 - | postV2UploadSubmissionMediaResponse422; - -export type postV2UploadSubmissionMediaResponse = - postV2UploadSubmissionMediaResponseComposite & { - headers: Headers; - }; - -export const getPostV2UploadSubmissionMediaUrl = () => { - return `/api/store/submissions/media`; -}; - -export const postV2UploadSubmissionMedia = async ( - bodyPostV2UploadSubmissionMedia: BodyPostV2UploadSubmissionMedia, - options?: RequestInit, -): Promise => { - const formData = new FormData(); - formData.append(`file`, bodyPostV2UploadSubmissionMedia.file); - - return customMutator( - getPostV2UploadSubmissionMediaUrl(), - { - ...options, - method: "POST", - body: formData, - }, - ); -}; - -export const getPostV2UploadSubmissionMediaMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2UploadSubmissionMedia }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2UploadSubmissionMedia }, - TContext -> => { - const mutationKey = ["postV2UploadSubmissionMedia"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: BodyPostV2UploadSubmissionMedia } - > = (props) => { - const { data } = props ?? {}; - - return postV2UploadSubmissionMedia(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2UploadSubmissionMediaMutationResult = NonNullable< - Awaited> ->; -export type PostV2UploadSubmissionMediaMutationBody = - BodyPostV2UploadSubmissionMedia; -export type PostV2UploadSubmissionMediaMutationError = HTTPValidationError; - -/** - * @summary Upload submission media - */ -export const usePostV2UploadSubmissionMedia = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: BodyPostV2UploadSubmissionMedia }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: BodyPostV2UploadSubmissionMedia }, - TContext -> => { - const mutationOptions = - getPostV2UploadSubmissionMediaMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Generate an image for a store listing submission. - -Args: - agent_id (str): ID of the agent to generate an image for - user_id (str): ID of the authenticated user - -Returns: - JSONResponse: JSON containing the URL of the generated image - * @summary Generate submission image - */ -export type postV2GenerateSubmissionImageResponse200 = { - data: unknown; - status: 200; -}; - -export type postV2GenerateSubmissionImageResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2GenerateSubmissionImageResponseComposite = - | postV2GenerateSubmissionImageResponse200 - | postV2GenerateSubmissionImageResponse422; - -export type postV2GenerateSubmissionImageResponse = - postV2GenerateSubmissionImageResponseComposite & { - headers: Headers; - }; - -export const getPostV2GenerateSubmissionImageUrl = ( - params: PostV2GenerateSubmissionImageParams, -) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/api/store/submissions/generate_image?${stringifiedParams}` - : `/api/store/submissions/generate_image`; -}; - -export const postV2GenerateSubmissionImage = async ( - params: PostV2GenerateSubmissionImageParams, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2GenerateSubmissionImageUrl(params), - { - ...options, - method: "POST", - }, - ); -}; - -export const getPostV2GenerateSubmissionImageMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { params: PostV2GenerateSubmissionImageParams }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { params: PostV2GenerateSubmissionImageParams }, - TContext -> => { - const mutationKey = ["postV2GenerateSubmissionImage"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { params: PostV2GenerateSubmissionImageParams } - > = (props) => { - const { params } = props ?? {}; - - return postV2GenerateSubmissionImage(params, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2GenerateSubmissionImageMutationResult = NonNullable< - Awaited> ->; - -export type PostV2GenerateSubmissionImageMutationError = HTTPValidationError; - -/** - * @summary Generate submission image - */ -export const usePostV2GenerateSubmissionImage = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { params: PostV2GenerateSubmissionImageParams }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { params: PostV2GenerateSubmissionImageParams }, - TContext -> => { - const mutationOptions = - getPostV2GenerateSubmissionImageMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; -/** - * Download the agent file by streaming its content. - -Args: - store_listing_version_id (str): The ID of the agent to download - -Returns: - StreamingResponse: A streaming response containing the agent's graph data. - -Raises: - HTTPException: If the agent is not found or an unexpected error occurs. - * @summary Download agent file - */ -export type getV2DownloadAgentFileResponse200 = { - data: unknown; - status: 200; -}; - -export type getV2DownloadAgentFileResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type getV2DownloadAgentFileResponseComposite = - | getV2DownloadAgentFileResponse200 - | getV2DownloadAgentFileResponse422; - -export type getV2DownloadAgentFileResponse = - getV2DownloadAgentFileResponseComposite & { - headers: Headers; - }; - -export const getGetV2DownloadAgentFileUrl = (storeListingVersionId: string) => { - return `/api/store/download/agents/${storeListingVersionId}`; -}; - -export const getV2DownloadAgentFile = async ( - storeListingVersionId: string, - options?: RequestInit, -): Promise => { - return customMutator( - getGetV2DownloadAgentFileUrl(storeListingVersionId), - { - ...options, - method: "GET", - }, - ); -}; - -export const getGetV2DownloadAgentFileQueryKey = ( - storeListingVersionId: string, -) => { - return [`/api/store/download/agents/${storeListingVersionId}`] as const; -}; - -export const getGetV2DownloadAgentFileQueryOptions = < - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -) => { - const { query: queryOptions, request: requestOptions } = options ?? {}; - - const queryKey = - queryOptions?.queryKey ?? - getGetV2DownloadAgentFileQueryKey(storeListingVersionId); - - const queryFn: QueryFunction< - Awaited> - > = ({ signal }) => - getV2DownloadAgentFile(storeListingVersionId, { - signal, - ...requestOptions, - }); - - return { - queryKey, - queryFn, - enabled: !!storeListingVersionId, - ...queryOptions, - } as UseQueryOptions< - Awaited>, - TError, - TData - > & { queryKey: DataTag }; -}; - -export type GetV2DownloadAgentFileQueryResult = NonNullable< - Awaited> ->; -export type GetV2DownloadAgentFileQueryError = HTTPValidationError; - -export function useGetV2DownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options: { - query: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): DefinedUseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2DownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>, - TError, - Awaited> - >, - "initialData" - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -export function useGetV2DownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -}; -/** - * @summary Download agent file - */ - -export function useGetV2DownloadAgentFile< - TData = Awaited>, - TError = HTTPValidationError, ->( - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseQueryResult & { - queryKey: DataTag; -} { - const queryOptions = getGetV2DownloadAgentFileQueryOptions( - storeListingVersionId, - options, - ); - - const query = useQuery(queryOptions, queryClient) as UseQueryResult< - TData, - TError - > & { queryKey: DataTag }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -/** - * @summary Download agent file - */ -export const prefetchGetV2DownloadAgentFileQuery = async < - TData = Awaited>, - TError = HTTPValidationError, ->( - queryClient: QueryClient, - storeListingVersionId: string, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; - }, -): Promise => { - const queryOptions = getGetV2DownloadAgentFileQueryOptions( - storeListingVersionId, - options, - ); - - await queryClient.prefetchQuery(queryOptions); - - return queryClient; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/turnstile/turnstile.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/turnstile/turnstile.ts deleted file mode 100644 index 6d9d937486a1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/turnstile/turnstile.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import { useMutation } from "@tanstack/react-query"; -import type { - MutationFunction, - QueryClient, - UseMutationOptions, - UseMutationResult, -} from "@tanstack/react-query"; - -import type { HTTPValidationError } from "../../models/hTTPValidationError"; - -import type { TurnstileVerifyRequest } from "../../models/turnstileVerifyRequest"; - -import type { TurnstileVerifyResponse } from "../../models/turnstileVerifyResponse"; - -import { customMutator } from "../../../mutators/custom-mutator"; - -type SecondParameter unknown> = Parameters[1]; - -/** - * Verify a Cloudflare Turnstile token. -This endpoint verifies a token returned by the Cloudflare Turnstile challenge -on the client side. It returns whether the verification was successful. - * @summary Verify Turnstile Token - */ -export type postV2VerifyTurnstileTokenResponse200 = { - data: TurnstileVerifyResponse; - status: 200; -}; - -export type postV2VerifyTurnstileTokenResponse422 = { - data: HTTPValidationError; - status: 422; -}; - -export type postV2VerifyTurnstileTokenResponseComposite = - | postV2VerifyTurnstileTokenResponse200 - | postV2VerifyTurnstileTokenResponse422; - -export type postV2VerifyTurnstileTokenResponse = - postV2VerifyTurnstileTokenResponseComposite & { - headers: Headers; - }; - -export const getPostV2VerifyTurnstileTokenUrl = () => { - return `/api/turnstile/verify`; -}; - -export const postV2VerifyTurnstileToken = async ( - turnstileVerifyRequest: TurnstileVerifyRequest, - options?: RequestInit, -): Promise => { - return customMutator( - getPostV2VerifyTurnstileTokenUrl(), - { - ...options, - method: "POST", - headers: { "Content-Type": "application/json", ...options?.headers }, - body: JSON.stringify(turnstileVerifyRequest), - }, - ); -}; - -export const getPostV2VerifyTurnstileTokenMutationOptions = < - TError = HTTPValidationError, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: TurnstileVerifyRequest }, - TContext - >; - request?: SecondParameter; -}): UseMutationOptions< - Awaited>, - TError, - { data: TurnstileVerifyRequest }, - TContext -> => { - const mutationKey = ["postV2VerifyTurnstileToken"]; - const { mutation: mutationOptions, request: requestOptions } = options - ? options.mutation && - "mutationKey" in options.mutation && - options.mutation.mutationKey - ? options - : { ...options, mutation: { ...options.mutation, mutationKey } } - : { mutation: { mutationKey }, request: undefined }; - - const mutationFn: MutationFunction< - Awaited>, - { data: TurnstileVerifyRequest } - > = (props) => { - const { data } = props ?? {}; - - return postV2VerifyTurnstileToken(data, requestOptions); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type PostV2VerifyTurnstileTokenMutationResult = NonNullable< - Awaited> ->; -export type PostV2VerifyTurnstileTokenMutationBody = TurnstileVerifyRequest; -export type PostV2VerifyTurnstileTokenMutationError = HTTPValidationError; - -/** - * @summary Verify Turnstile Token - */ -export const usePostV2VerifyTurnstileToken = < - TError = HTTPValidationError, - TContext = unknown, ->( - options?: { - mutation?: UseMutationOptions< - Awaited>, - TError, - { data: TurnstileVerifyRequest }, - TContext - >; - request?: SecondParameter; - }, - queryClient?: QueryClient, -): UseMutationResult< - Awaited>, - TError, - { data: TurnstileVerifyRequest }, - TContext -> => { - const mutationOptions = getPostV2VerifyTurnstileTokenMutationOptions(options); - - return useMutation(mutationOptions, queryClient); -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentials.ts deleted file mode 100644 index b91310a3ff61..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentials.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyCredentialsTitle } from "./aPIKeyCredentialsTitle"; -import type { APIKeyCredentialsExpiresAt } from "./aPIKeyCredentialsExpiresAt"; - -export interface APIKeyCredentials { - id?: string; - provider: string; - title?: APIKeyCredentialsTitle; - type?: "api_key"; - api_key: string; - /** Unix timestamp (seconds) indicating when the API key expires (if at all) */ - expires_at?: APIKeyCredentialsExpiresAt; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsExpiresAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsExpiresAt.ts deleted file mode 100644 index a33e4a5a0d7c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsExpiresAt.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Unix timestamp (seconds) indicating when the API key expires (if at all) - */ -export type APIKeyCredentialsExpiresAt = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsTitle.ts deleted file mode 100644 index 476b44abae04..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyCredentialsTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyCredentialsTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyPermission.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyPermission.ts deleted file mode 100644 index bae84cd1fd6d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyPermission.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyPermission = - (typeof APIKeyPermission)[keyof typeof APIKeyPermission]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const APIKeyPermission = { - EXECUTE_GRAPH: "EXECUTE_GRAPH", - READ_GRAPH: "READ_GRAPH", - EXECUTE_BLOCK: "EXECUTE_BLOCK", - READ_BLOCK: "READ_BLOCK", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyStatus.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyStatus.ts deleted file mode 100644 index 88dc84ae770d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyStatus.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyStatus = (typeof APIKeyStatus)[keyof typeof APIKeyStatus]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const APIKeyStatus = { - ACTIVE: "ACTIVE", - REVOKED: "REVOKED", - SUSPENDED: "SUSPENDED", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHash.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHash.ts deleted file mode 100644 index 0165c66c18b9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHash.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyStatus } from "./aPIKeyStatus"; -import type { APIKeyPermission } from "./aPIKeyPermission"; -import type { APIKeyWithoutHashLastUsedAt } from "./aPIKeyWithoutHashLastUsedAt"; -import type { APIKeyWithoutHashRevokedAt } from "./aPIKeyWithoutHashRevokedAt"; -import type { APIKeyWithoutHashDescription } from "./aPIKeyWithoutHashDescription"; - -export interface APIKeyWithoutHash { - id: string; - name: string; - prefix: string; - postfix: string; - status: APIKeyStatus; - permissions: APIKeyPermission[]; - created_at: string; - last_used_at: APIKeyWithoutHashLastUsedAt; - revoked_at: APIKeyWithoutHashRevokedAt; - description: APIKeyWithoutHashDescription; - user_id: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashDescription.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashDescription.ts deleted file mode 100644 index 46c9963d1042..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashDescription.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyWithoutHashDescription = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashLastUsedAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashLastUsedAt.ts deleted file mode 100644 index b8e1188d448a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashLastUsedAt.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyWithoutHashLastUsedAt = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashRevokedAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashRevokedAt.ts deleted file mode 100644 index bd658f46247b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/aPIKeyWithoutHashRevokedAt.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type APIKeyWithoutHashRevokedAt = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/addUserCreditsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/addUserCreditsResponse.ts deleted file mode 100644 index 7b5401117c0c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/addUserCreditsResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface AddUserCreditsResponse { - new_balance: number; - transaction_key: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/agentExecutionStatus.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/agentExecutionStatus.ts deleted file mode 100644 index 59e5c6f65517..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/agentExecutionStatus.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type AgentExecutionStatus = - (typeof AgentExecutionStatus)[keyof typeof AgentExecutionStatus]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const AgentExecutionStatus = { - INCOMPLETE: "INCOMPLETE", - QUEUED: "QUEUED", - RUNNING: "RUNNING", - COMPLETED: "COMPLETED", - TERMINATED: "TERMINATED", - FAILED: "FAILED", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/apiResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/apiResponse.ts deleted file mode 100644 index a2388702052a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/apiResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Document } from "./document"; - -export interface ApiResponse { - answer: string; - documents: Document[]; - success: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/autoTopUpConfig.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/autoTopUpConfig.ts deleted file mode 100644 index eb5e97806970..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/autoTopUpConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface AutoTopUpConfig { - amount: number; - threshold: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInput.ts deleted file mode 100644 index fa830948f611..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInput.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Node } from "./node"; -import type { Link } from "./link"; -import type { BaseGraphInputForkedFromId } from "./baseGraphInputForkedFromId"; -import type { BaseGraphInputForkedFromVersion } from "./baseGraphInputForkedFromVersion"; - -export interface BaseGraphInput { - id?: string; - version?: number; - is_active?: boolean; - name: string; - description: string; - nodes?: Node[]; - links?: Link[]; - forked_from_id?: BaseGraphInputForkedFromId; - forked_from_version?: BaseGraphInputForkedFromVersion; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromId.ts deleted file mode 100644 index a7636e58e237..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphInputForkedFromId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromVersion.ts deleted file mode 100644 index 65dc4e600401..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphInputForkedFromVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphInputForkedFromVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutput.ts deleted file mode 100644 index 8354b6344ec4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutput.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Node } from "./node"; -import type { Link } from "./link"; -import type { BaseGraphOutputForkedFromId } from "./baseGraphOutputForkedFromId"; -import type { BaseGraphOutputForkedFromVersion } from "./baseGraphOutputForkedFromVersion"; -import type { BaseGraphOutputInputSchema } from "./baseGraphOutputInputSchema"; -import type { BaseGraphOutputOutputSchema } from "./baseGraphOutputOutputSchema"; - -export interface BaseGraphOutput { - id?: string; - version?: number; - is_active?: boolean; - name: string; - description: string; - nodes?: Node[]; - links?: Link[]; - forked_from_id?: BaseGraphOutputForkedFromId; - forked_from_version?: BaseGraphOutputForkedFromVersion; - readonly input_schema: BaseGraphOutputInputSchema; - readonly output_schema: BaseGraphOutputOutputSchema; - readonly has_external_trigger: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromId.ts deleted file mode 100644 index 3775a99a5cde..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphOutputForkedFromId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromVersion.ts deleted file mode 100644 index b2f46fca2af1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputForkedFromVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphOutputForkedFromVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputInputSchema.ts deleted file mode 100644 index 8b64c5e13797..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphOutputInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputOutputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputOutputSchema.ts deleted file mode 100644 index ace1f96737cd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/baseGraphOutputOutputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BaseGraphOutputOutputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1Callback.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1Callback.ts deleted file mode 100644 index 5d6db6f16640..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1Callback.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface BodyPostV1Callback { - code: string; - state_token: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgent.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgent.ts deleted file mode 100644 index 1bab4130aecd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgent.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { BodyPostV1ExecuteGraphAgentInputs } from "./bodyPostV1ExecuteGraphAgentInputs"; -import type { BodyPostV1ExecuteGraphAgentCredentialsInputs } from "./bodyPostV1ExecuteGraphAgentCredentialsInputs"; - -export interface BodyPostV1ExecuteGraphAgent { - inputs?: BodyPostV1ExecuteGraphAgentInputs; - credentials_inputs?: BodyPostV1ExecuteGraphAgentCredentialsInputs; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentCredentialsInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentCredentialsInputs.ts deleted file mode 100644 index 6c37b20ef473..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentCredentialsInputs.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type BodyPostV1ExecuteGraphAgentCredentialsInputs = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentInputs.ts deleted file mode 100644 index 759c51fc1ad7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1ExecuteGraphAgentInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BodyPostV1ExecuteGraphAgentInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalytics.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalytics.ts deleted file mode 100644 index 92362203ae5d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalytics.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { BodyPostV1LogRawAnalyticsData } from "./bodyPostV1LogRawAnalyticsData"; - -export interface BodyPostV1LogRawAnalytics { - type: string; - /** The data to log */ - data: BodyPostV1LogRawAnalyticsData; - /** Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc. */ - data_index: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalyticsData.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalyticsData.ts deleted file mode 100644 index 0ef9be6f56e2..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV1LogRawAnalyticsData.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * The data to log - */ -export type BodyPostV1LogRawAnalyticsData = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddCreditsToUser.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddCreditsToUser.ts deleted file mode 100644 index af7f62831551..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddCreditsToUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface BodyPostV2AddCreditsToUser { - user_id: string; - amount: number; - comments: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddMarketplaceAgent.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddMarketplaceAgent.ts deleted file mode 100644 index 154ae8047c13..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2AddMarketplaceAgent.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface BodyPostV2AddMarketplaceAgent { - store_listing_version_id: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPreset.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPreset.ts deleted file mode 100644 index 33041abadc5d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPreset.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { BodyPostV2ExecuteAPresetInputs } from "./bodyPostV2ExecuteAPresetInputs"; - -export interface BodyPostV2ExecuteAPreset { - inputs?: BodyPostV2ExecuteAPresetInputs; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPresetInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPresetInputs.ts deleted file mode 100644 index fbfdc61bf2ef..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2ExecuteAPresetInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type BodyPostV2ExecuteAPresetInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2UploadSubmissionMedia.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2UploadSubmissionMedia.ts deleted file mode 100644 index 0fe626c328cf..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/bodyPostV2UploadSubmissionMedia.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface BodyPostV2UploadSubmissionMedia { - file: Blob; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequest.ts deleted file mode 100644 index b50b42628277..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequest.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Message } from "./message"; -import type { ChatRequestGraphId } from "./chatRequestGraphId"; - -export interface ChatRequest { - query: string; - conversation_history: Message[]; - message_id: string; - include_graph_data?: boolean; - graph_id?: ChatRequestGraphId; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequestGraphId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequestGraphId.ts deleted file mode 100644 index c5f583ee62d8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/chatRequestGraphId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ChatRequestGraphId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequest.ts deleted file mode 100644 index 660f5023aba5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequest.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyPermission } from "./aPIKeyPermission"; -import type { CreateAPIKeyRequestDescription } from "./createAPIKeyRequestDescription"; - -export interface CreateAPIKeyRequest { - name: string; - permissions: APIKeyPermission[]; - description?: CreateAPIKeyRequestDescription; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequestDescription.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequestDescription.ts deleted file mode 100644 index e284be9925da..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyRequestDescription.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CreateAPIKeyRequestDescription = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyResponse.ts deleted file mode 100644 index bfd8cf62eb4c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/createAPIKeyResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyWithoutHash } from "./aPIKeyWithoutHash"; - -export interface CreateAPIKeyResponse { - api_key: APIKeyWithoutHash; - plain_text_key: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/createGraph.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/createGraph.ts deleted file mode 100644 index 8548196cce72..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/createGraph.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Graph } from "./graph"; - -export interface CreateGraph { - graph: Graph; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/creator.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/creator.ts deleted file mode 100644 index 78744ea400cc..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/creator.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Creator { - name: string; - username: string; - description: string; - avatar_url: string; - num_agents: number; - agent_rating: number; - agent_runs: number; - is_featured: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/creatorDetails.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/creatorDetails.ts deleted file mode 100644 index a28c983faac2..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/creatorDetails.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface CreatorDetails { - name: string; - username: string; - description: string; - links: string[]; - avatar_url: string; - agent_rating: number; - agent_runs: number; - top_categories: string[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/creatorsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/creatorsResponse.ts deleted file mode 100644 index 315cbd1e3479..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/creatorsResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Creator } from "./creator"; -import type { Pagination } from "./pagination"; - -export interface CreatorsResponse { - creators: Creator[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionNeedsConfirmationResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionNeedsConfirmationResponse.ts deleted file mode 100644 index 6352fc9c3900..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionNeedsConfirmationResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface CredentialsDeletionNeedsConfirmationResponse { - deleted?: false; - need_confirmation?: true; - message: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponse.ts deleted file mode 100644 index 2c2a424aca11..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsDeletionResponseRevoked } from "./credentialsDeletionResponseRevoked"; - -export interface CredentialsDeletionResponse { - deleted?: true; - /** Indicates whether the credentials were also revoked by their provider. `None`/`null` if not applicable, e.g. when deleting non-revocable credentials such as API keys. */ - revoked: CredentialsDeletionResponseRevoked; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponseRevoked.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponseRevoked.ts deleted file mode 100644 index 08a8b79bd9a0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsDeletionResponseRevoked.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Indicates whether the credentials were also revoked by their provider. `None`/`null` if not applicable, e.g. when deleting non-revocable credentials such as API keys. - */ -export type CredentialsDeletionResponseRevoked = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInput.ts deleted file mode 100644 index e12660e8b8d0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInput.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInputTitle } from "./credentialsMetaInputTitle"; -import type { CredentialsMetaInputType } from "./credentialsMetaInputType"; - -export interface CredentialsMetaInput { - id: string; - title?: CredentialsMetaInputTitle; - /** Provider name for integrations. Can be any string value, including custom provider names. */ - provider: string; - type: CredentialsMetaInputType; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputTitle.ts deleted file mode 100644 index 1befe2af4ea9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaInputTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputType.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputType.ts deleted file mode 100644 index ec13a6f1a557..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaInputType.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaInputType = - (typeof CredentialsMetaInputType)[keyof typeof CredentialsMetaInputType]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const CredentialsMetaInputType = { - api_key: "api_key", - oauth2: "oauth2", - user_password: "user_password", - host_scoped: "host_scoped", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponse.ts deleted file mode 100644 index bff9e1b95d87..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponse.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaResponseType } from "./credentialsMetaResponseType"; -import type { CredentialsMetaResponseTitle } from "./credentialsMetaResponseTitle"; -import type { CredentialsMetaResponseScopes } from "./credentialsMetaResponseScopes"; -import type { CredentialsMetaResponseUsername } from "./credentialsMetaResponseUsername"; -import type { CredentialsMetaResponseHost } from "./credentialsMetaResponseHost"; - -export interface CredentialsMetaResponse { - id: string; - provider: string; - type: CredentialsMetaResponseType; - title: CredentialsMetaResponseTitle; - scopes: CredentialsMetaResponseScopes; - username: CredentialsMetaResponseUsername; - /** Host pattern for host-scoped credentials */ - host?: CredentialsMetaResponseHost; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseHost.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseHost.ts deleted file mode 100644 index d6b4a38cb397..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseHost.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Host pattern for host-scoped credentials - */ -export type CredentialsMetaResponseHost = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseScopes.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseScopes.ts deleted file mode 100644 index fe012b2bc275..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseScopes.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaResponseScopes = string[] | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseTitle.ts deleted file mode 100644 index 915705495b7c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaResponseTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseType.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseType.ts deleted file mode 100644 index a8b1fce8dc57..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseType.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaResponseType = - (typeof CredentialsMetaResponseType)[keyof typeof CredentialsMetaResponseType]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const CredentialsMetaResponseType = { - api_key: "api_key", - oauth2: "oauth2", - user_password: "user_password", - host_scoped: "host_scoped", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseUsername.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseUsername.ts deleted file mode 100644 index 79c173f738e1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/credentialsMetaResponseUsername.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CredentialsMetaResponseUsername = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/creditTransactionType.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/creditTransactionType.ts deleted file mode 100644 index 030b6258a60a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/creditTransactionType.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type CreditTransactionType = - (typeof CreditTransactionType)[keyof typeof CreditTransactionType]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const CreditTransactionType = { - TOP_UP: "TOP_UP", - USAGE: "USAGE", - GRANT: "GRANT", - REFUND: "REFUND", - CARD_CHECK: "CARD_CHECK", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteGraphResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/deleteGraphResponse.ts deleted file mode 100644 index 202d0d83afc6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteGraphResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface DeleteGraphResponse { - version_counts: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentials200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentials200.ts deleted file mode 100644 index 0673a0189fcb..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentials200.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsDeletionResponse } from "./credentialsDeletionResponse"; -import type { CredentialsDeletionNeedsConfirmationResponse } from "./credentialsDeletionNeedsConfirmationResponse"; - -export type DeleteV1DeleteCredentials200 = - | CredentialsDeletionResponse - | CredentialsDeletionNeedsConfirmationResponse; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentialsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentialsParams.ts deleted file mode 100644 index 33a1c580208f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteCredentialsParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type DeleteV1DeleteCredentialsParams = { - force?: boolean; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteExecutionSchedule200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteExecutionSchedule200.ts deleted file mode 100644 index 9f19067bc192..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/deleteV1DeleteExecutionSchedule200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type DeleteV1DeleteExecutionSchedule200 = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/document.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/document.ts deleted file mode 100644 index f65ea6887ffd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/document.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Document { - url: string; - relevance_score: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/executeGraphResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/executeGraphResponse.ts deleted file mode 100644 index 28971aec931d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/executeGraphResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface ExecuteGraphResponse { - graph_exec_id: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCredential200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCredential200.ts deleted file mode 100644 index 2cba7f9defaa..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCredential200.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OAuth2Credentials } from "./oAuth2Credentials"; -import type { APIKeyCredentials } from "./aPIKeyCredentials"; -import type { UserPasswordCredentials } from "./userPasswordCredentials"; -import type { HostScopedCredentialsOutput } from "./hostScopedCredentialsOutput"; - -export type GetV1GetCredential200 = - | OAuth2Credentials - | APIKeyCredentials - | UserPasswordCredentials - | HostScopedCredentialsOutput; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCreditHistoryParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCreditHistoryParams.ts deleted file mode 100644 index 90c8ecd364f5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetCreditHistoryParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1GetCreditHistoryParams = { - transaction_time?: string | null; - transaction_type?: string | null; - transaction_count_limit?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetExecutionDetails200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetExecutionDetails200.ts deleted file mode 100644 index 2bbd8a1492ad..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetExecutionDetails200.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecution } from "./graphExecution"; -import type { GraphExecutionWithNodes } from "./graphExecutionWithNodes"; - -export type GetV1GetExecutionDetails200 = - | GraphExecution - | GraphExecutionWithNodes; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetGraphVersionParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetGraphVersionParams.ts deleted file mode 100644 index 521a8754acb6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetGraphVersionParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1GetGraphVersionParams = { - for_export?: boolean; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetSpecificGraphParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetSpecificGraphParams.ts deleted file mode 100644 index e30abfea8633..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetSpecificGraphParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1GetSpecificGraphParams = { - version?: number | null; - for_export?: boolean; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetUserCredits200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetUserCredits200.ts deleted file mode 100644 index fb035936b617..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1GetUserCredits200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1GetUserCredits200 = { [key: string]: number }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListAvailableBlocks200Item.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListAvailableBlocks200Item.ts deleted file mode 100644 index 7a6892363dc6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListAvailableBlocks200Item.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1ListAvailableBlocks200Item = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200.ts deleted file mode 100644 index e919f16d5b53..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyWithoutHash } from "./aPIKeyWithoutHash"; -import type { GetV1ListUserApiKeys200AnyOf } from "./getV1ListUserApiKeys200AnyOf"; - -export type GetV1ListUserApiKeys200 = - | APIKeyWithoutHash[] - | GetV1ListUserApiKeys200AnyOf; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200AnyOf.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200AnyOf.ts deleted file mode 100644 index 39163009f033..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ListUserApiKeys200AnyOf.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1ListUserApiKeys200AnyOf = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1LoginParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1LoginParams.ts deleted file mode 100644 index 1b87a2aa670e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1LoginParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1LoginParams = { - scopes?: string; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ManagePaymentMethods200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ManagePaymentMethods200.ts deleted file mode 100644 index 9668a380fa61..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV1ManagePaymentMethods200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV1ManagePaymentMethods200 = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAdminListingsHistoryParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAdminListingsHistoryParams.ts deleted file mode 100644 index 44e04a817501..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAdminListingsHistoryParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { SubmissionStatus } from "./submissionStatus"; - -export type GetV2GetAdminListingsHistoryParams = { - status?: SubmissionStatus | null; - search?: string | null; - page?: number; - page_size?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAgentByStoreId200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAgentByStoreId200.ts deleted file mode 100644 index 350cf20566b3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAgentByStoreId200.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgent } from "./libraryAgent"; - -export type GetV2GetAgentByStoreId200 = LibraryAgent | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAllUsersHistoryParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAllUsersHistoryParams.ts deleted file mode 100644 index 2b1a82a1004a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetAllUsersHistoryParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CreditTransactionType } from "./creditTransactionType"; - -export type GetV2GetAllUsersHistoryParams = { - search?: string | null; - page?: number; - page_size?: number; - transaction_filter?: CreditTransactionType | null; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetLibraryAgentByGraphIdParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetLibraryAgentByGraphIdParams.ts deleted file mode 100644 index 0bb87f620a87..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetLibraryAgentByGraphIdParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV2GetLibraryAgentByGraphIdParams = { - version?: number | null; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListLibraryAgentsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListLibraryAgentsParams.ts deleted file mode 100644 index df934ad49112..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListLibraryAgentsParams.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentSort } from "./libraryAgentSort"; - -export type GetV2ListLibraryAgentsParams = { - /** - * Search term to filter agents - */ - search_term?: string | null; - /** - * Criteria to sort results by - */ - sort_by?: LibraryAgentSort; - /** - * Page number to retrieve (must be >= 1) - * @minimum 1 - */ - page?: number; - /** - * Number of agents per page (must be >= 1) - * @minimum 1 - */ - page_size?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListMySubmissionsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListMySubmissionsParams.ts deleted file mode 100644 index 515a5f04b83c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListMySubmissionsParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV2ListMySubmissionsParams = { - page?: number; - page_size?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListPresetsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListPresetsParams.ts deleted file mode 100644 index cf7fc3b2b684..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListPresetsParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV2ListPresetsParams = { - /** - * @minimum 1 - */ - page?: number; - /** - * @minimum 1 - */ - page_size?: number; - /** - * Allows to filter presets by a specific agent graph - */ - graph_id: string | null; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreAgentsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreAgentsParams.ts deleted file mode 100644 index dc3461ec6170..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreAgentsParams.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV2ListStoreAgentsParams = { - featured?: boolean; - creator?: string | null; - sorted_by?: string | null; - search_query?: string | null; - category?: string | null; - page?: number; - page_size?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreCreatorsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreCreatorsParams.ts deleted file mode 100644 index 61dfb39be0ce..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2ListStoreCreatorsParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GetV2ListStoreCreatorsParams = { - featured?: boolean; - search_query?: string | null; - sorted_by?: string | null; - page?: number; - page_size?: number; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graph.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graph.ts deleted file mode 100644 index 02516bee1fcc..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graph.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Node } from "./node"; -import type { Link } from "./link"; -import type { GraphForkedFromId } from "./graphForkedFromId"; -import type { GraphForkedFromVersion } from "./graphForkedFromVersion"; -import type { BaseGraphInput } from "./baseGraphInput"; - -export interface Graph { - id?: string; - version?: number; - is_active?: boolean; - name: string; - description: string; - nodes?: Node[]; - links?: Link[]; - forked_from_id?: GraphForkedFromId; - forked_from_version?: GraphForkedFromVersion; - sub_graphs?: BaseGraphInput[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecution.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecution.ts deleted file mode 100644 index 34ddcf3c3a73..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecution.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecutionPresetId } from "./graphExecutionPresetId"; -import type { AgentExecutionStatus } from "./agentExecutionStatus"; -import type { GraphExecutionStats } from "./graphExecutionStats"; -import type { GraphExecutionInputs } from "./graphExecutionInputs"; -import type { GraphExecutionOutputs } from "./graphExecutionOutputs"; - -export interface GraphExecution { - id?: string; - user_id: string; - graph_id: string; - graph_version: number; - preset_id?: GraphExecutionPresetId; - status: AgentExecutionStatus; - started_at: string; - ended_at: string; - stats: GraphExecutionStats; - inputs: GraphExecutionInputs; - outputs: GraphExecutionOutputs; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionInputs.ts deleted file mode 100644 index 9fcd2c1d524b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfo.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfo.ts deleted file mode 100644 index e195389275ed..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfo.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecutionJobInfoInputData } from "./graphExecutionJobInfoInputData"; -import type { GraphExecutionJobInfoInputCredentials } from "./graphExecutionJobInfoInputCredentials"; - -export interface GraphExecutionJobInfo { - user_id: string; - graph_id: string; - graph_version: number; - cron: string; - input_data: GraphExecutionJobInfoInputData; - input_credentials?: GraphExecutionJobInfoInputCredentials; - id: string; - name: string; - next_run_time: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputCredentials.ts deleted file mode 100644 index 1f24a877d5c1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type GraphExecutionJobInfoInputCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputData.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputData.ts deleted file mode 100644 index 270e530af550..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionJobInfoInputData.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionJobInfoInputData = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts deleted file mode 100644 index 387ccd4a655e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecutionMetaPresetId } from "./graphExecutionMetaPresetId"; -import type { AgentExecutionStatus } from "./agentExecutionStatus"; -import type { GraphExecutionMetaStats } from "./graphExecutionMetaStats"; - -export interface GraphExecutionMeta { - id?: string; - user_id: string; - graph_id: string; - graph_version: number; - preset_id?: GraphExecutionMetaPresetId; - status: AgentExecutionStatus; - started_at: string; - ended_at: string; - stats: GraphExecutionMetaStats; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaPresetId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaPresetId.ts deleted file mode 100644 index d2db05f894a1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaPresetId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionMetaPresetId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaStats.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaStats.ts deleted file mode 100644 index db50ec5495e5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMetaStats.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Stats } from "./stats"; - -export type GraphExecutionMetaStats = Stats | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionOutputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionOutputs.ts deleted file mode 100644 index 52b6d3ea1785..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionOutputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionOutputs = { [key: string]: unknown[] }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionPresetId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionPresetId.ts deleted file mode 100644 index 42789ed3d791..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionPresetId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionPresetId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionStats.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionStats.ts deleted file mode 100644 index 0db91702a19d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionStats.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Stats } from "./stats"; - -export type GraphExecutionStats = Stats | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodes.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodes.ts deleted file mode 100644 index e0d552ad6423..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecutionWithNodesPresetId } from "./graphExecutionWithNodesPresetId"; -import type { AgentExecutionStatus } from "./agentExecutionStatus"; -import type { GraphExecutionWithNodesStats } from "./graphExecutionWithNodesStats"; -import type { GraphExecutionWithNodesInputs } from "./graphExecutionWithNodesInputs"; -import type { GraphExecutionWithNodesOutputs } from "./graphExecutionWithNodesOutputs"; -import type { NodeExecutionResult } from "./nodeExecutionResult"; - -export interface GraphExecutionWithNodes { - id?: string; - user_id: string; - graph_id: string; - graph_version: number; - preset_id?: GraphExecutionWithNodesPresetId; - status: AgentExecutionStatus; - started_at: string; - ended_at: string; - stats: GraphExecutionWithNodesStats; - inputs: GraphExecutionWithNodesInputs; - outputs: GraphExecutionWithNodesOutputs; - node_executions: NodeExecutionResult[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesInputs.ts deleted file mode 100644 index 33beb3498351..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionWithNodesInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesOutputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesOutputs.ts deleted file mode 100644 index 4b8169fb3787..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesOutputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionWithNodesOutputs = { [key: string]: unknown[] }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesPresetId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesPresetId.ts deleted file mode 100644 index b212be49630d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesPresetId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphExecutionWithNodesPresetId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesStats.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesStats.ts deleted file mode 100644 index 73164b7376ce..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionWithNodesStats.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Stats } from "./stats"; - -export type GraphExecutionWithNodesStats = Stats | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromId.ts deleted file mode 100644 index 1b1f6e28a75f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphForkedFromId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromVersion.ts deleted file mode 100644 index 1d2d2f4ac4c9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphForkedFromVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphForkedFromVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMeta.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMeta.ts deleted file mode 100644 index f1f90a939996..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMeta.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphMetaForkedFromId } from "./graphMetaForkedFromId"; -import type { GraphMetaForkedFromVersion } from "./graphMetaForkedFromVersion"; -import type { BaseGraphOutput } from "./baseGraphOutput"; -import type { GraphMetaInputSchema } from "./graphMetaInputSchema"; -import type { GraphMetaOutputSchema } from "./graphMetaOutputSchema"; -import type { GraphMetaCredentialsInputSchema } from "./graphMetaCredentialsInputSchema"; - -export interface GraphMeta { - id?: string; - version?: number; - is_active?: boolean; - name: string; - description: string; - forked_from_id?: GraphMetaForkedFromId; - forked_from_version?: GraphMetaForkedFromVersion; - sub_graphs?: BaseGraphOutput[]; - user_id: string; - readonly input_schema: GraphMetaInputSchema; - readonly output_schema: GraphMetaOutputSchema; - readonly has_external_trigger: boolean; - readonly credentials_input_schema: GraphMetaCredentialsInputSchema; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaCredentialsInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaCredentialsInputSchema.ts deleted file mode 100644 index 1836bed23b40..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaCredentialsInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphMetaCredentialsInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromId.ts deleted file mode 100644 index e515e3a41e4c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphMetaForkedFromId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromVersion.ts deleted file mode 100644 index 774e920cc6b0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaForkedFromVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphMetaForkedFromVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaInputSchema.ts deleted file mode 100644 index 35851fa5ea48..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphMetaInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaOutputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaOutputSchema.ts deleted file mode 100644 index 778649e13801..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphMetaOutputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphMetaOutputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModel.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModel.ts deleted file mode 100644 index 09d4e7dc1707..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModel.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { NodeModel } from "./nodeModel"; -import type { Link } from "./link"; -import type { GraphModelForkedFromId } from "./graphModelForkedFromId"; -import type { GraphModelForkedFromVersion } from "./graphModelForkedFromVersion"; -import type { BaseGraphOutput } from "./baseGraphOutput"; -import type { GraphModelInputSchema } from "./graphModelInputSchema"; -import type { GraphModelOutputSchema } from "./graphModelOutputSchema"; -import type { GraphModelCredentialsInputSchema } from "./graphModelCredentialsInputSchema"; - -export interface GraphModel { - id?: string; - version?: number; - is_active?: boolean; - name: string; - description: string; - nodes?: NodeModel[]; - links?: Link[]; - forked_from_id?: GraphModelForkedFromId; - forked_from_version?: GraphModelForkedFromVersion; - sub_graphs?: BaseGraphOutput[]; - user_id: string; - readonly input_schema: GraphModelInputSchema; - readonly output_schema: GraphModelOutputSchema; - readonly has_external_trigger: boolean; - readonly credentials_input_schema: GraphModelCredentialsInputSchema; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelCredentialsInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelCredentialsInputSchema.ts deleted file mode 100644 index 90ce351a5b8c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelCredentialsInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphModelCredentialsInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromId.ts deleted file mode 100644 index fcf3d0efa8b5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphModelForkedFromId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromVersion.ts deleted file mode 100644 index 8b4f889115b1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelForkedFromVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphModelForkedFromVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelInputSchema.ts deleted file mode 100644 index 76da0b5642ce..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphModelInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelOutputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelOutputSchema.ts deleted file mode 100644 index 8d2b9560a282..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphModelOutputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type GraphModelOutputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hTTPValidationError.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hTTPValidationError.ts deleted file mode 100644 index b216ede240c9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hTTPValidationError.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ValidationError } from "./validationError"; - -export interface HTTPValidationError { - detail?: ValidationError[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInput.ts deleted file mode 100644 index e46e6d7979d4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInput.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { HostScopedCredentialsInputTitle } from "./hostScopedCredentialsInputTitle"; -import type { HostScopedCredentialsInputHeaders } from "./hostScopedCredentialsInputHeaders"; - -export interface HostScopedCredentialsInput { - id?: string; - provider: string; - title?: HostScopedCredentialsInputTitle; - type?: "host_scoped"; - /** The host/URI pattern to match against request URLs */ - host: string; - /** Key-value header map to add to matching requests */ - headers?: HostScopedCredentialsInputHeaders; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputHeaders.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputHeaders.ts deleted file mode 100644 index 97971ba57cf3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputHeaders.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Key-value header map to add to matching requests - */ -export type HostScopedCredentialsInputHeaders = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputTitle.ts deleted file mode 100644 index 718198d2b383..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsInputTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type HostScopedCredentialsInputTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutput.ts deleted file mode 100644 index 2fe39782dbe7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutput.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { HostScopedCredentialsOutputTitle } from "./hostScopedCredentialsOutputTitle"; -import type { HostScopedCredentialsOutputHeaders } from "./hostScopedCredentialsOutputHeaders"; - -export interface HostScopedCredentialsOutput { - id?: string; - provider: string; - title?: HostScopedCredentialsOutputTitle; - type?: "host_scoped"; - /** The host/URI pattern to match against request URLs */ - host: string; - /** Key-value header map to add to matching requests */ - headers?: HostScopedCredentialsOutputHeaders; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputHeaders.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputHeaders.ts deleted file mode 100644 index d5962d20bbb0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputHeaders.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Key-value header map to add to matching requests - */ -export type HostScopedCredentialsOutputHeaders = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputTitle.ts deleted file mode 100644 index bc96d6f73898..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/hostScopedCredentialsOutputTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type HostScopedCredentialsOutputTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgent.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgent.ts deleted file mode 100644 index 583be8150b6f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgent.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentImageUrl } from "./libraryAgentImageUrl"; -import type { LibraryAgentStatus } from "./libraryAgentStatus"; -import type { LibraryAgentInputSchema } from "./libraryAgentInputSchema"; -import type { LibraryAgentCredentialsInputSchema } from "./libraryAgentCredentialsInputSchema"; -import type { LibraryAgentTriggerSetupInfo } from "./libraryAgentTriggerSetupInfo"; - -/** - * Represents an agent in the library, including metadata for display and -user interaction within the system. - */ -export interface LibraryAgent { - id: string; - graph_id: string; - graph_version: number; - image_url: LibraryAgentImageUrl; - creator_name: string; - creator_image_url: string; - status: LibraryAgentStatus; - updated_at: string; - name: string; - description: string; - input_schema: LibraryAgentInputSchema; - /** Input schema for credentials required by the agent */ - credentials_input_schema: LibraryAgentCredentialsInputSchema; - /** Whether the agent has an external trigger (e.g. webhook) node */ - has_external_trigger: boolean; - trigger_setup_info?: LibraryAgentTriggerSetupInfo; - new_output: boolean; - can_access_graph: boolean; - is_latest_version: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchema.ts deleted file mode 100644 index 31c70b995f19..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchema.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentCredentialsInputSchemaAnyOf } from "./libraryAgentCredentialsInputSchemaAnyOf"; - -/** - * Input schema for credentials required by the agent - */ -export type LibraryAgentCredentialsInputSchema = - LibraryAgentCredentialsInputSchemaAnyOf | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchemaAnyOf.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchemaAnyOf.ts deleted file mode 100644 index 8f757805ab61..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentCredentialsInputSchemaAnyOf.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentCredentialsInputSchemaAnyOf = { - [key: string]: unknown; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentImageUrl.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentImageUrl.ts deleted file mode 100644 index cf329fdbfc16..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentImageUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentImageUrl = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentInputSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentInputSchema.ts deleted file mode 100644 index d2e5e527c9f8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentInputSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPreset.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPreset.ts deleted file mode 100644 index ce067a28508c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPreset.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetInputs } from "./libraryAgentPresetInputs"; -import type { LibraryAgentPresetCredentials } from "./libraryAgentPresetCredentials"; -import type { LibraryAgentPresetWebhookId } from "./libraryAgentPresetWebhookId"; - -/** - * Represents a preset configuration for a library agent. - */ -export interface LibraryAgentPreset { - graph_id: string; - graph_version: number; - inputs: LibraryAgentPresetInputs; - credentials: LibraryAgentPresetCredentials; - name: string; - description: string; - is_active?: boolean; - webhook_id?: LibraryAgentPresetWebhookId; - id: string; - user_id: string; - updated_at: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatable.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatable.ts deleted file mode 100644 index 38f9c1ed53ec..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatable.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetCreatableInputs } from "./libraryAgentPresetCreatableInputs"; -import type { LibraryAgentPresetCreatableCredentials } from "./libraryAgentPresetCreatableCredentials"; -import type { LibraryAgentPresetCreatableWebhookId } from "./libraryAgentPresetCreatableWebhookId"; - -/** - * Request model used when creating a new preset for a library agent. - */ -export interface LibraryAgentPresetCreatable { - graph_id: string; - graph_version: number; - inputs: LibraryAgentPresetCreatableInputs; - credentials: LibraryAgentPresetCreatableCredentials; - name: string; - description: string; - is_active?: boolean; - webhook_id?: LibraryAgentPresetCreatableWebhookId; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableCredentials.ts deleted file mode 100644 index 857ff545dbd9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type LibraryAgentPresetCreatableCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableFromGraphExecution.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableFromGraphExecution.ts deleted file mode 100644 index 90aea688b4c3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableFromGraphExecution.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Request model used when creating a new preset for a library agent. - */ -export interface LibraryAgentPresetCreatableFromGraphExecution { - graph_execution_id: string; - name: string; - description: string; - is_active?: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableInputs.ts deleted file mode 100644 index f1d90905aee5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetCreatableInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableWebhookId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableWebhookId.ts deleted file mode 100644 index 9b35cd693f97..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCreatableWebhookId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetCreatableWebhookId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCredentials.ts deleted file mode 100644 index f583a776fa50..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type LibraryAgentPresetCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetInputs.ts deleted file mode 100644 index ad5ee445bb72..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetResponse.ts deleted file mode 100644 index 2f24c2dff4df..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPreset } from "./libraryAgentPreset"; -import type { Pagination } from "./pagination"; - -/** - * Response schema for a list of agent presets and pagination info. - */ -export interface LibraryAgentPresetResponse { - presets: LibraryAgentPreset[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatable.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatable.ts deleted file mode 100644 index 672420da8eff..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetUpdatableInputs } from "./libraryAgentPresetUpdatableInputs"; -import type { LibraryAgentPresetUpdatableCredentials } from "./libraryAgentPresetUpdatableCredentials"; -import type { LibraryAgentPresetUpdatableName } from "./libraryAgentPresetUpdatableName"; -import type { LibraryAgentPresetUpdatableDescription } from "./libraryAgentPresetUpdatableDescription"; -import type { LibraryAgentPresetUpdatableIsActive } from "./libraryAgentPresetUpdatableIsActive"; - -/** - * Request model used when updating a preset for a library agent. - */ -export interface LibraryAgentPresetUpdatable { - inputs?: LibraryAgentPresetUpdatableInputs; - credentials?: LibraryAgentPresetUpdatableCredentials; - name?: LibraryAgentPresetUpdatableName; - description?: LibraryAgentPresetUpdatableDescription; - is_active?: LibraryAgentPresetUpdatableIsActive; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentials.ts deleted file mode 100644 index a30e3377bb66..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentials.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetUpdatableCredentialsAnyOf } from "./libraryAgentPresetUpdatableCredentialsAnyOf"; - -export type LibraryAgentPresetUpdatableCredentials = - LibraryAgentPresetUpdatableCredentialsAnyOf | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentialsAnyOf.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentialsAnyOf.ts deleted file mode 100644 index d36196eb05b0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableCredentialsAnyOf.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type LibraryAgentPresetUpdatableCredentialsAnyOf = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableDescription.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableDescription.ts deleted file mode 100644 index 485a8363f23f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableDescription.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetUpdatableDescription = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputs.ts deleted file mode 100644 index 487b08ab1a12..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputs.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetUpdatableInputsAnyOf } from "./libraryAgentPresetUpdatableInputsAnyOf"; - -export type LibraryAgentPresetUpdatableInputs = - LibraryAgentPresetUpdatableInputsAnyOf | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputsAnyOf.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputsAnyOf.ts deleted file mode 100644 index 96010a9fb000..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableInputsAnyOf.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetUpdatableInputsAnyOf = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableIsActive.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableIsActive.ts deleted file mode 100644 index de763759d83e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableIsActive.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetUpdatableIsActive = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableName.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableName.ts deleted file mode 100644 index 450e5a964f88..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetUpdatableName.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetUpdatableName = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetWebhookId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetWebhookId.ts deleted file mode 100644 index c09d7ad7335a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentPresetWebhookId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentPresetWebhookId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentResponse.ts deleted file mode 100644 index 2bc52229d5d7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgent } from "./libraryAgent"; -import type { Pagination } from "./pagination"; - -/** - * Response schema for a list of library agents and pagination info. - */ -export interface LibraryAgentResponse { - agents: LibraryAgent[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentSort.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentSort.ts deleted file mode 100644 index 9d077ad28ed3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentSort.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Possible sort options for sorting library agents. - */ -export type LibraryAgentSort = - (typeof LibraryAgentSort)[keyof typeof LibraryAgentSort]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const LibraryAgentSort = { - createdAt: "createdAt", - updatedAt: "updatedAt", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentStatus.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentStatus.ts deleted file mode 100644 index f47b34fce141..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentStatus.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentStatus = - (typeof LibraryAgentStatus)[keyof typeof LibraryAgentStatus]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const LibraryAgentStatus = { - COMPLETED: "COMPLETED", - HEALTHY: "HEALTHY", - WAITING: "WAITING", - ERROR: "ERROR", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfo.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfo.ts deleted file mode 100644 index 29bbc0ce14bd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfo.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentTriggerInfoConfigSchema } from "./libraryAgentTriggerInfoConfigSchema"; -import type { LibraryAgentTriggerInfoCredentialsInputName } from "./libraryAgentTriggerInfoCredentialsInputName"; - -export interface LibraryAgentTriggerInfo { - /** Provider name for integrations. Can be any string value, including custom provider names. */ - provider: string; - /** Input schema for the trigger block */ - config_schema: LibraryAgentTriggerInfoConfigSchema; - credentials_input_name: LibraryAgentTriggerInfoCredentialsInputName; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoConfigSchema.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoConfigSchema.ts deleted file mode 100644 index 2d002f0d4cfc..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoConfigSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Input schema for the trigger block - */ -export type LibraryAgentTriggerInfoConfigSchema = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoCredentialsInputName.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoCredentialsInputName.ts deleted file mode 100644 index 3915d46216ae..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerInfoCredentialsInputName.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type LibraryAgentTriggerInfoCredentialsInputName = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerSetupInfo.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerSetupInfo.ts deleted file mode 100644 index 2f7bb2bf3df0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentTriggerSetupInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentTriggerInfo } from "./libraryAgentTriggerInfo"; - -export type LibraryAgentTriggerSetupInfo = LibraryAgentTriggerInfo | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequest.ts deleted file mode 100644 index 17185d05ac01..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequest.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentUpdateRequestAutoUpdateVersion } from "./libraryAgentUpdateRequestAutoUpdateVersion"; -import type { LibraryAgentUpdateRequestIsFavorite } from "./libraryAgentUpdateRequestIsFavorite"; -import type { LibraryAgentUpdateRequestIsArchived } from "./libraryAgentUpdateRequestIsArchived"; - -/** - * Schema for updating a library agent via PUT. - -Includes flags for auto-updating version, marking as favorite, -archiving, or deleting. - */ -export interface LibraryAgentUpdateRequest { - /** Auto-update the agent version */ - auto_update_version?: LibraryAgentUpdateRequestAutoUpdateVersion; - /** Mark the agent as a favorite */ - is_favorite?: LibraryAgentUpdateRequestIsFavorite; - /** Archive the agent */ - is_archived?: LibraryAgentUpdateRequestIsArchived; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestAutoUpdateVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestAutoUpdateVersion.ts deleted file mode 100644 index c3760774bf2e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestAutoUpdateVersion.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Auto-update the agent version - */ -export type LibraryAgentUpdateRequestAutoUpdateVersion = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsArchived.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsArchived.ts deleted file mode 100644 index 1bcc3b1032a8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsArchived.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Archive the agent - */ -export type LibraryAgentUpdateRequestIsArchived = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsFavorite.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsFavorite.ts deleted file mode 100644 index 8c8af320aa1c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/libraryAgentUpdateRequestIsFavorite.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Mark the agent as a favorite - */ -export type LibraryAgentUpdateRequestIsFavorite = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/link.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/link.ts deleted file mode 100644 index a23f8a1c479b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/link.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Link { - id?: string; - source_id: string; - sink_id: string; - source_name: string; - sink_name: string; - is_static?: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/logRawMetricRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/logRawMetricRequest.ts deleted file mode 100644 index 89e0f1037e9e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/logRawMetricRequest.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface LogRawMetricRequest { - /** @minLength 1 */ - metric_name: string; - metric_value: number; - /** @minLength 1 */ - data_string: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/loginResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/loginResponse.ts deleted file mode 100644 index 59d221aec280..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/loginResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface LoginResponse { - login_url: string; - state_token: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/message.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/message.ts deleted file mode 100644 index 488998be605a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/message.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Message { - query: string; - response: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgent.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/myAgent.ts deleted file mode 100644 index d998aa62f4a1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgent.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { MyAgentAgentImage } from "./myAgentAgentImage"; - -export interface MyAgent { - agent_id: string; - agent_version: number; - agent_name: string; - agent_image?: MyAgentAgentImage; - description: string; - last_edited: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentAgentImage.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentAgentImage.ts deleted file mode 100644 index 36c088d6bb5f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentAgentImage.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type MyAgentAgentImage = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentsResponse.ts deleted file mode 100644 index ad054e547bb5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/myAgentsResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { MyAgent } from "./myAgent"; -import type { Pagination } from "./pagination"; - -export interface MyAgentsResponse { - agents: MyAgent[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/node.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/node.ts deleted file mode 100644 index 9f3740a02bce..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/node.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { NodeInputDefault } from "./nodeInputDefault"; -import type { NodeMetadata } from "./nodeMetadata"; -import type { Link } from "./link"; - -export interface Node { - id?: string; - block_id: string; - input_default?: NodeInputDefault; - metadata?: NodeMetadata; - input_links?: Link[]; - output_links?: Link[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResult.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResult.ts deleted file mode 100644 index b87ee7aa22d7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResult.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { AgentExecutionStatus } from "./agentExecutionStatus"; -import type { NodeExecutionResultInputData } from "./nodeExecutionResultInputData"; -import type { NodeExecutionResultOutputData } from "./nodeExecutionResultOutputData"; -import type { NodeExecutionResultQueueTime } from "./nodeExecutionResultQueueTime"; -import type { NodeExecutionResultStartTime } from "./nodeExecutionResultStartTime"; -import type { NodeExecutionResultEndTime } from "./nodeExecutionResultEndTime"; - -export interface NodeExecutionResult { - user_id: string; - graph_id: string; - graph_version: number; - graph_exec_id: string; - node_exec_id: string; - node_id: string; - block_id: string; - status: AgentExecutionStatus; - input_data: NodeExecutionResultInputData; - output_data: NodeExecutionResultOutputData; - add_time: string; - queue_time: NodeExecutionResultQueueTime; - start_time: NodeExecutionResultStartTime; - end_time: NodeExecutionResultEndTime; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultEndTime.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultEndTime.ts deleted file mode 100644 index e86cccf507c1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultEndTime.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeExecutionResultEndTime = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultInputData.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultInputData.ts deleted file mode 100644 index 9e4033a46d15..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultInputData.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeExecutionResultInputData = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultOutputData.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultOutputData.ts deleted file mode 100644 index 746d6e85e569..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultOutputData.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeExecutionResultOutputData = { [key: string]: unknown[] }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultQueueTime.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultQueueTime.ts deleted file mode 100644 index 438572926a7e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultQueueTime.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeExecutionResultQueueTime = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultStartTime.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultStartTime.ts deleted file mode 100644 index a8c389e19c28..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeExecutionResultStartTime.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeExecutionResultStartTime = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeInputDefault.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeInputDefault.ts deleted file mode 100644 index 191a06379d92..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeInputDefault.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeInputDefault = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeMetadata.ts deleted file mode 100644 index 3b3ab525e666..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeMetadata = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModel.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModel.ts deleted file mode 100644 index f81abc5f88c0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModel.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { NodeModelInputDefault } from "./nodeModelInputDefault"; -import type { NodeModelMetadata } from "./nodeModelMetadata"; -import type { Link } from "./link"; -import type { NodeModelWebhookId } from "./nodeModelWebhookId"; -import type { NodeModelWebhook } from "./nodeModelWebhook"; - -export interface NodeModel { - id?: string; - block_id: string; - input_default?: NodeModelInputDefault; - metadata?: NodeModelMetadata; - input_links?: Link[]; - output_links?: Link[]; - graph_id: string; - graph_version: number; - webhook_id?: NodeModelWebhookId; - webhook?: NodeModelWebhook; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelInputDefault.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelInputDefault.ts deleted file mode 100644 index 4ec2ae520406..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelInputDefault.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeModelInputDefault = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelMetadata.ts deleted file mode 100644 index 8cf1fbdf959d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeModelMetadata = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhook.ts deleted file mode 100644 index fe03e981e345..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhook.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { Webhook } from "./webhook"; - -export type NodeModelWebhook = Webhook | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhookId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhookId.ts deleted file mode 100644 index 82eb4173a501..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/nodeModelWebhookId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NodeModelWebhookId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreference.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreference.ts deleted file mode 100644 index 08bedfc794a5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreference.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { NotificationPreferencePreferences } from "./notificationPreferencePreferences"; - -export interface NotificationPreference { - user_id: string; - email: string; - /** Which notifications the user wants */ - preferences?: NotificationPreferencePreferences; - daily_limit?: number; - emails_sent_today?: number; - last_reset_date?: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTO.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTO.ts deleted file mode 100644 index 97cefcc15cc1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTO.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { NotificationPreferenceDTOPreferences } from "./notificationPreferenceDTOPreferences"; - -export interface NotificationPreferenceDTO { - /** User's email address */ - email: string; - /** Which notifications the user wants */ - preferences: NotificationPreferenceDTOPreferences; - /** Max emails per day */ - daily_limit: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTOPreferences.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTOPreferences.ts deleted file mode 100644 index 66c2762295be..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferenceDTOPreferences.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Which notifications the user wants - */ -export type NotificationPreferenceDTOPreferences = { [key: string]: boolean }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferencePreferences.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferencePreferences.ts deleted file mode 100644 index efdcd19d9c82..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationPreferencePreferences.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Which notifications the user wants - */ -export type NotificationPreferencePreferences = { [key: string]: boolean }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationType.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/notificationType.ts deleted file mode 100644 index 2a57ec2170a6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/notificationType.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type NotificationType = - (typeof NotificationType)[keyof typeof NotificationType]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const NotificationType = { - AGENT_RUN: "AGENT_RUN", - ZERO_BALANCE: "ZERO_BALANCE", - LOW_BALANCE: "LOW_BALANCE", - BLOCK_EXECUTION_FAILED: "BLOCK_EXECUTION_FAILED", - CONTINUOUS_AGENT_ERROR: "CONTINUOUS_AGENT_ERROR", - DAILY_SUMMARY: "DAILY_SUMMARY", - WEEKLY_SUMMARY: "WEEKLY_SUMMARY", - MONTHLY_SUMMARY: "MONTHLY_SUMMARY", - REFUND_REQUEST: "REFUND_REQUEST", - REFUND_PROCESSED: "REFUND_PROCESSED", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2Credentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2Credentials.ts deleted file mode 100644 index bb722084fd27..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2Credentials.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OAuth2CredentialsTitle } from "./oAuth2CredentialsTitle"; -import type { OAuth2CredentialsUsername } from "./oAuth2CredentialsUsername"; -import type { OAuth2CredentialsAccessTokenExpiresAt } from "./oAuth2CredentialsAccessTokenExpiresAt"; -import type { OAuth2CredentialsRefreshToken } from "./oAuth2CredentialsRefreshToken"; -import type { OAuth2CredentialsRefreshTokenExpiresAt } from "./oAuth2CredentialsRefreshTokenExpiresAt"; -import type { OAuth2CredentialsMetadata } from "./oAuth2CredentialsMetadata"; - -export interface OAuth2Credentials { - id?: string; - provider: string; - title?: OAuth2CredentialsTitle; - type?: "oauth2"; - username?: OAuth2CredentialsUsername; - access_token: string; - access_token_expires_at?: OAuth2CredentialsAccessTokenExpiresAt; - refresh_token?: OAuth2CredentialsRefreshToken; - refresh_token_expires_at?: OAuth2CredentialsRefreshTokenExpiresAt; - scopes: string[]; - metadata?: OAuth2CredentialsMetadata; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsAccessTokenExpiresAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsAccessTokenExpiresAt.ts deleted file mode 100644 index d4b56c864a90..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsAccessTokenExpiresAt.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsAccessTokenExpiresAt = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsMetadata.ts deleted file mode 100644 index c9dda9c94076..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsMetadata = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshToken.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshToken.ts deleted file mode 100644 index eafe163e2450..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshToken.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsRefreshToken = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshTokenExpiresAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshTokenExpiresAt.ts deleted file mode 100644 index 5587d031e6df..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsRefreshTokenExpiresAt.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsRefreshTokenExpiresAt = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsTitle.ts deleted file mode 100644 index d7c13518931d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsUsername.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsUsername.ts deleted file mode 100644 index 653dd0bbea8f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/oAuth2CredentialsUsername.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OAuth2CredentialsUsername = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/onboardingStep.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/onboardingStep.ts deleted file mode 100644 index 877b52ab8f86..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/onboardingStep.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type OnboardingStep = - (typeof OnboardingStep)[keyof typeof OnboardingStep]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const OnboardingStep = { - WELCOME: "WELCOME", - USAGE_REASON: "USAGE_REASON", - INTEGRATIONS: "INTEGRATIONS", - AGENT_CHOICE: "AGENT_CHOICE", - AGENT_NEW_RUN: "AGENT_NEW_RUN", - AGENT_INPUT: "AGENT_INPUT", - CONGRATS: "CONGRATS", - GET_RESULTS: "GET_RESULTS", - RUN_AGENTS: "RUN_AGENTS", - MARKETPLACE_VISIT: "MARKETPLACE_VISIT", - MARKETPLACE_ADD_AGENT: "MARKETPLACE_ADD_AGENT", - MARKETPLACE_RUN_AGENT: "MARKETPLACE_RUN_AGENT", - BUILDER_OPEN: "BUILDER_OPEN", - BUILDER_SAVE_AGENT: "BUILDER_SAVE_AGENT", - BUILDER_RUN_AGENT: "BUILDER_RUN_AGENT", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/pagination.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/pagination.ts deleted file mode 100644 index 7590f225f81f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/pagination.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Pagination { - /** Total number of items. */ - total_items: number; - /** Total number of pages. */ - total_pages: number; - /** Current_page page number. */ - current_page: number; - /** Number of items per page. */ - page_size: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentials201.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentials201.ts deleted file mode 100644 index 95ff25b5a1d0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentials201.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OAuth2Credentials } from "./oAuth2Credentials"; -import type { APIKeyCredentials } from "./aPIKeyCredentials"; -import type { UserPasswordCredentials } from "./userPasswordCredentials"; -import type { HostScopedCredentialsOutput } from "./hostScopedCredentialsOutput"; - -export type PostV1CreateCredentials201 = - | OAuth2Credentials - | APIKeyCredentials - | UserPasswordCredentials - | HostScopedCredentialsOutput; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentialsBody.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentialsBody.ts deleted file mode 100644 index ec8f114071a8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1CreateCredentialsBody.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OAuth2Credentials } from "./oAuth2Credentials"; -import type { APIKeyCredentials } from "./aPIKeyCredentials"; -import type { UserPasswordCredentials } from "./userPasswordCredentials"; -import type { HostScopedCredentialsInput } from "./hostScopedCredentialsInput"; - -export type PostV1CreateCredentialsBody = - | OAuth2Credentials - | APIKeyCredentials - | UserPasswordCredentials - | HostScopedCredentialsInput; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphAgentParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphAgentParams.ts deleted file mode 100644 index 72c8020c5e7b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphAgentParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1ExecuteGraphAgentParams = { - preset_id?: string | null; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlock200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlock200.ts deleted file mode 100644 index 3bf2112f4553..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlock200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1ExecuteGraphBlock200 = { [key: string]: unknown[] }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlockBody.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlockBody.ts deleted file mode 100644 index dd2a65d662b4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1ExecuteGraphBlockBody.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1ExecuteGraphBlockBody = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1HandlePostmarkEmailWebhooksBody.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1HandlePostmarkEmailWebhooksBody.ts deleted file mode 100644 index 699f5aa12416..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1HandlePostmarkEmailWebhooksBody.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkDeliveryWebhook } from "./postmarkDeliveryWebhook"; -import type { PostmarkBounceWebhook } from "./postmarkBounceWebhook"; -import type { PostmarkSpamComplaintWebhook } from "./postmarkSpamComplaintWebhook"; -import type { PostmarkOpenWebhook } from "./postmarkOpenWebhook"; -import type { PostmarkClickWebhook } from "./postmarkClickWebhook"; -import type { PostmarkSubscriptionChangeWebhook } from "./postmarkSubscriptionChangeWebhook"; - -export type PostV1HandlePostmarkEmailWebhooksBody = - | PostmarkDeliveryWebhook - | PostmarkBounceWebhook - | PostmarkSpamComplaintWebhook - | PostmarkOpenWebhook - | PostmarkClickWebhook - | PostmarkSubscriptionChangeWebhook; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1OneClickEmailUnsubscribeParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1OneClickEmailUnsubscribeParams.ts deleted file mode 100644 index c577d928bea5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1OneClickEmailUnsubscribeParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1OneClickEmailUnsubscribeParams = { - token: string; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1RefundCreditTransactionBody.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1RefundCreditTransactionBody.ts deleted file mode 100644 index 32b2fdb83ed0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1RefundCreditTransactionBody.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1RefundCreditTransactionBody = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecution200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecution200.ts deleted file mode 100644 index 741d340019f9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecution200.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { GraphExecutionMeta } from "./graphExecutionMeta"; - -export type PostV1StopGraphExecution200 = GraphExecutionMeta | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecutionsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecutionsParams.ts deleted file mode 100644 index 8ab146040c86..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1StopGraphExecutionsParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1StopGraphExecutionsParams = { - graph_id: string; - graph_exec_id: string; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1UpdateUserEmail200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV1UpdateUserEmail200.ts deleted file mode 100644 index 02089a220089..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV1UpdateUserEmail200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV1UpdateUserEmail200 = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2CreateANewPresetBody.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV2CreateANewPresetBody.ts deleted file mode 100644 index 36b8114b614c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2CreateANewPresetBody.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { LibraryAgentPresetCreatable } from "./libraryAgentPresetCreatable"; -import type { LibraryAgentPresetCreatableFromGraphExecution } from "./libraryAgentPresetCreatableFromGraphExecution"; - -export type PostV2CreateANewPresetBody = - | LibraryAgentPresetCreatable - | LibraryAgentPresetCreatableFromGraphExecution; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2ExecuteAPreset200.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV2ExecuteAPreset200.ts deleted file mode 100644 index fc0248fbe386..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2ExecuteAPreset200.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV2ExecuteAPreset200 = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2GenerateSubmissionImageParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postV2GenerateSubmissionImageParams.ts deleted file mode 100644 index 952b6e684f8d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postV2GenerateSubmissionImageParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostV2GenerateSubmissionImageParams = { - agent_id: string; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceEnum.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceEnum.ts deleted file mode 100644 index b9f7d70c3663..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceEnum.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkBounceEnum = - (typeof PostmarkBounceEnum)[keyof typeof PostmarkBounceEnum]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const PostmarkBounceEnum = { - NUMBER_1: 1, - NUMBER_2: 2, - NUMBER_16: 16, - NUMBER_32: 32, - NUMBER_64: 64, - NUMBER_128: 128, - NUMBER_256: 256, - NUMBER_512: 512, - NUMBER_1024: 1024, - NUMBER_2048: 2048, - NUMBER_4096: 4096, - NUMBER_8192: 8192, - NUMBER_16384: 16384, - NUMBER_100000: 100000, - NUMBER_100001: 100001, - NUMBER_100002: 100002, - NUMBER_100003: 100003, - NUMBER_100006: 100006, - NUMBER_100007: 100007, - NUMBER_100008: 100008, - NUMBER_100009: 100009, - NUMBER_100010: 100010, -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhook.ts deleted file mode 100644 index 8705792b6c34..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhook.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkBounceEnum } from "./postmarkBounceEnum"; -import type { PostmarkBounceWebhookMetadata } from "./postmarkBounceWebhookMetadata"; - -export interface PostmarkBounceWebhook { - RecordType?: "Bounce"; - ID: number; - Type: string; - TypeCode: PostmarkBounceEnum; - Tag: string; - MessageID: string; - Details: string; - Email: string; - From: string; - BouncedAt: string; - Inactive: boolean; - DumpAvailable: boolean; - CanActivate: boolean; - Subject: string; - ServerID: number; - MessageStream: string; - Content: string; - Name: string; - Description: string; - Metadata: PostmarkBounceWebhookMetadata; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhookMetadata.ts deleted file mode 100644 index b44b9760bd87..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkBounceWebhookMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkBounceWebhookMetadata = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhook.ts deleted file mode 100644 index 69923fa1361e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhook.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkClickWebhookMetadata } from "./postmarkClickWebhookMetadata"; -import type { PostmarkClickWebhookOS } from "./postmarkClickWebhookOS"; -import type { PostmarkClickWebhookClient } from "./postmarkClickWebhookClient"; -import type { PostmarkClickWebhookGeo } from "./postmarkClickWebhookGeo"; - -export interface PostmarkClickWebhook { - RecordType?: "Click"; - MessageStream: string; - Metadata: PostmarkClickWebhookMetadata; - Recipient: string; - MessageID: string; - ReceivedAt: string; - Platform: string; - ClickLocation: string; - OriginalLink: string; - Tag: string; - UserAgent: string; - OS: PostmarkClickWebhookOS; - Client: PostmarkClickWebhookClient; - Geo: PostmarkClickWebhookGeo; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookClient.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookClient.ts deleted file mode 100644 index 1153e6ab2730..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookClient.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkClickWebhookClient = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookGeo.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookGeo.ts deleted file mode 100644 index 3236c128ef3d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookGeo.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkClickWebhookGeo = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookMetadata.ts deleted file mode 100644 index 1c105912b2c3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkClickWebhookMetadata = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookOS.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookOS.ts deleted file mode 100644 index b61d05a9aa50..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkClickWebhookOS.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkClickWebhookOS = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhook.ts deleted file mode 100644 index f4fd9be41763..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhook.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkDeliveryWebhookMetadata } from "./postmarkDeliveryWebhookMetadata"; - -export interface PostmarkDeliveryWebhook { - RecordType?: "Delivery"; - ServerID: number; - MessageStream: string; - MessageID: string; - Recipient: string; - Tag: string; - DeliveredAt: string; - Details: string; - Metadata: PostmarkDeliveryWebhookMetadata; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhookMetadata.ts deleted file mode 100644 index ec054608e8b1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkDeliveryWebhookMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkDeliveryWebhookMetadata = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhook.ts deleted file mode 100644 index c31c737c19f4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhook.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkOpenWebhookMetadata } from "./postmarkOpenWebhookMetadata"; -import type { PostmarkOpenWebhookOS } from "./postmarkOpenWebhookOS"; -import type { PostmarkOpenWebhookClient } from "./postmarkOpenWebhookClient"; -import type { PostmarkOpenWebhookGeo } from "./postmarkOpenWebhookGeo"; - -export interface PostmarkOpenWebhook { - RecordType?: "Open"; - MessageStream: string; - Metadata: PostmarkOpenWebhookMetadata; - FirstOpen: boolean; - Recipient: string; - MessageID: string; - ReceivedAt: string; - Platform: string; - ReadSeconds: number; - Tag: string; - UserAgent: string; - OS: PostmarkOpenWebhookOS; - Client: PostmarkOpenWebhookClient; - Geo: PostmarkOpenWebhookGeo; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookClient.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookClient.ts deleted file mode 100644 index 92bb4ca083d9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookClient.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkOpenWebhookClient = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookGeo.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookGeo.ts deleted file mode 100644 index f1523d16c2fd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookGeo.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkOpenWebhookGeo = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookMetadata.ts deleted file mode 100644 index 1fb7dcf546e8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkOpenWebhookMetadata = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookOS.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookOS.ts deleted file mode 100644 index 003197ae47bf..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkOpenWebhookOS.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkOpenWebhookOS = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhook.ts deleted file mode 100644 index 6752559fd062..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhook.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkSpamComplaintWebhookMetadata } from "./postmarkSpamComplaintWebhookMetadata"; - -export interface PostmarkSpamComplaintWebhook { - RecordType?: "SpamComplaint"; - ID: number; - Type: string; - TypeCode: number; - Tag: string; - MessageID: string; - Details: string; - Email: string; - From: string; - BouncedAt: string; - Inactive: boolean; - DumpAvailable: boolean; - CanActivate: boolean; - Subject: string; - ServerID: number; - MessageStream: string; - Content: string; - Name: string; - Description: string; - Metadata: PostmarkSpamComplaintWebhookMetadata; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhookMetadata.ts deleted file mode 100644 index 9f2c41f27e01..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSpamComplaintWebhookMetadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkSpamComplaintWebhookMetadata = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhook.ts deleted file mode 100644 index cf8c1c540b96..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhook.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { PostmarkSubscriptionChangeWebhookMetadata } from "./postmarkSubscriptionChangeWebhookMetadata"; - -export interface PostmarkSubscriptionChangeWebhook { - RecordType?: "SubscriptionChange"; - MessageID: string; - ServerID: number; - MessageStream: string; - ChangedAt: string; - Recipient: string; - Origin: string; - SuppressSending: boolean; - SuppressionReason: string; - Tag: string; - Metadata: PostmarkSubscriptionChangeWebhookMetadata; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhookMetadata.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhookMetadata.ts deleted file mode 100644 index e534c685c4b1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/postmarkSubscriptionChangeWebhookMetadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type PostmarkSubscriptionChangeWebhookMetadata = { - [key: string]: string; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/profile.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/profile.ts deleted file mode 100644 index 072e9ae27074..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/profile.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface Profile { - name: string; - username: string; - description: string; - links: string[]; - avatar_url: string; - is_featured?: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetails.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetails.ts deleted file mode 100644 index e92dbab59d73..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetails.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ProfileDetailsAvatarUrl } from "./profileDetailsAvatarUrl"; - -export interface ProfileDetails { - name: string; - username: string; - description: string; - links: string[]; - avatar_url?: ProfileDetailsAvatarUrl; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetailsAvatarUrl.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetailsAvatarUrl.ts deleted file mode 100644 index ff303842e5e0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/profileDetailsAvatarUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ProfileDetailsAvatarUrl = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstants.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstants.ts deleted file mode 100644 index ad848f5b8f31..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstants.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ProviderConstantsPROVIDERNAMES } from "./providerConstantsPROVIDERNAMES"; - -/** - * Model that exposes all provider names as a constant in the OpenAPI schema. -This is designed to be converted by Orval into a TypeScript constant. - */ -export interface ProviderConstants { - /** All available provider names as a constant mapping */ - PROVIDER_NAMES?: ProviderConstantsPROVIDERNAMES; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstantsPROVIDERNAMES.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstantsPROVIDERNAMES.ts deleted file mode 100644 index 3392e30e649e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/providerConstantsPROVIDERNAMES.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * All available provider names as a constant mapping - */ -export type ProviderConstantsPROVIDERNAMES = { [key: string]: string }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/providerEnumResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/providerEnumResponse.ts deleted file mode 100644 index f012216405c6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/providerEnumResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Response containing a provider from the enum. - */ -export interface ProviderEnumResponse { - /** A provider name from the complete list of providers */ - provider: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/providerName.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/providerName.ts deleted file mode 100644 index 198bb7a5bfbf..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/providerName.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ProviderName = (typeof ProviderName)[keyof typeof ProviderName]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const ProviderName = { - aiml_api: "aiml_api", - anthropic: "anthropic", - apollo: "apollo", - compass: "compass", - discord: "discord", - d_id: "d_id", - e2b: "e2b", - exa: "exa", - fal: "fal", - generic_webhook: "generic_webhook", - github: "github", - google: "google", - google_maps: "google_maps", - groq: "groq", - http: "http", - hubspot: "hubspot", - ideogram: "ideogram", - jina: "jina", - linear: "linear", - llama_api: "llama_api", - medium: "medium", - mem0: "mem0", - notion: "notion", - nvidia: "nvidia", - ollama: "ollama", - openai: "openai", - openweathermap: "openweathermap", - open_router: "open_router", - pinecone: "pinecone", - reddit: "reddit", - replicate: "replicate", - revid: "revid", - screenshotone: "screenshotone", - slant3d: "slant3d", - smartlead: "smartlead", - smtp: "smtp", - twitter: "twitter", - todoist: "todoist", - unreal_speech: "unreal_speech", - zerobounce: "zerobounce", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/providerNamesResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/providerNamesResponse.ts deleted file mode 100644 index 930770a29a3c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/providerNamesResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Response containing list of all provider names. - */ -export interface ProviderNamesResponse { - /** List of all available provider names */ - providers?: string[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequest.ts deleted file mode 100644 index 1b658170afec..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequest.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { RefundRequestResult } from "./refundRequestResult"; - -export interface RefundRequest { - id: string; - user_id: string; - transaction_key: string; - amount: number; - reason: string; - result?: RefundRequestResult; - status: string; - created_at: string; - updated_at: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequestResult.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequestResult.ts deleted file mode 100644 index 165c4832ef04..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/refundRequestResult.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type RefundRequestResult = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/requestTopUp.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/requestTopUp.ts deleted file mode 100644 index 12d9a4b9fdd7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/requestTopUp.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface RequestTopUp { - credit_amount: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequest.ts deleted file mode 100644 index b987faedcd4c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequest.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ReviewSubmissionRequestInternalComments } from "./reviewSubmissionRequestInternalComments"; - -export interface ReviewSubmissionRequest { - store_listing_version_id: string; - is_approved: boolean; - comments: string; - internal_comments?: ReviewSubmissionRequestInternalComments; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequestInternalComments.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequestInternalComments.ts deleted file mode 100644 index d3969adb0ec5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/reviewSubmissionRequestInternalComments.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ReviewSubmissionRequestInternalComments = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequest.ts deleted file mode 100644 index 5f530cc843b9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ScheduleCreationRequestGraphVersion } from "./scheduleCreationRequestGraphVersion"; -import type { ScheduleCreationRequestInputs } from "./scheduleCreationRequestInputs"; -import type { ScheduleCreationRequestCredentials } from "./scheduleCreationRequestCredentials"; - -export interface ScheduleCreationRequest { - graph_version?: ScheduleCreationRequestGraphVersion; - name: string; - cron: string; - inputs: ScheduleCreationRequestInputs; - credentials?: ScheduleCreationRequestCredentials; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestCredentials.ts deleted file mode 100644 index c57c8604e437..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type ScheduleCreationRequestCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestGraphVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestGraphVersion.ts deleted file mode 100644 index 8049053e557c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestGraphVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ScheduleCreationRequestGraphVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestInputs.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestInputs.ts deleted file mode 100644 index ce851b98f690..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/scheduleCreationRequestInputs.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ScheduleCreationRequestInputs = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/setGraphActiveVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/setGraphActiveVersion.ts deleted file mode 100644 index d057bd2cc2e4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/setGraphActiveVersion.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface SetGraphActiveVersion { - active_graph_version: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/stats.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/stats.ts deleted file mode 100644 index a98a0e45d720..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/stats.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StatsError } from "./statsError"; - -export interface Stats { - /** Execution cost (cents) */ - cost?: number; - /** Seconds from start to end of run */ - duration?: number; - /** CPU sec of duration */ - duration_cpu_only?: number; - /** Seconds of total node runtime */ - node_exec_time?: number; - /** CPU sec of node_exec_time */ - node_exec_time_cpu_only?: number; - /** Number of node executions */ - node_exec_count?: number; - /** Number of node errors */ - node_error_count?: number; - /** Error message if any */ - error?: StatsError; - [key: string]: unknown; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/statsError.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/statsError.ts deleted file mode 100644 index c2e0f8b5f85f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/statsError.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Error message if any - */ -export type StatsError = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgent.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgent.ts deleted file mode 100644 index f34807629a5b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgent.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export interface StoreAgent { - slug: string; - agent_name: string; - agent_image: string; - creator: string; - creator_avatar: string; - sub_heading: string; - description: string; - runs: number; - rating: number; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetails.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetails.ts deleted file mode 100644 index 3f64cc9699a6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetails.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreAgentDetailsActiveVersionId } from "./storeAgentDetailsActiveVersionId"; - -export interface StoreAgentDetails { - store_listing_version_id: string; - slug: string; - agent_name: string; - agent_video: string; - agent_image: string[]; - creator: string; - creator_avatar: string; - sub_heading: string; - description: string; - categories: string[]; - runs: number; - rating: number; - versions: string[]; - last_updated: string; - active_version_id?: StoreAgentDetailsActiveVersionId; - has_approved_version?: boolean; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetailsActiveVersionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetailsActiveVersionId.ts deleted file mode 100644 index cb8f6a67a3ec..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentDetailsActiveVersionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreAgentDetailsActiveVersionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentsResponse.ts deleted file mode 100644 index aa18e6e06b7e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeAgentsResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreAgent } from "./storeAgent"; -import type { Pagination } from "./pagination"; - -export interface StoreAgentsResponse { - agents: StoreAgent[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersions.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersions.ts deleted file mode 100644 index c4facb7fb577..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersions.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreListingWithVersionsActiveVersionId } from "./storeListingWithVersionsActiveVersionId"; -import type { StoreListingWithVersionsCreatorEmail } from "./storeListingWithVersionsCreatorEmail"; -import type { StoreListingWithVersionsLatestVersion } from "./storeListingWithVersionsLatestVersion"; -import type { StoreSubmission } from "./storeSubmission"; - -/** - * A store listing with its version history - */ -export interface StoreListingWithVersions { - listing_id: string; - slug: string; - agent_id: string; - agent_version: number; - active_version_id?: StoreListingWithVersionsActiveVersionId; - has_approved_version?: boolean; - creator_email?: StoreListingWithVersionsCreatorEmail; - latest_version?: StoreListingWithVersionsLatestVersion; - versions?: StoreSubmission[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsActiveVersionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsActiveVersionId.ts deleted file mode 100644 index d497a6592132..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsActiveVersionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreListingWithVersionsActiveVersionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsCreatorEmail.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsCreatorEmail.ts deleted file mode 100644 index bb1be13a854c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsCreatorEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreListingWithVersionsCreatorEmail = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsLatestVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsLatestVersion.ts deleted file mode 100644 index b5306f0a2bfb..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingWithVersionsLatestVersion.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreSubmission } from "./storeSubmission"; - -export type StoreListingWithVersionsLatestVersion = StoreSubmission | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingsWithVersionsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingsWithVersionsResponse.ts deleted file mode 100644 index 0d1ac8aeb39f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeListingsWithVersionsResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreListingWithVersions } from "./storeListingWithVersions"; -import type { Pagination } from "./pagination"; - -/** - * Response model for listings with version history - */ -export interface StoreListingsWithVersionsResponse { - listings: StoreListingWithVersions[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReview.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeReview.ts deleted file mode 100644 index 3a36db855b1d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReview.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreReviewComments } from "./storeReviewComments"; - -export interface StoreReview { - score: number; - comments?: StoreReviewComments; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewComments.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewComments.ts deleted file mode 100644 index 4506670becc0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewComments.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreReviewComments = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreate.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreate.ts deleted file mode 100644 index 936598e92740..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreate.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreReviewCreateComments } from "./storeReviewCreateComments"; - -export interface StoreReviewCreate { - store_listing_version_id: string; - score: number; - comments?: StoreReviewCreateComments; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreateComments.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreateComments.ts deleted file mode 100644 index 64a98a6252e0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeReviewCreateComments.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreReviewCreateComments = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmission.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmission.ts deleted file mode 100644 index 9bc1db6f7a67..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmission.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { SubmissionStatus } from "./submissionStatus"; -import type { StoreSubmissionStoreListingVersionId } from "./storeSubmissionStoreListingVersionId"; -import type { StoreSubmissionVersion } from "./storeSubmissionVersion"; -import type { StoreSubmissionReviewerId } from "./storeSubmissionReviewerId"; -import type { StoreSubmissionReviewComments } from "./storeSubmissionReviewComments"; -import type { StoreSubmissionInternalComments } from "./storeSubmissionInternalComments"; -import type { StoreSubmissionReviewedAt } from "./storeSubmissionReviewedAt"; -import type { StoreSubmissionChangesSummary } from "./storeSubmissionChangesSummary"; - -export interface StoreSubmission { - agent_id: string; - agent_version: number; - name: string; - sub_heading: string; - slug: string; - description: string; - image_urls: string[]; - date_submitted: string; - status: SubmissionStatus; - runs: number; - rating: number; - store_listing_version_id?: StoreSubmissionStoreListingVersionId; - version?: StoreSubmissionVersion; - reviewer_id?: StoreSubmissionReviewerId; - review_comments?: StoreSubmissionReviewComments; - internal_comments?: StoreSubmissionInternalComments; - reviewed_at?: StoreSubmissionReviewedAt; - changes_summary?: StoreSubmissionChangesSummary; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionChangesSummary.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionChangesSummary.ts deleted file mode 100644 index e093e8b4e971..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionChangesSummary.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionChangesSummary = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionEditRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionEditRequest.ts new file mode 100644 index 000000000000..82614c0b9e54 --- /dev/null +++ b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionEditRequest.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v7.11.2 🍺 + * Do not edit manually. + * AutoGPT Agent Server + * This server is used to execute agents that are created by the AutoGPT system. + * OpenAPI spec version: 0.1 + */ +import type { StoreSubmissionEditRequestVideoUrl } from './storeSubmissionEditRequestVideoUrl'; +import type { StoreSubmissionEditRequestChangesSummary } from './storeSubmissionEditRequestChangesSummary'; + +export interface StoreSubmissionEditRequest { + name: string; + sub_heading: string; + video_url?: StoreSubmissionEditRequestVideoUrl; + image_urls?: string[]; + description?: string; + categories?: string[]; + changes_summary?: StoreSubmissionEditRequestChangesSummary; +} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionInternalComments.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionInternalComments.ts deleted file mode 100644 index ce3c6c45b19f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionInternalComments.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionInternalComments = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequest.ts deleted file mode 100644 index 20cf007b2aed..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequest.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreSubmissionRequestVideoUrl } from "./storeSubmissionRequestVideoUrl"; -import type { StoreSubmissionRequestChangesSummary } from "./storeSubmissionRequestChangesSummary"; - -export interface StoreSubmissionRequest { - agent_id: string; - agent_version: number; - slug: string; - name: string; - sub_heading: string; - video_url?: StoreSubmissionRequestVideoUrl; - image_urls?: string[]; - description?: string; - categories?: string[]; - changes_summary?: StoreSubmissionRequestChangesSummary; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestChangesSummary.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestChangesSummary.ts deleted file mode 100644 index 1078c1f1cdd6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestChangesSummary.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionRequestChangesSummary = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestVideoUrl.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestVideoUrl.ts deleted file mode 100644 index 7ce72c00ffe1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionRequestVideoUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionRequestVideoUrl = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewComments.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewComments.ts deleted file mode 100644 index 13ae778840ab..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewComments.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionReviewComments = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewedAt.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewedAt.ts deleted file mode 100644 index 5e12ef999cae..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewedAt.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionReviewedAt = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewerId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewerId.ts deleted file mode 100644 index a89c18be9ad7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionReviewerId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionReviewerId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionStoreListingVersionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionStoreListingVersionId.ts deleted file mode 100644 index 87be23bc9330..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionStoreListingVersionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionStoreListingVersionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionVersion.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionVersion.ts deleted file mode 100644 index 7a9623e1bc42..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionVersion.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type StoreSubmissionVersion = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionsResponse.ts deleted file mode 100644 index 0995244b9ca4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionsResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { StoreSubmission } from "./storeSubmission"; -import type { Pagination } from "./pagination"; - -export interface StoreSubmissionsResponse { - submissions: StoreSubmission[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/submissionStatus.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/submissionStatus.ts deleted file mode 100644 index fd3a42fea5a8..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/submissionStatus.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type SubmissionStatus = - (typeof SubmissionStatus)[keyof typeof SubmissionStatus]; - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const SubmissionStatus = { - DRAFT: "DRAFT", - PENDING: "PENDING", - APPROVED: "APPROVED", - REJECTED: "REJECTED", -} as const; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistory.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistory.ts deleted file mode 100644 index 20303cbaa16f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistory.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { UserTransaction } from "./userTransaction"; -import type { TransactionHistoryNextTransactionTime } from "./transactionHistoryNextTransactionTime"; - -export interface TransactionHistory { - transactions: UserTransaction[]; - next_transaction_time: TransactionHistoryNextTransactionTime; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistoryNextTransactionTime.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistoryNextTransactionTime.ts deleted file mode 100644 index e18d217480a4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/transactionHistoryNextTransactionTime.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type TransactionHistoryNextTransactionTime = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParams.ts deleted file mode 100644 index 005fbf10425e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { TriggeredPresetSetupParamsTriggerConfig } from "./triggeredPresetSetupParamsTriggerConfig"; -import type { TriggeredPresetSetupParamsAgentCredentials } from "./triggeredPresetSetupParamsAgentCredentials"; - -export interface TriggeredPresetSetupParams { - name: string; - description?: string; - trigger_config: TriggeredPresetSetupParamsTriggerConfig; - agent_credentials?: TriggeredPresetSetupParamsAgentCredentials; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsAgentCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsAgentCredentials.ts deleted file mode 100644 index 03f52d444f69..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsAgentCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type TriggeredPresetSetupParamsAgentCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsTriggerConfig.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsTriggerConfig.ts deleted file mode 100644 index 3d515dfd09dc..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupParamsTriggerConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type TriggeredPresetSetupParamsTriggerConfig = { - [key: string]: unknown; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequest.ts deleted file mode 100644 index 2b9bcc7b04a6..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { TriggeredPresetSetupRequestTriggerConfig } from "./triggeredPresetSetupRequestTriggerConfig"; -import type { TriggeredPresetSetupRequestAgentCredentials } from "./triggeredPresetSetupRequestAgentCredentials"; - -export interface TriggeredPresetSetupRequest { - name: string; - description?: string; - graph_id: string; - graph_version: number; - trigger_config: TriggeredPresetSetupRequestTriggerConfig; - agent_credentials?: TriggeredPresetSetupRequestAgentCredentials; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestAgentCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestAgentCredentials.ts deleted file mode 100644 index 863475d5fffd..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestAgentCredentials.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CredentialsMetaInput } from "./credentialsMetaInput"; - -export type TriggeredPresetSetupRequestAgentCredentials = { - [key: string]: CredentialsMetaInput; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestTriggerConfig.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestTriggerConfig.ts deleted file mode 100644 index 0d67511ada61..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/triggeredPresetSetupRequestTriggerConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type TriggeredPresetSetupRequestTriggerConfig = { - [key: string]: unknown; -}; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequest.ts deleted file mode 100644 index 75bdc2f91eae..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { TurnstileVerifyRequestAction } from "./turnstileVerifyRequestAction"; - -/** - * Request model for verifying a Turnstile token. - */ -export interface TurnstileVerifyRequest { - /** The Turnstile token to verify */ - token: string; - /** The action that the user is attempting to perform */ - action?: TurnstileVerifyRequestAction; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequestAction.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequestAction.ts deleted file mode 100644 index 9506b2993ba9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyRequestAction.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * The action that the user is attempting to perform - */ -export type TurnstileVerifyRequestAction = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponse.ts deleted file mode 100644 index 6ee4d8966d9e..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponse.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { TurnstileVerifyResponseError } from "./turnstileVerifyResponseError"; -import type { TurnstileVerifyResponseChallengeTimestamp } from "./turnstileVerifyResponseChallengeTimestamp"; -import type { TurnstileVerifyResponseHostname } from "./turnstileVerifyResponseHostname"; -import type { TurnstileVerifyResponseAction } from "./turnstileVerifyResponseAction"; - -/** - * Response model for the Turnstile verification endpoint. - */ -export interface TurnstileVerifyResponse { - /** Whether the token verification was successful */ - success: boolean; - /** Error message if verification failed */ - error?: TurnstileVerifyResponseError; - /** Timestamp of the challenge (ISO format) */ - challenge_timestamp?: TurnstileVerifyResponseChallengeTimestamp; - /** Hostname of the site where the challenge was solved */ - hostname?: TurnstileVerifyResponseHostname; - /** The action associated with this verification */ - action?: TurnstileVerifyResponseAction; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseAction.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseAction.ts deleted file mode 100644 index 254fb74a2731..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseAction.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * The action associated with this verification - */ -export type TurnstileVerifyResponseAction = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseChallengeTimestamp.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseChallengeTimestamp.ts deleted file mode 100644 index b0a4d5dbfcc3..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseChallengeTimestamp.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Timestamp of the challenge (ISO format) - */ -export type TurnstileVerifyResponseChallengeTimestamp = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseError.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseError.ts deleted file mode 100644 index b9a3d06271f9..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseError.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Error message if verification failed - */ -export type TurnstileVerifyResponseError = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseHostname.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseHostname.ts deleted file mode 100644 index f293b4baec0c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/turnstileVerifyResponseHostname.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -/** - * Hostname of the site where the challenge was solved - */ -export type TurnstileVerifyResponseHostname = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/updatePermissionsRequest.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/updatePermissionsRequest.ts deleted file mode 100644 index add6cd289532..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/updatePermissionsRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { APIKeyPermission } from "./aPIKeyPermission"; - -export interface UpdatePermissionsRequest { - permissions: APIKeyPermission[]; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userHistoryResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userHistoryResponse.ts deleted file mode 100644 index 5cbfe74cf68d..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userHistoryResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { UserTransaction } from "./userTransaction"; -import type { Pagination } from "./pagination"; - -/** - * Response model for listings with version history - */ -export interface UserHistoryResponse { - history: UserTransaction[]; - pagination: Pagination; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdate.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdate.ts deleted file mode 100644 index 4772393d30d7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdate.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { UserOnboardingUpdateCompletedSteps } from "./userOnboardingUpdateCompletedSteps"; -import type { UserOnboardingUpdateNotificationDot } from "./userOnboardingUpdateNotificationDot"; -import type { UserOnboardingUpdateNotified } from "./userOnboardingUpdateNotified"; -import type { UserOnboardingUpdateUsageReason } from "./userOnboardingUpdateUsageReason"; -import type { UserOnboardingUpdateIntegrations } from "./userOnboardingUpdateIntegrations"; -import type { UserOnboardingUpdateOtherIntegrations } from "./userOnboardingUpdateOtherIntegrations"; -import type { UserOnboardingUpdateSelectedStoreListingVersionId } from "./userOnboardingUpdateSelectedStoreListingVersionId"; -import type { UserOnboardingUpdateAgentInput } from "./userOnboardingUpdateAgentInput"; -import type { UserOnboardingUpdateOnboardingAgentExecutionId } from "./userOnboardingUpdateOnboardingAgentExecutionId"; -import type { UserOnboardingUpdateAgentRuns } from "./userOnboardingUpdateAgentRuns"; - -export interface UserOnboardingUpdate { - completedSteps?: UserOnboardingUpdateCompletedSteps; - notificationDot?: UserOnboardingUpdateNotificationDot; - notified?: UserOnboardingUpdateNotified; - usageReason?: UserOnboardingUpdateUsageReason; - integrations?: UserOnboardingUpdateIntegrations; - otherIntegrations?: UserOnboardingUpdateOtherIntegrations; - selectedStoreListingVersionId?: UserOnboardingUpdateSelectedStoreListingVersionId; - agentInput?: UserOnboardingUpdateAgentInput; - onboardingAgentExecutionId?: UserOnboardingUpdateOnboardingAgentExecutionId; - agentRuns?: UserOnboardingUpdateAgentRuns; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInput.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInput.ts deleted file mode 100644 index 7aff087768f4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInput.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { UserOnboardingUpdateAgentInputAnyOf } from "./userOnboardingUpdateAgentInputAnyOf"; - -export type UserOnboardingUpdateAgentInput = - UserOnboardingUpdateAgentInputAnyOf | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInputAnyOf.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInputAnyOf.ts deleted file mode 100644 index e80858c009e7..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentInputAnyOf.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateAgentInputAnyOf = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentRuns.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentRuns.ts deleted file mode 100644 index 86b63b2d3684..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateAgentRuns.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateAgentRuns = number | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateCompletedSteps.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateCompletedSteps.ts deleted file mode 100644 index 587c501dfe3c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateCompletedSteps.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OnboardingStep } from "./onboardingStep"; - -export type UserOnboardingUpdateCompletedSteps = OnboardingStep[] | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateIntegrations.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateIntegrations.ts deleted file mode 100644 index 2b9d88820aad..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateIntegrations.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateIntegrations = string[] | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotificationDot.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotificationDot.ts deleted file mode 100644 index 8e3b3f5715b0..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotificationDot.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateNotificationDot = boolean | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotified.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotified.ts deleted file mode 100644 index 5b6bfe7f2f67..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateNotified.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { OnboardingStep } from "./onboardingStep"; - -export type UserOnboardingUpdateNotified = OnboardingStep[] | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOnboardingAgentExecutionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOnboardingAgentExecutionId.ts deleted file mode 100644 index 22a9287ef6ff..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOnboardingAgentExecutionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateOnboardingAgentExecutionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOtherIntegrations.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOtherIntegrations.ts deleted file mode 100644 index 646db4c0564b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateOtherIntegrations.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateOtherIntegrations = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateSelectedStoreListingVersionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateSelectedStoreListingVersionId.ts deleted file mode 100644 index a4e565d2dd92..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateSelectedStoreListingVersionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateSelectedStoreListingVersionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateUsageReason.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateUsageReason.ts deleted file mode 100644 index 27963ed9b4ea..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userOnboardingUpdateUsageReason.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserOnboardingUpdateUsageReason = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentials.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentials.ts deleted file mode 100644 index b1738849cb52..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentials.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { UserPasswordCredentialsTitle } from "./userPasswordCredentialsTitle"; - -export interface UserPasswordCredentials { - id?: string; - provider: string; - title?: UserPasswordCredentialsTitle; - type?: "user_password"; - username: string; - password: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentialsTitle.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentialsTitle.ts deleted file mode 100644 index c23ec7506a1a..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userPasswordCredentialsTitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserPasswordCredentialsTitle = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransaction.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransaction.ts deleted file mode 100644 index a43062920ecf..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransaction.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { CreditTransactionType } from "./creditTransactionType"; -import type { UserTransactionDescription } from "./userTransactionDescription"; -import type { UserTransactionUsageGraphId } from "./userTransactionUsageGraphId"; -import type { UserTransactionUsageExecutionId } from "./userTransactionUsageExecutionId"; -import type { UserTransactionUserEmail } from "./userTransactionUserEmail"; -import type { UserTransactionReason } from "./userTransactionReason"; -import type { UserTransactionAdminEmail } from "./userTransactionAdminEmail"; -import type { UserTransactionExtraData } from "./userTransactionExtraData"; - -export interface UserTransaction { - transaction_key?: string; - transaction_time?: string; - transaction_type?: CreditTransactionType; - amount?: number; - running_balance?: number; - current_balance?: number; - description?: UserTransactionDescription; - usage_graph_id?: UserTransactionUsageGraphId; - usage_execution_id?: UserTransactionUsageExecutionId; - usage_node_count?: number; - usage_start_time?: string; - user_id: string; - user_email?: UserTransactionUserEmail; - reason?: UserTransactionReason; - admin_email?: UserTransactionAdminEmail; - extra_data?: UserTransactionExtraData; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionAdminEmail.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionAdminEmail.ts deleted file mode 100644 index 6caa54836ae4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionAdminEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionAdminEmail = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionDescription.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionDescription.ts deleted file mode 100644 index 2e47fb24363c..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionDescription.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionDescription = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionExtraData.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionExtraData.ts deleted file mode 100644 index 2d1150ae93dc..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionExtraData.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionExtraData = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionReason.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionReason.ts deleted file mode 100644 index 6a5dfd23d1b1..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionReason.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionReason = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageExecutionId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageExecutionId.ts deleted file mode 100644 index cd5675662bf5..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageExecutionId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionUsageExecutionId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageGraphId.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageGraphId.ts deleted file mode 100644 index 57eaa4505ba2..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUsageGraphId.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionUsageGraphId = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUserEmail.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUserEmail.ts deleted file mode 100644 index f3fa3968043f..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/userTransactionUserEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type UserTransactionUserEmail = string | null; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/validationError.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/validationError.ts deleted file mode 100644 index 0d6b4688dda4..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/validationError.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { ValidationErrorLocItem } from "./validationErrorLocItem"; - -export interface ValidationError { - loc: ValidationErrorLocItem[]; - msg: string; - type: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/validationErrorLocItem.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/validationErrorLocItem.ts deleted file mode 100644 index 28822590d36b..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/validationErrorLocItem.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type ValidationErrorLocItem = string | number; diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/webhook.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/webhook.ts deleted file mode 100644 index c1decc30a1de..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/webhook.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ -import type { WebhookConfig } from "./webhookConfig"; - -export interface Webhook { - id?: string; - user_id: string; - /** Provider name for integrations. Can be any string value, including custom provider names. */ - provider: string; - credentials_id: string; - webhook_type: string; - resource: string; - events: string[]; - config?: WebhookConfig; - secret: string; - provider_webhook_id: string; - readonly url: string; -} diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/webhookConfig.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/webhookConfig.ts deleted file mode 100644 index 0a6619bbb001..000000000000 --- a/autogpt_platform/frontend/src/app/api/__generated__/models/webhookConfig.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * AutoGPT Agent Server - * This server is used to execute agents that are created by the AutoGPT system. - * OpenAPI spec version: 0.1 - */ - -export type WebhookConfig = { [key: string]: unknown }; diff --git a/autogpt_platform/frontend/src/app/api/auth/user/route.ts b/autogpt_platform/frontend/src/app/api/auth/user/route.ts new file mode 100644 index 000000000000..896385d865be --- /dev/null +++ b/autogpt_platform/frontend/src/app/api/auth/user/route.ts @@ -0,0 +1,39 @@ +import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; +import { NextResponse } from "next/server"; + +export async function GET() { + const supabase = await getServerSupabase(); + const { data, error } = await supabase.auth.getUser(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json(data); +} + +export async function PUT(request: Request) { + try { + const supabase = await getServerSupabase(); + const { email } = await request.json(); + + if (!email) { + return NextResponse.json({ error: "Email is required" }, { status: 400 }); + } + + const { data, error } = await supabase.auth.updateUser({ + email, + }); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json(data); + } catch { + return NextResponse.json( + { error: "Failed to update user email" }, + { status: 500 }, + ); + } +} diff --git a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts index e6b66d762ec6..aa443cf3c168 100644 --- a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts +++ b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts @@ -1,7 +1,24 @@ +import { + createRequestHeaders, + getServerAuthToken, +} from "@/lib/autogpt-server-api/helpers"; +import { isServerSide } from "@/lib/utils/is-server-side"; +import { getAgptServerBaseUrl } from "@/lib/env-config"; + +import { transformDates } from "./date-transformer"; + const FRONTEND_BASE_URL = process.env.NEXT_PUBLIC_FRONTEND_BASE_URL || "http://localhost:3000"; const API_PROXY_BASE_URL = `${FRONTEND_BASE_URL}/api/proxy`; // Sending request via nextjs Server +const getBaseUrl = (): string => { + if (!isServerSide()) { + return API_PROXY_BASE_URL; + } else { + return getAgptServerBaseUrl(); + } +}; + const getBody = (c: Response | Request): Promise => { const contentType = c.headers.get("content-type"); @@ -30,11 +47,12 @@ export const customMutator = async ( | "DELETE" | "PATCH"; const data = requestOptions.body; - const headers: Record = { + let headers: Record = { ...((requestOptions.headers as Record) || {}), }; const isFormData = data instanceof FormData; + const contentType = isFormData ? "multipart/form-data" : "application/json"; // Currently, only two content types are handled here: application/json and multipart/form-data if (!isFormData && data && !headers["Content-Type"]) { @@ -45,18 +63,49 @@ export const customMutator = async ( ? "?" + new URLSearchParams(params).toString() : ""; - const response = await fetch(`${API_PROXY_BASE_URL}${url}${queryString}`, { + const baseUrl = getBaseUrl(); + + // The caching in React Query in our system depends on the url, so the base_url could be different for the server and client sides. + const fullUrl = `${baseUrl}${url}${queryString}`; + + if (isServerSide()) { + try { + const token = await getServerAuthToken(); + const authHeaders = createRequestHeaders(token, !!data, contentType); + headers = { ...headers, ...authHeaders }; + } catch (error) { + console.warn("Failed to get server auth token:", error); + } + } + + const response = await fetch(fullUrl, { ...requestOptions, method, headers, body: data, }); + // Error handling for server-side requests + // We do not need robust error handling for server-side requests; we only need to log the error message and throw the error. + // What happens if the server-side request fails? + // 1. The error will be logged in the terminal, then. + // 2. The error will be thrown, so the cached data for this particular queryKey will be empty, then. + // 3. The client-side will send the request again via the proxy. If it fails again, the error will be handled on the client side. + // 4. If the request succeeds on the server side, the data will be cached, and the client will use it instead of sending a request to the proxy. + + if (!response.ok && isServerSide()) { + console.error("Request failed on server side", response, fullUrl); + throw new Error(`Request failed with status ${response.status}`); + } + const response_data = await getBody(response); + // Transform ISO date strings to Date objects in the response data + const transformedData = transformDates(response_data); + return { status: response.status, - data: response_data, + data: transformedData, headers: response.headers, } as T; }; diff --git a/autogpt_platform/frontend/src/app/api/mutators/date-transformer.ts b/autogpt_platform/frontend/src/app/api/mutators/date-transformer.ts new file mode 100644 index 000000000000..575495a3dbc4 --- /dev/null +++ b/autogpt_platform/frontend/src/app/api/mutators/date-transformer.ts @@ -0,0 +1,51 @@ +/** + * Date transformation utility for converting ISO date strings to Date objects + * in API responses. This handles the conversion recursively for nested objects. + */ + +// ISO date regex pattern to match strings that look like ISO dates +const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/; + +/** + * Validates if a string is a valid ISO date and can be parsed + */ +function isValidISODate(dateString: string): boolean { + if (!ISO_DATE_REGEX.test(dateString)) { + return false; + } + + const date = new Date(dateString); + return !isNaN(date.getTime()); +} + +/** + * Recursively transforms ISO date strings to Date objects in an object or array + * @param obj - The object or array to transform + * @returns The transformed object with Date objects + */ +export function transformDates(obj: T): T { + if (typeof obj !== "object" || obj === null) return obj; + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map(transformDates) as T; + } + + // Handle objects + const transformed = {} as T; + + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string" && isValidISODate(value)) { + // Convert ISO date string to Date object + (transformed as any)[key] = new Date(value); + } else if (typeof value === "object") { + // Recursively transform nested objects/arrays + (transformed as any)[key] = transformDates(value); + } else { + // Keep primitive values as-is + (transformed as any)[key] = value; + } + } + + return transformed; +} diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index c63413e9ac01..32d6184fb7c9 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -443,6 +443,24 @@ } } }, + "/api/integrations/ayrshare/sso_url": { + "get": { + "tags": ["v1", "integrations"], + "summary": "Get Ayrshare Sso Url", + "description": "Generate an SSO URL for Ayrshare social media integration.\n\nReturns:\n dict: Contains the SSO URL for Ayrshare integration", + "operationId": "getV1GetAyrshareSsoUrl", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AyrshareSSOResponse" } + } + } + } + } + } + }, "/api/integrations/providers": { "get": { "tags": ["v1", "integrations"], @@ -633,6 +651,56 @@ } } }, + "/api/auth/user/timezone": { + "get": { + "tags": ["v1", "auth"], + "summary": "Get user timezone", + "description": "Get user timezone setting.", + "operationId": "getV1Get user timezone", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TimezoneResponse" } + } + } + } + } + }, + "post": { + "tags": ["v1", "auth"], + "summary": "Update user timezone", + "description": "Update user timezone. The timezone should be a valid IANA timezone identifier.", + "operationId": "postV1Update user timezone", + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateTimezoneRequest" } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TimezoneResponse" } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, "/api/auth/user/preferences": { "get": { "tags": ["v1", "auth"], @@ -823,6 +891,64 @@ } } }, + "/api/files/upload": { + "post": { + "tags": ["v1", "files"], + "summary": "Upload file to cloud storage", + "description": "Upload a file to cloud storage and return a storage key that can be used\nwith FileStoreBlock and AgentFileInputBlock.\n\nArgs:\n file: The file to upload\n user_id: The user ID\n provider: Cloud storage provider (\"gcs\", \"s3\", \"azure\")\n expiration_hours: Hours until file expires (1-48)\n\nReturns:\n Dict containing the cloud storage path and signed URL", + "operationId": "postV1Upload file to cloud storage", + "parameters": [ + { + "name": "provider", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "gcs", + "title": "Provider" + } + }, + { + "name": "expiration_hours", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 24, + "title": "Expiration Hours" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_postV1Upload_file_to_cloud_storage" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UploadFileResponse" } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, "/api/credits": { "get": { "tags": ["v1", "credits"], @@ -1504,8 +1630,8 @@ "/api/executions": { "get": { "tags": ["v1", "graphs"], - "summary": "Get all executions", - "operationId": "getV1Get all executions", + "summary": "List all executions", + "operationId": "getV1List all executions", "responses": { "200": { "description": "Successful Response", @@ -1516,7 +1642,7 @@ "$ref": "#/components/schemas/GraphExecutionMeta" }, "type": "array", - "title": "Response Getv1Get All Executions" + "title": "Response Getv1List All Executions" } } } @@ -1527,14 +1653,41 @@ "/api/graphs/{graph_id}/executions": { "get": { "tags": ["v1", "graphs"], - "summary": "Get graph executions", - "operationId": "getV1Get graph executions", + "summary": "List graph executions", + "operationId": "getV1List graph executions", "parameters": [ { "name": "graph_id", "in": "path", "required": true, "schema": { "type": "string", "title": "Graph Id" } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "description": "Page number (1-indexed)", + "default": 1, + "title": "Page" + }, + "description": "Page number (1-indexed)" + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "description": "Number of executions per page", + "default": 25, + "title": "Page Size" + }, + "description": "Number of executions per page" } ], "responses": { @@ -1543,11 +1696,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GraphExecutionMeta" - }, - "title": "Response Getv1Get Graph Executions" + "$ref": "#/components/schemas/GraphExecutionsPaginated" } } } @@ -2391,6 +2540,30 @@ "tags": ["v2", "store", "private"], "summary": "Get my agents", "operationId": "getV2Get my agents", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 20, + "title": "Page Size" + } + } + ], "responses": { "200": { "description": "Successful Response", @@ -2399,6 +2572,14 @@ "schema": { "$ref": "#/components/schemas/MyAgentsResponse" } } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } } } } @@ -2516,6 +2697,50 @@ } } }, + "/api/store/submissions/{store_listing_version_id}": { + "put": { + "tags": ["v2", "store", "private"], + "summary": "Edit store submission", + "description": "Edit an existing store listing submission.\n\nArgs:\n store_listing_version_id (str): ID of the store listing version to edit\n submission_request (StoreSubmissionRequest): The updated submission details\n user_id (str): ID of the authenticated user editing the listing\n\nReturns:\n StoreSubmission: The updated store submission\n\nRaises:\n HTTPException: If there is an error editing the submission", + "operationId": "putV2Edit store submission", + "parameters": [ + { + "name": "store_listing_version_id", + "in": "path", + "required": true, + "schema": { "type": "string", "title": "Store Listing Version Id" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StoreSubmissionEditRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/StoreSubmission" } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, "/api/store/submissions/media": { "post": { "tags": ["v2", "store", "private"], @@ -2535,7 +2760,253 @@ "responses": { "200": { "description": "Successful Response", - "content": { "application/json": { "schema": {} } } + "content": { "application/json": { "schema": {} } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, + "/api/store/submissions/generate_image": { + "post": { + "tags": ["v2", "store", "private"], + "summary": "Generate submission image", + "description": "Generate an image for a store listing submission.\n\nArgs:\n agent_id (str): ID of the agent to generate an image for\n user_id (str): ID of the authenticated user\n\nReturns:\n JSONResponse: JSON containing the URL of the generated image", + "operationId": "postV2Generate submission image", + "parameters": [ + { + "name": "agent_id", + "in": "query", + "required": true, + "schema": { "type": "string", "title": "Agent Id" } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { "application/json": { "schema": {} } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, + "/api/store/download/agents/{store_listing_version_id}": { + "get": { + "tags": ["v2", "store", "public"], + "summary": "Download agent file", + "description": "Download the agent file by streaming its content.\n\nArgs:\n store_listing_version_id (str): The ID of the agent to download\n\nReturns:\n StreamingResponse: A streaming response containing the agent's graph data.\n\nRaises:\n HTTPException: If the agent is not found or an unexpected error occurs.", + "operationId": "getV2Download agent file", + "parameters": [ + { + "name": "store_listing_version_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the agent to download", + "title": "Store Listing Version Id" + }, + "description": "The ID of the agent to download" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { "application/json": { "schema": {} } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, + "/api/builder/suggestions": { + "get": { + "tags": ["v2"], + "summary": "Get Builder suggestions", + "description": "Get all suggestions for the Blocks Menu.", + "operationId": "getV2Get builder suggestions", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SuggestionsResponse" } + } + } + } + } + } + }, + "/api/builder/categories": { + "get": { + "tags": ["v2"], + "summary": "Get Builder block categories", + "description": "Get all block categories with a specified number of blocks per category.", + "operationId": "getV2Get builder block categories", + "parameters": [ + { + "name": "blocks_per_category", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 3, + "title": "Blocks Per Category" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockCategoryResponse" + }, + "title": "Response Getv2Get Builder Block Categories" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, + "/api/builder/blocks": { + "get": { + "tags": ["v2"], + "summary": "Get Builder blocks", + "description": "Get blocks based on either category, type, or provider.", + "operationId": "getV2Get builder blocks", + "parameters": [ + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Category" + } + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": ["all", "input", "action", "output"], + "type": "string" + }, + { "type": "null" } + ], + "title": "Type" + } + }, + { + "name": "provider", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "description": "Provider name for integrations. Can be any string value, including custom provider names." + }, + { "type": "null" } + ], + "title": "Provider" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 1, "title": "Page" } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 50, "title": "Page Size" } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/BlockResponse" } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } + } + }, + "/api/builder/providers": { + "get": { + "tags": ["v2"], + "summary": "Get Builder integration providers", + "description": "Get all integration providers with their block counts.", + "operationId": "getV2Get builder integration providers", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 1, "title": "Page" } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 50, "title": "Page Size" } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProviderResponse" } + } + } }, "422": { "description": "Validation Error", @@ -2548,24 +3019,28 @@ } } }, - "/api/store/submissions/generate_image": { + "/api/builder/search": { "post": { "tags": ["v2", "store", "private"], - "summary": "Generate submission image", - "description": "Generate an image for a store listing submission.\n\nArgs:\n agent_id (str): ID of the agent to generate an image for\n user_id (str): ID of the authenticated user\n\nReturns:\n JSONResponse: JSON containing the URL of the generated image", - "operationId": "postV2Generate submission image", - "parameters": [ - { - "name": "agent_id", - "in": "query", - "required": true, - "schema": { "type": "string", "title": "Agent Id" } - } - ], + "summary": "Builder search", + "description": "Search for blocks (including integrations), marketplace agents, and user library agents.", + "operationId": "postV2Builder search", + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SearchRequest" } + } + }, + "required": true + }, "responses": { "200": { "description": "Successful Response", - "content": { "application/json": { "schema": {} } } + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SearchResponse" } + } + } }, "422": { "description": "Validation Error", @@ -2578,35 +3053,18 @@ } } }, - "/api/store/download/agents/{store_listing_version_id}": { + "/api/builder/counts": { "get": { - "tags": ["v2", "store", "public"], - "summary": "Download agent file", - "description": "Download the agent file by streaming its content.\n\nArgs:\n store_listing_version_id (str): The ID of the agent to download\n\nReturns:\n StreamingResponse: A streaming response containing the agent's graph data.\n\nRaises:\n HTTPException: If the agent is not found or an unexpected error occurs.", - "operationId": "getV2Download agent file", - "parameters": [ - { - "name": "store_listing_version_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The ID of the agent to download", - "title": "Store Listing Version Id" - }, - "description": "The ID of the agent to download" - } - ], + "tags": ["v2"], + "summary": "Get Builder item counts", + "description": "Get item counts for the menu categories in the Blocks Menu.", + "operationId": "getV2Get builder item counts", "responses": { "200": { "description": "Successful Response", - "content": { "application/json": { "schema": {} } } - }, - "422": { - "description": "Validation Error", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + "schema": { "$ref": "#/components/schemas/CountResponse" } } } } @@ -3790,6 +4248,24 @@ "required": ["amount", "threshold"], "title": "AutoTopUpConfig" }, + "AyrshareSSOResponse": { + "properties": { + "sso_url": { + "type": "string", + "title": "Sso Url", + "description": "The SSO URL for Ayrshare integration" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "title": "Expires At", + "description": "ISO timestamp when the URL expires" + } + }, + "type": "object", + "required": ["sso_url", "expires_at"], + "title": "AyrshareSSOResponse" + }, "BaseGraph-Input": { "properties": { "id": { "type": "string", "title": "Id" }, @@ -3885,6 +4361,33 @@ ], "title": "BaseGraph" }, + "BlockCategoryResponse": { + "properties": { + "name": { "type": "string", "title": "Name" }, + "total_blocks": { "type": "integer", "title": "Total Blocks" }, + "blocks": { + "items": { "additionalProperties": true, "type": "object" }, + "type": "array", + "title": "Blocks" + } + }, + "type": "object", + "required": ["name", "total_blocks", "blocks"], + "title": "BlockCategoryResponse" + }, + "BlockResponse": { + "properties": { + "blocks": { + "items": { "additionalProperties": true, "type": "object" }, + "type": "array", + "title": "Blocks" + }, + "pagination": { "$ref": "#/components/schemas/Pagination" } + }, + "type": "object", + "required": ["blocks", "pagination"], + "title": "BlockResponse" + }, "Body_postV1Callback": { "properties": { "code": { @@ -3934,6 +4437,14 @@ "required": ["type", "data", "data_index"], "title": "Body_postV1LogRawAnalytics" }, + "Body_postV1Upload_file_to_cloud_storage": { + "properties": { + "file": { "type": "string", "format": "binary", "title": "File" } + }, + "type": "object", + "required": ["file"], + "title": "Body_postV1Upload file to cloud storage" + }, "Body_postV2Add_credits_to_user": { "properties": { "user_id": { "type": "string", "title": "User Id" }, @@ -3997,6 +4508,31 @@ "required": ["query", "conversation_history", "message_id"], "title": "ChatRequest" }, + "CountResponse": { + "properties": { + "all_blocks": { "type": "integer", "title": "All Blocks" }, + "input_blocks": { "type": "integer", "title": "Input Blocks" }, + "action_blocks": { "type": "integer", "title": "Action Blocks" }, + "output_blocks": { "type": "integer", "title": "Output Blocks" }, + "integrations": { "type": "integer", "title": "Integrations" }, + "marketplace_agents": { + "type": "integer", + "title": "Marketplace Agents" + }, + "my_agents": { "type": "integer", "title": "My Agents" } + }, + "type": "object", + "required": [ + "all_blocks", + "input_blocks", + "action_blocks", + "output_blocks", + "integrations", + "marketplace_agents", + "my_agents" + ], + "title": "CountResponse" + }, "CreateAPIKeyRequest": { "properties": { "name": { "type": "string", "title": "Name" }, @@ -4306,6 +4842,7 @@ }, "type": "object", "required": [ + "id", "user_id", "graph_id", "graph_version", @@ -4383,6 +4920,7 @@ }, "type": "object", "required": [ + "id", "user_id", "graph_id", "graph_version", @@ -4438,6 +4976,7 @@ }, "type": "object", "required": [ + "id", "user_id", "graph_id", "graph_version", @@ -4451,6 +4990,20 @@ ], "title": "GraphExecutionWithNodes" }, + "GraphExecutionsPaginated": { + "properties": { + "executions": { + "items": { "$ref": "#/components/schemas/GraphExecutionMeta" }, + "type": "array", + "title": "Executions" + }, + "pagination": { "$ref": "#/components/schemas/Pagination" } + }, + "type": "object", + "required": ["executions", "pagination"], + "title": "GraphExecutionsPaginated", + "description": "Response schema for paginated graph executions." + }, "GraphMeta": { "properties": { "id": { "type": "string", "title": "Id" }, @@ -4689,6 +5242,11 @@ "type": "object", "title": "Input Schema" }, + "output_schema": { + "additionalProperties": true, + "type": "object", + "title": "Output Schema" + }, "credentials_input_schema": { "anyOf": [ { "additionalProperties": true, "type": "object" }, @@ -4731,6 +5289,7 @@ "name", "description", "input_schema", + "output_schema", "credentials_input_schema", "has_external_trigger", "new_output", @@ -5278,7 +5837,9 @@ "WEEKLY_SUMMARY", "MONTHLY_SUMMARY", "REFUND_REQUEST", - "REFUND_PROCESSED" + "REFUND_PROCESSED", + "AGENT_APPROVED", + "AGENT_REJECTED" ], "title": "NotificationType" }, @@ -5735,6 +6296,23 @@ "required": ["name", "username", "description", "links"], "title": "ProfileDetails" }, + "Provider": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Provider name for integrations. Can be any string value, including custom provider names." + }, + "description": { "type": "string", "title": "Description" }, + "integration_count": { + "type": "integer", + "title": "Integration Count" + } + }, + "type": "object", + "required": ["name", "description", "integration_count"], + "title": "Provider" + }, "ProviderConstants": { "properties": { "PROVIDER_NAMES": { @@ -5774,6 +6352,19 @@ "title": "ProviderNamesResponse", "description": "Response containing list of all provider names." }, + "ProviderResponse": { + "properties": { + "providers": { + "items": { "$ref": "#/components/schemas/Provider" }, + "type": "array", + "title": "Providers" + }, + "pagination": { "$ref": "#/components/schemas/Pagination" } + }, + "type": "object", + "required": ["providers", "pagination"], + "title": "ProviderResponse" + }, "RefundRequest": { "properties": { "id": { "type": "string", "title": "Id" }, @@ -5860,6 +6451,86 @@ "required": ["name", "cron", "inputs"], "title": "ScheduleCreationRequest" }, + "SearchRequest": { + "properties": { + "search_query": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Search Query" + }, + "filter": { + "anyOf": [ + { + "items": { + "type": "string", + "enum": [ + "blocks", + "integrations", + "marketplace_agents", + "my_agents" + ] + }, + "type": "array" + }, + { "type": "null" } + ], + "title": "Filter" + }, + "by_creator": { + "anyOf": [ + { "items": { "type": "string" }, "type": "array" }, + { "type": "null" } + ], + "title": "By Creator" + }, + "search_id": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Search Id" + }, + "page": { + "anyOf": [{ "type": "integer" }, { "type": "null" }], + "title": "Page" + }, + "page_size": { + "anyOf": [{ "type": "integer" }, { "type": "null" }], + "title": "Page Size" + } + }, + "type": "object", + "title": "SearchRequest" + }, + "SearchResponse": { + "properties": { + "items": { + "items": { + "anyOf": [ + { "additionalProperties": true, "type": "object" }, + { "$ref": "#/components/schemas/LibraryAgent" }, + { "$ref": "#/components/schemas/StoreAgent" } + ] + }, + "type": "array", + "title": "Items" + }, + "total_items": { + "additionalProperties": { "type": "integer" }, + "propertyNames": { + "enum": [ + "blocks", + "integrations", + "marketplace_agents", + "my_agents" + ] + }, + "type": "object", + "title": "Total Items" + }, + "page": { "type": "integer", "title": "Page" }, + "more_pages": { "type": "boolean", "title": "More Pages" } + }, + "type": "object", + "required": ["items", "total_items", "page", "more_pages"], + "title": "SearchResponse" + }, "SetGraphActiveVersion": { "properties": { "active_graph_version": { @@ -5919,6 +6590,11 @@ "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Error", "description": "Error message if any" + }, + "activity_status": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Activity Status", + "description": "AI-generated summary of what the agent did" } }, "additionalProperties": true, @@ -6160,6 +6836,16 @@ "changes_summary": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Changes Summary" + }, + "video_url": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Video Url" + }, + "categories": { + "items": { "type": "string" }, + "type": "array", + "title": "Categories", + "default": [] } }, "type": "object", @@ -6178,6 +6864,40 @@ ], "title": "StoreSubmission" }, + "StoreSubmissionEditRequest": { + "properties": { + "name": { "type": "string", "title": "Name" }, + "sub_heading": { "type": "string", "title": "Sub Heading" }, + "video_url": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Video Url" + }, + "image_urls": { + "items": { "type": "string" }, + "type": "array", + "title": "Image Urls", + "default": [] + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "categories": { + "items": { "type": "string" }, + "type": "array", + "title": "Categories", + "default": [] + }, + "changes_summary": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Changes Summary" + } + }, + "type": "object", + "required": ["name", "sub_heading"], + "title": "StoreSubmissionEditRequest" + }, "StoreSubmissionRequest": { "properties": { "agent_id": { "type": "string", "title": "Agent Id" }, @@ -6239,6 +6959,657 @@ "enum": ["DRAFT", "PENDING", "APPROVED", "REJECTED"], "title": "SubmissionStatus" }, + "SuggestionsResponse": { + "properties": { + "otto_suggestions": { + "items": { "type": "string" }, + "type": "array", + "title": "Otto Suggestions" + }, + "recent_searches": { + "items": { "type": "string" }, + "type": "array", + "title": "Recent Searches" + }, + "providers": { + "items": { + "type": "string", + "description": "Provider name for integrations. Can be any string value, including custom provider names." + }, + "type": "array", + "title": "Providers" + }, + "top_blocks": { + "items": { "additionalProperties": true, "type": "object" }, + "type": "array", + "title": "Top Blocks" + } + }, + "type": "object", + "required": [ + "otto_suggestions", + "recent_searches", + "providers", + "top_blocks" + ], + "title": "SuggestionsResponse" + }, + "TimezoneResponse": { + "properties": { + "timezone": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Coyhaique", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" + ], + "minLength": 1 + }, + { "type": "string" } + ], + "title": "Timezone" + } + }, + "type": "object", + "required": ["timezone"], + "title": "TimezoneResponse" + }, "TransactionHistory": { "properties": { "transactions": { @@ -6348,6 +7719,635 @@ "required": ["permissions"], "title": "UpdatePermissionsRequest" }, + "UpdateTimezoneRequest": { + "properties": { + "timezone": { + "type": "string", + "enum": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Coyhaique", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" + ], + "minLength": 1, + "title": "Timezone" + } + }, + "type": "object", + "required": ["timezone"], + "title": "UpdateTimezoneRequest" + }, + "UploadFileResponse": { + "properties": { + "file_uri": { "type": "string", "title": "File Uri" }, + "file_name": { "type": "string", "title": "File Name" }, + "size": { "type": "integer", "title": "Size" }, + "content_type": { "type": "string", "title": "Content Type" }, + "expires_in_hours": { "type": "integer", "title": "Expires In Hours" } + }, + "type": "object", + "required": [ + "file_uri", + "file_name", + "size", + "content_type", + "expires_in_hours" + ], + "title": "UploadFileResponse" + }, "UserHistoryResponse": { "properties": { "history": { diff --git a/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts index d6965656e2aa..ad39185dbda5 100644 --- a/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts +++ b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts @@ -3,19 +3,12 @@ import { makeAuthenticatedFileUpload, makeAuthenticatedRequest, } from "@/lib/autogpt-server-api/helpers"; +import { getAgptServerBaseUrl } from "@/lib/env-config"; import { NextRequest, NextResponse } from "next/server"; -function getBackendBaseUrl() { - if (process.env.NEXT_PUBLIC_AGPT_SERVER_URL) { - return process.env.NEXT_PUBLIC_AGPT_SERVER_URL.replace("/api", ""); - } - - return "http://localhost:8006"; -} - function buildBackendUrl(path: string[], queryString: string): string { const backendPath = path.join("/"); - return `${getBackendBaseUrl()}/${backendPath}${queryString}`; + return `${getAgptServerBaseUrl()}/${backendPath}${queryString}`; } async function handleJsonRequest( @@ -105,8 +98,23 @@ function createResponse( } } -function createErrorResponse(error: unknown): NextResponse { - console.error("API proxy error:", error); +function createErrorResponse( + error: unknown, + path: string, + method: string, +): NextResponse { + if ( + error && + typeof error === "object" && + "status" in error && + [401, 403].includes(error.status as number) + ) { + // Log this since it indicates a potential frontend bug + console.warn( + `Authentication error in API proxy for ${method} ${path}:`, + "message" in error ? error.message : error, + ); + } // If it's our custom ApiError, preserve the original status and response if (error instanceof ApiError) { @@ -154,7 +162,6 @@ async function handler( const contentType = req.headers.get("Content-Type"); let responseBody: any; - const responseStatus: number = 200; const responseHeaders: Record = { "Content-Type": "application/json", }; @@ -173,9 +180,13 @@ async function handler( return createUnsupportedContentTypeResponse(contentType); } - return createResponse(responseBody, responseStatus, responseHeaders); + return createResponse(responseBody, 200, responseHeaders); } catch (error) { - return createErrorResponse(error); + return createErrorResponse( + error, + path.map((s) => `/${s}`).join(""), + method, + ); } } diff --git a/autogpt_platform/frontend/src/app/globals.css b/autogpt_platform/frontend/src/app/globals.css index 07761c480b69..c96e31ab6fb9 100644 --- a/autogpt_platform/frontend/src/app/globals.css +++ b/autogpt_platform/frontend/src/app/globals.css @@ -2,6 +2,8 @@ @tailwind components; @tailwind utilities; +@plugin 'tailwind-scrollbar'; + @layer base { :root { --background: 0 0% 98%; /* neutral-50#FAFAFA */ diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index b7ffc804a088..93649bc52d89 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -28,7 +28,7 @@ export default async function RootLayout({ > diff --git a/autogpt_platform/frontend/src/app/providers.tsx b/autogpt_platform/frontend/src/app/providers.tsx index 3db56ac25e56..ef14bb723c7b 100644 --- a/autogpt_platform/frontend/src/app/providers.tsx +++ b/autogpt_platform/frontend/src/app/providers.tsx @@ -1,31 +1,35 @@ "use client"; -import * as React from "react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import { ThemeProviderProps } from "next-themes"; -import { BackendAPIProvider } from "@/lib/autogpt-server-api/context"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import CredentialsProvider from "@/components/integrations/credentials-provider"; import { LaunchDarklyProvider } from "@/services/feature-flags/feature-flag-provider"; +import CredentialsProvider from "@/components/integrations/credentials-provider"; import OnboardingProvider from "@/components/onboarding/onboarding-provider"; -import { QueryClientProvider } from "@tanstack/react-query"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { BackendAPIProvider } from "@/lib/autogpt-server-api/context"; import { getQueryClient } from "@/lib/react-query/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { + ThemeProvider as NextThemesProvider, + ThemeProviderProps, +} from "next-themes"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; export function Providers({ children, ...props }: ThemeProviderProps) { const queryClient = getQueryClient(); return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ); } diff --git a/autogpt_platform/frontend/src/components/CustomNode.tsx b/autogpt_platform/frontend/src/components/CustomNode.tsx index bf5df316c7c5..ac6bba0bb487 100644 --- a/autogpt_platform/frontend/src/components/CustomNode.tsx +++ b/autogpt_platform/frontend/src/components/CustomNode.tsx @@ -31,7 +31,7 @@ import { parseKeys, setNestedProperty, } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/atoms/Button/Button"; import { Switch } from "@/components/ui/switch"; import { TextRenderer } from "@/components/ui/render"; import { history } from "./history"; @@ -54,8 +54,10 @@ import { CopyIcon, ExitIcon, } from "@radix-ui/react-icons"; - +import { Key } from "@phosphor-icons/react"; import useCredits from "@/hooks/useCredits"; +import { getV1GetAyrshareSsoUrl } from "@/app/api/__generated__/endpoints/integrations/integrations"; +import { toast } from "@/components/molecules/Toast/use-toast"; export type ConnectionData = Array<{ edge_id: string; @@ -112,6 +114,8 @@ export const CustomNode = React.memo( const flowContext = useContext(FlowContext); const api = useBackendAPI(); const { formatCredits } = useCredits(); + const [isLoading, setIsLoading] = useState(false); + let nodeFlowId = ""; if (data.uiType === BlockUIType.AGENT) { @@ -183,7 +187,19 @@ export const CustomNode = React.memo( useEffect(() => { isInitialSetup.current = false; - setHardcodedValues(fillDefaults(data.hardcodedValues, data.inputSchema)); + if (data.uiType === BlockUIType.AGENT) { + setHardcodedValues({ + ...data.hardcodedValues, + inputs: fillDefaults( + data.hardcodedValues.inputs ?? {}, + data.inputSchema, + ), + }); + } else { + setHardcodedValues( + fillDefaults(data.hardcodedValues, data.inputSchema), + ); + } }, []); const setErrors = useCallback( @@ -241,6 +257,59 @@ export const CustomNode = React.memo( return renderHandles(schema.properties); }; + const generateAyrshareSSOHandles = () => { + const handleSSOLogin = async () => { + setIsLoading(true); + try { + const { + data: { sso_url }, + } = await getV1GetAyrshareSsoUrl(); + const popup = window.open(sso_url, "_blank", "popup=true"); + if (!popup) { + throw new Error( + "Please allow popups for this site to be able to login with Ayrshare", + ); + } + } catch (error) { + toast({ + title: "Error", + description: `Error getting SSO URL: ${error}`, + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + +
+ ); + }; + const generateInputHandles = ( schema: BlockIORootSchema, nodeType: BlockUIType, @@ -827,8 +896,18 @@ export const CustomNode = React.memo( (A Webhook URL will be generated when you save the agent)

))} - {data.inputSchema && - generateInputHandles(data.inputSchema, data.uiType)} + {data.uiType === BlockUIType.AYRSHARE ? ( + <> + {generateAyrshareSSOHandles()} + {generateInputHandles( + data.inputSchema, + BlockUIType.STANDARD, + )} + + ) : ( + data.inputSchema && + generateInputHandles(data.inputSchema, data.uiType) + )}
) : ( diff --git a/autogpt_platform/frontend/src/components/Flow.tsx b/autogpt_platform/frontend/src/components/Flow.tsx index f5fbc91b0178..1cf7dcc4b9a4 100644 --- a/autogpt_platform/frontend/src/components/Flow.tsx +++ b/autogpt_platform/frontend/src/components/Flow.tsx @@ -36,6 +36,7 @@ import { LibraryAgent, } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import { Key, storage } from "@/services/storage/local-storage"; import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils"; import { history } from "./history"; import { CustomEdge } from "./CustomEdge"; @@ -56,6 +57,8 @@ import PrimaryActionBar from "@/components/PrimaryActionButton"; import OttoChatWidget from "@/components/OttoChatWidget"; import { useToast } from "@/components/molecules/Toast/use-toast"; import { useCopyPaste } from "../hooks/useCopyPaste"; +import NewControlPanel from "@/app/(platform)/build/components/NewBlockMenu/NewControlPanel/NewControlPanel"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; // This is for the history, this is the minimum distance a block must move before it is logged // It helps to prevent spamming the history with small movements especially when pressing on a input in a block @@ -98,6 +101,11 @@ const FlowEditor: React.FC<{ const [flowExecutionID, setFlowExecutionID] = useState< GraphExecutionID | undefined >(); + // State to control if blocks menu should be pinned open + const [pinBlocksPopover, setPinBlocksPopover] = useState(false); + // State to control if save popover should be pinned open + const [pinSavePopover, setPinSavePopover] = useState(false); + const { agentName, setAgentName, @@ -115,6 +123,7 @@ const FlowEditor: React.FC<{ isRunning, isStopping, isScheduling, + graphExecutionError, nodes, setNodes, edges, @@ -148,17 +157,10 @@ const FlowEditor: React.FC<{ }>({}); const isDragging = useRef(false); - // State to control if blocks menu should be pinned open - const [pinBlocksPopover, setPinBlocksPopover] = useState(false); - // State to control if save popover should be pinned open - const [pinSavePopover, setPinSavePopover] = useState(false); - const runnerUIRef = useRef(null); const { toast } = useToast(); - const TUTORIAL_STORAGE_KEY = "shepherd-tour"; - // It stores the dimension of all nodes with position as well const [nodeDimensions, setNodeDimensions] = useState({}); @@ -181,13 +183,13 @@ const FlowEditor: React.FC<{ useEffect(() => { if (params.get("resetTutorial") === "true") { - localStorage.removeItem(TUTORIAL_STORAGE_KEY); + storage.clean(Key.SHEPHERD_TOUR); router.push(pathname); - } else if (!localStorage.getItem(TUTORIAL_STORAGE_KEY)) { + } else if (!storage.get(Key.SHEPHERD_TOUR)) { const emptyNodes = (forceRemove: boolean = false) => forceRemove ? (setNodes([]), setEdges([]), true) : nodes.length === 0; startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover); - localStorage.setItem(TUTORIAL_STORAGE_KEY, "yes"); + storage.set(Key.SHEPHERD_TOUR, "yes"); } }, [router, pathname, params, setEdges, setNodes, nodes.length]); @@ -674,6 +676,8 @@ const FlowEditor: React.FC<{ runnerUIRef.current?.openRunInputDialog(); }, [isScheduling, savedAgent, toast, saveAgent]); + const isNewBlockEnabled = useGetFlag(Flag.NEW_BLOCK_MENU); + return ( - - } - botChildren={ - - } - /> + {isNewBlockEnabled ? ( + + ) : ( + + } + botChildren={ + + } + /> + )} + {!graphHasWebhookNodes ? ( diff --git a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx index e5a07d1d5a99..1d2134321b72 100644 --- a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx +++ b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx @@ -59,7 +59,8 @@ const PrimaryActionBar: React.FC = ({ onClick={onClickRunAgent} disabled={!onClickRunAgent} title="Run the agent" - data-id="primary-action-run-agent" + aria-label="Run the agent" + data-testid="primary-action-run-agent" > Run diff --git a/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx b/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx index deae14d0d6e1..9370c1c21112 100644 --- a/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx +++ b/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx @@ -19,6 +19,7 @@ import RunnerOutputUI, { interface RunnerUIWrapperProps { graph: GraphMeta; nodes: Node[]; + graphExecutionError?: string | null; saveAndRun: ( inputs: Record, credentialsInputs: Record, @@ -38,7 +39,10 @@ export interface RunnerUIWrapperRef { } const RunnerUIWrapper = forwardRef( - ({ graph, nodes, saveAndRun, createRunSchedule }, ref) => { + ( + { graph, nodes, graphExecutionError, saveAndRun, createRunSchedule }, + ref, + ) => { const [isRunInputDialogOpen, setIsRunInputDialogOpen] = useState(false); const [isRunnerOutputOpen, setIsRunnerOutputOpen] = useState(false); @@ -103,6 +107,7 @@ const RunnerUIWrapper = forwardRef( isOpen={isRunnerOutputOpen} doClose={() => setIsRunnerOutputOpen(false)} outputs={graphOutputs} + graphExecutionError={graphExecutionError} /> ); diff --git a/autogpt_platform/frontend/src/components/admin/marketplace/approve-reject-buttons.tsx b/autogpt_platform/frontend/src/components/admin/marketplace/approve-reject-buttons.tsx index b0e6b1d9d654..a2ac8deb942d 100644 --- a/autogpt_platform/frontend/src/components/admin/marketplace/approve-reject-buttons.tsx +++ b/autogpt_platform/frontend/src/components/admin/marketplace/approve-reject-buttons.tsx @@ -29,6 +29,8 @@ export function ApproveRejectButtons({ const [isApproveDialogOpen, setIsApproveDialogOpen] = useState(false); const [isRejectDialogOpen, setIsRejectDialogOpen] = useState(false); + const isApproved = version.status === "APPROVED"; + const handleApproveSubmit = async (formData: FormData) => { setIsApproveDialogOpen(false); try { @@ -51,18 +53,20 @@ export function ApproveRejectButtons({ return ( <> - + {!isApproved && ( + + )} {/* Approve Dialog */} @@ -124,9 +128,13 @@ export function ApproveRejectButtons({ - Reject Agent + + {isApproved ? "Revoke Approved Agent" : "Reject Agent"} + - Please provide feedback on why this agent is being rejected. + {isApproved + ? "Are you sure you want to revoke approval for this agent? This will remove it from the marketplace." + : "Please provide feedback on why this agent is being rejected."} @@ -167,7 +175,7 @@ export function ApproveRejectButtons({ Cancel diff --git a/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx b/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx index 569bdc7eaf6f..e1f936da5b6c 100644 --- a/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx +++ b/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx @@ -83,7 +83,8 @@ export function ExpandableRow({ /> )} - {latestVersion?.status === SubmissionStatus.PENDING && ( + {(latestVersion?.status === SubmissionStatus.PENDING || + latestVersion?.status === SubmissionStatus.APPROVED) && ( )}
@@ -188,7 +189,8 @@ export function ExpandableRow({ } /> )} - {version.status === SubmissionStatus.PENDING && ( + {(version.status === SubmissionStatus.PENDING || + version.status === SubmissionStatus.APPROVED) && ( )}
diff --git a/autogpt_platform/frontend/src/components/admin/spending/add-money-button.tsx b/autogpt_platform/frontend/src/components/admin/spending/add-money-button.tsx index ee84464df08f..64b5860d2792 100644 --- a/autogpt_platform/frontend/src/components/admin/spending/add-money-button.tsx +++ b/autogpt_platform/frontend/src/components/admin/spending/add-money-button.tsx @@ -15,6 +15,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Input } from "@/components/ui/input"; import { useRouter } from "next/navigation"; import { addDollars } from "@/app/(platform)/admin/spending/actions"; +import { useToast } from "@/components/molecules/Toast/use-toast"; export function AdminAddMoneyButton({ userId, @@ -30,18 +31,32 @@ export function AdminAddMoneyButton({ defaultComments?: string; }) { const router = useRouter(); + const { toast } = useToast(); const [isAddMoneyDialogOpen, setIsAddMoneyDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const [dollarAmount, setDollarAmount] = useState( defaultAmount ? Math.abs(defaultAmount / 100).toFixed(2) : "1.00", ); const handleApproveSubmit = async (formData: FormData) => { - setIsAddMoneyDialogOpen(false); + setIsSubmitting(true); try { await addDollars(formData); + setIsAddMoneyDialogOpen(false); + toast({ + title: "Success", + description: `Added $${dollarAmount} to ${userEmail}'s balance`, + }); router.refresh(); // Refresh the current route } catch (error) { console.error("Error adding dollars:", error); + toast({ + title: "Error", + description: "Failed to add dollars. Please try again.", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); } }; @@ -122,10 +137,13 @@ export function AdminAddMoneyButton({ type="button" variant="outline" onClick={() => setIsAddMoneyDialogOpen(false)} + disabled={isSubmitting} > Cancel - + diff --git a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx b/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx deleted file mode 100644 index 7d6dc483828f..000000000000 --- a/autogpt_platform/frontend/src/components/agents/agent-runs-selector-list.tsx +++ /dev/null @@ -1,190 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { Plus } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { - GraphExecutionID, - GraphExecutionMeta, - LibraryAgent, - LibraryAgentPreset, - LibraryAgentPresetID, - Schedule, - ScheduleID, -} from "@/lib/autogpt-server-api"; - -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Button } from "@/components/agptui/Button"; -import { Badge } from "@/components/ui/badge"; - -import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip"; -import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card"; - -interface AgentRunsSelectorListProps { - agent: LibraryAgent; - agentRuns: GraphExecutionMeta[]; - agentPresets: LibraryAgentPreset[]; - schedules: Schedule[]; - selectedView: { type: "run" | "preset" | "schedule"; id?: string }; - allowDraftNewRun?: boolean; - onSelectRun: (id: GraphExecutionID) => void; - onSelectPreset: (preset: LibraryAgentPresetID) => void; - onSelectSchedule: (id: ScheduleID) => void; - onSelectDraftNewRun: () => void; - doDeleteRun: (id: GraphExecutionMeta) => void; - doDeletePreset: (id: LibraryAgentPresetID) => void; - doDeleteSchedule: (id: ScheduleID) => void; - className?: string; -} - -export default function AgentRunsSelectorList({ - agent, - agentRuns, - agentPresets, - schedules, - selectedView, - allowDraftNewRun = true, - onSelectRun, - onSelectPreset, - onSelectSchedule, - onSelectDraftNewRun, - doDeleteRun, - doDeletePreset, - doDeleteSchedule, - className, -}: AgentRunsSelectorListProps): React.ReactElement { - const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">( - "runs", - ); - - useEffect(() => { - if (selectedView.type === "schedule") { - setActiveListTab("scheduled"); - } else { - setActiveListTab("runs"); - } - }, [selectedView]); - - const listItemClasses = "h-28 w-72 lg:h-32 xl:w-80"; - - return ( - - ); -} diff --git a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx index 48b29c96b566..2b680649e013 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx @@ -127,7 +127,10 @@ export const AgentInfo: FC = ({ return (
{/* Title */} -
+
{name}
@@ -137,6 +140,7 @@ export const AgentInfo: FC = ({ by
@@ -157,6 +161,7 @@ export const AgentInfo: FC = ({ "inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3", "transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400", )} + data-testid={"agent-add-library-button"} onClick={libraryAction} disabled={adding} > @@ -170,6 +175,7 @@ export const AgentInfo: FC = ({ "inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3", "transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40", )} + data-testid={"agent-download-button"} onClick={handleDownload} disabled={downloading} > @@ -200,7 +206,10 @@ export const AgentInfo: FC = ({
Description
-
+
{longDescription}
diff --git a/autogpt_platform/frontend/src/components/agptui/BecomeACreator.tsx b/autogpt_platform/frontend/src/components/agptui/BecomeACreator.tsx index cea48b63d7a4..954ac1dd33db 100644 --- a/autogpt_platform/frontend/src/components/agptui/BecomeACreator.tsx +++ b/autogpt_platform/frontend/src/components/agptui/BecomeACreator.tsx @@ -1,24 +1,19 @@ "use client"; import * as React from "react"; -import { PublishAgentPopout } from "./composite/PublishAgentPopout"; +import { PublishAgentModal } from "../contextual/PublishAgentModal/PublishAgentModal"; + interface BecomeACreatorProps { title?: string; description?: string; buttonText?: string; - onButtonClick?: () => void; } -export const BecomeACreator: React.FC = ({ +export function BecomeACreator({ title = "Become a creator", description = "Join a community where your AI creations can inspire, engage, and be downloaded by users around the world.", buttonText = "Upload your agent", - onButtonClick, -}) => { - const handleButtonClick = () => { - onButtonClick?.(); - }; - +}: BecomeACreatorProps) { return (
{/* Title */} @@ -41,12 +36,9 @@ export const BecomeACreator: React.FC = ({ {description}

- +
); -}; +} diff --git a/autogpt_platform/frontend/src/components/agptui/BreadCrumbs.tsx b/autogpt_platform/frontend/src/components/agptui/BreadCrumbs.tsx deleted file mode 100644 index 069f7b3e3acb..000000000000 --- a/autogpt_platform/frontend/src/components/agptui/BreadCrumbs.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from "react"; -import Link from "next/link"; - -interface BreadcrumbItem { - name: string; - link: string; -} - -interface BreadCrumbsProps { - items: BreadcrumbItem[]; -} - -export const BreadCrumbs: React.FC = ({ items }) => { - return ( -
- {/* - Commented out for now, but keeping until we have approval to remove - - */} -
- {items.map((item, index) => ( - - - - {item.name} - - - {index < items.length - 1 && ( - - / - - )} - - ))} -
-
- ); -}; diff --git a/autogpt_platform/frontend/src/components/agptui/CreatorInfoCard.tsx b/autogpt_platform/frontend/src/components/agptui/CreatorInfoCard.tsx index 5c3b9b1d01e3..1a89ef64eed3 100644 --- a/autogpt_platform/frontend/src/components/agptui/CreatorInfoCard.tsx +++ b/autogpt_platform/frontend/src/components/agptui/CreatorInfoCard.tsx @@ -41,7 +41,10 @@ export const CreatorInfoCard: React.FC = ({
-
+
{username}
diff --git a/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx b/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx index c27035b62265..38c75a02be31 100644 --- a/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx +++ b/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx @@ -56,7 +56,10 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) { return (
-

+

Profile

@@ -92,13 +95,18 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
-