From 5d0faab4b1386286f29d6fd0527b2f157fd6464e Mon Sep 17 00:00:00 2001 From: Swifty Date: Thu, 5 Jun 2025 15:13:04 +0200 Subject: [PATCH 01/29] Delete .github/workflows/platform-autogpt-deploy-dev.yaml (#10118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: - [ ] ...
Example test plan - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly
#### For configuration changes: - [ ] `.env.example` 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**)
Examples of configuration changes - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases
--- .../platform-autogpt-deploy-dev.yaml | 282 ------------------ 1 file changed, 282 deletions(-) delete mode 100644 .github/workflows/platform-autogpt-deploy-dev.yaml diff --git a/.github/workflows/platform-autogpt-deploy-dev.yaml b/.github/workflows/platform-autogpt-deploy-dev.yaml deleted file mode 100644 index 4ce86add3d..0000000000 --- a/.github/workflows/platform-autogpt-deploy-dev.yaml +++ /dev/null @@ -1,282 +0,0 @@ -name: AutoGPT Platform - Dev Deploy PR Event Dispatcher - -on: - pull_request: - types: [closed] - issue_comment: - types: [created] - -permissions: - issues: write - pull-requests: write - -jobs: - dispatch: - runs-on: ubuntu-latest - steps: - - name: Check comment permissions and deployment status - id: check_status - if: github.event_name == 'issue_comment' && github.event.issue.pull_request - uses: actions/github-script@v7 - with: - script: | - const commentBody = context.payload.comment.body.trim(); - const commentUser = context.payload.comment.user.login; - const prAuthor = context.payload.issue.user.login; - const authorAssociation = context.payload.comment.author_association; - const triggeringCommentId = context.payload.comment.id; - - // Check permissions - const hasPermission = ( - authorAssociation === 'OWNER' || - authorAssociation === 'MEMBER' || - authorAssociation === 'COLLABORATOR' - ); - - core.setOutput('comment_body', commentBody); - core.setOutput('has_permission', hasPermission); - - if (!hasPermission && (commentBody === '!deploy' || commentBody === '!undeploy')) { - core.setOutput('permission_denied', 'true'); - return; - } - - if (commentBody !== '!deploy' && commentBody !== '!undeploy') { - return; - } - - // Get all comments to check deployment status - const commentsResponse = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100 - }); - - // Filter out the triggering comment - const commentsData = commentsResponse.data.filter(comment => comment.id !== triggeringCommentId); - - // Find the last deploy and undeploy commands - let lastDeployIndex = -2; - let lastUndeployIndex = -1; - - console.log(`Found ${commentsResponse.data.length} total comments, using ${commentsData.length} for status check after filtering`); - - // Iterate through comments in reverse to find the most recent commands - for (let i = commentsData.length - 1; i >= 0; i--) { - const currentCommentBody = commentsData[i].body.trim(); - console.log(`Processing comment ${i}: ${currentCommentBody}`); - - if (currentCommentBody === '!deploy' && lastDeployIndex === -2) { - lastDeployIndex = i; - } else if (currentCommentBody === '!undeploy' && lastUndeployIndex === -1) { - lastUndeployIndex = i; - } - - // Break early if we found both - if (lastDeployIndex !== -2 && lastUndeployIndex !== -1) { - break; - } - } - - console.log(`Last deploy index: ${lastDeployIndex}`); - console.log(`Last undeploy index: ${lastUndeployIndex}`); - - // Currently deployed if there's a deploy command after the last undeploy - const isCurrentlyDeployed = lastDeployIndex > lastUndeployIndex; - - // Determine actions based on current state and requested command - if (commentBody === '!deploy') { - if (isCurrentlyDeployed) { - core.setOutput('deploy_blocked', 'already_deployed'); - } else { - core.setOutput('should_deploy', 'true'); - } - } else if (commentBody === '!undeploy') { - if (!isCurrentlyDeployed) { - // Check if there was ever a deploy - const hasEverDeployed = lastDeployIndex !== -2; - core.setOutput('undeploy_blocked', hasEverDeployed ? 'already_undeployed' : 'never_deployed'); - } else { - core.setOutput('should_undeploy', 'true'); - } - } - - core.setOutput('has_active_deployment', isCurrentlyDeployed); - - - name: Post permission denied comment - if: steps.check_status.outputs.permission_denied == 'true' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `❌ **Permission denied**: Only the repository owners, members, or collaborators can use deployment commands.` - }); - - - name: Post deploy blocked comment - if: steps.check_status.outputs.deploy_blocked == 'already_deployed' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `⚠️ **Deploy skipped**: This PR already has an active deployment. Use \`!undeploy\` first if you want to redeploy.` - }); - - - name: Post undeploy blocked comment - if: steps.check_status.outputs.undeploy_blocked != '' - uses: actions/github-script@v7 - with: - script: | - const reason = '${{ steps.check_status.outputs.undeploy_blocked }}'; - let message; - - if (reason === 'never_deployed') { - message = `⚠️ **Undeploy skipped**: This PR has never been deployed. Use \`!deploy\` first.`; - } else if (reason === 'already_undeployed') { - message = `⚠️ **Undeploy skipped**: This PR is already undeployed.`; - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: message - }); - - - name: Get PR details for deployment - id: pr_details - if: steps.check_status.outputs.should_deploy == 'true' || steps.check_status.outputs.should_undeploy == 'true' - uses: actions/github-script@v7 - with: - script: | - const pr = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number - }); - core.setOutput('pr_number', pr.data.number); - core.setOutput('pr_title', pr.data.title); - core.setOutput('pr_state', pr.data.state); - - - name: Dispatch Deploy Event - if: steps.check_status.outputs.should_deploy == 'true' - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.DISPATCH_TOKEN }} - repository: Significant-Gravitas/AutoGPT_cloud_infrastructure - event-type: pr-event - client-payload: | - { - "action": "deploy", - "pr_number": "${{ steps.pr_details.outputs.pr_number }}", - "pr_title": "${{ steps.pr_details.outputs.pr_title }}", - "pr_state": "${{ steps.pr_details.outputs.pr_state }}", - "repo": "${{ github.repository }}" - } - - - name: Post deploy success comment - if: steps.check_status.outputs.should_deploy == 'true' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `🚀 **Deploying PR #${{ steps.pr_details.outputs.pr_number }}** to development environment...` - }); - - - name: Dispatch Undeploy Event (from comment) - if: steps.check_status.outputs.should_undeploy == 'true' - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.DISPATCH_TOKEN }} - repository: Significant-Gravitas/AutoGPT_cloud_infrastructure - event-type: pr-event - client-payload: | - { - "action": "undeploy", - "pr_number": "${{ steps.pr_details.outputs.pr_number }}", - "pr_title": "${{ steps.pr_details.outputs.pr_title }}", - "pr_state": "${{ steps.pr_details.outputs.pr_state }}", - "repo": "${{ github.repository }}" - } - - - name: Post undeploy success comment - if: steps.check_status.outputs.should_undeploy == 'true' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `🗑️ **Undeploying PR #${{ steps.pr_details.outputs.pr_number }}** from development environment...` - }); - - - name: Check deployment status on PR close - id: check_pr_close - if: github.event_name == 'pull_request' && github.event.action == 'closed' - uses: actions/github-script@v7 - with: - script: | - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - - let lastDeployIndex = -1; - let lastUndeployIndex = -1; - - comments.data.forEach((comment, index) => { - if (comment.body.trim() === '!deploy') { - lastDeployIndex = index; - } else if (comment.body.trim() === '!undeploy') { - lastUndeployIndex = index; - } - }); - - // Should undeploy if there's a !deploy without a subsequent !undeploy - const shouldUndeploy = lastDeployIndex !== -1 && lastDeployIndex > lastUndeployIndex; - core.setOutput('should_undeploy', shouldUndeploy); - - - name: Dispatch Undeploy Event (PR closed with active deployment) - if: >- - github.event_name == 'pull_request' && - github.event.action == 'closed' && - steps.check_pr_close.outputs.should_undeploy == 'true' - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.DISPATCH_TOKEN }} - repository: Significant-Gravitas/AutoGPT_cloud_infrastructure - event-type: pr-event - client-payload: | - { - "action": "undeploy", - "pr_number": "${{ github.event.pull_request.number }}", - "pr_title": "${{ github.event.pull_request.title }}", - "pr_state": "${{ github.event.pull_request.state }}", - "repo": "${{ github.repository }}" - } - - - name: Post PR close undeploy comment - if: >- - github.event_name == 'pull_request' && - github.event.action == 'closed' && - steps.check_pr_close.outputs.should_undeploy == 'true' - uses: actions/github-script@v7 - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `🧹 **Auto-undeploying**: PR closed with active deployment. Cleaning up development environment for PR #${{ github.event.pull_request.number }}.` - }); \ No newline at end of file From 3a20c5a4bb12b3828d31e7eed75d81775e682c0a Mon Sep 17 00:00:00 2001 From: Swifty Date: Thu, 5 Jun 2025 19:28:48 +0200 Subject: [PATCH 02/29] restore dev deploy (#10122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Changes 🏗️ ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: - [ ] ...
Example test plan - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly
#### For configuration changes: - [ ] `.env.example` 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**)
Examples of configuration changes - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases
--- .../platform-autogpt-deploy-dev.yaml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/platform-autogpt-deploy-dev.yaml diff --git a/.github/workflows/platform-autogpt-deploy-dev.yaml b/.github/workflows/platform-autogpt-deploy-dev.yaml new file mode 100644 index 0000000000..44da8641ec --- /dev/null +++ b/.github/workflows/platform-autogpt-deploy-dev.yaml @@ -0,0 +1,51 @@ +name: AutoGPT Platform - Deploy Dev Environment + +on: + push: + branches: [ dev ] + paths: + - 'autogpt_platform/**' + +permissions: + contents: 'read' + id-token: 'write' + +jobs: + migrate: + environment: develop + name: Run migrations for AutoGPT Platform + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install prisma + + - name: Run Backend Migrations + working-directory: ./autogpt_platform/backend + run: | + python -m prisma migrate deploy + env: + DATABASE_URL: ${{ secrets.BACKEND_DATABASE_URL }} + DIRECT_URL: ${{ secrets.BACKEND_DATABASE_URL }} + + trigger: + needs: migrate + runs-on: ubuntu-latest + steps: + - name: Trigger deploy workflow + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.DEPLOY_TOKEN }} + repository: Significant-Gravitas/AutoGPT_cloud_infrastructure + event-type: build_deploy_dev + client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "repository": "${{ github.repository }}"}' From 1ff924e26039e090b6c98650ef17de0a19725908 Mon Sep 17 00:00:00 2001 From: Toran Bruce Richards Date: Mon, 16 Jun 2025 09:00:16 +0100 Subject: [PATCH 03/29] Fix(frontend): Update StoreCard component to use `bg-background` instead of hardcoded `bg-white` (#9963) Fixes #9868 This pull request updates the `StoreCard` component in `autogpt_platform/frontend/src/components/agptui/StoreCard.tsx` to replace the hardcoded Tailwind CSS class `bg-white` with the more flexible `bg-background` utility class. This change ensures better consistency with the application's theming and makes it easier to adapt to different color schemes, such as light and dark modes. #### Changes: - **Before:** `className="... bg-white ... dark:bg-transparent ..."` ![image](https://github.com/user-attachments/assets/9eb2b595-8712-405b-ba7d-babd2361e344) - **After:** `className="... bg-background ... dark:bg-transparent ..."` ![image](https://github.com/user-attachments/assets/58affa1b-7160-4961-b9f2-5fdc15c2439e) #### Motivation: - Removes the white background on the cards, which weren't part of the designs. No functional or visual changes are expected except for improved support for custom themes. --- This PR was entirely generated by an AI Agent. **Please review and let me know if additional changes are needed!** Co-authored-by: itsababseh <36419647+itsababseh@users.noreply.github.com> --- autogpt_platform/frontend/src/components/agptui/StoreCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/components/agptui/StoreCard.tsx b/autogpt_platform/frontend/src/components/agptui/StoreCard.tsx index 57919dc2eb..bc024713f9 100644 --- a/autogpt_platform/frontend/src/components/agptui/StoreCard.tsx +++ b/autogpt_platform/frontend/src/components/agptui/StoreCard.tsx @@ -32,7 +32,7 @@ export const StoreCard: React.FC = ({ return (
Date: Mon, 16 Jun 2025 09:01:10 +0100 Subject: [PATCH 04/29] fix(platform/backend): skip invalid graphs when listing in block menu (#10159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Background & Summary of Changes If a user has a single invalid Agent in their Library (i.e one with a Block which doesn't exist) currently the Blocks menu does not return any Agent results. Valid agents should still load even when some stored graphs are malformed. Graphs which are malformed should just be skipped rather than breaking the entire process, this PR implements that fix, unblocking users with a malformed Agent in their Library (me!). ## Testing I have tested this PR in the dev deployment (where I have this issue on my account) and have confirmed that Agents now show up in the list: | Before this Change | After this Change | | ------------------ | ----------------- | | ![Before change screenshot](https://github.com/user-attachments/assets/9263da25-ff4a-4dfa-bd96-19dfd689ddac) | ![After change screenshot](https://github.com/user-attachments/assets/86219055-b97b-456c-a270-80d729c909da) | ## Changes 🏗️ - Validate each graph’s serialization in get_graphs and skip any that raise an exception - Added error logging for invalid graphs ## Checklist 📋 For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] poetry run format - [ ] poetry run test For configuration changes: - [x] .env.example is updated or already compatible with my changes - [x] docker-compose.yml is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under Changes) Fixes [OPEN-2461: Loading a Library Agent with an invalid block causes all Library Agent Loading to fail in Builder Blocks Menu](https://linear.app/autogpt/issue/OPEN-2461/loading-a-library-agent-with-an-invalid-block-causes-all-library-agent) --- autogpt_platform/backend/backend/data/graph.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index c556ba0450..7733ec3518 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -655,7 +655,10 @@ async def get_graphs( graph_models = [] for graph in graphs: try: - graph_models.append(GraphModel.from_db(graph)) + graph_model = GraphModel.from_db(graph) + # Trigger serialization to validate that the graph is well formed. + graph_model.model_dump() + graph_models.append(graph_model) except Exception as e: logger.error(f"Error processing graph {graph.id}: {e}") continue From f950f35af8960bb3fe94a81434c984e3acd0aeb3 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Mon, 16 Jun 2025 10:02:12 -0500 Subject: [PATCH 05/29] [Snyk] Security upgrade requests from 2.31.0 to 2.32.4 (#10148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![snyk-top-banner](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg) ### Snyk has created this PR to fix 1 vulnerabilities in the pip dependencies of this project. #### Snyk changed the following file(s): - `docs/requirements.txt`
⚠️ Warning ``` mkdocs-material 9.2.8 requires requests, which is not installed. mkdocs-material 9.2.8 has requirement pymdown-extensions~=10.3, but you have pymdown-extensions 10.2.1. ```
--- > [!IMPORTANT] > > - Check the changes in this PR to ensure they won't cause issues with your project. > - Max score is 1000. Note that the real score may have changed since the PR was raised. > - This PR was automatically created by Snyk using the credentials of a real user. > - Some vulnerabilities couldn't be fully fixed and so Snyk will still find them when the project is tested again. This may be because the vulnerability existed within more than one direct dependency, but not all of the affected dependencies could be upgraded. --- **Note:** _You are seeing this because you or someone else with access to this repository has authorized Snyk to open fix PRs._ For more information: 🧐 [View latest project report](https://app.snyk.io/org/significant-gravitas/project/7c1b6d4c-2625-44c8-8403-42505b3997f8?utm_source=github&utm_medium=referral&page=fix-pr) 📜 [Customise PR templates](https://docs.snyk.io/scan-using-snyk/pull-requests/snyk-fix-pull-or-merge-requests/customize-pr-templates?utm_source=github&utm_content=fix-pr-template) 🛠 [Adjust project settings](https://app.snyk.io/org/significant-gravitas/project/7c1b6d4c-2625-44c8-8403-42505b3997f8?utm_source=github&utm_medium=referral&page=fix-pr/settings) 📚 [Read about Snyk's upgrade logic](https://docs.snyk.io/scan-with-snyk/snyk-open-source/manage-vulnerabilities/upgrade-package-versions-to-fix-vulnerabilities?utm_source=github&utm_content=fix-pr-template) --- **Learn how to fix vulnerabilities with free interactive lessons:** 🦉 [Learn about vulnerability in an interactive lesson of Snyk Learn.](https://learn.snyk.io/?loc=fix-pr) [//]: # 'snyk:metadata:{"customTemplate":{"variablesUsed":[],"fieldsUsed":[]},"dependencies":[{"name":"requests","from":"2.31.0","to":"2.32.4"}],"env":"prod","issuesToFix":["SNYK-PYTHON-REQUESTS-10305723"],"prId":"1b1e0485-c36e-4cb8-a03c-422057cc5b21","prPublicId":"1b1e0485-c36e-4cb8-a03c-422057cc5b21","packageManager":"pip","priorityScoreList":[678],"projectPublicId":"7c1b6d4c-2625-44c8-8403-42505b3997f8","projectUrl":"https://app.snyk.io/org/significant-gravitas/project/7c1b6d4c-2625-44c8-8403-42505b3997f8?utm_source=github&utm_medium=referral&page=fix-pr","prType":"fix","templateFieldSources":{"branchName":"default","commitMessage":"default","description":"default","title":"default"},"templateVariants":["updated-fix-title","pr-warning-shown","priorityScore"],"type":"auto","upgrade":[],"vulns":["SNYK-PYTHON-REQUESTS-10305723"],"patch":[],"isBreakingChange":false,"remediationStrategy":"vuln"}' --------- Co-authored-by: Swifty Co-authored-by: snyk-bot --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6075801360..ea0eab8c2a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,3 +5,4 @@ pymdown-extensions mkdocs-git-revision-date-localized-plugin zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability +requests>=2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability From 81d3eb7c346a91bb6098e236d10df6cc06a945b7 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Mon, 16 Jun 2025 10:22:08 -0500 Subject: [PATCH 06/29] feat(backend, frontend): make changes to use our security modules more effectively (#10123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doing the CASA Audit and this is something to check ### Changes 🏗️ - limits APIs to use their specific endpoints - use expected trusted sources for each block and requests call - Use cryptographically valid string comparisons - Don't log secrets ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Testing in dev branch once merged --------- Co-authored-by: Swifty --- .gitignore | 1 + .../autogpt_libs/api_key/key_manager.py | 3 +- .../autogpt_libs/auth/middleware.py | 7 +- autogpt_platform/autogpt_libs/poetry.lock | 149 +++++++++++++++++- autogpt_platform/autogpt_libs/pyproject.toml | 3 + .../backend/backend/blocks/exa/contents.py | 4 +- .../backend/backend/blocks/exa/search.py | 4 +- .../backend/backend/blocks/exa/similar.py | 4 +- .../backend/backend/blocks/http.py | 21 ++- .../backend/backend/blocks/hubspot/company.py | 10 +- .../backend/backend/blocks/hubspot/contact.py | 10 +- .../backend/blocks/hubspot/engagement.py | 6 +- .../backend/backend/blocks/ideogram.py | 8 +- .../backend/backend/blocks/jina/chunking.py | 4 +- .../backend/backend/blocks/jina/embeddings.py | 4 +- .../backend/blocks/jina/fact_checker.py | 3 +- .../backend/backend/blocks/medium.py | 4 +- .../backend/backend/blocks/nvidia/deepfake.py | 4 +- .../backend/backend/blocks/screenshotone.py | 14 +- .../backend/backend/blocks/slant3d/base.py | 4 +- .../backend/backend/blocks/slant3d/order.py | 5 +- autogpt_platform/backend/backend/cli.py | 38 +++-- .../backend/integrations/credentials_store.py | 2 +- .../backend/integrations/oauth/github.py | 8 +- .../backend/integrations/oauth/google.py | 2 +- .../backend/integrations/oauth/linear.py | 6 +- .../backend/integrations/oauth/notion.py | 4 +- .../backend/integrations/oauth/todoist.py | 7 +- .../backend/integrations/oauth/twitter.py | 25 +-- .../backend/integrations/webhooks/github.py | 10 +- .../backend/integrations/webhooks/slant3d.py | 4 +- .../server/routers/postmark/postmark.py | 4 +- autogpt_platform/backend/poetry.lock | 3 + .../src/app/(platform)/auth/callback/route.ts | 14 +- .../frontend/src/components/CustomNode.tsx | 10 +- autogpt_platform/frontend/src/lib/utils.ts | 5 + 36 files changed, 312 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index d00ab276ce..ce70fab9f1 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ autogpt_platform/backend/settings.py *.ign.* .test-contents +.claude/settings.local.json diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/api_key/key_manager.py b/autogpt_platform/autogpt_libs/autogpt_libs/api_key/key_manager.py index 257250a753..0ac5f8793c 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/api_key/key_manager.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/api_key/key_manager.py @@ -31,4 +31,5 @@ class APIKeyManager: """Verify if a provided API key matches the stored hash.""" if not provided_key.startswith(self.PREFIX): return False - return hashlib.sha256(provided_key.encode()).hexdigest() == stored_hash + provided_hash = hashlib.sha256(provided_key.encode()).hexdigest() + return secrets.compare_digest(provided_hash, stored_hash) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py index d00fe1a05d..eb583ac1fc 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py @@ -1,5 +1,6 @@ import inspect import logging +import secrets from typing import Any, Callable, Optional from fastapi import HTTPException, Request, Security @@ -93,7 +94,11 @@ class APIKeyValidator: self.error_message = error_message async def default_validator(self, api_key: str) -> bool: - return api_key == self.expected_token + if not self.expected_token: + raise ValueError( + "Expected Token Required to be set when uisng API Key Validator default validation" + ) + return secrets.compare_digest(api_key, self.expected_token) async def __call__( self, request: Request, api_key: str = Security(APIKeyHeader) diff --git a/autogpt_platform/autogpt_libs/poetry.lock b/autogpt_platform/autogpt_libs/poetry.lock index 12a4970d50..155098d059 100644 --- a/autogpt_platform/autogpt_libs/poetry.lock +++ b/autogpt_platform/autogpt_libs/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -177,7 +177,7 @@ files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] -markers = {main = "python_version < \"3.11\"", dev = "python_full_version < \"3.11.3\""} +markers = {main = "python_version == \"3.10\"", dev = "python_full_version < \"3.11.3\""} [[package]] name = "attrs" @@ -323,6 +323,21 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -375,7 +390,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -399,6 +414,27 @@ files = [ [package.extras] tests = ["coverage", "coveralls", "dill", "mock", "nose"] +[[package]] +name = "fastapi" +version = "0.115.12" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -895,6 +931,47 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "launchdarkly-eventsource" +version = "1.2.4" +description = "LaunchDarkly SSE Client" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "launchdarkly_eventsource-1.2.4-py3-none-any.whl", hash = "sha256:048ef8c4440d0d8219778661ee4d4b5e12aa6ed2c29a3004417ede44c2386e8c"}, + {file = "launchdarkly_eventsource-1.2.4.tar.gz", hash = "sha256:b8b9342681f55e1d35c56243431cbbaca4eb9812d6785f8de204af322104e066"}, +] + +[package.dependencies] +urllib3 = ">=1.26.0,<3" + +[[package]] +name = "launchdarkly-server-sdk" +version = "9.11.1" +description = "LaunchDarkly SDK for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "launchdarkly_server_sdk-9.11.1-py3-none-any.whl", hash = "sha256:128569cebf666dd115cc0ba03c48ff75f6acc9788301a7e2c3a54d06107e445a"}, + {file = "launchdarkly_server_sdk-9.11.1.tar.gz", hash = "sha256:150e29656cb8c506d1967f3c59e62b69310d345ec27217640a6146dd1db5d250"}, +] + +[package.dependencies] +certifi = ">=2018.4.16" +expiringdict = ">=1.1.4" +launchdarkly-eventsource = ">=1.2.4,<2.0.0" +pyRFC3339 = ">=1.0" +semver = ">=2.10.2" +urllib3 = ">=1.26.0,<3" + +[package.extras] +consul = ["python-consul (>=1.0.1)"] +dynamodb = ["boto3 (>=1.9.71)"] +redis = ["redis (>=2.10.5)"] +test-filesource = ["pyyaml (>=5.3.1)", "watchdog (>=3.0.0)"] + [[package]] name = "multidict" version = "6.1.0" @@ -1412,6 +1489,18 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pyrfc3339" +version = "2.0.1" +description = "Generate and parse RFC 3339 timestamps" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyRFC3339-2.0.1-py3-none-any.whl", hash = "sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d"}, + {file = "pyrfc3339-2.0.1.tar.gz", hash = "sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b"}, +] + [[package]] name = "pytest" version = "8.3.3" @@ -1604,6 +1693,18 @@ files = [ {file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"}, ] +[[package]] +name = "semver" +version = "3.0.4" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, +] + [[package]] name = "six" version = "1.16.0" @@ -1628,6 +1729,24 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "storage3" version = "0.11.0" @@ -1704,7 +1823,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, @@ -1755,6 +1874,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.34.3" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, + {file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "websockets" version = "12.0" @@ -2037,4 +2176,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "78ebf65cdef769cfbe92fe204f01e32d219cca9ee5a6ca9e657aa0630be63802" +content-hash = "d92143928a88ca3a56ac200c335910eafac938940022fed8bd0d17c95040b54f" diff --git a/autogpt_platform/autogpt_libs/pyproject.toml b/autogpt_platform/autogpt_libs/pyproject.toml index 2f2d05ac7a..71d6eeb1f6 100644 --- a/autogpt_platform/autogpt_libs/pyproject.toml +++ b/autogpt_platform/autogpt_libs/pyproject.toml @@ -17,6 +17,9 @@ pyjwt = "^2.10.1" pytest-asyncio = "^0.26.0" pytest-mock = "^3.14.0" supabase = "^2.15.1" +launchdarkly-server-sdk = "^9.11.1" +fastapi = "^0.115.12" +uvicorn = "^0.34.3" [tool.poetry.group.dev.dependencies] redis = "^5.2.1" diff --git a/autogpt_platform/backend/backend/blocks/exa/contents.py b/autogpt_platform/backend/backend/blocks/exa/contents.py index 7210af433d..87817e14be 100644 --- a/autogpt_platform/backend/backend/blocks/exa/contents.py +++ b/autogpt_platform/backend/backend/blocks/exa/contents.py @@ -9,7 +9,7 @@ from backend.blocks.exa._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class ContentRetrievalSettings(BaseModel): @@ -79,7 +79,7 @@ class ExaContentsBlock(Block): } try: - response = requests.post(url, headers=headers, json=payload) + response = Requests().post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() yield "results", data.get("results", []) diff --git a/autogpt_platform/backend/backend/blocks/exa/search.py b/autogpt_platform/backend/backend/blocks/exa/search.py index 5915455a56..b003fbf6fa 100644 --- a/autogpt_platform/backend/backend/blocks/exa/search.py +++ b/autogpt_platform/backend/backend/blocks/exa/search.py @@ -9,7 +9,7 @@ from backend.blocks.exa._auth import ( from backend.blocks.exa.helpers import ContentSettings from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class ExaSearchBlock(Block): @@ -136,7 +136,7 @@ class ExaSearchBlock(Block): payload[api_field] = value try: - response = requests.post(url, headers=headers, json=payload) + response = Requests().post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() # Extract just the results array from the response diff --git a/autogpt_platform/backend/backend/blocks/exa/similar.py b/autogpt_platform/backend/backend/blocks/exa/similar.py index 036d26a481..61f4ab9db5 100644 --- a/autogpt_platform/backend/backend/blocks/exa/similar.py +++ b/autogpt_platform/backend/backend/blocks/exa/similar.py @@ -8,7 +8,7 @@ from backend.blocks.exa._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests from .helpers import ContentSettings @@ -120,7 +120,7 @@ class ExaFindSimilarBlock(Block): payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z") try: - response = requests.post(url, headers=headers, json=payload) + response = Requests().post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() yield "results", data.get("results", []) diff --git a/autogpt_platform/backend/backend/blocks/http.py b/autogpt_platform/backend/backend/blocks/http.py index d186e7f70b..40ed6a684b 100644 --- a/autogpt_platform/backend/backend/blocks/http.py +++ b/autogpt_platform/backend/backend/blocks/http.py @@ -114,9 +114,24 @@ class SendWebRequestBlock(Block): body = input_data.body if isinstance(body, str): try: - body = json.loads(body) - except json.JSONDecodeError: - # plain text – treat as form‑field value instead + # Validate JSON string length to prevent DoS attacks + if len(body) > 10_000_000: # 10MB limit + raise ValueError("JSON body too large") + + parsed_body = json.loads(body) + + # Validate that parsed JSON is safe (basic object/array/primitive types) + if ( + isinstance(parsed_body, (dict, list, str, int, float, bool)) + or parsed_body is None + ): + body = parsed_body + else: + # Unexpected type, treat as plain text + input_data.json_format = False + + except (json.JSONDecodeError, ValueError): + # Invalid JSON or too large – treat as form‑field value instead input_data.json_format = False # ─── Prepare files (if any) ────────────────────────────────── diff --git a/autogpt_platform/backend/backend/blocks/hubspot/company.py b/autogpt_platform/backend/backend/blocks/hubspot/company.py index 81d0fdaf9e..002aeeef81 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/company.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/company.py @@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class HubSpotCompanyBlock(Block): @@ -45,7 +45,7 @@ class HubSpotCompanyBlock(Block): } if input_data.operation == "create": - response = requests.post( + response = Requests().post( base_url, headers=headers, json={"properties": input_data.company_data} ) result = response.json() @@ -67,14 +67,14 @@ class HubSpotCompanyBlock(Block): } ] } - response = requests.post(search_url, headers=headers, json=search_data) + response = Requests().post(search_url, headers=headers, json=search_data) result = response.json() yield "company", result.get("results", [{}])[0] yield "status", "retrieved" elif input_data.operation == "update": # First get company ID by domain - search_response = requests.post( + search_response = Requests().post( f"{base_url}/search", headers=headers, json={ @@ -94,7 +94,7 @@ class HubSpotCompanyBlock(Block): company_id = search_response.json().get("results", [{}])[0].get("id") if company_id: - response = requests.patch( + response = Requests().patch( f"{base_url}/{company_id}", headers=headers, json={"properties": input_data.company_data}, diff --git a/autogpt_platform/backend/backend/blocks/hubspot/contact.py b/autogpt_platform/backend/backend/blocks/hubspot/contact.py index b27649e1dc..b930062a1c 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/contact.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/contact.py @@ -5,7 +5,7 @@ from backend.blocks.hubspot._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class HubSpotContactBlock(Block): @@ -45,7 +45,7 @@ class HubSpotContactBlock(Block): } if input_data.operation == "create": - response = requests.post( + response = Requests().post( base_url, headers=headers, json={"properties": input_data.contact_data} ) result = response.json() @@ -68,13 +68,13 @@ class HubSpotContactBlock(Block): } ] } - response = requests.post(search_url, headers=headers, json=search_data) + response = Requests().post(search_url, headers=headers, json=search_data) result = response.json() yield "contact", result.get("results", [{}])[0] yield "status", "retrieved" elif input_data.operation == "update": - search_response = requests.post( + search_response = Requests().post( f"{base_url}/search", headers=headers, json={ @@ -94,7 +94,7 @@ class HubSpotContactBlock(Block): contact_id = search_response.json().get("results", [{}])[0].get("id") if contact_id: - response = requests.patch( + response = Requests().patch( f"{base_url}/{contact_id}", headers=headers, json={"properties": input_data.contact_data}, diff --git a/autogpt_platform/backend/backend/blocks/hubspot/engagement.py b/autogpt_platform/backend/backend/blocks/hubspot/engagement.py index 15d0296117..eb22a00f2a 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/engagement.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/engagement.py @@ -7,7 +7,7 @@ from backend.blocks.hubspot._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class HubSpotEngagementBlock(Block): @@ -66,7 +66,7 @@ class HubSpotEngagementBlock(Block): } } - response = requests.post(email_url, headers=headers, json=email_data) + response = Requests().post(email_url, headers=headers, json=email_data) result = response.json() yield "result", result yield "status", "email_sent" @@ -80,7 +80,7 @@ class HubSpotEngagementBlock(Block): params = {"limit": 100, "after": from_date.isoformat()} - response = requests.get(engagement_url, headers=headers, params=params) + response = Requests().get(engagement_url, headers=headers, params=params) engagements = response.json() # Process engagement metrics diff --git a/autogpt_platform/backend/backend/blocks/ideogram.py b/autogpt_platform/backend/backend/blocks/ideogram.py index ca9ba69a80..66c2380d12 100644 --- a/autogpt_platform/backend/backend/blocks/ideogram.py +++ b/autogpt_platform/backend/backend/blocks/ideogram.py @@ -12,7 +12,7 @@ from backend.data.model import ( SchemaField, ) from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests TEST_CREDENTIALS = APIKeyCredentials( id="01234567-89ab-cdef-0123-456789abcdef", @@ -267,7 +267,7 @@ class IdeogramModelBlock(Block): } try: - response = requests.post(url, json=data, headers=headers) + response = Requests().post(url, json=data, headers=headers) return response.json()["data"][0]["url"] except RequestException as e: raise Exception(f"Failed to fetch image: {str(e)}") @@ -280,14 +280,14 @@ class IdeogramModelBlock(Block): try: # Step 1: Download the image from the provided URL - image_response = requests.get(image_url) + image_response = Requests().get(image_url) # Step 2: Send the downloaded image to the upscale API files = { "image_file": ("image.png", image_response.content, "image/png"), } - response = requests.post( + response = Requests().post( url, headers=headers, data={"image_request": "{}"}, diff --git a/autogpt_platform/backend/backend/blocks/jina/chunking.py b/autogpt_platform/backend/backend/blocks/jina/chunking.py index 0ebc72f1ca..75a5d858f2 100644 --- a/autogpt_platform/backend/backend/blocks/jina/chunking.py +++ b/autogpt_platform/backend/backend/blocks/jina/chunking.py @@ -5,7 +5,7 @@ from backend.blocks.jina._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class JinaChunkingBlock(Block): @@ -55,7 +55,7 @@ class JinaChunkingBlock(Block): "max_chunk_length": str(input_data.max_chunk_length), } - response = requests.post(url, headers=headers, json=data) + response = Requests().post(url, headers=headers, json=data) result = response.json() all_chunks.extend(result.get("chunks", [])) diff --git a/autogpt_platform/backend/backend/blocks/jina/embeddings.py b/autogpt_platform/backend/backend/blocks/jina/embeddings.py index 67a17bf2c3..dd7966d500 100644 --- a/autogpt_platform/backend/backend/blocks/jina/embeddings.py +++ b/autogpt_platform/backend/backend/blocks/jina/embeddings.py @@ -5,7 +5,7 @@ from backend.blocks.jina._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class JinaEmbeddingBlock(Block): @@ -38,6 +38,6 @@ class JinaEmbeddingBlock(Block): "Authorization": f"Bearer {credentials.api_key.get_secret_value()}", } data = {"input": input_data.texts, "model": input_data.model} - response = requests.post(url, headers=headers, json=data) + response = Requests().post(url, headers=headers, json=data) embeddings = [e["embedding"] for e in response.json()["data"]] yield "embeddings", embeddings diff --git a/autogpt_platform/backend/backend/blocks/jina/fact_checker.py b/autogpt_platform/backend/backend/blocks/jina/fact_checker.py index c9b8c08d1d..91c17f564e 100644 --- a/autogpt_platform/backend/backend/blocks/jina/fact_checker.py +++ b/autogpt_platform/backend/backend/blocks/jina/fact_checker.py @@ -1,7 +1,5 @@ from urllib.parse import quote -import requests - from backend.blocks.jina._auth import ( JinaCredentials, JinaCredentialsField, @@ -9,6 +7,7 @@ from backend.blocks.jina._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField +from backend.util.request import requests class FactCheckerBlock(Block): diff --git a/autogpt_platform/backend/backend/blocks/medium.py b/autogpt_platform/backend/backend/blocks/medium.py index 6d871b4caa..d68bed3bcf 100644 --- a/autogpt_platform/backend/backend/blocks/medium.py +++ b/autogpt_platform/backend/backend/blocks/medium.py @@ -13,7 +13,7 @@ from backend.data.model import ( SecretField, ) from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests TEST_CREDENTIALS = APIKeyCredentials( id="01234567-89ab-cdef-0123-456789abcdef", @@ -160,7 +160,7 @@ class PublishToMediumBlock(Block): "notifyFollowers": notify_followers, } - response = requests.post( + response = Requests().post( f"https://api.medium.com/v1/users/{author_id}/posts", headers=headers, json=data, diff --git a/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py b/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py index f7fa145312..3792cdf53f 100644 --- a/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py +++ b/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py @@ -5,7 +5,7 @@ from backend.blocks.nvidia._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests from backend.util.type import MediaFileType @@ -59,7 +59,7 @@ class NvidiaDeepfakeDetectBlock(Block): } try: - response = requests.post(url, headers=headers, json=payload) + response = Requests().post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() diff --git a/autogpt_platform/backend/backend/blocks/screenshotone.py b/autogpt_platform/backend/backend/blocks/screenshotone.py index fed43b4281..5620b8119b 100644 --- a/autogpt_platform/backend/backend/blocks/screenshotone.py +++ b/autogpt_platform/backend/backend/blocks/screenshotone.py @@ -121,11 +121,10 @@ class ScreenshotWebPageBlock(Block): """ Takes a screenshot using the ScreenshotOne API """ - api = Requests(trusted_origins=["https://api.screenshotone.com"]) + api = Requests() - # Build API URL with parameters + # Build API parameters params = { - "access_key": credentials.api_key.get_secret_value(), "url": url, "viewport_width": viewport_width, "viewport_height": viewport_height, @@ -137,7 +136,14 @@ class ScreenshotWebPageBlock(Block): "cache": str(cache).lower(), } - response = api.get("https://api.screenshotone.com/take", params=params) + # Use header-based authentication instead of query parameter + headers = { + "X-Access-Key": credentials.api_key.get_secret_value(), + } + + response = api.get( + "https://api.screenshotone.com/take", params=params, headers=headers + ) return { "image": store_media_file( diff --git a/autogpt_platform/backend/backend/blocks/slant3d/base.py b/autogpt_platform/backend/backend/blocks/slant3d/base.py index d5d1681e1d..6919e5ec49 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/base.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/base.py @@ -1,7 +1,7 @@ from typing import Any, Dict from backend.data.block import Block -from backend.util.request import requests +from backend.util.request import Requests from ._api import Color, CustomerDetails, OrderItem, Profile @@ -16,7 +16,7 @@ class Slant3DBlockBase(Block): def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict: url = f"{self.BASE_URL}/{endpoint}" - response = requests.request( + response = Requests().request( method=method, url=url, headers=self._get_headers(api_key), **kwargs ) diff --git a/autogpt_platform/backend/backend/blocks/slant3d/order.py b/autogpt_platform/backend/backend/blocks/slant3d/order.py index a1a342a98e..6c9f4ffec3 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/order.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/order.py @@ -1,11 +1,10 @@ import uuid from typing import List -import requests as baserequests - from backend.data.block import BlockOutput, BlockSchema from backend.data.model import APIKeyCredentials, SchemaField from backend.util import settings +from backend.util.request import req from backend.util.settings import BehaveAs from ._api import ( @@ -181,7 +180,7 @@ class Slant3DEstimateOrderBlock(Slant3DBlockBase): yield "total_price", result["totalPrice"] yield "shipping_cost", result["shippingCost"] yield "printing_cost", result["printingCost"] - except baserequests.HTTPError as e: + except req.HTTPError as e: yield "error", str(f"Error estimating order: {e} {e.response.text}") raise diff --git a/autogpt_platform/backend/backend/cli.py b/autogpt_platform/backend/backend/cli.py index 87fadb88de..858188f084 100755 --- a/autogpt_platform/backend/backend/cli.py +++ b/autogpt_platform/backend/backend/cli.py @@ -121,16 +121,17 @@ def reddit(server_address: str): """ Create an event graph """ - import requests - from backend.usecases.reddit_marketing import create_test_graph + from backend.util.request import Requests test_graph = create_test_graph() url = f"{server_address}/graphs" headers = {"Content-Type": "application/json"} data = test_graph.model_dump_json() - response = requests.post(url, headers=headers, data=data) + response = Requests(trusted_origins=[server_address]).post( + url, headers=headers, data=data + ) graph_id = response.json()["id"] print(f"Graph created with ID: {graph_id}") @@ -142,16 +143,18 @@ def populate_db(server_address: str): """ Create an event graph """ - import requests from backend.usecases.sample import create_test_graph + from backend.util.request import Requests test_graph = create_test_graph() url = f"{server_address}/graphs" headers = {"Content-Type": "application/json"} data = test_graph.model_dump_json() - response = requests.post(url, headers=headers, data=data) + response = Requests(trusted_origins=[server_address]).post( + url, headers=headers, data=data + ) graph_id = response.json()["id"] @@ -159,7 +162,9 @@ def populate_db(server_address: str): execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" text = "Hello, World!" input_data = {"input": text} - response = requests.post(execute_url, headers=headers, json=input_data) + response = Requests(trusted_origins=[server_address]).post( + execute_url, headers=headers, json=input_data + ) schedule_url = f"{server_address}/graphs/{graph_id}/schedules" data = { @@ -167,7 +172,9 @@ def populate_db(server_address: str): "cron": "*/5 * * * *", "input_data": {"input": "Hello, World!"}, } - response = requests.post(schedule_url, headers=headers, json=data) + response = Requests(trusted_origins=[server_address]).post( + schedule_url, headers=headers, json=data + ) print("Database populated with: \n- graph\n- execution\n- schedule") @@ -178,21 +185,25 @@ def graph(server_address: str): """ Create an event graph """ - import requests from backend.usecases.sample import create_test_graph + from backend.util.request import Requests url = f"{server_address}/graphs" headers = {"Content-Type": "application/json"} data = create_test_graph().model_dump_json() - response = requests.post(url, headers=headers, data=data) + response = Requests(trusted_origins=[server_address]).post( + url, headers=headers, data=data + ) if response.status_code == 200: print(response.json()["id"]) execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" text = "Hello, World!" input_data = {"input": text} - response = requests.post(execute_url, headers=headers, json=input_data) + response = Requests(trusted_origins=[server_address]).post( + execute_url, headers=headers, json=input_data + ) else: print("Failed to send graph") @@ -206,12 +217,15 @@ def execute(graph_id: str, content: dict): """ Create an event graph """ - import requests + + from backend.util.request import Requests headers = {"Content-Type": "application/json"} execute_url = f"http://0.0.0.0:8000/graphs/{graph_id}/execute" - requests.post(execute_url, headers=headers, json=content) + Requests(trusted_origins=["http://0.0.0.0:8000"]).post( + execute_url, headers=headers, json=content + ) @test.command() diff --git a/autogpt_platform/backend/backend/integrations/credentials_store.py b/autogpt_platform/backend/backend/integrations/credentials_store.py index 847b20fa6c..a5a88dfbcb 100644 --- a/autogpt_platform/backend/backend/integrations/credentials_store.py +++ b/autogpt_platform/backend/backend/integrations/credentials_store.py @@ -398,7 +398,7 @@ class IntegrationCredentialsStore: ( state for state in oauth_states - if state.token == token + if secrets.compare_digest(state.token, token) and state.provider == provider and state.expires_at > now.timestamp() ), diff --git a/autogpt_platform/backend/backend/integrations/oauth/github.py b/autogpt_platform/backend/backend/integrations/oauth/github.py index e6b3db37b4..3358295170 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/github.py +++ b/autogpt_platform/backend/backend/integrations/oauth/github.py @@ -4,7 +4,7 @@ from urllib.parse import urlencode from backend.data.model import OAuth2Credentials from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests from .base import BaseOAuthHandler @@ -59,7 +59,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): "X-GitHub-Api-Version": "2022-11-28", } - requests.delete( + Requests().delete( url=self.revoke_url.format(client_id=self.client_id), auth=(self.client_id, self.client_secret), headers=headers, @@ -89,7 +89,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): **params, } headers = {"Accept": "application/json"} - response = requests.post(self.token_url, data=request_body, headers=headers) + response = Requests().post(self.token_url, data=request_body, headers=headers) token_data: dict = response.json() username = self._request_username(token_data["access_token"]) @@ -132,7 +132,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): "X-GitHub-Api-Version": "2022-11-28", } - response = requests.get(url, headers=headers) + response = Requests().get(url, headers=headers) if not response.ok: return None diff --git a/autogpt_platform/backend/backend/integrations/oauth/google.py b/autogpt_platform/backend/backend/integrations/oauth/google.py index 310eb5ae73..4de5c7365a 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/google.py +++ b/autogpt_platform/backend/backend/integrations/oauth/google.py @@ -76,7 +76,7 @@ class GoogleOAuthHandler(BaseOAuthHandler): logger.debug(f"Scopes granted by Google: {granted_scopes}") google_creds = flow.credentials - logger.debug(f"Received credentials: {google_creds}") + logger.debug("Received credentials") logger.debug("Requesting user email") username = self._request_email(google_creds) diff --git a/autogpt_platform/backend/backend/integrations/oauth/linear.py b/autogpt_platform/backend/backend/integrations/oauth/linear.py index fd9d379c1e..afc84da22e 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/linear.py +++ b/autogpt_platform/backend/backend/integrations/oauth/linear.py @@ -7,7 +7,7 @@ from pydantic import SecretStr from backend.blocks.linear._api import LinearAPIException from backend.data.model import APIKeyCredentials, OAuth2Credentials from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests from .base import BaseOAuthHandler @@ -53,7 +53,7 @@ class LinearOAuthHandler(BaseOAuthHandler): "Authorization": f"Bearer {credentials.access_token.get_secret_value()}" } - response = requests.post(self.revoke_url, headers=headers) + response = Requests().post(self.revoke_url, headers=headers) if not response.ok: try: error_data = response.json() @@ -95,7 +95,7 @@ class LinearOAuthHandler(BaseOAuthHandler): headers = { "Content-Type": "application/x-www-form-urlencoded" } # Correct header for token request - response = requests.post(self.token_url, data=request_body, headers=headers) + response = Requests().post(self.token_url, data=request_body, headers=headers) if not response.ok: try: diff --git a/autogpt_platform/backend/backend/integrations/oauth/notion.py b/autogpt_platform/backend/backend/integrations/oauth/notion.py index 3cd3249fef..10f699d61f 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/notion.py +++ b/autogpt_platform/backend/backend/integrations/oauth/notion.py @@ -4,7 +4,7 @@ from urllib.parse import urlencode from backend.data.model import OAuth2Credentials from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests from .base import BaseOAuthHandler @@ -52,7 +52,7 @@ class NotionOAuthHandler(BaseOAuthHandler): "Authorization": f"Basic {auth_str}", "Accept": "application/json", } - response = requests.post(self.token_url, json=request_body, headers=headers) + response = Requests().post(self.token_url, json=request_body, headers=headers) token_data = response.json() # Email is only available for non-bot users email = ( diff --git a/autogpt_platform/backend/backend/integrations/oauth/todoist.py b/autogpt_platform/backend/backend/integrations/oauth/todoist.py index 543a7de84b..014be26332 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/todoist.py +++ b/autogpt_platform/backend/backend/integrations/oauth/todoist.py @@ -1,10 +1,9 @@ import urllib.parse from typing import ClassVar, Optional -import requests - from backend.data.model import OAuth2Credentials, ProviderName from backend.integrations.oauth.base import BaseOAuthHandler +from backend.util.request import Requests class TodoistOAuthHandler(BaseOAuthHandler): @@ -48,12 +47,12 @@ class TodoistOAuthHandler(BaseOAuthHandler): "redirect_uri": self.redirect_uri, } - response = requests.post(self.TOKEN_URL, data=data) + response = Requests().post(self.TOKEN_URL, data=data) response.raise_for_status() tokens = response.json() - response = requests.post( + response = Requests().post( "https://api.todoist.com/sync/v9/sync", headers={"Authorization": f"Bearer {tokens['access_token']}"}, data={"sync_token": "*", "resource_types": '["user"]'}, diff --git a/autogpt_platform/backend/backend/integrations/oauth/twitter.py b/autogpt_platform/backend/backend/integrations/oauth/twitter.py index 519ccd354e..8645c0814c 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/twitter.py +++ b/autogpt_platform/backend/backend/integrations/oauth/twitter.py @@ -2,10 +2,9 @@ import time import urllib.parse from typing import ClassVar, Optional -import requests - from backend.data.model import OAuth2Credentials, ProviderName from backend.integrations.oauth.base import BaseOAuthHandler +from backend.util.request import Requests, req class TwitterOAuthHandler(BaseOAuthHandler): @@ -78,7 +77,9 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = requests.post(self.TOKEN_URL, headers=headers, data=data, auth=auth) + response = Requests().post( + self.TOKEN_URL, headers=headers, data=data, auth=auth + ) response.raise_for_status() tokens = response.json() @@ -102,7 +103,7 @@ class TwitterOAuthHandler(BaseOAuthHandler): params = {"user.fields": "username"} - response = requests.get( + response = Requests().get( f"{self.USERNAME_URL}?{urllib.parse.urlencode(params)}", headers=headers ) response.raise_for_status() @@ -122,13 +123,12 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = requests.post(self.TOKEN_URL, headers=header, data=data, auth=auth) + response = Requests().post(self.TOKEN_URL, headers=header, data=data, auth=auth) try: response.raise_for_status() - except requests.exceptions.HTTPError as e: - print("HTTP Error:", e) - print("Response Content:", response.text) + except req.exceptions.HTTPError: + print(f"HTTP Error: {response.status_code}") raise tokens = response.json() @@ -159,13 +159,14 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = requests.post(self.REVOKE_URL, headers=header, data=data, auth=auth) + response = Requests().post( + self.REVOKE_URL, headers=header, data=data, auth=auth + ) try: response.raise_for_status() - except requests.exceptions.HTTPError as e: - print("HTTP Error:", e) - print("Response Content:", response.text) + except req.exceptions.HTTPError: + print(f"HTTP Error: {response.status_code}") raise return response.status_code == 200 diff --git a/autogpt_platform/backend/backend/integrations/webhooks/github.py b/autogpt_platform/backend/backend/integrations/webhooks/github.py index 6a39192045..90cb06d303 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/github.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/github.py @@ -2,13 +2,13 @@ import hashlib import hmac import logging -import requests from fastapi import HTTPException, Request from strenum import StrEnum from backend.data import integrations from backend.data.model import Credentials from backend.integrations.providers import ProviderName +from backend.util.request import Requests, req from ._base import BaseWebhooksManager @@ -73,7 +73,7 @@ class GithubWebhooksManager(BaseWebhooksManager): repo, github_hook_id = webhook.resource, webhook.provider_webhook_id ping_url = f"{self.GITHUB_API_URL}/repos/{repo}/hooks/{github_hook_id}/pings" - response = requests.post(ping_url, headers=headers) + response = Requests().post(ping_url, headers=headers) if response.status_code != 204: error_msg = extract_github_error_msg(response) @@ -110,7 +110,7 @@ class GithubWebhooksManager(BaseWebhooksManager): }, } - response = requests.post( + response = Requests().post( f"{self.GITHUB_API_URL}/repos/{resource}/hooks", headers=headers, json=webhook_data, @@ -153,7 +153,7 @@ class GithubWebhooksManager(BaseWebhooksManager): f"Unsupported webhook type '{webhook.webhook_type}'" ) - response = requests.delete(delete_url, headers=headers) + response = Requests().delete(delete_url, headers=headers) if response.status_code not in [204, 404]: # 204 means successful deletion, 404 means the webhook was already deleted @@ -166,7 +166,7 @@ class GithubWebhooksManager(BaseWebhooksManager): # --8<-- [end:GithubWebhooksManager] -def extract_github_error_msg(response: requests.Response) -> str: +def extract_github_error_msg(response: req.Response) -> str: error_msgs = [] resp = response.json() if resp.get("message"): diff --git a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py index 189ab72083..7c923435a9 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py @@ -1,12 +1,12 @@ import logging -import requests from fastapi import Request from backend.data import integrations from backend.data.model import APIKeyCredentials, Credentials from backend.integrations.providers import ProviderName from backend.integrations.webhooks._base import BaseWebhooksManager +from backend.util.request import Requests logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class Slant3DWebhooksManager(BaseWebhooksManager): # Slant3D's API doesn't use events list, just register for all order updates payload = {"endPoint": ingress_url} - response = requests.post( + response = Requests().post( f"{self.BASE_URL}/customer/webhookSubscribe", headers=headers, json=payload ) diff --git a/autogpt_platform/backend/backend/server/routers/postmark/postmark.py b/autogpt_platform/backend/backend/server/routers/postmark/postmark.py index ae744bfc10..d9c1b17863 100644 --- a/autogpt_platform/backend/backend/server/routers/postmark/postmark.py +++ b/autogpt_platform/backend/backend/server/routers/postmark/postmark.py @@ -36,11 +36,11 @@ logger = logging.getLogger(__name__) @router.post("/unsubscribe") async def unsubscribe_via_one_click(token: Annotated[str, Query()]): - logger.info(f"Received unsubscribe request from One Click Unsubscribe: {token}") + logger.info("Received unsubscribe request from One Click Unsubscribe") try: await unsubscribe_user_by_token(token) except Exception as e: - logger.exception("Unsubscribe token %s failed: %s", token, e) + logger.exception("Unsubscribe failed: %s", e) raise HTTPException( status_code=500, detail={"message": str(e), "hint": "Verify Postmark token settings."}, diff --git a/autogpt_platform/backend/poetry.lock b/autogpt_platform/backend/poetry.lock index 00411f1535..842ebe16f2 100644 --- a/autogpt_platform/backend/poetry.lock +++ b/autogpt_platform/backend/poetry.lock @@ -303,13 +303,16 @@ develop = true [package.dependencies] colorama = "^0.4.6" expiringdict = "^1.2.2" +fastapi = "^0.115.12" google-cloud-logging = "^3.12.1" +launchdarkly-server-sdk = "^9.11.1" pydantic = "^2.11.4" pydantic-settings = "^2.9.1" pyjwt = "^2.10.1" pytest-asyncio = "^0.26.0" pytest-mock = "^3.14.0" supabase = "^2.15.1" +uvicorn = "^0.34.3" [package.source] type = "directory" diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts index 54db111dfd..e0a991138e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts @@ -11,13 +11,25 @@ async function shouldShowOnboarding() { ); } +// Validate redirect URL to prevent open redirect attacks +function validateRedirectUrl(url: string): string { + // Only allow relative URLs that start with / + if (url.startsWith("/") && !url.startsWith("//")) { + return url; + } + // Default to home page for any invalid URLs + return "/"; +} + // Handle the callback to complete the user session login export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); // if "next" is in param, use it as the redirect URL - let next = searchParams.get("next") ?? "/"; + const nextParam = searchParams.get("next") ?? "/"; + // Validate redirect URL to prevent open redirect attacks + let next = validateRedirectUrl(nextParam); if (code) { const supabase = await getServerSupabase(); diff --git a/autogpt_platform/frontend/src/components/CustomNode.tsx b/autogpt_platform/frontend/src/components/CustomNode.tsx index 0658ba9f7b..52ed10a867 100644 --- a/autogpt_platform/frontend/src/components/CustomNode.tsx +++ b/autogpt_platform/frontend/src/components/CustomNode.tsx @@ -27,6 +27,7 @@ import { cn, getValue, hasNonNullNonObjectValue, + isObject, parseKeys, setNestedProperty, } from "@/lib/utils"; @@ -435,8 +436,15 @@ export const CustomNode = React.memo( if (activeKey) { try { const parsedValue = JSON.parse(value); - handleInputChange(activeKey, parsedValue); + // Validate that the parsed value is safe before using it + if (isObject(parsedValue) || Array.isArray(parsedValue)) { + handleInputChange(activeKey, parsedValue); + } else { + // For primitive values, use the original string + handleInputChange(activeKey, value); + } } catch (error) { + // If JSON parsing fails, treat as plain text handleInputChange(activeKey, value); } } diff --git a/autogpt_platform/frontend/src/lib/utils.ts b/autogpt_platform/frontend/src/lib/utils.ts index 0d3e1773ce..b1ba8dc0dc 100644 --- a/autogpt_platform/frontend/src/lib/utils.ts +++ b/autogpt_platform/frontend/src/lib/utils.ts @@ -396,3 +396,8 @@ export function getValue(key: string, value: any) { export function isEmptyOrWhitespace(str: string | undefined | null): boolean { return !str || str.trim().length === 0; } + +/** Chech if a value is an object or not */ +export function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} From 97e72cb4850cf787b15387249045154512e31dd4 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 17 Jun 2025 13:38:24 +0400 Subject: [PATCH 07/29] feat(backend): Make execution engine async-first (#10138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduced async execution for blocks and the execution engine. Paralellism will be achieved through a single process asynchronous execution instead of process concurrency. ### Changes 🏗️ * Support async execution for the graph executor * Removed process creation for node execution * Update all blocks to support async executions ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Manual graph executions, tested many of the impacted blocks. --- .../autogpt_libs/utils/synchronize.py | 52 +- autogpt_platform/backend/.env.example | 1 - .../backend/backend/blocks/agent.py | 80 ++- .../blocks/ai_image_generator_block.py | 18 +- .../backend/blocks/ai_music_generator.py | 12 +- .../blocks/ai_shortform_video_block.py | 33 +- .../backend/backend/blocks/apollo/_api.py | 24 +- .../backend/blocks/apollo/organization.py | 8 +- .../backend/backend/blocks/apollo/people.py | 8 +- .../backend/backend/blocks/basic.py | 24 +- .../backend/backend/blocks/block.py | 4 +- .../backend/backend/blocks/branching.py | 4 +- .../backend/backend/blocks/code_executor.py | 40 +- .../backend/blocks/code_extraction_block.py | 2 +- .../backend/blocks/compass/triggers.py | 2 +- .../blocks/count_words_and_char_block.py | 2 +- .../backend/backend/blocks/csv.py | 2 +- .../backend/backend/blocks/decoder_block.py | 2 +- .../backend/backend/blocks/discord.py | 49 +- .../backend/backend/blocks/email_block.py | 2 +- .../backend/backend/blocks/exa/contents.py | 5 +- .../backend/backend/blocks/exa/search.py | 5 +- .../backend/backend/blocks/exa/similar.py | 5 +- .../backend/blocks/fal/ai_video_generator.py | 60 ++- .../backend/backend/blocks/flux_kontext.py | 8 +- .../blocks/generic_webhook/triggers.py | 2 +- .../backend/backend/blocks/github/checks.py | 16 +- .../backend/backend/blocks/github/issues.py | 91 ++-- .../backend/blocks/github/pull_requests.py | 77 +-- .../backend/backend/blocks/github/repo.py | 226 ++++---- .../backend/backend/blocks/github/statuses.py | 10 +- .../backend/backend/blocks/github/triggers.py | 7 +- .../backend/backend/blocks/google/calendar.py | 17 +- .../backend/backend/blocks/google/gmail.py | 39 +- .../backend/backend/blocks/google/sheets.py | 13 +- .../backend/backend/blocks/google_maps.py | 2 +- .../backend/backend/blocks/helpers/http.py | 11 +- .../backend/backend/blocks/http.py | 117 ++--- .../backend/backend/blocks/hubspot/company.py | 19 +- .../backend/backend/blocks/hubspot/contact.py | 16 +- .../backend/blocks/hubspot/engagement.py | 10 +- .../backend/backend/blocks/ideogram.py | 21 +- autogpt_platform/backend/backend/blocks/io.py | 8 +- .../backend/backend/blocks/iteration.py | 2 +- .../backend/backend/blocks/jina/chunking.py | 4 +- .../backend/backend/blocks/jina/embeddings.py | 4 +- .../backend/blocks/jina/fact_checker.py | 7 +- .../backend/backend/blocks/jina/search.py | 8 +- .../backend/backend/blocks/linear/_api.py | 43 +- .../backend/backend/blocks/linear/comment.py | 8 +- .../backend/backend/blocks/linear/issues.py | 22 +- .../backend/backend/blocks/linear/projects.py | 8 +- .../backend/backend/blocks/llm.py | 123 +++-- .../backend/backend/blocks/maths.py | 4 +- .../backend/backend/blocks/media.py | 18 +- .../backend/backend/blocks/medium.py | 9 +- .../backend/backend/blocks/mem0.py | 6 +- .../backend/backend/blocks/nvidia/deepfake.py | 5 +- .../backend/backend/blocks/pinecone.py | 6 +- .../backend/backend/blocks/reddit.py | 4 +- .../backend/blocks/replicate_flux_advanced.py | 8 +- .../backend/backend/blocks/rss.py | 6 +- .../backend/backend/blocks/sampling.py | 2 +- .../backend/backend/blocks/screenshotone.py | 14 +- .../backend/backend/blocks/search.py | 8 +- .../backend/backend/blocks/slant3d/base.py | 30 +- .../backend/blocks/slant3d/filament.py | 4 +- .../backend/backend/blocks/slant3d/order.py | 51 +- .../backend/backend/blocks/slant3d/slicing.py | 4 +- .../backend/backend/blocks/slant3d/webhook.py | 7 +- .../backend/blocks/smart_decision_maker.py | 4 +- .../backend/backend/blocks/smartlead/_api.py | 16 +- .../backend/blocks/smartlead/campaign.py | 24 +- .../backend/backend/blocks/talking_head.py | 20 +- .../backend/backend/blocks/text.py | 12 +- .../backend/blocks/text_to_speech_block.py | 10 +- .../backend/backend/blocks/time_blocks.py | 12 +- .../backend/blocks/todoist/comments.py | 10 +- .../backend/backend/blocks/todoist/labels.py | 16 +- .../backend/blocks/todoist/projects.py | 12 +- .../backend/blocks/todoist/sections.py | 8 +- .../backend/backend/blocks/todoist/tasks.py | 14 +- .../direct_message/direct_message_lookup.py | 2 +- .../direct_message/manage_direct_message.py | 4 +- .../blocks/twitter/lists/list_follows.py | 8 +- .../blocks/twitter/lists/list_lookup.py | 4 +- .../blocks/twitter/lists/list_members.py | 8 +- .../twitter/lists/list_tweets_lookup.py | 2 +- .../blocks/twitter/lists/manage_lists.py | 6 +- .../blocks/twitter/lists/pinned_lists.py | 6 +- .../blocks/twitter/spaces/search_spaces.py | 2 +- .../blocks/twitter/spaces/spaces_lookup.py | 8 +- .../backend/blocks/twitter/tweets/bookmark.py | 6 +- .../backend/blocks/twitter/tweets/hide.py | 4 +- .../backend/blocks/twitter/tweets/like.py | 8 +- .../backend/blocks/twitter/tweets/manage.py | 6 +- .../backend/blocks/twitter/tweets/quote.py | 2 +- .../backend/blocks/twitter/tweets/retweet.py | 6 +- .../backend/blocks/twitter/tweets/timeline.py | 6 +- .../blocks/twitter/tweets/tweet_lookup.py | 4 +- .../backend/blocks/twitter/users/blocks.py | 2 +- .../backend/blocks/twitter/users/follows.py | 8 +- .../backend/blocks/twitter/users/mutes.py | 6 +- .../blocks/twitter/users/user_lookup.py | 4 +- .../backend/backend/blocks/xml_parser.py | 2 +- .../backend/backend/blocks/youtube.py | 2 +- .../blocks/zerobounce/validate_emails.py | 2 +- autogpt_platform/backend/backend/cli.py | 24 +- .../backend/backend/data/block.py | 22 +- .../backend/backend/data/execution.py | 7 +- .../backend/backend/data/graph.py | 2 +- .../backend/backend/data/includes.py | 23 +- .../backend/backend/data/notifications.py | 9 +- .../backend/backend/data/redis.py | 47 +- .../backend/backend/executor/__init__.py | 3 +- .../backend/backend/executor/database.py | 23 + .../backend/backend/executor/manager.py | 484 ++++++++++-------- .../backend/backend/executor/scheduler.py | 8 +- .../backend/backend/executor/utils.py | 318 +++++++----- .../backend/integrations/credentials_store.py | 105 ++-- .../backend/integrations/creds_manager.py | 107 ++-- .../backend/integrations/oauth/base.py | 16 +- .../backend/integrations/oauth/github.py | 31 +- .../backend/integrations/oauth/google.py | 8 +- .../backend/integrations/oauth/linear.py | 38 +- .../backend/integrations/oauth/notion.py | 12 +- .../backend/integrations/oauth/todoist.py | 15 +- .../backend/integrations/oauth/twitter.py | 51 +- .../backend/integrations/webhooks/github.py | 21 +- .../webhooks/graph_lifecycle_hooks.py | 4 +- .../backend/integrations/webhooks/slant3d.py | 2 +- .../backend/notifications/notifications.py | 76 ++- .../backend/server/external/routes/v1.py | 8 +- .../backend/server/integrations/router.py | 42 +- .../backend/backend/server/rest_api.py | 4 +- .../backend/backend/server/routers/v1.py | 63 +-- .../backend/backend/server/routers/v1_test.py | 10 +- .../server/v2/library/routes/presets.py | 4 +- .../backend/server/v2/store/image_gen.py | 16 +- .../backend/backend/util/decorator.py | 45 +- autogpt_platform/backend/backend/util/file.py | 7 +- .../backend/backend/util/metrics.py | 11 +- .../backend/backend/util/request.py | 348 +++++++++---- .../backend/backend/util/settings.py | 6 - autogpt_platform/backend/backend/util/test.py | 28 +- autogpt_platform/backend/poetry.lock | 116 ++++- autogpt_platform/backend/pyproject.toml | 2 + .../backend/test/block/test_block.py | 4 +- .../backend/test/executor/test_tool_use.py | 10 +- .../backend/test/util/test_request.py | 12 +- .../frontend/src/components/CustomEdge.tsx | 8 +- .../src/components/RunnerUIWrapper.tsx | 5 +- autogpt_platform/frontend/src/lib/utils.ts | 2 +- 153 files changed, 2243 insertions(+), 1809 deletions(-) diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/utils/synchronize.py b/autogpt_platform/autogpt_libs/autogpt_libs/utils/synchronize.py index d8221eea0f..348ae4d78d 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/utils/synchronize.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/utils/synchronize.py @@ -1,15 +1,15 @@ -from contextlib import contextmanager -from threading import Lock +import asyncio +from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any from expiringdict import ExpiringDict if TYPE_CHECKING: - from redis import Redis - from redis.lock import Lock as RedisLock + from redis.asyncio import Redis as AsyncRedis + from redis.asyncio.lock import Lock as AsyncRedisLock -class RedisKeyedMutex: +class AsyncRedisKeyedMutex: """ This class provides a mutex that can be locked and unlocked by a specific key, using Redis as a distributed locking provider. @@ -17,41 +17,45 @@ class RedisKeyedMutex: in case the key is not unlocked for a specified duration, to prevent memory leaks. """ - def __init__(self, redis: "Redis", timeout: int | None = 60): + def __init__(self, redis: "AsyncRedis", timeout: int | None = 60): self.redis = redis self.timeout = timeout - self.locks: dict[Any, "RedisLock"] = ExpiringDict( + self.locks: dict[Any, "AsyncRedisLock"] = ExpiringDict( max_len=6000, max_age_seconds=self.timeout ) - self.locks_lock = Lock() + self.locks_lock = asyncio.Lock() - @contextmanager - def locked(self, key: Any): - lock = self.acquire(key) + @asynccontextmanager + async def locked(self, key: Any): + lock = await self.acquire(key) try: yield finally: - if lock.locked() and lock.owned(): - lock.release() + if (await lock.locked()) and (await lock.owned()): + await lock.release() - def acquire(self, key: Any) -> "RedisLock": + async def acquire(self, key: Any) -> "AsyncRedisLock": """Acquires and returns a lock with the given key""" - with self.locks_lock: + async with self.locks_lock: if key not in self.locks: self.locks[key] = self.redis.lock( str(key), self.timeout, thread_local=False ) lock = self.locks[key] - lock.acquire() + await lock.acquire() return lock - def release(self, key: Any): - if (lock := self.locks.get(key)) and lock.locked() and lock.owned(): - lock.release() + async def release(self, key: Any): + if ( + (lock := self.locks.get(key)) + and (await lock.locked()) + and (await lock.owned()) + ): + await lock.release() - def release_all_locks(self): + async def release_all_locks(self): """Call this on process termination to ensure all locks are released""" - self.locks_lock.acquire(blocking=False) - for lock in self.locks.values(): - if lock.locked() and lock.owned(): - lock.release() + async with self.locks_lock: + for lock in self.locks.values(): + if (await lock.locked()) and (await lock.owned()): + await lock.release() diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index 4c5830d704..18343d7725 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -13,7 +13,6 @@ PRISMA_SCHEMA="postgres/schema.prisma" # EXECUTOR NUM_GRAPH_WORKERS=10 -NUM_NODE_WORKERS=3 BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] diff --git a/autogpt_platform/backend/backend/blocks/agent.py b/autogpt_platform/backend/backend/blocks/agent.py index a68cb05204..c25d99458d 100644 --- a/autogpt_platform/backend/backend/blocks/agent.py +++ b/autogpt_platform/backend/backend/blocks/agent.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Any, Optional @@ -61,37 +62,78 @@ class AgentExecutorBlock(Block): categories={BlockCategory.AGENT}, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: - from backend.data.execution import ExecutionEventType + async def run(self, input_data: Input, **kwargs) -> BlockOutput: + from backend.executor import utils as execution_utils - event_bus = execution_utils.get_execution_event_bus() - - graph_exec = execution_utils.add_graph_execution( + graph_exec = await execution_utils.add_graph_execution( graph_id=input_data.graph_id, graph_version=input_data.graph_version, user_id=input_data.user_id, inputs=input_data.inputs, node_credentials_input_map=input_data.node_credentials_input_map, + use_db_query=False, ) - log_id = f"Graph #{input_data.graph_id}-V{input_data.graph_version}, exec-id: {graph_exec.id}" + + try: + async for name, data in self._run( + graph_id=input_data.graph_id, + graph_version=input_data.graph_version, + graph_exec_id=graph_exec.id, + user_id=input_data.user_id, + ): + yield name, data + except asyncio.CancelledError: + logger.warning( + f"Execution of graph {input_data.graph_id} version {input_data.graph_version} was cancelled." + ) + await execution_utils.stop_graph_execution( + graph_exec.id, use_db_query=False + ) + except Exception as e: + logger.error( + f"Execution of graph {input_data.graph_id} version {input_data.graph_version} failed: {e}, stopping execution." + ) + await execution_utils.stop_graph_execution( + graph_exec.id, use_db_query=False + ) + raise + + async def _run( + self, + graph_id: str, + graph_version: int, + graph_exec_id: str, + user_id: str, + ) -> BlockOutput: + + from backend.data.execution import ExecutionEventType + from backend.executor import utils as execution_utils + + event_bus = execution_utils.get_async_execution_event_bus() + + log_id = f"Graph #{graph_id}-V{graph_version}, exec-id: {graph_exec_id}" logger.info(f"Starting execution of {log_id}") - for event in event_bus.listen( - user_id=graph_exec.user_id, - graph_id=graph_exec.graph_id, - graph_exec_id=graph_exec.id, + async for event in event_bus.listen( + user_id=user_id, + graph_id=graph_id, + graph_exec_id=graph_exec_id, ): + if event.status not in [ + ExecutionStatus.COMPLETED, + ExecutionStatus.TERMINATED, + ExecutionStatus.FAILED, + ]: + logger.debug( + f"Execution {log_id} received event {event.event_type} with status {event.status}" + ) + continue + if event.event_type == ExecutionEventType.GRAPH_EXEC_UPDATE: - if event.status in [ - ExecutionStatus.COMPLETED, - ExecutionStatus.TERMINATED, - ExecutionStatus.FAILED, - ]: - logger.info(f"Execution {log_id} ended with status {event.status}") - break - else: - continue + # If the graph execution is COMPLETED, TERMINATED, or FAILED, + # we can stop listening for further events. + break logger.debug( f"Execution {log_id} produced input {event.input_data} output {event.output_data}" diff --git a/autogpt_platform/backend/backend/blocks/ai_image_generator_block.py b/autogpt_platform/backend/backend/blocks/ai_image_generator_block.py index 230f3acd88..39c0d4ac54 100644 --- a/autogpt_platform/backend/backend/blocks/ai_image_generator_block.py +++ b/autogpt_platform/backend/backend/blocks/ai_image_generator_block.py @@ -165,7 +165,7 @@ class AIImageGeneratorBlock(Block): }, ) - def _run_client( + async def _run_client( self, credentials: APIKeyCredentials, model_name: str, input_params: dict ): try: @@ -173,7 +173,7 @@ class AIImageGeneratorBlock(Block): client = ReplicateClient(api_token=credentials.api_key.get_secret_value()) # Run the model with input parameters - output = client.run(model_name, input=input_params, wait=False) + output = await client.async_run(model_name, input=input_params, wait=False) # Process output if isinstance(output, list) and len(output) > 0: @@ -195,7 +195,7 @@ class AIImageGeneratorBlock(Block): except Exception as e: raise RuntimeError(f"Unexpected error during model execution: {e}") - def generate_image(self, input_data: Input, credentials: APIKeyCredentials): + async def generate_image(self, input_data: Input, credentials: APIKeyCredentials): try: # Handle style-based prompt modification for models without native style support modified_prompt = input_data.prompt @@ -213,7 +213,7 @@ class AIImageGeneratorBlock(Block): "steps": 40, "cfg_scale": 7.0, } - output = self._run_client( + output = await self._run_client( credentials, "stability-ai/stable-diffusion-3.5-medium", input_params, @@ -231,7 +231,7 @@ class AIImageGeneratorBlock(Block): "output_format": "jpg", # Set to jpg for Flux models "output_quality": 90, } - output = self._run_client( + output = await self._run_client( credentials, "black-forest-labs/flux-1.1-pro", input_params ) return output @@ -246,7 +246,7 @@ class AIImageGeneratorBlock(Block): "output_format": "jpg", "output_quality": 90, } - output = self._run_client( + output = await self._run_client( credentials, "black-forest-labs/flux-1.1-pro-ultra", input_params ) return output @@ -257,7 +257,7 @@ class AIImageGeneratorBlock(Block): "size": SIZE_TO_RECRAFT_DIMENSIONS[input_data.size], "style": input_data.style.value, } - output = self._run_client( + output = await self._run_client( credentials, "recraft-ai/recraft-v3", input_params ) return output @@ -296,9 +296,9 @@ class AIImageGeneratorBlock(Block): style_text = style_map.get(style, "") return f"{style_text} of" if style_text else "" - def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs): + async def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs): try: - url = self.generate_image(input_data, credentials) + url = await self.generate_image(input_data, credentials) if url: yield "image_url", url else: diff --git a/autogpt_platform/backend/backend/blocks/ai_music_generator.py b/autogpt_platform/backend/backend/blocks/ai_music_generator.py index ce9cf45498..b4561bd513 100644 --- a/autogpt_platform/backend/backend/blocks/ai_music_generator.py +++ b/autogpt_platform/backend/backend/blocks/ai_music_generator.py @@ -1,5 +1,5 @@ +import asyncio import logging -import time from enum import Enum from typing import Literal @@ -142,7 +142,7 @@ class AIMusicGeneratorBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: max_retries = 3 @@ -154,7 +154,7 @@ class AIMusicGeneratorBlock(Block): logger.debug( f"[AIMusicGeneratorBlock] - Running model (attempt {attempt + 1})" ) - result = self.run_model( + result = await self.run_model( api_key=credentials.api_key, music_gen_model_version=input_data.music_gen_model_version, prompt=input_data.prompt, @@ -176,13 +176,13 @@ class AIMusicGeneratorBlock(Block): last_error = f"Unexpected error: {str(e)}" logger.error(f"[AIMusicGeneratorBlock] - Error: {last_error}") if attempt < max_retries - 1: - time.sleep(retry_delay) + await asyncio.sleep(retry_delay) continue # If we've exhausted all retries, yield the error yield "error", f"Failed after {max_retries} attempts. Last error: {last_error}" - def run_model( + async def run_model( self, api_key: SecretStr, music_gen_model_version: MusicGenModelVersion, @@ -199,7 +199,7 @@ class AIMusicGeneratorBlock(Block): client = ReplicateClient(api_token=api_key.get_secret_value()) # Run the model with parameters - output = client.run( + output = await client.async_run( "meta/musicgen:671ac645ce5e552cc63a54a2bbff63fcf798043055d2dac5fc9e36a837eedcfb", input={ "prompt": prompt, diff --git a/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py b/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py index df2b3a2726..c3c4e36472 100644 --- a/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py +++ b/autogpt_platform/backend/backend/blocks/ai_shortform_video_block.py @@ -1,3 +1,4 @@ +import asyncio import logging import time from enum import Enum @@ -13,7 +14,7 @@ from backend.data.model import ( SchemaField, ) from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests TEST_CREDENTIALS = APIKeyCredentials( id="01234567-89ab-cdef-0123-456789abcdef", @@ -216,29 +217,29 @@ class AIShortformVideoCreatorBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def create_webhook(self): + async def create_webhook(self): url = "https://webhook.site/token" headers = {"Accept": "application/json", "Content-Type": "application/json"} - response = requests.post(url, headers=headers) + response = await Requests().post(url, headers=headers) webhook_data = response.json() return webhook_data["uuid"], f"https://webhook.site/{webhook_data['uuid']}" - def create_video(self, api_key: SecretStr, payload: dict) -> dict: + async def create_video(self, api_key: SecretStr, payload: dict) -> dict: url = "https://www.revid.ai/api/public/v2/render" headers = {"key": api_key.get_secret_value()} - response = requests.post(url, json=payload, headers=headers) + response = await Requests().post(url, json=payload, headers=headers) logger.debug( - f"API Response Status Code: {response.status_code}, Content: {response.text}" + f"API Response Status Code: {response.status}, Content: {response.text}" ) return response.json() - def check_video_status(self, api_key: SecretStr, pid: str) -> dict: + async def check_video_status(self, api_key: SecretStr, pid: str) -> dict: url = f"https://www.revid.ai/api/public/v2/status?pid={pid}" headers = {"key": api_key.get_secret_value()} - response = requests.get(url, headers=headers) + response = await Requests().get(url, headers=headers) return response.json() - def wait_for_video( + async def wait_for_video( self, api_key: SecretStr, pid: str, @@ -247,7 +248,7 @@ class AIShortformVideoCreatorBlock(Block): ) -> str: start_time = time.time() while time.time() - start_time < max_wait_time: - status = self.check_video_status(api_key, pid) + status = await self.check_video_status(api_key, pid) logger.debug(f"Video status: {status}") if status.get("status") == "ready" and "videoUrl" in status: @@ -260,16 +261,16 @@ class AIShortformVideoCreatorBlock(Block): logger.error(f"Video creation failed: {status.get('message')}") raise ValueError(f"Video creation failed: {status.get('message')}") - time.sleep(10) + await asyncio.sleep(10) logger.error("Video creation timed out") raise TimeoutError("Video creation timed out") - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: # Create a new Webhook.site URL - webhook_token, webhook_url = self.create_webhook() + webhook_token, webhook_url = await self.create_webhook() logger.debug(f"Webhook URL: {webhook_url}") audio_url = input_data.background_music.audio_url @@ -306,7 +307,7 @@ class AIShortformVideoCreatorBlock(Block): } logger.debug("Creating video...") - response = self.create_video(credentials.api_key, payload) + response = await self.create_video(credentials.api_key, payload) pid = response.get("pid") if not pid: @@ -318,6 +319,8 @@ class AIShortformVideoCreatorBlock(Block): logger.debug( f"Video created with project ID: {pid}. Waiting for completion..." ) - video_url = self.wait_for_video(credentials.api_key, pid, webhook_token) + video_url = await self.wait_for_video( + credentials.api_key, pid, webhook_token + ) logger.debug(f"Video ready: {video_url}") yield "video_url", video_url diff --git a/autogpt_platform/backend/backend/blocks/apollo/_api.py b/autogpt_platform/backend/backend/blocks/apollo/_api.py index 157235ff0f..f4915561b3 100644 --- a/autogpt_platform/backend/backend/blocks/apollo/_api.py +++ b/autogpt_platform/backend/backend/blocks/apollo/_api.py @@ -27,14 +27,15 @@ class ApolloClient: def _get_headers(self) -> dict[str, str]: return {"x-api-key": self.credentials.api_key.get_secret_value()} - def search_people(self, query: SearchPeopleRequest) -> List[Contact]: + async def search_people(self, query: SearchPeopleRequest) -> List[Contact]: """Search for people in Apollo""" - response = self.requests.get( + response = await self.requests.get( f"{self.API_URL}/mixed_people/search", headers=self._get_headers(), params=query.model_dump(exclude={"credentials", "max_results"}), ) - parsed_response = SearchPeopleResponse(**response.json()) + data = response.json() + parsed_response = SearchPeopleResponse(**data) if parsed_response.pagination.total_entries == 0: return [] @@ -52,27 +53,29 @@ class ApolloClient: and len(parsed_response.people) > 0 ): query.page += 1 - response = self.requests.get( + response = await self.requests.get( f"{self.API_URL}/mixed_people/search", headers=self._get_headers(), params=query.model_dump(exclude={"credentials", "max_results"}), ) - parsed_response = SearchPeopleResponse(**response.json()) + data = response.json() + parsed_response = SearchPeopleResponse(**data) people.extend(parsed_response.people[: query.max_results - len(people)]) logger.info(f"Found {len(people)} people") return people[: query.max_results] if query.max_results else people - def search_organizations( + async def search_organizations( self, query: SearchOrganizationsRequest ) -> List[Organization]: """Search for organizations in Apollo""" - response = self.requests.get( + response = await self.requests.get( f"{self.API_URL}/mixed_companies/search", headers=self._get_headers(), params=query.model_dump(exclude={"credentials", "max_results"}), ) - parsed_response = SearchOrganizationsResponse(**response.json()) + data = response.json() + parsed_response = SearchOrganizationsResponse(**data) if parsed_response.pagination.total_entries == 0: return [] @@ -90,12 +93,13 @@ class ApolloClient: and len(parsed_response.organizations) > 0 ): query.page += 1 - response = self.requests.get( + response = await self.requests.get( f"{self.API_URL}/mixed_companies/search", headers=self._get_headers(), params=query.model_dump(exclude={"credentials", "max_results"}), ) - parsed_response = SearchOrganizationsResponse(**response.json()) + data = response.json() + parsed_response = SearchOrganizationsResponse(**data) organizations.extend( parsed_response.organizations[ : query.max_results - len(organizations) diff --git a/autogpt_platform/backend/backend/blocks/apollo/organization.py b/autogpt_platform/backend/backend/blocks/apollo/organization.py index 37537a6461..869f1be050 100644 --- a/autogpt_platform/backend/backend/blocks/apollo/organization.py +++ b/autogpt_platform/backend/backend/blocks/apollo/organization.py @@ -201,19 +201,19 @@ To find IDs, identify the values for organization_id when you call this endpoint ) @staticmethod - def search_organizations( + async def search_organizations( query: SearchOrganizationsRequest, credentials: ApolloCredentials ) -> list[Organization]: client = ApolloClient(credentials) - return client.search_organizations(query) + return await client.search_organizations(query) - def run( + async def run( self, input_data: Input, *, credentials: ApolloCredentials, **kwargs ) -> BlockOutput: query = SearchOrganizationsRequest( **input_data.model_dump(exclude={"credentials"}) ) - organizations = self.search_organizations(query, credentials) + organizations = await self.search_organizations(query, credentials) for organization in organizations: yield "organization", organization yield "organizations", organizations diff --git a/autogpt_platform/backend/backend/blocks/apollo/people.py b/autogpt_platform/backend/backend/blocks/apollo/people.py index 628bb5dc7c..4eb8bd3de7 100644 --- a/autogpt_platform/backend/backend/blocks/apollo/people.py +++ b/autogpt_platform/backend/backend/blocks/apollo/people.py @@ -373,13 +373,13 @@ class SearchPeopleBlock(Block): ) @staticmethod - def search_people( + async def search_people( query: SearchPeopleRequest, credentials: ApolloCredentials ) -> list[Contact]: client = ApolloClient(credentials) - return client.search_people(query) + return await client.search_people(query) - def run( + async def run( self, input_data: Input, *, @@ -388,7 +388,7 @@ class SearchPeopleBlock(Block): ) -> BlockOutput: query = SearchPeopleRequest(**input_data.model_dump(exclude={"credentials"})) - people = self.search_people(query, credentials) + people = await self.search_people(query, credentials) for person in people: yield "person", person yield "people", people diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 2c7edeb0ad..63906a13d9 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -30,14 +30,14 @@ class FileStoreBlock(Block): static_output=True, ) - def run( + async def run( self, input_data: Input, *, graph_exec_id: str, **kwargs, ) -> BlockOutput: - file_path = store_media_file( + file_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.file_in, return_content=False, @@ -84,7 +84,7 @@ class StoreValueBlock(Block): static_output=True, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.data or input_data.input @@ -110,7 +110,7 @@ class PrintToConsoleBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.text yield "status", "printed" @@ -151,7 +151,7 @@ class FindInDictionaryBlock(Block): categories={BlockCategory.BASIC}, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: obj = input_data.input key = input_data.key @@ -241,7 +241,7 @@ class AddToDictionaryBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: updated_dict = input_data.dictionary.copy() if input_data.value is not None and input_data.key: @@ -319,7 +319,7 @@ class AddToListBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: entries_added = input_data.entries.copy() if input_data.entry: entries_added.append(input_data.entry) @@ -366,7 +366,7 @@ class FindInListBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: yield "index", input_data.list.index(input_data.value) yield "found", True @@ -396,7 +396,7 @@ class NoteBlock(Block): block_type=BlockType.NOTE, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.text @@ -442,7 +442,7 @@ class CreateDictionaryBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # The values are already validated by Pydantic schema yield "dictionary", input_data.values @@ -490,7 +490,7 @@ class CreateListBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # The values are already validated by Pydantic schema yield "list", input_data.values @@ -525,7 +525,7 @@ class UniversalTypeConverterBlock(Block): output_schema=UniversalTypeConverterBlock.Output, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: converted_value = convert( input_data.value, diff --git a/autogpt_platform/backend/backend/blocks/block.py b/autogpt_platform/backend/backend/blocks/block.py index 01e8af7238..e1745d3055 100644 --- a/autogpt_platform/backend/backend/blocks/block.py +++ b/autogpt_platform/backend/backend/blocks/block.py @@ -38,7 +38,7 @@ class BlockInstallationBlock(Block): disabled=True, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: code = input_data.code if search := re.search(r"class (\w+)\(Block\):", code): @@ -64,7 +64,7 @@ class BlockInstallationBlock(Block): from backend.util.test import execute_block_test - execute_block_test(block) + await execute_block_test(block) yield "success", "Block installed successfully." except Exception as e: os.remove(file_path) diff --git a/autogpt_platform/backend/backend/blocks/branching.py b/autogpt_platform/backend/backend/blocks/branching.py index a3424d3374..8bfe8c4172 100644 --- a/autogpt_platform/backend/backend/blocks/branching.py +++ b/autogpt_platform/backend/backend/blocks/branching.py @@ -70,7 +70,7 @@ class ConditionBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: operator = input_data.operator value1 = input_data.value1 @@ -180,7 +180,7 @@ class IfInputMatchesBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: if input_data.input == input_data.value or input_data.input is input_data.value: yield "result", True yield "yes_output", input_data.yes_value diff --git a/autogpt_platform/backend/backend/blocks/code_executor.py b/autogpt_platform/backend/backend/blocks/code_executor.py index 409c02bab0..e25231e90e 100644 --- a/autogpt_platform/backend/backend/blocks/code_executor.py +++ b/autogpt_platform/backend/backend/blocks/code_executor.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Literal -from e2b_code_interpreter import Sandbox +from e2b_code_interpreter import AsyncSandbox from pydantic import SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema @@ -123,7 +123,7 @@ class CodeExecutionBlock(Block): }, ) - def execute_code( + async def execute_code( self, code: str, language: ProgrammingLanguage, @@ -135,21 +135,21 @@ class CodeExecutionBlock(Block): try: sandbox = None if template_id: - sandbox = Sandbox( + sandbox = await AsyncSandbox.create( template=template_id, api_key=api_key, timeout=timeout ) else: - sandbox = Sandbox(api_key=api_key, timeout=timeout) + sandbox = await AsyncSandbox.create(api_key=api_key, timeout=timeout) if not sandbox: raise Exception("Sandbox not created") # Running setup commands for cmd in setup_commands: - sandbox.commands.run(cmd) + await sandbox.commands.run(cmd) # Executing the code - execution = sandbox.run_code( + execution = await sandbox.run_code( code, language=language.value, on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error @@ -167,11 +167,11 @@ class CodeExecutionBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - response, stdout_logs, stderr_logs = self.execute_code( + response, stdout_logs, stderr_logs = await self.execute_code( input_data.code, input_data.language, input_data.setup_commands, @@ -278,11 +278,11 @@ class InstantiationBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - sandbox_id, response, stdout_logs, stderr_logs = self.execute_code( + sandbox_id, response, stdout_logs, stderr_logs = await self.execute_code( input_data.setup_code, input_data.language, input_data.setup_commands, @@ -303,7 +303,7 @@ class InstantiationBlock(Block): except Exception as e: yield "error", str(e) - def execute_code( + async def execute_code( self, code: str, language: ProgrammingLanguage, @@ -315,21 +315,21 @@ class InstantiationBlock(Block): try: sandbox = None if template_id: - sandbox = Sandbox( + sandbox = await AsyncSandbox.create( template=template_id, api_key=api_key, timeout=timeout ) else: - sandbox = Sandbox(api_key=api_key, timeout=timeout) + sandbox = await AsyncSandbox.create(api_key=api_key, timeout=timeout) if not sandbox: raise Exception("Sandbox not created") # Running setup commands for cmd in setup_commands: - sandbox.commands.run(cmd) + await sandbox.commands.run(cmd) # Executing the code - execution = sandbox.run_code( + execution = await sandbox.run_code( code, language=language.value, on_error=lambda e: sandbox.kill(), # Kill the sandbox if there is an error @@ -409,7 +409,7 @@ class StepExecutionBlock(Block): }, ) - def execute_step_code( + async def execute_step_code( self, sandbox_id: str, code: str, @@ -417,12 +417,12 @@ class StepExecutionBlock(Block): api_key: str, ): try: - sandbox = Sandbox.connect(sandbox_id=sandbox_id, api_key=api_key) + sandbox = await AsyncSandbox.connect(sandbox_id=sandbox_id, api_key=api_key) if not sandbox: raise Exception("Sandbox not found") # Executing the code - execution = sandbox.run_code(code, language=language.value) + execution = await sandbox.run_code(code, language=language.value) if execution.error: raise Exception(execution.error) @@ -436,11 +436,11 @@ class StepExecutionBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - response, stdout_logs, stderr_logs = self.execute_step_code( + response, stdout_logs, stderr_logs = await self.execute_step_code( input_data.sandbox_id, input_data.step_code, input_data.language, diff --git a/autogpt_platform/backend/backend/blocks/code_extraction_block.py b/autogpt_platform/backend/backend/blocks/code_extraction_block.py index ab1e35aa5d..33bf225bfd 100644 --- a/autogpt_platform/backend/backend/blocks/code_extraction_block.py +++ b/autogpt_platform/backend/backend/blocks/code_extraction_block.py @@ -49,7 +49,7 @@ class CodeExtractionBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: # List of supported programming languages with mapped aliases language_aliases = { "html": ["html", "htm"], diff --git a/autogpt_platform/backend/backend/blocks/compass/triggers.py b/autogpt_platform/backend/backend/blocks/compass/triggers.py index 662e39ecea..6eac52ce53 100644 --- a/autogpt_platform/backend/backend/blocks/compass/triggers.py +++ b/autogpt_platform/backend/backend/blocks/compass/triggers.py @@ -56,5 +56,5 @@ class CompassAITriggerBlock(Block): # ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "transcription", input_data.payload.transcription diff --git a/autogpt_platform/backend/backend/blocks/count_words_and_char_block.py b/autogpt_platform/backend/backend/blocks/count_words_and_char_block.py index 13f9e39779..ddbcf07876 100644 --- a/autogpt_platform/backend/backend/blocks/count_words_and_char_block.py +++ b/autogpt_platform/backend/backend/blocks/count_words_and_char_block.py @@ -30,7 +30,7 @@ class WordCharacterCountBlock(Block): test_output=[("word_count", 4), ("character_count", 19)], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: text = input_data.text word_count = len(text.split()) diff --git a/autogpt_platform/backend/backend/blocks/csv.py b/autogpt_platform/backend/backend/blocks/csv.py index 3cc3575b31..f69eeff4a9 100644 --- a/autogpt_platform/backend/backend/blocks/csv.py +++ b/autogpt_platform/backend/backend/blocks/csv.py @@ -69,7 +69,7 @@ class ReadCsvBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: import csv from io import StringIO diff --git a/autogpt_platform/backend/backend/blocks/decoder_block.py b/autogpt_platform/backend/backend/blocks/decoder_block.py index 033cdfb0b3..754d79b068 100644 --- a/autogpt_platform/backend/backend/blocks/decoder_block.py +++ b/autogpt_platform/backend/backend/blocks/decoder_block.py @@ -34,6 +34,6 @@ This is a "quoted" string.""", ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: decoded_text = codecs.decode(input_data.text, "unicode_escape") yield "decoded_text", decoded_text diff --git a/autogpt_platform/backend/backend/blocks/discord.py b/autogpt_platform/backend/backend/blocks/discord.py index 08ba8af074..91aba5f414 100644 --- a/autogpt_platform/backend/backend/blocks/discord.py +++ b/autogpt_platform/backend/backend/blocks/discord.py @@ -1,4 +1,3 @@ -import asyncio from typing import Literal import aiohttp @@ -74,7 +73,11 @@ class ReadDiscordMessagesBlock(Block): ("username", "test_user"), ], test_mock={ - "run_bot": lambda token: asyncio.Future() # Create a Future object for mocking + "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", + } }, ) @@ -106,37 +109,24 @@ class ReadDiscordMessagesBlock(Block): if attachment.filename.endswith((".txt", ".py")): async with aiohttp.ClientSession() as session: async with session.get(attachment.url) as response: - file_content = await response.text() + 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()) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - while True: - for output_name, output_value in self.__run(input_data, credentials): - yield output_name, output_value - break + async for output_name, output_value in self.__run(input_data, credentials): + yield output_name, output_value - def __run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput: + async def __run( + self, input_data: Input, credentials: APIKeyCredentials + ) -> BlockOutput: try: - loop = asyncio.get_event_loop() - future = self.run_bot(credentials.api_key) - - # If it's a Future (mock), set the result - if isinstance(future, asyncio.Future): - future.set_result( - { - "output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.", - "channel_name": "general", - "username": "test_user", - } - ) - - result = loop.run_until_complete(future) + result = await self.run_bot(credentials.api_key) # For testing purposes, use the mocked result if isinstance(result, dict): @@ -190,7 +180,7 @@ class SendDiscordMessageBlock(Block): }, test_output=[("status", "Message sent")], test_mock={ - "send_message": lambda token, channel_name, message_content: asyncio.Future() + "send_message": lambda token, channel_name, message_content: "Message sent" }, test_credentials=TEST_CREDENTIALS, ) @@ -222,23 +212,16 @@ class SendDiscordMessageBlock(Block): """Splits a message into chunks not exceeding the Discord limit.""" return [message[i : i + limit] for i in range(0, len(message), limit)] - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - loop = asyncio.get_event_loop() - future = self.send_message( + result = await self.send_message( credentials.api_key.get_secret_value(), input_data.channel_name, input_data.message_content, ) - # If it's a Future (mock), set the result - if isinstance(future, asyncio.Future): - future.set_result("Message sent") - - result = loop.run_until_complete(future) - # For testing purposes, use the mocked result if isinstance(result, str): self.output_data = result diff --git a/autogpt_platform/backend/backend/blocks/email_block.py b/autogpt_platform/backend/backend/blocks/email_block.py index 4159886cee..3738bf0de8 100644 --- a/autogpt_platform/backend/backend/blocks/email_block.py +++ b/autogpt_platform/backend/backend/blocks/email_block.py @@ -121,7 +121,7 @@ class SendEmailBlock(Block): return "Email sent successfully" - def run( + async def run( self, input_data: Input, *, credentials: SMTPCredentials, **kwargs ) -> BlockOutput: yield "status", self.send_email( diff --git a/autogpt_platform/backend/backend/blocks/exa/contents.py b/autogpt_platform/backend/backend/blocks/exa/contents.py index 87817e14be..920a5ac82f 100644 --- a/autogpt_platform/backend/backend/blocks/exa/contents.py +++ b/autogpt_platform/backend/backend/blocks/exa/contents.py @@ -62,7 +62,7 @@ class ExaContentsBlock(Block): output_schema=ExaContentsBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: ExaCredentials, **kwargs ) -> BlockOutput: url = "https://api.exa.ai/contents" @@ -79,8 +79,7 @@ class ExaContentsBlock(Block): } try: - response = Requests().post(url, headers=headers, json=payload) - response.raise_for_status() + response = await Requests().post(url, headers=headers, json=payload) data = response.json() yield "results", data.get("results", []) except Exception as e: diff --git a/autogpt_platform/backend/backend/blocks/exa/search.py b/autogpt_platform/backend/backend/blocks/exa/search.py index b003fbf6fa..1f4d0005ce 100644 --- a/autogpt_platform/backend/backend/blocks/exa/search.py +++ b/autogpt_platform/backend/backend/blocks/exa/search.py @@ -91,7 +91,7 @@ class ExaSearchBlock(Block): output_schema=ExaSearchBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: ExaCredentials, **kwargs ) -> BlockOutput: url = "https://api.exa.ai/search" @@ -136,8 +136,7 @@ class ExaSearchBlock(Block): payload[api_field] = value try: - response = Requests().post(url, headers=headers, json=payload) - response.raise_for_status() + response = await Requests().post(url, headers=headers, json=payload) data = response.json() # Extract just the results array from the response yield "results", data.get("results", []) diff --git a/autogpt_platform/backend/backend/blocks/exa/similar.py b/autogpt_platform/backend/backend/blocks/exa/similar.py index 61f4ab9db5..36dc23c5c5 100644 --- a/autogpt_platform/backend/backend/blocks/exa/similar.py +++ b/autogpt_platform/backend/backend/blocks/exa/similar.py @@ -78,7 +78,7 @@ class ExaFindSimilarBlock(Block): output_schema=ExaFindSimilarBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: ExaCredentials, **kwargs ) -> BlockOutput: url = "https://api.exa.ai/findSimilar" @@ -120,8 +120,7 @@ class ExaFindSimilarBlock(Block): payload[api_field] = value.strftime("%Y-%m-%dT%H:%M:%S.000Z") try: - response = Requests().post(url, headers=headers, json=payload) - response.raise_for_status() + response = await Requests().post(url, headers=headers, json=payload) data = response.json() yield "results", data.get("results", []) except Exception as e: diff --git a/autogpt_platform/backend/backend/blocks/fal/ai_video_generator.py b/autogpt_platform/backend/backend/blocks/fal/ai_video_generator.py index fc2152e0ee..2e795f0d78 100644 --- a/autogpt_platform/backend/backend/blocks/fal/ai_video_generator.py +++ b/autogpt_platform/backend/backend/blocks/fal/ai_video_generator.py @@ -1,10 +1,8 @@ +import asyncio import logging -import time from enum import Enum from typing import Any -import httpx - from backend.blocks.fal._auth import ( TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT, @@ -14,6 +12,7 @@ from backend.blocks.fal._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField +from backend.util.request import ClientResponseError, Requests logger = logging.getLogger(__name__) @@ -66,35 +65,37 @@ class AIVideoGeneratorBlock(Block): ) def _get_headers(self, api_key: str) -> dict[str, str]: - """Get headers for FAL API requests.""" + """Get headers for FAL API Requests.""" return { "Authorization": f"Key {api_key}", "Content-Type": "application/json", } - def _submit_request( + async def _submit_request( self, url: str, headers: dict[str, str], data: dict[str, Any] ) -> dict[str, Any]: """Submit a request to the FAL API.""" try: - response = httpx.post(url, headers=headers, json=data) - response.raise_for_status() + response = await Requests().post(url, headers=headers, json=data) return response.json() - except httpx.HTTPError as e: + except ClientResponseError as e: logger.error(f"FAL API request failed: {str(e)}") raise RuntimeError(f"Failed to submit request: {str(e)}") - def _poll_status(self, status_url: str, headers: dict[str, str]) -> dict[str, Any]: + async def _poll_status( + self, status_url: str, headers: dict[str, str] + ) -> dict[str, Any]: """Poll the status endpoint until completion or failure.""" try: - response = httpx.get(status_url, headers=headers) - response.raise_for_status() + response = await Requests().get(status_url, headers=headers) return response.json() - except httpx.HTTPError as e: + except ClientResponseError as e: logger.error(f"Failed to get status: {str(e)}") raise RuntimeError(f"Failed to get status: {str(e)}") - def generate_video(self, input_data: Input, credentials: FalCredentials) -> str: + async def generate_video( + self, input_data: Input, credentials: FalCredentials + ) -> str: """Generate video using the specified FAL model.""" base_url = "https://queue.fal.run" api_key = credentials.api_key.get_secret_value() @@ -110,8 +111,9 @@ class AIVideoGeneratorBlock(Block): try: # Submit request to queue - submit_response = httpx.post(submit_url, headers=headers, json=submit_data) - submit_response.raise_for_status() + submit_response = await Requests().post( + submit_url, headers=headers, json=submit_data + ) request_data = submit_response.json() # Get request_id and urls from initial response @@ -122,14 +124,23 @@ class AIVideoGeneratorBlock(Block): if not all([request_id, status_url, result_url]): raise ValueError("Missing required data in submission response") + # Ensure status_url is a string + if not isinstance(status_url, str): + raise ValueError("Invalid status URL format") + + # Ensure result_url is a string + if not isinstance(result_url, str): + raise ValueError("Invalid result URL format") + # Poll for status with exponential backoff max_attempts = 30 attempt = 0 base_wait_time = 5 while attempt < max_attempts: - status_response = httpx.get(f"{status_url}?logs=1", headers=headers) - status_response.raise_for_status() + status_response = await Requests().get( + f"{status_url}?logs=1", headers=headers + ) status_data = status_response.json() # Process new logs only @@ -152,8 +163,7 @@ class AIVideoGeneratorBlock(Block): status = status_data.get("status") if status == "COMPLETED": # Get the final result - result_response = httpx.get(result_url, headers=headers) - result_response.raise_for_status() + result_response = await Requests().get(result_url, headers=headers) result_data = result_response.json() if "video" not in result_data or not isinstance( @@ -162,8 +172,8 @@ class AIVideoGeneratorBlock(Block): raise ValueError("Invalid response format - missing video data") video_url = result_data["video"].get("url") - if not video_url: - raise ValueError("No video URL in response") + if not video_url or not isinstance(video_url, str): + raise ValueError("No valid video URL in response") return video_url @@ -183,19 +193,19 @@ class AIVideoGeneratorBlock(Block): logger.info(f"[FAL Generation] Status: Unknown status: {status}") wait_time = min(base_wait_time * (2**attempt), 60) # Cap at 60 seconds - time.sleep(wait_time) + await asyncio.sleep(wait_time) attempt += 1 raise RuntimeError("Maximum polling attempts reached") - except httpx.HTTPError as e: + except ClientResponseError as e: raise RuntimeError(f"API request failed: {str(e)}") - def run( + async def run( self, input_data: Input, *, credentials: FalCredentials, **kwargs ) -> BlockOutput: try: - video_url = self.generate_video(input_data, credentials) + video_url = await self.generate_video(input_data, credentials) yield "video_url", video_url except Exception as e: error_message = str(e) diff --git a/autogpt_platform/backend/backend/blocks/flux_kontext.py b/autogpt_platform/backend/backend/blocks/flux_kontext.py index 1cff9dbba2..f391b41939 100644 --- a/autogpt_platform/backend/backend/blocks/flux_kontext.py +++ b/autogpt_platform/backend/backend/blocks/flux_kontext.py @@ -123,14 +123,14 @@ class AIImageEditorBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs, ) -> BlockOutput: - result = self.run_model( + result = await self.run_model( api_key=credentials.api_key, model_name=input_data.model.api_name, prompt=input_data.prompt, @@ -140,7 +140,7 @@ class AIImageEditorBlock(Block): ) yield "output_image", result - def run_model( + async def run_model( self, api_key: SecretStr, model_name: str, @@ -157,7 +157,7 @@ class AIImageEditorBlock(Block): **({"seed": seed} if seed is not None else {}), } - output: FileOutput | list[FileOutput] = client.run( # type: ignore + output: FileOutput | list[FileOutput] = await client.async_run( # type: ignore model_name, input=input_params, wait=False, diff --git a/autogpt_platform/backend/backend/blocks/generic_webhook/triggers.py b/autogpt_platform/backend/backend/blocks/generic_webhook/triggers.py index 66c106b0c5..66660ac57d 100644 --- a/autogpt_platform/backend/backend/blocks/generic_webhook/triggers.py +++ b/autogpt_platform/backend/backend/blocks/generic_webhook/triggers.py @@ -46,6 +46,6 @@ class GenericWebhookTriggerBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "constants", input_data.constants yield "payload", input_data.payload diff --git a/autogpt_platform/backend/backend/blocks/github/checks.py b/autogpt_platform/backend/backend/blocks/github/checks.py index 070b5179e8..9b9aecdf07 100644 --- a/autogpt_platform/backend/backend/blocks/github/checks.py +++ b/autogpt_platform/backend/backend/blocks/github/checks.py @@ -129,7 +129,7 @@ class GithubCreateCheckRunBlock(Block): ) @staticmethod - def create_check_run( + async def create_check_run( credentials: GithubCredentials, repo_url: str, name: str, @@ -172,7 +172,7 @@ class GithubCreateCheckRunBlock(Block): data.output = output_data check_runs_url = f"{repo_url}/check-runs" - response = api.post( + response = await api.post( check_runs_url, data=data.model_dump_json(exclude_none=True) ) result = response.json() @@ -183,7 +183,7 @@ class GithubCreateCheckRunBlock(Block): "status": result["status"], } - def run( + async def run( self, input_data: Input, *, @@ -191,7 +191,7 @@ class GithubCreateCheckRunBlock(Block): **kwargs, ) -> BlockOutput: try: - result = self.create_check_run( + result = await self.create_check_run( credentials=credentials, repo_url=input_data.repo_url, name=input_data.name, @@ -292,7 +292,7 @@ class GithubUpdateCheckRunBlock(Block): ) @staticmethod - def update_check_run( + async def update_check_run( credentials: GithubCredentials, repo_url: str, check_run_id: int, @@ -325,7 +325,7 @@ class GithubUpdateCheckRunBlock(Block): data.output = output_data check_run_url = f"{repo_url}/check-runs/{check_run_id}" - response = api.patch( + response = await api.patch( check_run_url, data=data.model_dump_json(exclude_none=True) ) result = response.json() @@ -337,7 +337,7 @@ class GithubUpdateCheckRunBlock(Block): "conclusion": result.get("conclusion"), } - def run( + async def run( self, input_data: Input, *, @@ -345,7 +345,7 @@ class GithubUpdateCheckRunBlock(Block): **kwargs, ) -> BlockOutput: try: - result = self.update_check_run( + result = await self.update_check_run( credentials=credentials, repo_url=input_data.repo_url, check_run_id=input_data.check_run_id, diff --git a/autogpt_platform/backend/backend/blocks/github/issues.py b/autogpt_platform/backend/backend/blocks/github/issues.py index e62821a36e..42c027c493 100644 --- a/autogpt_platform/backend/backend/blocks/github/issues.py +++ b/autogpt_platform/backend/backend/blocks/github/issues.py @@ -80,7 +80,7 @@ class GithubCommentBlock(Block): ) @staticmethod - def post_comment( + async def post_comment( credentials: GithubCredentials, issue_url: str, body_text: str ) -> tuple[int, str]: api = get_api(credentials) @@ -88,18 +88,18 @@ class GithubCommentBlock(Block): if "pull" in issue_url: issue_url = issue_url.replace("pull", "issues") comments_url = issue_url + "/comments" - response = api.post(comments_url, json=data) + response = await api.post(comments_url, json=data) comment = response.json() return comment["id"], comment["html_url"] - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - id, url = self.post_comment( + id, url = await self.post_comment( credentials, input_data.issue_url, input_data.comment, @@ -171,7 +171,7 @@ class GithubUpdateCommentBlock(Block): ) @staticmethod - def update_comment( + async def update_comment( credentials: GithubCredentials, comment_url: str, body_text: str ) -> tuple[int, str]: api = get_api(credentials, convert_urls=False) @@ -179,11 +179,11 @@ class GithubUpdateCommentBlock(Block): url = convert_comment_url_to_api_endpoint(comment_url) logger.info(url) - response = api.patch(url, json=data) + response = await api.patch(url, json=data) comment = response.json() return comment["id"], comment["html_url"] - def run( + async def run( self, input_data: Input, *, @@ -209,7 +209,7 @@ class GithubUpdateCommentBlock(Block): raise ValueError( "Must provide either comment_url or comment_id and issue_url" ) - id, url = self.update_comment( + id, url = await self.update_comment( credentials, input_data.comment_url, input_data.comment, @@ -288,7 +288,7 @@ class GithubListCommentsBlock(Block): ) @staticmethod - def list_comments( + async def list_comments( credentials: GithubCredentials, issue_url: str ) -> list[Output.CommentItem]: parsed_url = urlparse(issue_url) @@ -305,7 +305,7 @@ class GithubListCommentsBlock(Block): # Set convert_urls=False since we're already providing an API URL api = get_api(credentials, convert_urls=False) - response = api.get(api_url) + response = await api.get(api_url) comments = response.json() parsed_comments: list[GithubListCommentsBlock.Output.CommentItem] = [ { @@ -318,18 +318,19 @@ class GithubListCommentsBlock(Block): ] return parsed_comments - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - comments = self.list_comments( + comments = await self.list_comments( credentials, input_data.issue_url, ) - yield from (("comment", comment) for comment in comments) + for comment in comments: + yield "comment", comment yield "comments", comments @@ -381,24 +382,24 @@ class GithubMakeIssueBlock(Block): ) @staticmethod - def create_issue( + async def create_issue( credentials: GithubCredentials, repo_url: str, title: str, body: str ) -> tuple[int, str]: api = get_api(credentials) data = {"title": title, "body": body} issues_url = repo_url + "/issues" - response = api.post(issues_url, json=data) + response = await api.post(issues_url, json=data) issue = response.json() return issue["number"], issue["html_url"] - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - number, url = self.create_issue( + number, url = await self.create_issue( credentials, input_data.repo_url, input_data.title, @@ -451,25 +452,25 @@ class GithubReadIssueBlock(Block): ) @staticmethod - def read_issue( + async def read_issue( credentials: GithubCredentials, issue_url: str ) -> tuple[str, str, str]: api = get_api(credentials) - response = api.get(issue_url) + response = await api.get(issue_url) data = response.json() title = data.get("title", "No title found") body = data.get("body", "No body content found") user = data.get("user", {}).get("login", "No user found") return title, body, user - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - title, body, user = self.read_issue( + title, body, user = await self.read_issue( credentials, input_data.issue_url, ) @@ -531,30 +532,30 @@ class GithubListIssuesBlock(Block): ) @staticmethod - def list_issues( + async def list_issues( credentials: GithubCredentials, repo_url: str ) -> list[Output.IssueItem]: api = get_api(credentials) issues_url = repo_url + "/issues" - response = api.get(issues_url) + response = await api.get(issues_url) data = response.json() issues: list[GithubListIssuesBlock.Output.IssueItem] = [ {"title": issue["title"], "url": issue["html_url"]} for issue in data ] return issues - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - issues = self.list_issues( + for issue in await self.list_issues( credentials, input_data.repo_url, - ) - yield from (("issue", issue) for issue in issues) + ): + yield "issue", issue class GithubAddLabelBlock(Block): @@ -593,21 +594,23 @@ class GithubAddLabelBlock(Block): ) @staticmethod - def add_label(credentials: GithubCredentials, issue_url: str, label: str) -> str: + async def add_label( + credentials: GithubCredentials, issue_url: str, label: str + ) -> str: api = get_api(credentials) data = {"labels": [label]} labels_url = issue_url + "/labels" - api.post(labels_url, json=data) + await api.post(labels_url, json=data) return "Label added successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.add_label( + status = await self.add_label( credentials, input_data.issue_url, input_data.label, @@ -653,20 +656,22 @@ class GithubRemoveLabelBlock(Block): ) @staticmethod - def remove_label(credentials: GithubCredentials, issue_url: str, label: str) -> str: + async def remove_label( + credentials: GithubCredentials, issue_url: str, label: str + ) -> str: api = get_api(credentials) label_url = issue_url + f"/labels/{label}" - api.delete(label_url) + await api.delete(label_url) return "Label removed successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.remove_label( + status = await self.remove_label( credentials, input_data.issue_url, input_data.label, @@ -714,7 +719,7 @@ class GithubAssignIssueBlock(Block): ) @staticmethod - def assign_issue( + async def assign_issue( credentials: GithubCredentials, issue_url: str, assignee: str, @@ -722,17 +727,17 @@ class GithubAssignIssueBlock(Block): api = get_api(credentials) assignees_url = issue_url + "/assignees" data = {"assignees": [assignee]} - api.post(assignees_url, json=data) + await api.post(assignees_url, json=data) return "Issue assigned successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.assign_issue( + status = await self.assign_issue( credentials, input_data.issue_url, input_data.assignee, @@ -780,7 +785,7 @@ class GithubUnassignIssueBlock(Block): ) @staticmethod - def unassign_issue( + async def unassign_issue( credentials: GithubCredentials, issue_url: str, assignee: str, @@ -788,17 +793,17 @@ class GithubUnassignIssueBlock(Block): api = get_api(credentials) assignees_url = issue_url + "/assignees" data = {"assignees": [assignee]} - api.delete(assignees_url, json=data) + await api.delete(assignees_url, json=data) return "Issue unassigned successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.unassign_issue( + status = await self.unassign_issue( credentials, input_data.issue_url, input_data.assignee, diff --git a/autogpt_platform/backend/backend/blocks/github/pull_requests.py b/autogpt_platform/backend/backend/blocks/github/pull_requests.py index b29db0ff34..dbb940217c 100644 --- a/autogpt_platform/backend/backend/blocks/github/pull_requests.py +++ b/autogpt_platform/backend/backend/blocks/github/pull_requests.py @@ -65,28 +65,31 @@ class GithubListPullRequestsBlock(Block): ) @staticmethod - def list_prs(credentials: GithubCredentials, repo_url: str) -> list[Output.PRItem]: + async def list_prs( + credentials: GithubCredentials, repo_url: str + ) -> list[Output.PRItem]: api = get_api(credentials) pulls_url = repo_url + "/pulls" - response = api.get(pulls_url) + response = await api.get(pulls_url) data = response.json() pull_requests: list[GithubListPullRequestsBlock.Output.PRItem] = [ {"title": pr["title"], "url": pr["html_url"]} for pr in data ] return pull_requests - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - pull_requests = self.list_prs( + pull_requests = await self.list_prs( credentials, input_data.repo_url, ) - yield from (("pull_request", pr) for pr in pull_requests) + for pr in pull_requests: + yield "pull_request", pr class GithubMakePullRequestBlock(Block): @@ -153,7 +156,7 @@ class GithubMakePullRequestBlock(Block): ) @staticmethod - def create_pr( + async def create_pr( credentials: GithubCredentials, repo_url: str, title: str, @@ -164,11 +167,11 @@ class GithubMakePullRequestBlock(Block): api = get_api(credentials) pulls_url = repo_url + "/pulls" data = {"title": title, "body": body, "head": head, "base": base} - response = api.post(pulls_url, json=data) + response = await api.post(pulls_url, json=data) pr_data = response.json() return pr_data["number"], pr_data["html_url"] - def run( + async def run( self, input_data: Input, *, @@ -176,7 +179,7 @@ class GithubMakePullRequestBlock(Block): **kwargs, ) -> BlockOutput: try: - number, url = self.create_pr( + number, url = await self.create_pr( credentials, input_data.repo_url, input_data.title, @@ -242,39 +245,39 @@ class GithubReadPullRequestBlock(Block): ) @staticmethod - def read_pr(credentials: GithubCredentials, pr_url: str) -> tuple[str, str, str]: + async def read_pr( + credentials: GithubCredentials, pr_url: str + ) -> tuple[str, str, str]: api = get_api(credentials) - # Adjust the URL to access the issue endpoint for PR metadata issue_url = pr_url.replace("/pull/", "/issues/") - response = api.get(issue_url) + response = await api.get(issue_url) data = response.json() title = data.get("title", "No title found") body = data.get("body", "No body content found") - author = data.get("user", {}).get("login", "No user found") + author = data.get("user", {}).get("login", "Unknown author") return title, body, author @staticmethod - def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str: + async def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str: api = get_api(credentials) files_url = prepare_pr_api_url(pr_url=pr_url, path="files") - response = api.get(files_url) + response = await api.get(files_url) files = response.json() changes = [] for file in files: - filename = file.get("filename") - patch = file.get("patch") - if filename and patch: - changes.append(f"File: {filename}\n{patch}") - return "\n\n".join(changes) + filename = file.get("filename", "") + status = file.get("status", "") + changes.append(f"{filename}: {status}") + return "\n".join(changes) - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - title, body, author = self.read_pr( + title, body, author = await self.read_pr( credentials, input_data.pr_url, ) @@ -283,7 +286,7 @@ class GithubReadPullRequestBlock(Block): yield "author", author if input_data.include_pr_changes: - changes = self.read_pr_changes( + changes = await self.read_pr_changes( credentials, input_data.pr_url, ) @@ -330,16 +333,16 @@ class GithubAssignPRReviewerBlock(Block): ) @staticmethod - def assign_reviewer( + async def assign_reviewer( credentials: GithubCredentials, pr_url: str, reviewer: str ) -> str: api = get_api(credentials) reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers") data = {"reviewers": [reviewer]} - api.post(reviewers_url, json=data) + await api.post(reviewers_url, json=data) return "Reviewer assigned successfully" - def run( + async def run( self, input_data: Input, *, @@ -347,7 +350,7 @@ class GithubAssignPRReviewerBlock(Block): **kwargs, ) -> BlockOutput: try: - status = self.assign_reviewer( + status = await self.assign_reviewer( credentials, input_data.pr_url, input_data.reviewer, @@ -397,16 +400,16 @@ class GithubUnassignPRReviewerBlock(Block): ) @staticmethod - def unassign_reviewer( + async def unassign_reviewer( credentials: GithubCredentials, pr_url: str, reviewer: str ) -> str: api = get_api(credentials) reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers") data = {"reviewers": [reviewer]} - api.delete(reviewers_url, json=data) + await api.delete(reviewers_url, json=data) return "Reviewer unassigned successfully" - def run( + async def run( self, input_data: Input, *, @@ -414,7 +417,7 @@ class GithubUnassignPRReviewerBlock(Block): **kwargs, ) -> BlockOutput: try: - status = self.unassign_reviewer( + status = await self.unassign_reviewer( credentials, input_data.pr_url, input_data.reviewer, @@ -477,12 +480,12 @@ class GithubListPRReviewersBlock(Block): ) @staticmethod - def list_reviewers( + async def list_reviewers( credentials: GithubCredentials, pr_url: str ) -> list[Output.ReviewerItem]: api = get_api(credentials) reviewers_url = prepare_pr_api_url(pr_url=pr_url, path="requested_reviewers") - response = api.get(reviewers_url) + response = await api.get(reviewers_url) data = response.json() reviewers: list[GithubListPRReviewersBlock.Output.ReviewerItem] = [ {"username": reviewer["login"], "url": reviewer["html_url"]} @@ -490,18 +493,18 @@ class GithubListPRReviewersBlock(Block): ] return reviewers - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - reviewers = self.list_reviewers( + for reviewer in await self.list_reviewers( credentials, input_data.pr_url, - ) - yield from (("reviewer", reviewer) for reviewer in reviewers) + ): + yield "reviewer", reviewer def prepare_pr_api_url(pr_url: str, path: str) -> str: diff --git a/autogpt_platform/backend/backend/blocks/github/repo.py b/autogpt_platform/backend/backend/blocks/github/repo.py index 82bef9475b..f44cd95e1a 100644 --- a/autogpt_platform/backend/backend/blocks/github/repo.py +++ b/autogpt_platform/backend/backend/blocks/github/repo.py @@ -65,12 +65,12 @@ class GithubListTagsBlock(Block): ) @staticmethod - def list_tags( + async def list_tags( credentials: GithubCredentials, repo_url: str ) -> list[Output.TagItem]: api = get_api(credentials) tags_url = repo_url + "/tags" - response = api.get(tags_url) + response = await api.get(tags_url) data = response.json() repo_path = repo_url.replace("https://github.com/", "") tags: list[GithubListTagsBlock.Output.TagItem] = [ @@ -82,18 +82,19 @@ class GithubListTagsBlock(Block): ] return tags - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - tags = self.list_tags( + tags = await self.list_tags( credentials, input_data.repo_url, ) - yield from (("tag", tag) for tag in tags) + for tag in tags: + yield "tag", tag class GithubListBranchesBlock(Block): @@ -147,12 +148,12 @@ class GithubListBranchesBlock(Block): ) @staticmethod - def list_branches( + async def list_branches( credentials: GithubCredentials, repo_url: str ) -> list[Output.BranchItem]: api = get_api(credentials) branches_url = repo_url + "/branches" - response = api.get(branches_url) + response = await api.get(branches_url) data = response.json() repo_path = repo_url.replace("https://github.com/", "") branches: list[GithubListBranchesBlock.Output.BranchItem] = [ @@ -164,18 +165,19 @@ class GithubListBranchesBlock(Block): ] return branches - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - branches = self.list_branches( + branches = await self.list_branches( credentials, input_data.repo_url, ) - yield from (("branch", branch) for branch in branches) + for branch in branches: + yield "branch", branch class GithubListDiscussionsBlock(Block): @@ -234,7 +236,7 @@ class GithubListDiscussionsBlock(Block): ) @staticmethod - def list_discussions( + async def list_discussions( credentials: GithubCredentials, repo_url: str, num_discussions: int ) -> list[Output.DiscussionItem]: api = get_api(credentials) @@ -254,7 +256,7 @@ class GithubListDiscussionsBlock(Block): } """ variables = {"owner": owner, "repo": repo, "num": num_discussions} - response = api.post( + response = await api.post( "https://api.github.com/graphql", json={"query": query, "variables": variables}, ) @@ -265,17 +267,20 @@ class GithubListDiscussionsBlock(Block): ] return discussions - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - discussions = self.list_discussions( - credentials, input_data.repo_url, input_data.num_discussions + discussions = await self.list_discussions( + credentials, + input_data.repo_url, + input_data.num_discussions, ) - yield from (("discussion", discussion) for discussion in discussions) + for discussion in discussions: + yield "discussion", discussion class GithubListReleasesBlock(Block): @@ -329,30 +334,31 @@ class GithubListReleasesBlock(Block): ) @staticmethod - def list_releases( + async def list_releases( credentials: GithubCredentials, repo_url: str ) -> list[Output.ReleaseItem]: api = get_api(credentials) releases_url = repo_url + "/releases" - response = api.get(releases_url) + response = await api.get(releases_url) data = response.json() releases: list[GithubListReleasesBlock.Output.ReleaseItem] = [ {"name": release["name"], "url": release["html_url"]} for release in data ] return releases - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - releases = self.list_releases( + releases = await self.list_releases( credentials, input_data.repo_url, ) - yield from (("release", release) for release in releases) + for release in releases: + yield "release", release class GithubReadFileBlock(Block): @@ -405,40 +411,40 @@ class GithubReadFileBlock(Block): ) @staticmethod - def read_file( + async def read_file( credentials: GithubCredentials, repo_url: str, file_path: str, branch: str ) -> tuple[str, int]: api = get_api(credentials) content_url = repo_url + f"/contents/{file_path}?ref={branch}" - response = api.get(content_url) - content = response.json() + response = await api.get(content_url) + data = response.json() - if isinstance(content, list): + if isinstance(data, list): # Multiple entries of different types exist at this path - if not (file := next((f for f in content if f["type"] == "file"), None)): + if not (file := next((f for f in data if f["type"] == "file"), None)): raise TypeError("Not a file") - content = file + data = file - if content["type"] != "file": + if data["type"] != "file": raise TypeError("Not a file") - return content["content"], content["size"] + return data["content"], data["size"] - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - raw_content, size = self.read_file( + content, size = await self.read_file( credentials, input_data.repo_url, - input_data.file_path.lstrip("/"), + input_data.file_path, input_data.branch, ) - yield "raw_content", raw_content - yield "text_content", base64.b64decode(raw_content).decode("utf-8") + yield "raw_content", content + yield "text_content", base64.b64decode(content).decode("utf-8") yield "size", size @@ -515,52 +521,55 @@ class GithubReadFolderBlock(Block): ) @staticmethod - def read_folder( + async def read_folder( credentials: GithubCredentials, repo_url: str, folder_path: str, branch: str ) -> tuple[list[Output.FileEntry], list[Output.DirEntry]]: api = get_api(credentials) contents_url = repo_url + f"/contents/{folder_path}?ref={branch}" - response = api.get(contents_url) - content = response.json() + response = await api.get(contents_url) + data = response.json() - if not isinstance(content, list): + if not isinstance(data, list): raise TypeError("Not a folder") - files = [ + files: list[GithubReadFolderBlock.Output.FileEntry] = [ GithubReadFolderBlock.Output.FileEntry( name=entry["name"], path=entry["path"], size=entry["size"], ) - for entry in content + for entry in data if entry["type"] == "file" ] - dirs = [ + + dirs: list[GithubReadFolderBlock.Output.DirEntry] = [ GithubReadFolderBlock.Output.DirEntry( name=entry["name"], path=entry["path"], ) - for entry in content + for entry in data if entry["type"] == "dir" ] return files, dirs - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - files, dirs = self.read_folder( + files, dirs = await self.read_folder( credentials, input_data.repo_url, input_data.folder_path.lstrip("/"), input_data.branch, ) - yield from (("file", file) for file in files) - yield from (("dir", dir) for dir in dirs) + for file in files: + yield "file", file + for dir in dirs: + yield "dir", dir class GithubMakeBranchBlock(Block): @@ -606,32 +615,35 @@ class GithubMakeBranchBlock(Block): ) @staticmethod - def create_branch( + async def create_branch( credentials: GithubCredentials, repo_url: str, new_branch: str, source_branch: str, ) -> str: api = get_api(credentials) - # Get the SHA of the source branch ref_url = repo_url + f"/git/refs/heads/{source_branch}" - response = api.get(ref_url) - sha = response.json()["object"]["sha"] + response = await api.get(ref_url) + data = response.json() + sha = data["object"]["sha"] # Create the new branch - create_ref_url = repo_url + "/git/refs" - data = {"ref": f"refs/heads/{new_branch}", "sha": sha} - response = api.post(create_ref_url, json=data) + new_ref_url = repo_url + "/git/refs" + data = { + "ref": f"refs/heads/{new_branch}", + "sha": sha, + } + response = await api.post(new_ref_url, json=data) return "Branch created successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.create_branch( + status = await self.create_branch( credentials, input_data.repo_url, input_data.new_branch, @@ -678,22 +690,22 @@ class GithubDeleteBranchBlock(Block): ) @staticmethod - def delete_branch( + async def delete_branch( credentials: GithubCredentials, repo_url: str, branch: str ) -> str: api = get_api(credentials) ref_url = repo_url + f"/git/refs/heads/{branch}" - api.delete(ref_url) + await api.delete(ref_url) return "Branch deleted successfully" - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - status = self.delete_branch( + status = await self.delete_branch( credentials, input_data.repo_url, input_data.branch, @@ -761,7 +773,7 @@ class GithubCreateFileBlock(Block): ) @staticmethod - def create_file( + async def create_file( credentials: GithubCredentials, repo_url: str, file_path: str, @@ -770,23 +782,18 @@ class GithubCreateFileBlock(Block): commit_message: str, ) -> tuple[str, str]: api = get_api(credentials) - # Convert content to base64 - content_bytes = content.encode("utf-8") - content_base64 = base64.b64encode(content_bytes).decode("utf-8") - - # Create the file using the GitHub API - contents_url = f"{repo_url}/contents/{file_path}" + contents_url = repo_url + f"/contents/{file_path}" + content_base64 = base64.b64encode(content.encode()).decode() data = { "message": commit_message, "content": content_base64, "branch": branch, } - response = api.put(contents_url, json=data) - result = response.json() + response = await api.put(contents_url, json=data) + data = response.json() + return data["content"]["html_url"], data["commit"]["sha"] - return result["content"]["html_url"], result["commit"]["sha"] - - def run( + async def run( self, input_data: Input, *, @@ -794,7 +801,7 @@ class GithubCreateFileBlock(Block): **kwargs, ) -> BlockOutput: try: - url, sha = self.create_file( + url, sha = await self.create_file( credentials, input_data.repo_url, input_data.file_path, @@ -866,7 +873,7 @@ class GithubUpdateFileBlock(Block): ) @staticmethod - def update_file( + async def update_file( credentials: GithubCredentials, repo_url: str, file_path: str, @@ -875,30 +882,24 @@ class GithubUpdateFileBlock(Block): commit_message: str, ) -> tuple[str, str]: api = get_api(credentials) - - # First get the current file to get its SHA - contents_url = f"{repo_url}/contents/{file_path}" + contents_url = repo_url + f"/contents/{file_path}" params = {"ref": branch} - response = api.get(contents_url, params=params) - current_file = response.json() + response = await api.get(contents_url, params=params) + data = response.json() # Convert new content to base64 - content_bytes = content.encode("utf-8") - content_base64 = base64.b64encode(content_bytes).decode("utf-8") - - # Update the file + content_base64 = base64.b64encode(content.encode()).decode() data = { "message": commit_message, "content": content_base64, - "sha": current_file["sha"], + "sha": data["sha"], "branch": branch, } - response = api.put(contents_url, json=data) - result = response.json() + response = await api.put(contents_url, json=data) + data = response.json() + return data["content"]["html_url"], data["commit"]["sha"] - return result["content"]["html_url"], result["commit"]["sha"] - - def run( + async def run( self, input_data: Input, *, @@ -906,7 +907,7 @@ class GithubUpdateFileBlock(Block): **kwargs, ) -> BlockOutput: try: - url, sha = self.update_file( + url, sha = await self.update_file( credentials, input_data.repo_url, input_data.file_path, @@ -981,7 +982,7 @@ class GithubCreateRepositoryBlock(Block): ) @staticmethod - def create_repository( + async def create_repository( credentials: GithubCredentials, name: str, description: str, @@ -989,24 +990,19 @@ class GithubCreateRepositoryBlock(Block): auto_init: bool, gitignore_template: str, ) -> tuple[str, str]: - api = get_api(credentials, convert_urls=False) # Disable URL conversion + api = get_api(credentials) data = { "name": name, "description": description, "private": private, "auto_init": auto_init, + "gitignore_template": gitignore_template, } + response = await api.post("https://api.github.com/user/repos", json=data) + data = response.json() + return data["html_url"], data["clone_url"] - if gitignore_template: - data["gitignore_template"] = gitignore_template - - # Create repository using the user endpoint - response = api.post("https://api.github.com/user/repos", json=data) - result = response.json() - - return result["html_url"], result["clone_url"] - - def run( + async def run( self, input_data: Input, *, @@ -1014,7 +1010,7 @@ class GithubCreateRepositoryBlock(Block): **kwargs, ) -> BlockOutput: try: - url, clone_url = self.create_repository( + url, clone_url = await self.create_repository( credentials, input_data.name, input_data.description, @@ -1081,17 +1077,13 @@ class GithubListStargazersBlock(Block): ) @staticmethod - def list_stargazers( + async def list_stargazers( credentials: GithubCredentials, repo_url: str ) -> list[Output.StargazerItem]: api = get_api(credentials) - # Add /stargazers to the repo URL to get stargazers endpoint - stargazers_url = f"{repo_url}/stargazers" - # Set accept header to get starred_at timestamp - headers = {"Accept": "application/vnd.github.star+json"} - response = api.get(stargazers_url, headers=headers) + stargazers_url = repo_url + "/stargazers" + response = await api.get(stargazers_url) data = response.json() - stargazers: list[GithubListStargazersBlock.Output.StargazerItem] = [ { "username": stargazer["login"], @@ -1101,18 +1093,16 @@ class GithubListStargazersBlock(Block): ] return stargazers - def run( + async def run( self, input_data: Input, *, credentials: GithubCredentials, **kwargs, ) -> BlockOutput: - try: - stargazers = self.list_stargazers( - credentials, - input_data.repo_url, - ) - yield from (("stargazer", stargazer) for stargazer in stargazers) - except Exception as e: - yield "error", str(e) + stargazers = await self.list_stargazers( + credentials, + input_data.repo_url, + ) + for stargazer in stargazers: + yield "stargazer", stargazer diff --git a/autogpt_platform/backend/backend/blocks/github/statuses.py b/autogpt_platform/backend/backend/blocks/github/statuses.py index a69b0e3d61..a7e2b006aa 100644 --- a/autogpt_platform/backend/backend/blocks/github/statuses.py +++ b/autogpt_platform/backend/backend/blocks/github/statuses.py @@ -115,7 +115,7 @@ class GithubCreateStatusBlock(Block): ) @staticmethod - def create_status( + async def create_status( credentials: GithubFineGrainedAPICredentials, repo_url: str, sha: str, @@ -144,7 +144,9 @@ class GithubCreateStatusBlock(Block): data.description = description status_url = f"{repo_url}/statuses/{sha}" - response = api.post(status_url, data=data.model_dump_json(exclude_none=True)) + response = await api.post( + status_url, data=data.model_dump_json(exclude_none=True) + ) result = response.json() return { @@ -158,7 +160,7 @@ class GithubCreateStatusBlock(Block): "updated_at": result["updated_at"], } - def run( + async def run( self, input_data: Input, *, @@ -166,7 +168,7 @@ class GithubCreateStatusBlock(Block): **kwargs, ) -> BlockOutput: try: - result = self.create_status( + result = await self.create_status( credentials=credentials, repo_url=input_data.repo_url, sha=input_data.sha, diff --git a/autogpt_platform/backend/backend/blocks/github/triggers.py b/autogpt_platform/backend/backend/blocks/github/triggers.py index 0410ed02a3..83b1689b89 100644 --- a/autogpt_platform/backend/backend/blocks/github/triggers.py +++ b/autogpt_platform/backend/backend/blocks/github/triggers.py @@ -53,7 +53,7 @@ class GitHubTriggerBase: description="Error message if the payload could not be processed" ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "payload", input_data.payload yield "triggered_by_user", input_data.payload["sender"] @@ -148,8 +148,9 @@ class GithubPullRequestTriggerBlock(GitHubTriggerBase, Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore - yield from super().run(input_data, **kwargs) + async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore + async for name, value in super().run(input_data, **kwargs): + yield name, value yield "event", input_data.payload["action"] yield "number", input_data.payload["number"] yield "pull_request", input_data.payload["pull_request"] diff --git a/autogpt_platform/backend/backend/blocks/google/calendar.py b/autogpt_platform/backend/backend/blocks/google/calendar.py index f119527f26..27cc9e5958 100644 --- a/autogpt_platform/backend/backend/blocks/google/calendar.py +++ b/autogpt_platform/backend/backend/blocks/google/calendar.py @@ -1,3 +1,4 @@ +import asyncio import enum import uuid from datetime import datetime, timedelta, timezone @@ -168,7 +169,7 @@ class GoogleCalendarReadEventsBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: try: @@ -180,7 +181,8 @@ class GoogleCalendarReadEventsBlock(Block): ) # Call Google Calendar API - result = self._read_calendar( + result = await asyncio.to_thread( + self._read_calendar, service=service, calendarId=input_data.calendar_id, time_min=input_data.start_time.isoformat(), @@ -477,12 +479,13 @@ class GoogleCalendarCreateEventBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: try: service = self._build_service(credentials, **kwargs) + # Create event body # Get start and end times based on the timing option if input_data.timing.discriminator == "exact_timing": start_datetime = input_data.timing.start_datetime @@ -543,7 +546,8 @@ class GoogleCalendarCreateEventBlock(Block): event_body["recurrence"] = [rule] # Create the event - result = self._create_event( + result = await asyncio.to_thread( + self._create_event, service=service, calendar_id=input_data.calendar_id, event_body=event_body, @@ -551,8 +555,9 @@ class GoogleCalendarCreateEventBlock(Block): conference_data_version=1 if input_data.add_google_meet else 0, ) - yield "event_id", result.get("id", "") - yield "event_link", result.get("htmlLink", "") + yield "event_id", result["id"] + yield "event_link", result["htmlLink"] + except Exception as e: yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/google/gmail.py b/autogpt_platform/backend/backend/blocks/google/gmail.py index 780cc1b16f..a6d2db3665 100644 --- a/autogpt_platform/backend/backend/blocks/google/gmail.py +++ b/autogpt_platform/backend/backend/blocks/google/gmail.py @@ -1,3 +1,4 @@ +import asyncio import base64 from email.utils import parseaddr from typing import List @@ -128,11 +129,13 @@ class GmailReadBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: - service = self._build_service(credentials, **kwargs) - messages = self._read_emails(service, input_data.query, input_data.max_results) + service = GmailReadBlock._build_service(credentials, **kwargs) + messages = await asyncio.to_thread( + self._read_emails, service, input_data.query, input_data.max_results + ) for email in messages: yield "email", email yield "emails", messages @@ -286,14 +289,18 @@ class GmailSendBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = GmailReadBlock._build_service(credentials, **kwargs) - send_result = self._send_email( - service, input_data.to, input_data.subject, input_data.body + result = await asyncio.to_thread( + self._send_email, + service, + input_data.to, + input_data.subject, + input_data.body, ) - yield "result", send_result + yield "result", result def _send_email(self, service, to: str, subject: str, body: str) -> dict: if not to or not subject or not body: @@ -358,12 +365,12 @@ class GmailListLabelsBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = GmailReadBlock._build_service(credentials, **kwargs) - labels = self._list_labels(service) - yield "result", labels + result = await asyncio.to_thread(self._list_labels, service) + yield "result", result def _list_labels(self, service) -> list[dict]: results = service.users().labels().list(userId="me").execute() @@ -419,11 +426,13 @@ class GmailAddLabelBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = GmailReadBlock._build_service(credentials, **kwargs) - result = self._add_label(service, input_data.message_id, input_data.label_name) + result = await asyncio.to_thread( + 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: @@ -502,12 +511,12 @@ class GmailRemoveLabelBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = GmailReadBlock._build_service(credentials, **kwargs) - result = self._remove_label( - service, input_data.message_id, input_data.label_name + result = await asyncio.to_thread( + self._remove_label, service, input_data.message_id, input_data.label_name ) yield "result", result diff --git a/autogpt_platform/backend/backend/blocks/google/sheets.py b/autogpt_platform/backend/backend/blocks/google/sheets.py index 141e359184..6a866051ff 100644 --- a/autogpt_platform/backend/backend/blocks/google/sheets.py +++ b/autogpt_platform/backend/backend/blocks/google/sheets.py @@ -1,3 +1,5 @@ +import asyncio + from google.oauth2.credentials import Credentials from googleapiclient.discovery import build @@ -68,11 +70,13 @@ class GoogleSheetsReadBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = self._build_service(credentials, **kwargs) - data = self._read_sheet(service, input_data.spreadsheet_id, input_data.range) + data = await asyncio.to_thread( + self._read_sheet, service, input_data.spreadsheet_id, input_data.range + ) yield "result", data @staticmethod @@ -157,11 +161,12 @@ class GoogleSheetsWriteBlock(Block): }, ) - def run( + async def run( self, input_data: Input, *, credentials: GoogleCredentials, **kwargs ) -> BlockOutput: service = GoogleSheetsReadBlock._build_service(credentials, **kwargs) - result = self._write_sheet( + result = await asyncio.to_thread( + self._write_sheet, service, input_data.spreadsheet_id, input_data.range, diff --git a/autogpt_platform/backend/backend/blocks/google_maps.py b/autogpt_platform/backend/backend/blocks/google_maps.py index 9e7f793531..01e81c69c9 100644 --- a/autogpt_platform/backend/backend/blocks/google_maps.py +++ b/autogpt_platform/backend/backend/blocks/google_maps.py @@ -103,7 +103,7 @@ class GoogleMapsSearchBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: places = self.search_places( diff --git a/autogpt_platform/backend/backend/blocks/helpers/http.py b/autogpt_platform/backend/backend/blocks/helpers/http.py index 33579ba0d9..f68b9f5a8b 100644 --- a/autogpt_platform/backend/backend/blocks/helpers/http.py +++ b/autogpt_platform/backend/backend/blocks/helpers/http.py @@ -1,14 +1,17 @@ from typing import Any, Optional -from backend.util.request import requests +from backend.util.request import Requests class GetRequest: @classmethod - def get_request( + async def get_request( cls, url: str, headers: Optional[dict] = None, json: bool = False ) -> Any: if headers is None: headers = {} - response = requests.get(url, headers=headers) - return response.json() if json else response.text + response = await Requests().get(url, headers=headers) + if json: + return response.json() + else: + return response.text() diff --git a/autogpt_platform/backend/backend/blocks/http.py b/autogpt_platform/backend/backend/blocks/http.py index 40ed6a684b..9bb6d9b55e 100644 --- a/autogpt_platform/backend/backend/blocks/http.py +++ b/autogpt_platform/backend/backend/blocks/http.py @@ -1,10 +1,10 @@ import json import logging from enum import Enum -from io import BufferedReader +from io import BytesIO from pathlib import Path -from requests.exceptions import HTTPError, RequestException +import aiofiles from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField @@ -14,7 +14,7 @@ from backend.util.file import ( get_mime_type, store_media_file, ) -from backend.util.request import requests +from backend.util.request import Requests logger = logging.getLogger(name=__name__) @@ -77,39 +77,35 @@ class SendWebRequestBlock(Block): ) @staticmethod - def _prepare_files( + async def _prepare_files( graph_exec_id: str, files_name: str, files: list[MediaFileType], - ) -> tuple[list[tuple[str, tuple[str, BufferedReader, str]]], list[BufferedReader]]: - """Convert the `files` mapping into the structure expected by `requests`. - - Returns a tuple of (**files_payload**, **open_handles**) so we can close handles later. + ) -> list[tuple[str, tuple[str, BytesIO, str]]]: """ - files_payload: list[tuple[str, tuple[str, BufferedReader, str]]] = [] - open_handles: list[BufferedReader] = [] + Prepare files for the request by storing them and reading their content. + Returns a list of tuples in the format: + (files_name, (filename, BytesIO, mime_type)) + """ + files_payload: list[tuple[str, tuple[str, BytesIO, str]]] = [] for media in files: # Normalise to a list so we can repeat the same key - rel_path = store_media_file(graph_exec_id, media, return_content=False) + rel_path = await store_media_file( + graph_exec_id, media, return_content=False + ) abs_path = get_exec_file_path(graph_exec_id, rel_path) - try: - handle = open(abs_path, "rb") - except Exception as e: - for h in open_handles: - try: - h.close() - except Exception: - pass - raise RuntimeError(f"Failed to open file '{abs_path}': {e}") from e + async with aiofiles.open(abs_path, "rb") as f: + content = await f.read() + handle = BytesIO(content) + mime = get_mime_type(abs_path) + files_payload.append((files_name, (Path(abs_path).name, handle, mime))) - open_handles.append(handle) - mime = get_mime_type(abs_path) - files_payload.append((files_name, (Path(abs_path).name, handle, mime))) + return files_payload - return files_payload, open_handles - - def run(self, input_data: Input, *, graph_exec_id: str, **kwargs) -> BlockOutput: + async def run( + self, input_data: Input, *, graph_exec_id: str, **kwargs + ) -> BlockOutput: # ─── Parse/normalise body ──────────────────────────────────── body = input_data.body if isinstance(body, str): @@ -136,10 +132,9 @@ class SendWebRequestBlock(Block): # ─── Prepare files (if any) ────────────────────────────────── use_files = bool(input_data.files) - files_payload: list[tuple[str, tuple[str, BufferedReader, str]]] = [] - open_handles: list[BufferedReader] = [] + files_payload: list[tuple[str, tuple[str, BytesIO, str]]] = [] if use_files: - files_payload, open_handles = self._prepare_files( + files_payload = await self._prepare_files( graph_exec_id, input_data.files_name, input_data.files ) @@ -150,47 +145,27 @@ class SendWebRequestBlock(Block): ) # ─── Execute request ───────────────────────────────────────── - try: - response = requests.request( - input_data.method.value, - input_data.url, - headers=input_data.headers, - files=files_payload if use_files else None, - # * If files → multipart ⇒ pass form‑fields via data= - data=body if not input_data.json_format else None, - # * Else, choose JSON vs url‑encoded based on flag - json=body if (input_data.json_format and not use_files) else None, - ) + response = await Requests().request( + input_data.method.value, + input_data.url, + headers=input_data.headers, + files=files_payload if use_files else None, + # * If files → multipart ⇒ pass form‑fields via data= + data=body if not input_data.json_format else None, + # * Else, choose JSON vs url‑encoded based on flag + json=body if (input_data.json_format and not use_files) else None, + ) - # Decide how to parse the response - if input_data.json_format or response.headers.get( - "content-type", "" - ).startswith("application/json"): - result = ( - None - if (response.status_code == 204 or not response.content.strip()) - else response.json() - ) - else: - result = response.text + # Decide how to parse the response + if response.headers.get("content-type", "").startswith("application/json"): + result = None if response.status == 204 else response.json() + else: + result = response.text() - # Yield according to status code bucket - if 200 <= response.status_code < 300: - yield "response", result - elif 400 <= response.status_code < 500: - yield "client_error", result - else: - yield "server_error", result - - except HTTPError as e: - yield "error", f"HTTP error: {str(e)}" - except RequestException as e: - yield "error", f"Request error: {str(e)}" - except Exception as e: - yield "error", str(e) - finally: - for h in open_handles: - try: - h.close() - except Exception: - pass + # Yield according to status code bucket + if 200 <= response.status < 300: + yield "response", result + elif 400 <= response.status < 500: + yield "client_error", result + else: + yield "server_error", result diff --git a/autogpt_platform/backend/backend/blocks/hubspot/company.py b/autogpt_platform/backend/backend/blocks/hubspot/company.py index 002aeeef81..3026112259 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/company.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/company.py @@ -35,7 +35,7 @@ class HubSpotCompanyBlock(Block): output_schema=HubSpotCompanyBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs ) -> BlockOutput: base_url = "https://api.hubapi.com/crm/v3/objects/companies" @@ -45,7 +45,7 @@ class HubSpotCompanyBlock(Block): } if input_data.operation == "create": - response = Requests().post( + response = await Requests().post( base_url, headers=headers, json={"properties": input_data.company_data} ) result = response.json() @@ -67,14 +67,16 @@ class HubSpotCompanyBlock(Block): } ] } - response = Requests().post(search_url, headers=headers, json=search_data) - result = response.json() - yield "company", result.get("results", [{}])[0] + search_response = await Requests().post( + search_url, headers=headers, json=search_data + ) + search_result = search_response.json() + yield "search_company", search_result.get("results", [{}])[0] yield "status", "retrieved" elif input_data.operation == "update": # First get company ID by domain - search_response = Requests().post( + search_response = await Requests().post( f"{base_url}/search", headers=headers, json={ @@ -91,10 +93,11 @@ class HubSpotCompanyBlock(Block): ] }, ) - company_id = search_response.json().get("results", [{}])[0].get("id") + search_result = search_response.json() + company_id = search_result.get("results", [{}])[0].get("id") if company_id: - response = Requests().patch( + response = await Requests().patch( f"{base_url}/{company_id}", headers=headers, json={"properties": input_data.company_data}, diff --git a/autogpt_platform/backend/backend/blocks/hubspot/contact.py b/autogpt_platform/backend/backend/blocks/hubspot/contact.py index b930062a1c..2029adaca1 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/contact.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/contact.py @@ -35,7 +35,7 @@ class HubSpotContactBlock(Block): output_schema=HubSpotContactBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs ) -> BlockOutput: base_url = "https://api.hubapi.com/crm/v3/objects/contacts" @@ -45,7 +45,7 @@ class HubSpotContactBlock(Block): } if input_data.operation == "create": - response = Requests().post( + response = await Requests().post( base_url, headers=headers, json={"properties": input_data.contact_data} ) result = response.json() @@ -53,7 +53,6 @@ class HubSpotContactBlock(Block): yield "status", "created" elif input_data.operation == "get": - # Search for contact by email search_url = f"{base_url}/search" search_data = { "filterGroups": [ @@ -68,13 +67,15 @@ class HubSpotContactBlock(Block): } ] } - response = Requests().post(search_url, headers=headers, json=search_data) + response = await Requests().post( + search_url, headers=headers, json=search_data + ) result = response.json() yield "contact", result.get("results", [{}])[0] yield "status", "retrieved" elif input_data.operation == "update": - search_response = Requests().post( + search_response = await Requests().post( f"{base_url}/search", headers=headers, json={ @@ -91,10 +92,11 @@ class HubSpotContactBlock(Block): ] }, ) - contact_id = search_response.json().get("results", [{}])[0].get("id") + search_result = search_response.json() + contact_id = search_result.get("results", [{}])[0].get("id") if contact_id: - response = Requests().patch( + response = await Requests().patch( f"{base_url}/{contact_id}", headers=headers, json={"properties": input_data.contact_data}, diff --git a/autogpt_platform/backend/backend/blocks/hubspot/engagement.py b/autogpt_platform/backend/backend/blocks/hubspot/engagement.py index eb22a00f2a..7e4dbc3d01 100644 --- a/autogpt_platform/backend/backend/blocks/hubspot/engagement.py +++ b/autogpt_platform/backend/backend/blocks/hubspot/engagement.py @@ -42,7 +42,7 @@ class HubSpotEngagementBlock(Block): output_schema=HubSpotEngagementBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: HubSpotCredentials, **kwargs ) -> BlockOutput: base_url = "https://api.hubapi.com" @@ -66,7 +66,9 @@ class HubSpotEngagementBlock(Block): } } - response = Requests().post(email_url, headers=headers, json=email_data) + response = await Requests().post( + email_url, headers=headers, json=email_data + ) result = response.json() yield "result", result yield "status", "email_sent" @@ -80,7 +82,9 @@ class HubSpotEngagementBlock(Block): params = {"limit": 100, "after": from_date.isoformat()} - response = Requests().get(engagement_url, headers=headers, params=params) + response = await Requests().get( + engagement_url, headers=headers, params=params + ) engagements = response.json() # Process engagement metrics diff --git a/autogpt_platform/backend/backend/blocks/ideogram.py b/autogpt_platform/backend/backend/blocks/ideogram.py index 66c2380d12..468f8f1d1e 100644 --- a/autogpt_platform/backend/backend/blocks/ideogram.py +++ b/autogpt_platform/backend/backend/blocks/ideogram.py @@ -196,13 +196,13 @@ class IdeogramModelBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: seed = input_data.seed # Step 1: Generate the image - result = self.run_model( + result = await self.run_model( api_key=credentials.api_key, model_name=input_data.ideogram_model_name.value, prompt=input_data.prompt, @@ -217,14 +217,14 @@ class IdeogramModelBlock(Block): # Step 2: Upscale the image if requested if input_data.upscale == UpscaleOption.AI_UPSCALE: - result = self.upscale_image( + result = await self.upscale_image( api_key=credentials.api_key, image_url=result, ) yield "result", result - def run_model( + async def run_model( self, api_key: SecretStr, model_name: str, @@ -267,12 +267,12 @@ class IdeogramModelBlock(Block): } try: - response = Requests().post(url, json=data, headers=headers) + 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)}") - def upscale_image(self, api_key: SecretStr, image_url: str): + async def upscale_image(self, api_key: SecretStr, image_url: str): url = "https://api.ideogram.ai/upscale" headers = { "Api-Key": api_key.get_secret_value(), @@ -280,21 +280,22 @@ class IdeogramModelBlock(Block): try: # Step 1: Download the image from the provided URL - image_response = Requests().get(image_url) + response = await Requests().get(image_url) + image_content = response.content # Step 2: Send the downloaded image to the upscale API files = { - "image_file": ("image.png", image_response.content, "image/png"), + "image_file": ("image.png", image_content, "image/png"), } - response = Requests().post( + response = await Requests().post( url, headers=headers, data={"image_request": "{}"}, files=files, ) - return response.json()["data"][0]["url"] + return (response.json())["data"][0]["url"] except RequestException as e: raise Exception(f"Failed to upscale image: {str(e)}") diff --git a/autogpt_platform/backend/backend/blocks/io.py b/autogpt_platform/backend/backend/blocks/io.py index f9f9b85fb0..c42e6c1dd7 100644 --- a/autogpt_platform/backend/backend/blocks/io.py +++ b/autogpt_platform/backend/backend/blocks/io.py @@ -95,7 +95,7 @@ class AgentInputBlock(Block): } ) - def run(self, input_data: Input, *args, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput: if input_data.value is not None: yield "result", input_data.value @@ -186,7 +186,7 @@ class AgentOutputBlock(Block): static_output=True, ) - def run(self, input_data: Input, *args, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput: """ Attempts to format the recorded_value using the fmt_string if provided. If formatting fails or no fmt_string is given, returns the original recorded_value. @@ -436,7 +436,7 @@ class AgentFileInputBlock(AgentInputBlock): ], ) - def run( + async def run( self, input_data: Input, *, @@ -446,7 +446,7 @@ class AgentFileInputBlock(AgentInputBlock): if not input_data.value: return - file_path = store_media_file( + file_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.value, return_content=False, diff --git a/autogpt_platform/backend/backend/blocks/iteration.py b/autogpt_platform/backend/backend/blocks/iteration.py index 0159e62d21..c0b66a2ed0 100644 --- a/autogpt_platform/backend/backend/blocks/iteration.py +++ b/autogpt_platform/backend/backend/blocks/iteration.py @@ -53,7 +53,7 @@ class StepThroughItemsBlock(Block): test_mock={}, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: for data in [input_data.items, input_data.items_object, input_data.items_str]: if not data: continue diff --git a/autogpt_platform/backend/backend/blocks/jina/chunking.py b/autogpt_platform/backend/backend/blocks/jina/chunking.py index 75a5d858f2..052fa8e815 100644 --- a/autogpt_platform/backend/backend/blocks/jina/chunking.py +++ b/autogpt_platform/backend/backend/blocks/jina/chunking.py @@ -35,7 +35,7 @@ class JinaChunkingBlock(Block): output_schema=JinaChunkingBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: JinaCredentials, **kwargs ) -> BlockOutput: url = "https://segment.jina.ai/" @@ -55,7 +55,7 @@ class JinaChunkingBlock(Block): "max_chunk_length": str(input_data.max_chunk_length), } - response = Requests().post(url, headers=headers, json=data) + response = await Requests().post(url, headers=headers, json=data) result = response.json() all_chunks.extend(result.get("chunks", [])) diff --git a/autogpt_platform/backend/backend/blocks/jina/embeddings.py b/autogpt_platform/backend/backend/blocks/jina/embeddings.py index dd7966d500..abc2f9d6ae 100644 --- a/autogpt_platform/backend/backend/blocks/jina/embeddings.py +++ b/autogpt_platform/backend/backend/blocks/jina/embeddings.py @@ -29,7 +29,7 @@ class JinaEmbeddingBlock(Block): output_schema=JinaEmbeddingBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: JinaCredentials, **kwargs ) -> BlockOutput: url = "https://api.jina.ai/v1/embeddings" @@ -38,6 +38,6 @@ class JinaEmbeddingBlock(Block): "Authorization": f"Bearer {credentials.api_key.get_secret_value()}", } data = {"input": input_data.texts, "model": input_data.model} - response = Requests().post(url, headers=headers, json=data) + response = await Requests().post(url, headers=headers, json=data) embeddings = [e["embedding"] for e in response.json()["data"]] yield "embeddings", embeddings diff --git a/autogpt_platform/backend/backend/blocks/jina/fact_checker.py b/autogpt_platform/backend/backend/blocks/jina/fact_checker.py index 91c17f564e..9cf1e277fd 100644 --- a/autogpt_platform/backend/backend/blocks/jina/fact_checker.py +++ b/autogpt_platform/backend/backend/blocks/jina/fact_checker.py @@ -7,7 +7,7 @@ from backend.blocks.jina._auth import ( ) from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema from backend.data.model import SchemaField -from backend.util.request import requests +from backend.util.request import Requests class FactCheckerBlock(Block): @@ -34,7 +34,7 @@ class FactCheckerBlock(Block): output_schema=FactCheckerBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: JinaCredentials, **kwargs ) -> BlockOutput: encoded_statement = quote(input_data.statement) @@ -45,8 +45,7 @@ class FactCheckerBlock(Block): "Authorization": f"Bearer {credentials.api_key.get_secret_value()}", } - response = requests.get(url, headers=headers) - response.raise_for_status() + response = await Requests().get(url, headers=headers) data = response.json() if "data" in data: diff --git a/autogpt_platform/backend/backend/blocks/jina/search.py b/autogpt_platform/backend/backend/blocks/jina/search.py index 248d8af720..90a6eea51c 100644 --- a/autogpt_platform/backend/backend/blocks/jina/search.py +++ b/autogpt_platform/backend/backend/blocks/jina/search.py @@ -39,7 +39,7 @@ class SearchTheWebBlock(Block, GetRequest): test_mock={"get_request": lambda *args, **kwargs: "search content"}, ) - def run( + async def run( self, input_data: Input, *, credentials: JinaCredentials, **kwargs ) -> BlockOutput: # Encode the search query @@ -51,7 +51,7 @@ class SearchTheWebBlock(Block, GetRequest): # Prepend the Jina Search URL to the encoded query jina_search_url = f"https://s.jina.ai/{encoded_query}" - results = self.get_request(jina_search_url, headers=headers, json=False) + results = await self.get_request(jina_search_url, headers=headers, json=False) # Output the search results yield "results", results @@ -90,7 +90,7 @@ class ExtractWebsiteContentBlock(Block, GetRequest): test_mock={"get_request": lambda *args, **kwargs: "scraped content"}, ) - def run( + async def run( self, input_data: Input, *, credentials: JinaCredentials, **kwargs ) -> BlockOutput: if input_data.raw_content: @@ -103,5 +103,5 @@ class ExtractWebsiteContentBlock(Block, GetRequest): "Authorization": f"Bearer {credentials.api_key.get_secret_value()}", } - content = self.get_request(url, json=False, headers=headers) + content = await self.get_request(url, json=False, headers=headers) yield "content", content diff --git a/autogpt_platform/backend/backend/blocks/linear/_api.py b/autogpt_platform/backend/backend/blocks/linear/_api.py index c43f46fa70..0acee7fada 100644 --- a/autogpt_platform/backend/backend/blocks/linear/_api.py +++ b/autogpt_platform/backend/backend/blocks/linear/_api.py @@ -48,7 +48,7 @@ class LinearClient: raise_for_status=False, ) - def _execute_graphql_request( + async def _execute_graphql_request( self, query: str, variables: dict | None = None ) -> Any: """ @@ -65,19 +65,18 @@ class LinearClient: if variables: payload["variables"] = variables - response = self._requests.post(self.API_URL, json=payload) + response = await self._requests.post(self.API_URL, json=payload) if not response.ok: - try: error_data = response.json() error_message = error_data.get("errors", [{}])[0].get("message", "") except json.JSONDecodeError: - error_message = response.text + error_message = response.text() raise LinearAPIException( - f"Linear API request failed ({response.status_code}): {error_message}", - response.status_code, + f"Linear API request failed ({response.status}): {error_message}", + response.status, ) response_data = response.json() @@ -88,12 +87,12 @@ class LinearClient: ] raise LinearAPIException( f"Linear API returned errors: {', '.join(error_messages)}", - response.status_code, + response.status, ) return response_data["data"] - def query(self, query: str, variables: Optional[dict] = None) -> dict: + async def query(self, query: str, variables: Optional[dict] = None) -> dict: """Executes a GraphQL query. Args: @@ -103,9 +102,9 @@ class LinearClient: Returns: The response data. """ - return self._execute_graphql_request(query, variables) + return await self._execute_graphql_request(query, variables) - def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict: + async def mutate(self, mutation: str, variables: Optional[dict] = None) -> dict: """Executes a GraphQL mutation. Args: @@ -115,9 +114,11 @@ class LinearClient: Returns: The response data. """ - return self._execute_graphql_request(mutation, variables) + return await self._execute_graphql_request(mutation, variables) - def try_create_comment(self, issue_id: str, comment: str) -> CreateCommentResponse: + async def try_create_comment( + self, issue_id: str, comment: str + ) -> CreateCommentResponse: try: mutation = """ mutation CommentCreate($input: CommentCreateInput!) { @@ -138,13 +139,13 @@ class LinearClient: } } - added_comment = self.mutate(mutation, variables) + added_comment = await self.mutate(mutation, variables) # Select the commentCreate field from the mutation response return CreateCommentResponse(**added_comment["commentCreate"]) except LinearAPIException as e: raise e - def try_get_team_by_name(self, team_name: str) -> str: + async def try_get_team_by_name(self, team_name: str) -> str: try: query = """ query GetTeamId($searchTerm: String!) { @@ -167,12 +168,12 @@ class LinearClient: "searchTerm": team_name, } - team_id = self.query(query, variables) + team_id = await self.query(query, variables) return team_id["teams"]["nodes"][0]["id"] except LinearAPIException as e: raise e - def try_create_issue( + async def try_create_issue( self, team_id: str, title: str, @@ -211,12 +212,12 @@ class LinearClient: if priority: variables["input"]["priority"] = priority - added_issue = self.mutate(mutation, variables) + added_issue = await self.mutate(mutation, variables) return CreateIssueResponse(**added_issue["issueCreate"]) except LinearAPIException as e: raise e - def try_search_projects(self, term: str) -> list[Project]: + async def try_search_projects(self, term: str) -> list[Project]: try: query = """ query SearchProjects($term: String!, $includeComments: Boolean!) { @@ -238,14 +239,14 @@ class LinearClient: "includeComments": True, } - projects = self.query(query, variables) + projects = await self.query(query, variables) return [ Project(**project) for project in projects["searchProjects"]["nodes"] ] except LinearAPIException as e: raise e - def try_search_issues(self, term: str) -> list[Issue]: + async def try_search_issues(self, term: str) -> list[Issue]: try: query = """ query SearchIssues($term: String!, $includeComments: Boolean!) { @@ -266,7 +267,7 @@ class LinearClient: "includeComments": True, } - issues = self.query(query, variables) + issues = await self.query(query, variables) return [Issue(**issue) for issue in issues["searchIssues"]["nodes"]] except LinearAPIException as e: raise e diff --git a/autogpt_platform/backend/backend/blocks/linear/comment.py b/autogpt_platform/backend/backend/blocks/linear/comment.py index 6789fd12e3..d065609bf8 100644 --- a/autogpt_platform/backend/backend/blocks/linear/comment.py +++ b/autogpt_platform/backend/backend/blocks/linear/comment.py @@ -54,21 +54,21 @@ class LinearCreateCommentBlock(Block): ) @staticmethod - def create_comment( + async def create_comment( credentials: LinearCredentials, issue_id: str, comment: str ) -> tuple[str, str]: client = LinearClient(credentials=credentials) - response: CreateCommentResponse = client.try_create_comment( + response: CreateCommentResponse = await client.try_create_comment( issue_id=issue_id, comment=comment ) return response.comment.id, response.comment.body - def run( + async def run( self, input_data: Input, *, credentials: LinearCredentials, **kwargs ) -> BlockOutput: """Execute the comment creation""" try: - comment_id, comment_body = self.create_comment( + comment_id, comment_body = await self.create_comment( credentials=credentials, issue_id=input_data.issue_id, comment=input_data.comment, diff --git a/autogpt_platform/backend/backend/blocks/linear/issues.py b/autogpt_platform/backend/backend/blocks/linear/issues.py index f45e7fac0d..9f9d46d19a 100644 --- a/autogpt_platform/backend/backend/blocks/linear/issues.py +++ b/autogpt_platform/backend/backend/blocks/linear/issues.py @@ -67,7 +67,7 @@ class LinearCreateIssueBlock(Block): ) @staticmethod - def create_issue( + async def create_issue( credentials: LinearCredentials, team_name: str, title: str, @@ -76,15 +76,15 @@ class LinearCreateIssueBlock(Block): project_name: str | None = None, ) -> tuple[str, str]: client = LinearClient(credentials=credentials) - team_id = client.try_get_team_by_name(team_name=team_name) + team_id = await client.try_get_team_by_name(team_name=team_name) project_id: str | None = None if project_name: - projects = client.try_search_projects(term=project_name) + projects = await client.try_search_projects(term=project_name) if projects: project_id = projects[0].id else: raise LinearAPIException("Project not found", status_code=404) - response: CreateIssueResponse = client.try_create_issue( + response: CreateIssueResponse = await client.try_create_issue( team_id=team_id, title=title, description=description, @@ -93,12 +93,12 @@ class LinearCreateIssueBlock(Block): ) return response.issue.identifier, response.issue.title - def run( + async def run( self, input_data: Input, *, credentials: LinearCredentials, **kwargs ) -> BlockOutput: """Execute the issue creation""" try: - issue_id, issue_title = self.create_issue( + issue_id, issue_title = await self.create_issue( credentials=credentials, team_name=input_data.team_name, title=input_data.title, @@ -168,20 +168,22 @@ class LinearSearchIssuesBlock(Block): ) @staticmethod - def search_issues( + async def search_issues( credentials: LinearCredentials, term: str, ) -> list[Issue]: client = LinearClient(credentials=credentials) - response: list[Issue] = client.try_search_issues(term=term) + response: list[Issue] = await client.try_search_issues(term=term) return response - def run( + async def run( self, input_data: Input, *, credentials: LinearCredentials, **kwargs ) -> BlockOutput: """Execute the issue search""" try: - issues = self.search_issues(credentials=credentials, term=input_data.term) + issues = await self.search_issues( + credentials=credentials, term=input_data.term + ) yield "issues", issues except LinearAPIException as e: yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/linear/projects.py b/autogpt_platform/backend/backend/blocks/linear/projects.py index 695064a6a1..84f2b9ca53 100644 --- a/autogpt_platform/backend/backend/blocks/linear/projects.py +++ b/autogpt_platform/backend/backend/blocks/linear/projects.py @@ -69,20 +69,20 @@ class LinearSearchProjectsBlock(Block): ) @staticmethod - def search_projects( + async def search_projects( credentials: LinearCredentials, term: str, ) -> list[Project]: client = LinearClient(credentials=credentials) - response: list[Project] = client.try_search_projects(term=term) + response: list[Project] = await client.try_search_projects(term=term) return response - def run( + async def run( self, input_data: Input, *, credentials: LinearCredentials, **kwargs ) -> BlockOutput: """Execute the project search""" try: - projects = self.search_projects( + projects = await self.search_projects( credentials=credentials, term=input_data.term, ) diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index a1fedeec52..93f43c0690 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -3,14 +3,13 @@ import logging from abc import ABC from enum import Enum, EnumMeta from json import JSONDecodeError -from types import MappingProxyType from typing import Any, Iterable, List, Literal, NamedTuple, Optional import anthropic import ollama import openai from anthropic.types import ToolParam -from groq import Groq +from groq import AsyncGroq from pydantic import BaseModel, SecretStr from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema @@ -24,7 +23,6 @@ from backend.data.model import ( from backend.integrations.providers import ProviderName from backend.util import json from backend.util.logging import TruncatedLogger -from backend.util.settings import BehaveAs, Settings from backend.util.text import TextFormatter logger = TruncatedLogger(logging.getLogger(__name__), "[LLM-Block]") @@ -73,20 +71,7 @@ class ModelMetadata(NamedTuple): class LlmModelMeta(EnumMeta): - @property - def __members__(self) -> MappingProxyType: - if Settings().config.behave_as == BehaveAs.LOCAL: - members = super().__members__ - return MappingProxyType(members) - else: - removed_providers = ["ollama"] - existing_members = super().__members__ - members = { - name: member - for name, member in existing_members.items() - if LlmModel[name].provider not in removed_providers - } - return MappingProxyType(members) + pass class LlmModel(str, Enum, metaclass=LlmModelMeta): @@ -328,7 +313,7 @@ def estimate_token_count(prompt_messages: list[dict]) -> int: return int(estimated_tokens * 1.2) -def llm_call( +async def llm_call( credentials: APIKeyCredentials, llm_model: LlmModel, prompt: list[dict], @@ -370,7 +355,7 @@ def llm_call( if provider == "openai": tools_param = tools if tools else openai.NOT_GIVEN - oai_client = openai.OpenAI(api_key=credentials.api_key.get_secret_value()) + oai_client = openai.AsyncOpenAI(api_key=credentials.api_key.get_secret_value()) response_format = None if llm_model in [LlmModel.O1_MINI, LlmModel.O1_PREVIEW]: @@ -383,7 +368,7 @@ def llm_call( elif json_format: response_format = {"type": "json_object"} - response = oai_client.chat.completions.create( + response = await oai_client.chat.completions.create( model=llm_model.value, messages=prompt, # type: ignore response_format=response_format, # type: ignore @@ -439,9 +424,11 @@ def llm_call( messages.append({"role": p["role"], "content": p["content"]}) last_role = p["role"] - client = anthropic.Anthropic(api_key=credentials.api_key.get_secret_value()) + client = anthropic.AsyncAnthropic( + api_key=credentials.api_key.get_secret_value() + ) try: - resp = client.messages.create( + resp = await client.messages.create( model=llm_model.value, system=sysprompt, messages=messages, @@ -495,9 +482,9 @@ def llm_call( if tools: raise ValueError("Groq does not support tools.") - client = Groq(api_key=credentials.api_key.get_secret_value()) + client = AsyncGroq(api_key=credentials.api_key.get_secret_value()) response_format = {"type": "json_object"} if json_format else None - response = client.chat.completions.create( + response = await client.chat.completions.create( model=llm_model.value, messages=prompt, # type: ignore response_format=response_format, # type: ignore @@ -515,10 +502,10 @@ def llm_call( if tools: raise ValueError("Ollama does not support tools.") - client = ollama.Client(host=ollama_host) + client = ollama.AsyncClient(host=ollama_host) sys_messages = [p["content"] for p in prompt if p["role"] == "system"] usr_messages = [p["content"] for p in prompt if p["role"] != "system"] - response = client.generate( + response = await client.generate( model=llm_model.value, prompt=f"{sys_messages}\n\n{usr_messages}", stream=False, @@ -534,12 +521,12 @@ def llm_call( ) elif provider == "open_router": tools_param = tools if tools else openai.NOT_GIVEN - client = openai.OpenAI( + client = openai.AsyncOpenAI( base_url="https://openrouter.ai/api/v1", api_key=credentials.api_key.get_secret_value(), ) - response = client.chat.completions.create( + response = await client.chat.completions.create( extra_headers={ "HTTP-Referer": "https://agpt.co", "X-Title": "AutoGPT", @@ -581,12 +568,12 @@ def llm_call( ) elif provider == "llama_api": tools_param = tools if tools else openai.NOT_GIVEN - client = openai.OpenAI( + client = openai.AsyncOpenAI( base_url="https://api.llama.com/compat/v1/", api_key=credentials.api_key.get_secret_value(), ) - response = client.chat.completions.create( + response = await client.chat.completions.create( extra_headers={ "HTTP-Referer": "https://agpt.co", "X-Title": "AutoGPT", @@ -759,7 +746,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase): }, ) - def llm_call( + async def llm_call( self, credentials: APIKeyCredentials, llm_model: LlmModel, @@ -774,7 +761,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase): so that it can be mocked withing the block testing framework. """ self.prompt = prompt - return llm_call( + return await llm_call( credentials=credentials, llm_model=llm_model, prompt=prompt, @@ -784,7 +771,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase): ollama_host=ollama_host, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: logger.debug(f"Calling LLM with input data: {input_data}") @@ -838,7 +825,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase): for retry_count in range(input_data.retry): try: - llm_response = self.llm_call( + llm_response = await self.llm_call( credentials=credentials, llm_model=llm_model, prompt=prompt, @@ -978,17 +965,17 @@ class AITextGeneratorBlock(AIBlockBase): test_mock={"llm_call": lambda *args, **kwargs: "Response text"}, ) - def llm_call( + async def llm_call( self, input_data: AIStructuredResponseGeneratorBlock.Input, credentials: APIKeyCredentials, - ) -> str: + ) -> dict: block = AIStructuredResponseGeneratorBlock() - response = block.run_once(input_data, "response", credentials=credentials) + response = await block.run_once(input_data, "response", credentials=credentials) self.merge_llm_stats(block) return response["response"] - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: object_input_data = AIStructuredResponseGeneratorBlock.Input( @@ -998,7 +985,8 @@ class AITextGeneratorBlock(AIBlockBase): }, expected_format={}, ) - yield "response", self.llm_call(object_input_data, credentials) + response = await self.llm_call(object_input_data, credentials) + yield "response", response yield "prompt", self.prompt @@ -1080,23 +1068,27 @@ class AITextSummarizerBlock(AIBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - for output_name, output_data in self._run(input_data, credentials): + async for output_name, output_data in self._run(input_data, credentials): yield output_name, output_data - def _run(self, input_data: Input, credentials: APIKeyCredentials) -> BlockOutput: + async def _run( + self, input_data: Input, credentials: APIKeyCredentials + ) -> BlockOutput: chunks = self._split_text( input_data.text, input_data.max_tokens, input_data.chunk_overlap ) summaries = [] for chunk in chunks: - chunk_summary = self._summarize_chunk(chunk, input_data, credentials) + chunk_summary = await self._summarize_chunk(chunk, input_data, credentials) summaries.append(chunk_summary) - final_summary = self._combine_summaries(summaries, input_data, credentials) + final_summary = await self._combine_summaries( + summaries, input_data, credentials + ) yield "summary", final_summary yield "prompt", self.prompt @@ -1112,22 +1104,22 @@ class AITextSummarizerBlock(AIBlockBase): return chunks - def llm_call( + async def llm_call( self, input_data: AIStructuredResponseGeneratorBlock.Input, credentials: APIKeyCredentials, ) -> dict: block = AIStructuredResponseGeneratorBlock() - response = block.run_once(input_data, "response", credentials=credentials) + response = await block.run_once(input_data, "response", credentials=credentials) self.merge_llm_stats(block) return response - def _summarize_chunk( + async def _summarize_chunk( self, chunk: str, input_data: Input, credentials: APIKeyCredentials ) -> str: prompt = f"Summarize the following text in a {input_data.style} form. Focus your summary on the topic of `{input_data.focus}` if present, otherwise just provide a general summary:\n\n```{chunk}```" - llm_response = self.llm_call( + llm_response = await self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt=prompt, credentials=input_data.credentials, @@ -1139,7 +1131,7 @@ class AITextSummarizerBlock(AIBlockBase): return llm_response["summary"] - def _combine_summaries( + async def _combine_summaries( self, summaries: list[str], input_data: Input, credentials: APIKeyCredentials ) -> str: combined_text = "\n\n".join(summaries) @@ -1147,7 +1139,7 @@ class AITextSummarizerBlock(AIBlockBase): if len(combined_text.split()) <= input_data.max_tokens: prompt = f"Provide a final summary of the following section summaries in a {input_data.style} form, focus your summary on the topic of `{input_data.focus}` if present:\n\n ```{combined_text}```\n\n Just respond with the final_summary in the format specified." - llm_response = self.llm_call( + llm_response = await self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt=prompt, credentials=input_data.credentials, @@ -1162,7 +1154,8 @@ class AITextSummarizerBlock(AIBlockBase): return llm_response["final_summary"] else: # If combined summaries are still too long, recursively summarize - return self._run( + block = AITextSummarizerBlock() + return await block.run_once( AITextSummarizerBlock.Input( text=combined_text, credentials=input_data.credentials, @@ -1170,10 +1163,9 @@ class AITextSummarizerBlock(AIBlockBase): max_tokens=input_data.max_tokens, chunk_overlap=input_data.chunk_overlap, ), + "summary", credentials=credentials, - ).send(None)[ - 1 - ] # Get the first yielded value + ) class AIConversationBlock(AIBlockBase): @@ -1244,20 +1236,20 @@ class AIConversationBlock(AIBlockBase): }, ) - def llm_call( + async def llm_call( self, input_data: AIStructuredResponseGeneratorBlock.Input, credentials: APIKeyCredentials, - ) -> str: + ) -> dict: block = AIStructuredResponseGeneratorBlock() - response = block.run_once(input_data, "response", credentials=credentials) + response = await block.run_once(input_data, "response", credentials=credentials) self.merge_llm_stats(block) - return response["response"] + return response - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - response = self.llm_call( + response = await self.llm_call( AIStructuredResponseGeneratorBlock.Input( prompt=input_data.prompt, credentials=input_data.credentials, @@ -1269,7 +1261,6 @@ class AIConversationBlock(AIBlockBase): ), credentials=credentials, ) - yield "response", response yield "prompt", self.prompt @@ -1363,13 +1354,15 @@ class AIListGeneratorBlock(AIBlockBase): }, ) - def llm_call( + async def llm_call( self, input_data: AIStructuredResponseGeneratorBlock.Input, credentials: APIKeyCredentials, ) -> dict[str, str]: llm_block = AIStructuredResponseGeneratorBlock() - response = llm_block.run_once(input_data, "response", credentials=credentials) + response = await llm_block.run_once( + input_data, "response", credentials=credentials + ) self.merge_llm_stats(llm_block) return response @@ -1392,7 +1385,7 @@ class AIListGeneratorBlock(AIBlockBase): logger.error(f"Failed to convert string to list: {e}") raise ValueError("Invalid list format. Could not convert to list.") - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: logger.debug(f"Starting AIListGeneratorBlock.run with input data: {input_data}") @@ -1458,7 +1451,7 @@ class AIListGeneratorBlock(AIBlockBase): for attempt in range(input_data.max_retries): try: logger.debug("Calling LLM") - llm_response = self.llm_call( + llm_response = await self.llm_call( AIStructuredResponseGeneratorBlock.Input( sys_prompt=sys_prompt, prompt=prompt, diff --git a/autogpt_platform/backend/backend/blocks/maths.py b/autogpt_platform/backend/backend/blocks/maths.py index cb65de1c09..0559d9673d 100644 --- a/autogpt_platform/backend/backend/blocks/maths.py +++ b/autogpt_platform/backend/backend/blocks/maths.py @@ -52,7 +52,7 @@ class CalculatorBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: operation = input_data.operation a = input_data.a b = input_data.b @@ -107,7 +107,7 @@ class CountItemsBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: collection = input_data.collection try: diff --git a/autogpt_platform/backend/backend/blocks/media.py b/autogpt_platform/backend/backend/blocks/media.py index 6c9afcd10a..15a3d5d17e 100644 --- a/autogpt_platform/backend/backend/blocks/media.py +++ b/autogpt_platform/backend/backend/blocks/media.py @@ -39,7 +39,7 @@ class MediaDurationBlock(Block): output_schema=MediaDurationBlock.Output, ) - def run( + async def run( self, input_data: Input, *, @@ -47,7 +47,7 @@ class MediaDurationBlock(Block): **kwargs, ) -> BlockOutput: # 1) Store the input media locally - local_media_path = store_media_file( + local_media_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.media_in, return_content=False, @@ -105,7 +105,7 @@ class LoopVideoBlock(Block): output_schema=LoopVideoBlock.Output, ) - def run( + async def run( self, input_data: Input, *, @@ -114,7 +114,7 @@ class LoopVideoBlock(Block): **kwargs, ) -> BlockOutput: # 1) Store the input video locally - local_video_path = store_media_file( + local_video_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.video_in, return_content=False, @@ -146,7 +146,7 @@ class LoopVideoBlock(Block): looped_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac") # Return as data URI - video_out = store_media_file( + video_out = await store_media_file( graph_exec_id=graph_exec_id, file=output_filename, return_content=input_data.output_return_type == "data_uri", @@ -194,7 +194,7 @@ class AddAudioToVideoBlock(Block): output_schema=AddAudioToVideoBlock.Output, ) - def run( + async def run( self, input_data: Input, *, @@ -203,12 +203,12 @@ class AddAudioToVideoBlock(Block): **kwargs, ) -> BlockOutput: # 1) Store the inputs locally - local_video_path = store_media_file( + local_video_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.video_in, return_content=False, ) - local_audio_path = store_media_file( + local_audio_path = await store_media_file( graph_exec_id=graph_exec_id, file=input_data.audio_in, return_content=False, @@ -236,7 +236,7 @@ class AddAudioToVideoBlock(Block): final_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac") # 5) Return either path or data URI - video_out = store_media_file( + video_out = await store_media_file( graph_exec_id=graph_exec_id, file=output_filename, return_content=input_data.output_return_type == "data_uri", diff --git a/autogpt_platform/backend/backend/blocks/medium.py b/autogpt_platform/backend/backend/blocks/medium.py index d68bed3bcf..a8964ca940 100644 --- a/autogpt_platform/backend/backend/blocks/medium.py +++ b/autogpt_platform/backend/backend/blocks/medium.py @@ -130,7 +130,7 @@ class PublishToMediumBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def create_post( + async def create_post( self, api_key: SecretStr, author_id, @@ -160,18 +160,17 @@ class PublishToMediumBlock(Block): "notifyFollowers": notify_followers, } - response = Requests().post( + response = await Requests().post( f"https://api.medium.com/v1/users/{author_id}/posts", headers=headers, json=data, ) - return response.json() - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - response = self.create_post( + response = await self.create_post( credentials.api_key, input_data.author_id.get_secret_value(), input_data.title, diff --git a/autogpt_platform/backend/backend/blocks/mem0.py b/autogpt_platform/backend/backend/blocks/mem0.py index 64d7ca4bec..ad2c64f8f0 100644 --- a/autogpt_platform/backend/backend/blocks/mem0.py +++ b/autogpt_platform/backend/backend/blocks/mem0.py @@ -109,7 +109,7 @@ class AddMemoryBlock(Block, Mem0Base): test_mock={"_get_client": lambda credentials: MockMemoryClient()}, ) - def run( + async def run( self, input_data: Input, *, @@ -208,7 +208,7 @@ class SearchMemoryBlock(Block, Mem0Base): test_mock={"_get_client": lambda credentials: MockMemoryClient()}, ) - def run( + async def run( self, input_data: Input, *, @@ -288,7 +288,7 @@ class GetAllMemoriesBlock(Block, Mem0Base): test_mock={"_get_client": lambda credentials: MockMemoryClient()}, ) - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py b/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py index 3792cdf53f..f5205f6e72 100644 --- a/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py +++ b/autogpt_platform/backend/backend/blocks/nvidia/deepfake.py @@ -40,7 +40,7 @@ class NvidiaDeepfakeDetectBlock(Block): output_schema=NvidiaDeepfakeDetectBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: NvidiaCredentials, **kwargs ) -> BlockOutput: url = "https://ai.api.nvidia.com/v1/cv/hive/deepfake-image-detection" @@ -59,8 +59,7 @@ class NvidiaDeepfakeDetectBlock(Block): } try: - response = Requests().post(url, headers=headers, json=payload) - response.raise_for_status() + response = await Requests().post(url, headers=headers, json=payload) data = response.json() result = data.get("data", [{}])[0] diff --git a/autogpt_platform/backend/backend/blocks/pinecone.py b/autogpt_platform/backend/backend/blocks/pinecone.py index 6f8a83a24c..529940b7cf 100644 --- a/autogpt_platform/backend/backend/blocks/pinecone.py +++ b/autogpt_platform/backend/backend/blocks/pinecone.py @@ -56,7 +56,7 @@ class PineconeInitBlock(Block): output_schema=PineconeInitBlock.Output, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: pc = Pinecone(api_key=credentials.api_key.get_secret_value()) @@ -117,7 +117,7 @@ class PineconeQueryBlock(Block): output_schema=PineconeQueryBlock.Output, ) - def run( + async def run( self, input_data: Input, *, @@ -195,7 +195,7 @@ class PineconeInsertBlock(Block): output_schema=PineconeInsertBlock.Output, ) - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/reddit.py b/autogpt_platform/backend/backend/blocks/reddit.py index b3dca4ca74..c88407f6d1 100644 --- a/autogpt_platform/backend/backend/blocks/reddit.py +++ b/autogpt_platform/backend/backend/blocks/reddit.py @@ -146,7 +146,7 @@ class GetRedditPostsBlock(Block): subreddit = client.subreddit(input_data.subreddit) return subreddit.new(limit=input_data.post_limit or 10) - def run( + async def run( self, input_data: Input, *, credentials: RedditCredentials, **kwargs ) -> BlockOutput: current_time = datetime.now(tz=timezone.utc) @@ -207,7 +207,7 @@ class PostRedditCommentBlock(Block): raise ValueError("Failed to post comment.") return new_comment.id - def run( + async def run( self, input_data: Input, *, credentials: RedditCredentials, **kwargs ) -> BlockOutput: yield "comment_id", self.reply_post(credentials, input_data.data) diff --git a/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py b/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py index e75e0ad1cf..30744a28f7 100644 --- a/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py +++ b/autogpt_platform/backend/backend/blocks/replicate_flux_advanced.py @@ -159,7 +159,7 @@ class ReplicateFluxAdvancedModelBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: # If the seed is not provided, generate a random seed @@ -168,7 +168,7 @@ class ReplicateFluxAdvancedModelBlock(Block): seed = int.from_bytes(os.urandom(4), "big") # Run the model using the provided inputs - result = self.run_model( + result = await self.run_model( api_key=credentials.api_key, model_name=input_data.replicate_model_name.api_name, prompt=input_data.prompt, @@ -183,7 +183,7 @@ class ReplicateFluxAdvancedModelBlock(Block): ) yield "result", result - def run_model( + async def run_model( self, api_key: SecretStr, model_name, @@ -201,7 +201,7 @@ class ReplicateFluxAdvancedModelBlock(Block): client = ReplicateClient(api_token=api_key.get_secret_value()) # Run the model with additional parameters - output: FileOutput | list[FileOutput] = client.run( # type: ignore This is because they changed the return type, and didn't update the type hint! It should be overloaded depending on the value of `use_file_output` to `FileOutput | list[FileOutput]` but it's `Any | Iterator[Any]` + output: FileOutput | list[FileOutput] = await client.async_run( # type: ignore This is because they changed the return type, and didn't update the type hint! It should be overloaded depending on the value of `use_file_output` to `FileOutput | list[FileOutput]` but it's `Any | Iterator[Any]` f"{model_name}", input={ "prompt": prompt, diff --git a/autogpt_platform/backend/backend/blocks/rss.py b/autogpt_platform/backend/backend/blocks/rss.py index 9a5a17ebee..e3f5da3139 100644 --- a/autogpt_platform/backend/backend/blocks/rss.py +++ b/autogpt_platform/backend/backend/blocks/rss.py @@ -1,4 +1,4 @@ -import time +import asyncio from datetime import datetime, timedelta, timezone from typing import Any @@ -87,7 +87,7 @@ class ReadRSSFeedBlock(Block): def parse_feed(url: str) -> dict[str, Any]: return feedparser.parse(url) # type: ignore - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: keep_going = True start_time = datetime.now(timezone.utc) - timedelta( minutes=input_data.time_period @@ -113,4 +113,4 @@ class ReadRSSFeedBlock(Block): ), ) - time.sleep(input_data.polling_rate) + await asyncio.sleep(input_data.polling_rate) diff --git a/autogpt_platform/backend/backend/blocks/sampling.py b/autogpt_platform/backend/backend/blocks/sampling.py index d2257db06f..ffd509ff75 100644 --- a/autogpt_platform/backend/backend/blocks/sampling.py +++ b/autogpt_platform/backend/backend/blocks/sampling.py @@ -93,7 +93,7 @@ class DataSamplingBlock(Block): ) self.accumulated_data = [] - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: if input_data.accumulate: if isinstance(input_data.data, dict): self.accumulated_data.append(input_data.data) diff --git a/autogpt_platform/backend/backend/blocks/screenshotone.py b/autogpt_platform/backend/backend/blocks/screenshotone.py index 5620b8119b..7dd026a5c5 100644 --- a/autogpt_platform/backend/backend/blocks/screenshotone.py +++ b/autogpt_platform/backend/backend/blocks/screenshotone.py @@ -105,7 +105,7 @@ class ScreenshotWebPageBlock(Block): ) @staticmethod - def take_screenshot( + async def take_screenshot( credentials: APIKeyCredentials, graph_exec_id: str, url: str, @@ -136,26 +136,28 @@ class ScreenshotWebPageBlock(Block): "cache": str(cache).lower(), } + # Make the API request # Use header-based authentication instead of query parameter headers = { "X-Access-Key": credentials.api_key.get_secret_value(), } - response = api.get( + response = await api.get( "https://api.screenshotone.com/take", params=params, headers=headers ) + content = response.content return { - "image": store_media_file( + "image": await store_media_file( graph_exec_id=graph_exec_id, file=MediaFileType( - f"data:image/{format.value};base64,{b64encode(response.content).decode('utf-8')}" + f"data:image/{format.value};base64,{b64encode(content).decode('utf-8')}" ), return_content=True, ) } - def run( + async def run( self, input_data: Input, *, @@ -164,7 +166,7 @@ class ScreenshotWebPageBlock(Block): **kwargs, ) -> BlockOutput: try: - screenshot_data = self.take_screenshot( + screenshot_data = await self.take_screenshot( credentials=credentials, graph_exec_id=graph_exec_id, url=input_data.url, diff --git a/autogpt_platform/backend/backend/blocks/search.py b/autogpt_platform/backend/backend/blocks/search.py index 633ad31091..51eadf215e 100644 --- a/autogpt_platform/backend/backend/blocks/search.py +++ b/autogpt_platform/backend/backend/blocks/search.py @@ -36,10 +36,10 @@ class GetWikipediaSummaryBlock(Block, GetRequest): test_mock={"get_request": lambda url, json: {"extract": "summary content"}}, ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: topic = input_data.topic url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}" - response = self.get_request(url, json=True) + response = await self.get_request(url, json=True) if "extract" not in response: raise RuntimeError(f"Unable to parse Wikipedia response: {response}") yield "summary", response["extract"] @@ -113,14 +113,14 @@ class GetWeatherInformationBlock(Block, GetRequest): test_credentials=TEST_CREDENTIALS, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: units = "metric" if input_data.use_celsius else "imperial" api_key = credentials.api_key location = input_data.location url = f"http://api.openweathermap.org/data/2.5/weather?q={quote(location)}&appid={api_key}&units={units}" - weather_data = self.get_request(url, json=True) + weather_data = await self.get_request(url, json=True) if "main" in weather_data and "weather" in weather_data: yield "temperature", str(weather_data["main"]["temp"]) diff --git a/autogpt_platform/backend/backend/blocks/slant3d/base.py b/autogpt_platform/backend/backend/blocks/slant3d/base.py index 6919e5ec49..e368a1b451 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/base.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/base.py @@ -14,20 +14,25 @@ class Slant3DBlockBase(Block): def _get_headers(self, api_key: str) -> Dict[str, str]: return {"api-key": api_key, "Content-Type": "application/json"} - def _make_request(self, method: str, endpoint: str, api_key: str, **kwargs) -> Dict: + async def _make_request( + self, method: str, endpoint: str, api_key: str, **kwargs + ) -> Dict: url = f"{self.BASE_URL}/{endpoint}" - response = Requests().request( + response = await Requests().request( method=method, url=url, headers=self._get_headers(api_key), **kwargs ) + resp = response.json() if not response.ok: - error_msg = response.json().get("error", "Unknown error") + error_msg = resp.get("error", "Unknown error") raise RuntimeError(f"API request failed: {error_msg}") - return response.json() + return resp - def _check_valid_color(self, profile: Profile, color: Color, api_key: str) -> str: - response = self._make_request( + async def _check_valid_color( + self, profile: Profile, color: Color, api_key: str + ) -> str: + response = await self._make_request( "GET", "filament", api_key, @@ -48,10 +53,12 @@ Valid colors for {profile.value} are: ) return color_tag - def _convert_to_color(self, profile: Profile, color: Color, api_key: str) -> str: - return self._check_valid_color(profile, color, api_key) + async def _convert_to_color( + self, profile: Profile, color: Color, api_key: str + ) -> str: + return await self._check_valid_color(profile, color, api_key) - def _format_order_data( + async def _format_order_data( self, customer: CustomerDetails, order_number: str, @@ -61,6 +68,7 @@ Valid colors for {profile.value} are: """Helper function to format order data for API requests""" orders = [] for item in items: + color_tag = await self._convert_to_color(item.profile, item.color, api_key) order_data = { "email": customer.email, "phone": customer.phone, @@ -85,9 +93,7 @@ Valid colors for {profile.value} are: "order_quantity": item.quantity, "order_image_url": "", "order_sku": "NOT_USED", - "order_item_color": self._convert_to_color( - item.profile, item.color, api_key - ), + "order_item_color": color_tag, "profile": item.profile.value, } orders.append(order_data) diff --git a/autogpt_platform/backend/backend/blocks/slant3d/filament.py b/autogpt_platform/backend/backend/blocks/slant3d/filament.py index c232c2ba8d..0659a45561 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/filament.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/filament.py @@ -72,11 +72,11 @@ class Slant3DFilamentBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - result = self._make_request( + result = await self._make_request( "GET", "filament", credentials.api_key.get_secret_value() ) yield "filaments", result["filaments"] diff --git a/autogpt_platform/backend/backend/blocks/slant3d/order.py b/autogpt_platform/backend/backend/blocks/slant3d/order.py index 6c9f4ffec3..f1cab18d27 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/order.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/order.py @@ -4,7 +4,6 @@ from typing import List from backend.data.block import BlockOutput, BlockSchema from backend.data.model import APIKeyCredentials, SchemaField from backend.util import settings -from backend.util.request import req from backend.util.settings import BehaveAs from ._api import ( @@ -75,17 +74,17 @@ class Slant3DCreateOrderBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - order_data = self._format_order_data( + order_data = await self._format_order_data( input_data.customer, input_data.order_number, input_data.items, credentials.api_key.get_secret_value(), ) - result = self._make_request( + result = await self._make_request( "POST", "order", credentials.api_key.get_secret_value(), json=order_data ) yield "order_id", result["orderId"] @@ -161,28 +160,24 @@ class Slant3DEstimateOrderBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - order_data = self._format_order_data( + order_data = await self._format_order_data( input_data.customer, input_data.order_number, input_data.items, credentials.api_key.get_secret_value(), ) - try: - result = self._make_request( - "POST", - "order/estimate", - credentials.api_key.get_secret_value(), - json=order_data, - ) - yield "total_price", result["totalPrice"] - yield "shipping_cost", result["shippingCost"] - yield "printing_cost", result["printingCost"] - except req.HTTPError as e: - yield "error", str(f"Error estimating order: {e} {e.response.text}") - raise + result = await self._make_request( + "POST", + "order/estimate", + credentials.api_key.get_secret_value(), + json=order_data, + ) + yield "total_price", result["totalPrice"] + yield "shipping_cost", result["shippingCost"] + yield "printing_cost", result["printingCost"] class Slant3DEstimateShippingBlock(Slant3DBlockBase): @@ -245,17 +240,17 @@ class Slant3DEstimateShippingBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - order_data = self._format_order_data( + order_data = await self._format_order_data( input_data.customer, input_data.order_number, input_data.items, credentials.api_key.get_secret_value(), ) - result = self._make_request( + result = await self._make_request( "POST", "order/estimateShipping", credentials.api_key.get_secret_value(), @@ -311,11 +306,11 @@ class Slant3DGetOrdersBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - result = self._make_request( + result = await self._make_request( "GET", "order", credentials.api_key.get_secret_value() ) yield "orders", [str(order["orderId"]) for order in result["ordersData"]] @@ -358,11 +353,11 @@ class Slant3DTrackingBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - result = self._make_request( + result = await self._make_request( "GET", f"order/{input_data.order_id}/get-tracking", credentials.api_key.get_secret_value(), @@ -402,11 +397,11 @@ class Slant3DCancelOrderBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - result = self._make_request( + result = await self._make_request( "DELETE", f"order/{input_data.order_id}", credentials.api_key.get_secret_value(), diff --git a/autogpt_platform/backend/backend/blocks/slant3d/slicing.py b/autogpt_platform/backend/backend/blocks/slant3d/slicing.py index 1b868efc9e..6abe3045ac 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/slicing.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/slicing.py @@ -44,11 +44,11 @@ class Slant3DSlicerBlock(Slant3DBlockBase): }, ) - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: try: - result = self._make_request( + result = await self._make_request( "POST", "slicer", credentials.api_key.get_secret_value(), diff --git a/autogpt_platform/backend/backend/blocks/slant3d/webhook.py b/autogpt_platform/backend/backend/blocks/slant3d/webhook.py index 866a1c9afe..8a690cf1ad 100644 --- a/autogpt_platform/backend/backend/blocks/slant3d/webhook.py +++ b/autogpt_platform/backend/backend/blocks/slant3d/webhook.py @@ -37,7 +37,7 @@ class Slant3DTriggerBase: description="Error message if payload processing failed" ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "payload", input_data.payload yield "order_id", input_data.payload["orderId"] @@ -117,8 +117,9 @@ class Slant3DOrderWebhookBlock(Slant3DTriggerBase, Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore - yield from super().run(input_data, **kwargs) + async def run(self, input_data: Input, **kwargs) -> BlockOutput: # type: ignore + async for name, value in super().run(input_data, **kwargs): + yield name, value # Extract and normalize values from the payload yield "status", input_data.payload["status"] diff --git a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py index dc45cf0fbb..d8405ca2ca 100644 --- a/autogpt_platform/backend/backend/blocks/smart_decision_maker.py +++ b/autogpt_platform/backend/backend/blocks/smart_decision_maker.py @@ -417,7 +417,7 @@ class SmartDecisionMakerBlock(Block): return return_tool_functions - def run( + async def run( self, input_data: Input, *, @@ -487,7 +487,7 @@ class SmartDecisionMakerBlock(Block): ): prompt.append({"role": "user", "content": prefix + input_data.prompt}) - response = llm.llm_call( + response = await llm.llm_call( credentials=credentials, llm_model=input_data.model, prompt=prompt, diff --git a/autogpt_platform/backend/backend/blocks/smartlead/_api.py b/autogpt_platform/backend/backend/blocks/smartlead/_api.py index 8caa266c2c..cff1c63b72 100644 --- a/autogpt_platform/backend/backend/blocks/smartlead/_api.py +++ b/autogpt_platform/backend/backend/blocks/smartlead/_api.py @@ -27,9 +27,11 @@ class SmartLeadClient: def _handle_error(self, e: Exception) -> str: return e.__str__().replace(self.api_key, "API KEY") - def create_campaign(self, request: CreateCampaignRequest) -> CreateCampaignResponse: + async def create_campaign( + self, request: CreateCampaignRequest + ) -> CreateCampaignResponse: try: - response = self.requests.post( + response = await self.requests.post( self._add_auth_to_url(f"{self.API_URL}/campaigns/create"), json=request.model_dump(), ) @@ -40,11 +42,11 @@ class SmartLeadClient: except Exception as e: raise ValueError(f"Failed to create campaign: {self._handle_error(e)}") - def add_leads_to_campaign( + async def add_leads_to_campaign( self, request: AddLeadsRequest ) -> AddLeadsToCampaignResponse: try: - response = self.requests.post( + response = await self.requests.post( self._add_auth_to_url( f"{self.API_URL}/campaigns/{request.campaign_id}/leads" ), @@ -64,7 +66,7 @@ class SmartLeadClient: f"Failed to add leads to campaign: {self._handle_error(e)}" ) - def save_campaign_sequences( + async def save_campaign_sequences( self, campaign_id: int, request: SaveSequencesRequest ) -> SaveSequencesResponse: """ @@ -84,13 +86,13 @@ class SmartLeadClient: - MANUAL_PERCENTAGE: Requires variant_distribution_percentage in seq_variants """ try: - response = self.requests.post( + response = await self.requests.post( self._add_auth_to_url( f"{self.API_URL}/campaigns/{campaign_id}/sequences" ), json=request.model_dump(exclude_none=True), ) - return SaveSequencesResponse(**response.json()) + return SaveSequencesResponse(**(response.json())) except Exception as e: raise ValueError( f"Failed to save campaign sequences: {e.__str__().replace(self.api_key, 'API KEY')}" diff --git a/autogpt_platform/backend/backend/blocks/smartlead/campaign.py b/autogpt_platform/backend/backend/blocks/smartlead/campaign.py index 3b4ef95749..0e6c72416b 100644 --- a/autogpt_platform/backend/backend/blocks/smartlead/campaign.py +++ b/autogpt_platform/backend/backend/blocks/smartlead/campaign.py @@ -80,20 +80,20 @@ class CreateCampaignBlock(Block): ) @staticmethod - def create_campaign( + async def create_campaign( name: str, credentials: SmartLeadCredentials ) -> CreateCampaignResponse: client = SmartLeadClient(credentials.api_key.get_secret_value()) - return client.create_campaign(CreateCampaignRequest(name=name)) + return await client.create_campaign(CreateCampaignRequest(name=name)) - def run( + async def run( self, input_data: Input, *, credentials: SmartLeadCredentials, **kwargs, ) -> BlockOutput: - response = self.create_campaign(input_data.name, credentials) + response = await self.create_campaign(input_data.name, credentials) yield "id", response.id yield "name", response.name @@ -193,11 +193,11 @@ class AddLeadToCampaignBlock(Block): ) @staticmethod - def add_leads_to_campaign( + async def add_leads_to_campaign( campaign_id: int, lead_list: list[LeadInput], credentials: SmartLeadCredentials ) -> AddLeadsToCampaignResponse: client = SmartLeadClient(credentials.api_key.get_secret_value()) - return client.add_leads_to_campaign( + return await client.add_leads_to_campaign( AddLeadsRequest( campaign_id=campaign_id, lead_list=lead_list, @@ -210,14 +210,14 @@ class AddLeadToCampaignBlock(Block): ), ) - def run( + async def run( self, input_data: Input, *, credentials: SmartLeadCredentials, **kwargs, ) -> BlockOutput: - response = self.add_leads_to_campaign( + response = await self.add_leads_to_campaign( input_data.campaign_id, input_data.lead_list, credentials ) @@ -297,22 +297,22 @@ class SaveCampaignSequencesBlock(Block): ) @staticmethod - def save_campaign_sequences( + async def save_campaign_sequences( campaign_id: int, sequences: list[Sequence], credentials: SmartLeadCredentials ) -> SaveSequencesResponse: client = SmartLeadClient(credentials.api_key.get_secret_value()) - return client.save_campaign_sequences( + return await client.save_campaign_sequences( campaign_id=campaign_id, request=SaveSequencesRequest(sequences=sequences) ) - def run( + async def run( self, input_data: Input, *, credentials: SmartLeadCredentials, **kwargs, ) -> BlockOutput: - response = self.save_campaign_sequences( + response = await self.save_campaign_sequences( input_data.campaign_id, input_data.sequences, credentials ) diff --git a/autogpt_platform/backend/backend/blocks/talking_head.py b/autogpt_platform/backend/backend/blocks/talking_head.py index b57a9b0da6..3861cb7752 100644 --- a/autogpt_platform/backend/backend/blocks/talking_head.py +++ b/autogpt_platform/backend/backend/blocks/talking_head.py @@ -1,4 +1,4 @@ -import time +import asyncio from typing import Literal from pydantic import SecretStr @@ -11,7 +11,7 @@ from backend.data.model import ( SchemaField, ) from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests TEST_CREDENTIALS = APIKeyCredentials( id="01234567-89ab-cdef-0123-456789abcdef", @@ -113,26 +113,26 @@ class CreateTalkingAvatarVideoBlock(Block): test_credentials=TEST_CREDENTIALS, ) - def create_clip(self, api_key: SecretStr, payload: dict) -> dict: + async def create_clip(self, api_key: SecretStr, payload: dict) -> dict: url = "https://api.d-id.com/clips" headers = { "accept": "application/json", "content-type": "application/json", "authorization": f"Basic {api_key.get_secret_value()}", } - response = requests.post(url, json=payload, headers=headers) + response = await Requests().post(url, json=payload, headers=headers) return response.json() - def get_clip_status(self, api_key: SecretStr, clip_id: str) -> dict: + async def get_clip_status(self, api_key: SecretStr, clip_id: str) -> dict: url = f"https://api.d-id.com/clips/{clip_id}" headers = { "accept": "application/json", "authorization": f"Basic {api_key.get_secret_value()}", } - response = requests.get(url, headers=headers) + response = await Requests().get(url, headers=headers) return response.json() - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: # Create the clip @@ -153,12 +153,12 @@ class CreateTalkingAvatarVideoBlock(Block): "driver_id": input_data.driver_id, } - response = self.create_clip(credentials.api_key, payload) + response = await self.create_clip(credentials.api_key, payload) clip_id = response["id"] # Poll for clip status for _ in range(input_data.max_polling_attempts): - status_response = self.get_clip_status(credentials.api_key, clip_id) + status_response = await self.get_clip_status(credentials.api_key, clip_id) if status_response["status"] == "done": yield "video_url", status_response["result_url"] return @@ -167,6 +167,6 @@ class CreateTalkingAvatarVideoBlock(Block): f"Clip creation failed: {status_response.get('error', 'Unknown error')}" ) - time.sleep(input_data.polling_interval) + await asyncio.sleep(input_data.polling_interval) raise TimeoutError("Clip creation timed out") diff --git a/autogpt_platform/backend/backend/blocks/text.py b/autogpt_platform/backend/backend/blocks/text.py index 21351bbbd6..f4357a468c 100644 --- a/autogpt_platform/backend/backend/blocks/text.py +++ b/autogpt_platform/backend/backend/blocks/text.py @@ -43,7 +43,7 @@ class MatchTextPatternBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: output = input_data.data or input_data.text flags = 0 if not input_data.case_sensitive: @@ -133,7 +133,7 @@ class ExtractTextInformationBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: flags = 0 if not input_data.case_sensitive: flags = flags | re.IGNORECASE @@ -201,7 +201,7 @@ class FillTextTemplateBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", formatter.format_string(input_data.format, input_data.values) @@ -232,7 +232,7 @@ class CombineTextsBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: combined_text = input_data.delimiter.join(input_data.input) yield "output", combined_text @@ -267,7 +267,7 @@ class TextSplitBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: if len(input_data.text) == 0: yield "texts", [] else: @@ -301,5 +301,5 @@ class TextReplaceBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.text.replace(input_data.old, input_data.new) diff --git a/autogpt_platform/backend/backend/blocks/text_to_speech_block.py b/autogpt_platform/backend/backend/blocks/text_to_speech_block.py index 989dc54e12..f0b7c107de 100644 --- a/autogpt_platform/backend/backend/blocks/text_to_speech_block.py +++ b/autogpt_platform/backend/backend/blocks/text_to_speech_block.py @@ -10,7 +10,7 @@ from backend.data.model import ( SchemaField, ) from backend.integrations.providers import ProviderName -from backend.util.request import requests +from backend.util.request import Requests TEST_CREDENTIALS = APIKeyCredentials( id="01234567-89ab-cdef-0123-456789abcdef", @@ -71,7 +71,7 @@ class UnrealTextToSpeechBlock(Block): ) @staticmethod - def call_unreal_speech_api( + async def call_unreal_speech_api( api_key: SecretStr, text: str, voice_id: str ) -> dict[str, Any]: url = "https://api.v7.unrealspeech.com/speech" @@ -88,13 +88,13 @@ class UnrealTextToSpeechBlock(Block): "TimestampType": "sentence", } - response = requests.post(url, headers=headers, json=data) + response = await Requests().post(url, headers=headers, json=data) return response.json() - def run( + async def run( self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs ) -> BlockOutput: - api_response = self.call_unreal_speech_api( + api_response = await self.call_unreal_speech_api( credentials.api_key, input_data.text, input_data.voice_id, diff --git a/autogpt_platform/backend/backend/blocks/time_blocks.py b/autogpt_platform/backend/backend/blocks/time_blocks.py index adeeb3bee0..05d8e3699f 100644 --- a/autogpt_platform/backend/backend/blocks/time_blocks.py +++ b/autogpt_platform/backend/backend/blocks/time_blocks.py @@ -1,3 +1,4 @@ +import asyncio import time from datetime import datetime, timedelta from typing import Any, Union @@ -37,7 +38,7 @@ class GetCurrentTimeBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: current_time = time.strftime(input_data.format) yield "time", current_time @@ -87,7 +88,7 @@ class GetCurrentDateBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: offset = int(input_data.offset) except ValueError: @@ -132,7 +133,7 @@ class GetCurrentDateAndTimeBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: current_date_time = time.strftime(input_data.format) yield "date_time", current_date_time @@ -183,7 +184,7 @@ class CountdownTimerBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: seconds = int(input_data.seconds) minutes = int(input_data.minutes) hours = int(input_data.hours) @@ -192,5 +193,6 @@ class CountdownTimerBlock(Block): total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400 for _ in range(input_data.repeat): - time.sleep(total_seconds) + if total_seconds > 0: + await asyncio.sleep(total_seconds) yield "output_message", input_data.input_message diff --git a/autogpt_platform/backend/backend/blocks/todoist/comments.py b/autogpt_platform/backend/backend/blocks/todoist/comments.py index 210aa504b3..703afb696f 100644 --- a/autogpt_platform/backend/backend/blocks/todoist/comments.py +++ b/autogpt_platform/backend/backend/blocks/todoist/comments.py @@ -108,7 +108,7 @@ class TodoistCreateCommentBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -215,7 +215,7 @@ class TodoistGetCommentsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -307,7 +307,7 @@ class TodoistGetCommentBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -371,7 +371,7 @@ class TodoistUpdateCommentBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -429,7 +429,7 @@ class TodoistDeleteCommentBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/todoist/labels.py b/autogpt_platform/backend/backend/blocks/todoist/labels.py index fc1c381a42..4700ebb6c0 100644 --- a/autogpt_platform/backend/backend/blocks/todoist/labels.py +++ b/autogpt_platform/backend/backend/blocks/todoist/labels.py @@ -80,7 +80,7 @@ class TodoistCreateLabelBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -174,7 +174,7 @@ class TodoistListLabelsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -248,7 +248,7 @@ class TodoistGetLabelBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -321,7 +321,7 @@ class TodoistUpdateLabelBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -389,7 +389,7 @@ class TodoistDeleteLabelBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -444,7 +444,7 @@ class TodoistGetSharedLabelsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -499,7 +499,7 @@ class TodoistRenameSharedLabelsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -551,7 +551,7 @@ class TodoistRemoveSharedLabelsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/todoist/projects.py b/autogpt_platform/backend/backend/blocks/todoist/projects.py index 6955e0c136..33ad7950fa 100644 --- a/autogpt_platform/backend/backend/blocks/todoist/projects.py +++ b/autogpt_platform/backend/backend/blocks/todoist/projects.py @@ -95,7 +95,7 @@ class TodoistListProjectsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -185,7 +185,7 @@ class TodoistCreateProjectBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -277,7 +277,7 @@ class TodoistGetProjectBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -375,7 +375,7 @@ class TodoistUpdateProjectBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -438,7 +438,7 @@ class TodoistDeleteProjectBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -548,7 +548,7 @@ class TodoistListCollaboratorsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/todoist/sections.py b/autogpt_platform/backend/backend/blocks/todoist/sections.py index fd9273c0c3..764f7e166e 100644 --- a/autogpt_platform/backend/backend/blocks/todoist/sections.py +++ b/autogpt_platform/backend/backend/blocks/todoist/sections.py @@ -96,7 +96,7 @@ class TodoistListSectionsBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -166,7 +166,7 @@ class TodoistListSectionsBlock(Block): # except Exception as e: # raise e -# def run( +# async def run( # self, # input_data: Input, # *, @@ -238,7 +238,7 @@ class TodoistGetSectionBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -295,7 +295,7 @@ class TodoistDeleteSectionBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/todoist/tasks.py b/autogpt_platform/backend/backend/blocks/todoist/tasks.py index 67786efe11..d50124a9ef 100644 --- a/autogpt_platform/backend/backend/blocks/todoist/tasks.py +++ b/autogpt_platform/backend/backend/blocks/todoist/tasks.py @@ -130,7 +130,7 @@ class TodoistCreateTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -261,7 +261,7 @@ class TodoistGetTasksBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -345,7 +345,7 @@ class TodoistGetTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -452,7 +452,7 @@ class TodoistUpdateTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -539,7 +539,7 @@ class TodoistCloseTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -592,7 +592,7 @@ class TodoistReopenTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, @@ -645,7 +645,7 @@ class TodoistDeleteTaskBlock(Block): except Exception as e: raise e - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/direct_message/direct_message_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/direct_message/direct_message_lookup.py index 14aee69379..99c5bcab79 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/direct_message/direct_message_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/direct_message/direct_message_lookup.py @@ -162,7 +162,7 @@ # except tweepy.TweepyException: # raise -# def run( +# async def run( # self, # input_data: Input, # *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/direct_message/manage_direct_message.py b/autogpt_platform/backend/backend/blocks/twitter/direct_message/manage_direct_message.py index caeb470bf6..19fdb2819f 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/direct_message/manage_direct_message.py +++ b/autogpt_platform/backend/backend/blocks/twitter/direct_message/manage_direct_message.py @@ -122,7 +122,7 @@ # print(f"Unexpected error: {str(e)}") # raise -# def run( +# async def run( # self, # input_data: Input, # *, @@ -239,7 +239,7 @@ # print(f"Unexpected error: {str(e)}") # raise -# def run( +# async def run( # self, # input_data: Input, # *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/list_follows.py b/autogpt_platform/backend/backend/blocks/twitter/lists/list_follows.py index 10722ce146..62c6c05f0c 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/list_follows.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/list_follows.py @@ -68,7 +68,7 @@ class TwitterUnfollowListBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -131,7 +131,7 @@ class TwitterFollowListBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -276,7 +276,7 @@ class TwitterFollowListBlock(Block): # except tweepy.TweepyException: # raise -# def run( +# async def run( # self, # input_data: Input, # *, @@ -438,7 +438,7 @@ class TwitterFollowListBlock(Block): # except tweepy.TweepyException: # raise -# def run( +# async def run( # self, # input_data: Input, # *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/list_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/lists/list_lookup.py index 5860b6e165..6dbaf2b23d 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/list_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/list_lookup.py @@ -140,7 +140,7 @@ class TwitterGetListBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -312,7 +312,7 @@ class TwitterGetOwnedListsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/list_members.py b/autogpt_platform/backend/backend/blocks/twitter/lists/list_members.py index 8ad5125c20..9bcd8f15a2 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/list_members.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/list_members.py @@ -90,7 +90,7 @@ class TwitterRemoveListMemberBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -164,7 +164,7 @@ class TwitterAddListMemberBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -327,7 +327,7 @@ class TwitterGetListMembersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -493,7 +493,7 @@ class TwitterGetListMembershipsBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/list_tweets_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/lists/list_tweets_lookup.py index 5faa855019..bda25e1d2d 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/list_tweets_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/list_tweets_lookup.py @@ -178,7 +178,7 @@ class TwitterGetListTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/manage_lists.py b/autogpt_platform/backend/backend/blocks/twitter/lists/manage_lists.py index 800b1219c9..2ba8158f9c 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/manage_lists.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/manage_lists.py @@ -64,7 +64,7 @@ class TwitterDeleteListBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -158,7 +158,7 @@ class TwitterUpdateListBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -263,7 +263,7 @@ class TwitterCreateListBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/lists/pinned_lists.py b/autogpt_platform/backend/backend/blocks/twitter/lists/pinned_lists.py index 7da93b531a..a31d1059f6 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/lists/pinned_lists.py +++ b/autogpt_platform/backend/backend/blocks/twitter/lists/pinned_lists.py @@ -76,7 +76,7 @@ class TwitterUnpinListBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -140,7 +140,7 @@ class TwitterPinListBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -257,7 +257,7 @@ class TwitterGetPinnedListsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/spaces/search_spaces.py b/autogpt_platform/backend/backend/blocks/twitter/spaces/search_spaces.py index 2640d78bf9..77b28fa654 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/spaces/search_spaces.py +++ b/autogpt_platform/backend/backend/blocks/twitter/spaces/search_spaces.py @@ -158,7 +158,7 @@ class TwitterSearchSpacesBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/spaces/spaces_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/spaces/spaces_lookup.py index bf1f527970..d4ff5459e4 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/spaces/spaces_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/spaces/spaces_lookup.py @@ -186,7 +186,7 @@ class TwitterGetSpacesBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -341,7 +341,7 @@ class TwitterGetSpaceByIdBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -477,7 +477,7 @@ class TwitterGetSpaceBuyersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -618,7 +618,7 @@ class TwitterGetSpaceTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/bookmark.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/bookmark.py index 50add77be8..ec8976fc2f 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/bookmark.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/bookmark.py @@ -85,7 +85,7 @@ class TwitterBookmarkTweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -262,7 +262,7 @@ class TwitterGetBookmarkedTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -362,7 +362,7 @@ class TwitterRemoveBookmarkTweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py index 78ab250c7f..65faa315ae 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py @@ -68,7 +68,7 @@ class TwitterHideReplyBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -140,7 +140,7 @@ class TwitterUnhideReplyBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/like.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/like.py index 609c1c6bb4..8bbc30e8e9 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/like.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/like.py @@ -90,7 +90,7 @@ class TwitterLikeTweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -249,7 +249,7 @@ class TwitterGetLikingUsersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -467,7 +467,7 @@ class TwitterGetLikedTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -564,7 +564,7 @@ class TwitterUnlikeTweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py index e9a59afec2..6dca0d74c8 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py @@ -211,7 +211,7 @@ class TwitterPostTweetBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -288,7 +288,7 @@ class TwitterDeleteTweetBlock(Block): except Exception: raise - def run( + async def run( self, input_data: Input, *, @@ -508,7 +508,7 @@ class TwitterSearchRecentTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py index 40df117b1c..b15271b072 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py @@ -186,7 +186,7 @@ class TwitterGetQuoteTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/retweet.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/retweet.py index c0645e54fd..9b1ba81b78 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/retweet.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/retweet.py @@ -85,7 +85,7 @@ class TwitterRetweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -162,7 +162,7 @@ class TwitterRemoveRetweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -328,7 +328,7 @@ class TwitterGetRetweetersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/timeline.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/timeline.py index 1dcce6d12a..ca89039c2e 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/timeline.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/timeline.py @@ -234,7 +234,7 @@ class TwitterGetUserMentionsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -467,7 +467,7 @@ class TwitterGetHomeTimelineBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -713,7 +713,7 @@ class TwitterGetUserTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/tweet_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/tweet_lookup.py index a71eb11b60..5021161b9e 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/tweet_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/tweet_lookup.py @@ -153,7 +153,7 @@ class TwitterGetTweetBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -327,7 +327,7 @@ class TwitterGetTweetsBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/users/blocks.py b/autogpt_platform/backend/backend/blocks/twitter/users/blocks.py index c83632067a..ca118e91e2 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/users/blocks.py +++ b/autogpt_platform/backend/backend/blocks/twitter/users/blocks.py @@ -147,7 +147,7 @@ class TwitterGetBlockedUsersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/users/follows.py b/autogpt_platform/backend/backend/blocks/twitter/users/follows.py index a810750cbd..160ffe9b35 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/users/follows.py +++ b/autogpt_platform/backend/backend/blocks/twitter/users/follows.py @@ -82,7 +82,7 @@ class TwitterUnfollowUserBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -152,7 +152,7 @@ class TwitterFollowUserBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -308,7 +308,7 @@ class TwitterGetFollowersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -482,7 +482,7 @@ class TwitterGetFollowingBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/users/mutes.py b/autogpt_platform/backend/backend/blocks/twitter/users/mutes.py index 6be5534bee..36bb4028f9 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/users/mutes.py +++ b/autogpt_platform/backend/backend/blocks/twitter/users/mutes.py @@ -82,7 +82,7 @@ class TwitterUnmuteUserBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -232,7 +232,7 @@ class TwitterGetMutedUsersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -318,7 +318,7 @@ class TwitterMuteUserBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/twitter/users/user_lookup.py b/autogpt_platform/backend/backend/blocks/twitter/users/user_lookup.py index 7c4bc8322d..585ebff3db 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/users/user_lookup.py +++ b/autogpt_platform/backend/backend/blocks/twitter/users/user_lookup.py @@ -168,7 +168,7 @@ class TwitterGetUserBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, @@ -357,7 +357,7 @@ class TwitterGetUsersBlock(Block): except tweepy.TweepyException: raise - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/blocks/xml_parser.py b/autogpt_platform/backend/backend/blocks/xml_parser.py index 523fe1a69d..a3d5854499 100644 --- a/autogpt_platform/backend/backend/blocks/xml_parser.py +++ b/autogpt_platform/backend/backend/blocks/xml_parser.py @@ -25,7 +25,7 @@ class XMLParserBlock(Block): ], ) - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: try: tokens = tokenize(input_data.input_xml) parser = Parser(tokens) diff --git a/autogpt_platform/backend/backend/blocks/youtube.py b/autogpt_platform/backend/backend/blocks/youtube.py index 648d9d6dae..cc16c4ae41 100644 --- a/autogpt_platform/backend/backend/blocks/youtube.py +++ b/autogpt_platform/backend/backend/blocks/youtube.py @@ -79,7 +79,7 @@ class TranscribeYoutubeVideoBlock(Block): except Exception: raise ValueError(f"No transcripts found for the video: {video_id}") - def run(self, input_data: Input, **kwargs) -> BlockOutput: + async def run(self, input_data: Input, **kwargs) -> BlockOutput: video_id = self.extract_video_id(input_data.youtube_url) yield "video_id", video_id diff --git a/autogpt_platform/backend/backend/blocks/zerobounce/validate_emails.py b/autogpt_platform/backend/backend/blocks/zerobounce/validate_emails.py index ee87a8f285..b23e822ddc 100644 --- a/autogpt_platform/backend/backend/blocks/zerobounce/validate_emails.py +++ b/autogpt_platform/backend/backend/blocks/zerobounce/validate_emails.py @@ -159,7 +159,7 @@ class ValidateEmailsBlock(Block): client = ZeroBounceClient(credentials.api_key.get_secret_value()) return client.validate_email(email, ip_address) - def run( + async def run( self, input_data: Input, *, diff --git a/autogpt_platform/backend/backend/cli.py b/autogpt_platform/backend/backend/cli.py index 858188f084..988961b2de 100755 --- a/autogpt_platform/backend/backend/cli.py +++ b/autogpt_platform/backend/backend/cli.py @@ -117,7 +117,7 @@ def test(): @test.command() @click.argument("server_address") -def reddit(server_address: str): +async def reddit(server_address: str): """ Create an event graph """ @@ -129,7 +129,7 @@ def reddit(server_address: str): headers = {"Content-Type": "application/json"} data = test_graph.model_dump_json() - response = Requests(trusted_origins=[server_address]).post( + response = await Requests(trusted_origins=[server_address]).post( url, headers=headers, data=data ) @@ -139,7 +139,7 @@ def reddit(server_address: str): @test.command() @click.argument("server_address") -def populate_db(server_address: str): +async def populate_db(server_address: str): """ Create an event graph """ @@ -152,13 +152,13 @@ def populate_db(server_address: str): headers = {"Content-Type": "application/json"} data = test_graph.model_dump_json() - response = Requests(trusted_origins=[server_address]).post( + response = await Requests(trusted_origins=[server_address]).post( url, headers=headers, data=data ) graph_id = response.json()["id"] - if response.status_code == 200: + if response.status == 200: execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" text = "Hello, World!" input_data = {"input": text} @@ -181,7 +181,7 @@ def populate_db(server_address: str): @test.command() @click.argument("server_address") -def graph(server_address: str): +async def graph(server_address: str): """ Create an event graph """ @@ -192,28 +192,28 @@ def graph(server_address: str): url = f"{server_address}/graphs" headers = {"Content-Type": "application/json"} data = create_test_graph().model_dump_json() - response = Requests(trusted_origins=[server_address]).post( + response = await Requests(trusted_origins=[server_address]).post( url, headers=headers, data=data ) - if response.status_code == 200: + if response.status == 200: print(response.json()["id"]) execute_url = f"{server_address}/graphs/{response.json()['id']}/execute" text = "Hello, World!" input_data = {"input": text} - response = Requests(trusted_origins=[server_address]).post( + response = await Requests(trusted_origins=[server_address]).post( execute_url, headers=headers, json=input_data ) else: print("Failed to send graph") - print(f"Response: {response.text}") + print(f"Response: {response.text()}") @test.command() @click.argument("graph_id") @click.argument("content") -def execute(graph_id: str, content: dict): +async def execute(graph_id: str, content: dict): """ Create an event graph """ @@ -223,7 +223,7 @@ def execute(graph_id: str, content: dict): headers = {"Content-Type": "application/json"} execute_url = f"http://0.0.0.0:8000/graphs/{graph_id}/execute" - Requests(trusted_origins=["http://0.0.0.0:8000"]).post( + await Requests(trusted_origins=["http://0.0.0.0:8000"]).post( execute_url, headers=headers, json=content ) diff --git a/autogpt_platform/backend/backend/data/block.py b/autogpt_platform/backend/backend/data/block.py index eca44fd7cd..b511547f1e 100644 --- a/autogpt_platform/backend/backend/data/block.py +++ b/autogpt_platform/backend/backend/data/block.py @@ -1,12 +1,12 @@ import functools import inspect from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator as AsyncGen from enum import Enum from typing import ( TYPE_CHECKING, Any, ClassVar, - Generator, Generic, Optional, Sequence, @@ -42,7 +42,7 @@ app_config = Config() BlockData = tuple[str, Any] # Input & Output data should be a tuple of (name, data). BlockInput = dict[str, Any] # Input: 1 input pin consumes 1 data. -BlockOutput = Generator[BlockData, None, None] # Output: 1 output pin produces n data. +BlockOutput = AsyncGen[BlockData, None] # Output: 1 output pin produces n data. CompletedBlockOutput = dict[str, list[Any]] # Completed stream, collected as a dict. @@ -388,7 +388,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): return cls() @abstractmethod - def run(self, input_data: BlockSchemaInputType, **kwargs) -> BlockOutput: + async def run(self, input_data: BlockSchemaInputType, **kwargs) -> BlockOutput: """ Run the block with the given input data. Args: @@ -406,10 +406,16 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): output_name: One of the output name defined in Block's output_schema. output_data: The data for the output_name, matching the defined schema. """ - pass + # --- satisfy the type checker, never executed ------------- + if False: # noqa: SIM115 + yield "name", "value" # pyright: ignore[reportMissingYield] + raise NotImplementedError(f"{self.name} does not implement the run method.") - def run_once(self, input_data: BlockSchemaInputType, output: str, **kwargs) -> Any: - for name, data in self.run(input_data, **kwargs): + async def run_once( + self, input_data: BlockSchemaInputType, output: str, **kwargs + ) -> Any: + async for item in self.run(input_data, **kwargs): + name, data = item if name == output: return data raise ValueError(f"{self.name} did not produce any output for {output}") @@ -458,13 +464,13 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): "uiType": self.block_type.value, } - def execute(self, input_data: BlockInput, **kwargs) -> BlockOutput: + async def execute(self, input_data: BlockInput, **kwargs) -> BlockOutput: if error := self.input_schema.validate_data(input_data): raise ValueError( f"Unable to execute block with invalid input data: {error}" ) - for output_name, output_data in self.run( + async for output_name, output_data in self.run( self.input_schema(**input_data), **kwargs ): if output_name == "error": diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py index 757c788f2f..016a7f1e91 100644 --- a/autogpt_platform/backend/backend/data/execution.py +++ b/autogpt_platform/backend/backend/data/execution.py @@ -50,6 +50,7 @@ from .block import ( from .db import BaseDbModel from .includes import ( EXECUTION_RESULT_INCLUDE, + EXECUTION_RESULT_ORDER, GRAPH_EXECUTION_INCLUDE_WITH_NODES, graph_execution_include, ) @@ -744,6 +745,7 @@ async def get_node_executions( executions = await AgentNodeExecution.prisma().find_many( where=where_clause, include=EXECUTION_RESULT_INCLUDE, + order=EXECUTION_RESULT_ORDER, take=limit, ) res = [NodeExecutionResult.from_db(execution) for execution in executions] @@ -765,11 +767,8 @@ async def get_latest_node_execution( {"executionStatus": ExecutionStatus.FAILED}, ], }, - order=[ - {"queuedTime": "desc"}, - {"addedTime": "desc"}, - ], include=EXECUTION_RESULT_INCLUDE, + order=EXECUTION_RESULT_ORDER, ) if not execution: return None diff --git a/autogpt_platform/backend/backend/data/graph.py b/autogpt_platform/backend/backend/data/graph.py index 7733ec3518..d3d3ae8a02 100644 --- a/autogpt_platform/backend/backend/data/graph.py +++ b/autogpt_platform/backend/backend/data/graph.py @@ -1085,7 +1085,7 @@ async def fix_llm_provider_credentials(): ) continue - store.update_creds(user_id, credentials) + await store.update_creds(user_id, credentials) await AgentNode.prisma().update( where={"id": node_id}, data={"constantInput": Json(node_preset_input)}, diff --git a/autogpt_platform/backend/backend/data/includes.py b/autogpt_platform/backend/backend/data/includes.py index d763579d91..347bb9ef91 100644 --- a/autogpt_platform/backend/backend/data/includes.py +++ b/autogpt_platform/backend/backend/data/includes.py @@ -14,9 +14,15 @@ AGENT_GRAPH_INCLUDE: prisma.types.AgentGraphInclude = { "Nodes": {"include": AGENT_NODE_INCLUDE} } +EXECUTION_RESULT_ORDER: list[prisma.types.AgentNodeExecutionOrderByInput] = [ + {"queuedTime": "desc"}, + # Fallback: Incomplete execs has no queuedTime. + {"addedTime": "desc"}, +] + EXECUTION_RESULT_INCLUDE: prisma.types.AgentNodeExecutionInclude = { - "Input": True, - "Output": True, + "Input": {"order_by": {"time": "asc"}}, + "Output": {"order_by": {"time": "asc"}}, "Node": True, "GraphExecution": True, } @@ -25,17 +31,8 @@ MAX_NODE_EXECUTIONS_FETCH = 1000 GRAPH_EXECUTION_INCLUDE_WITH_NODES: prisma.types.AgentGraphExecutionInclude = { "NodeExecutions": { - "include": { - "Input": True, - "Output": True, - "Node": True, - "GraphExecution": True, - }, - "order_by": [ - {"queuedTime": "desc"}, - # Fallback: Incomplete execs has no queuedTime. - {"addedTime": "desc"}, - ], + "include": EXECUTION_RESULT_INCLUDE, + "order_by": EXECUTION_RESULT_ORDER, "take": MAX_NODE_EXECUTIONS_FETCH, # Avoid loading excessive node executions. } } diff --git a/autogpt_platform/backend/backend/data/notifications.py b/autogpt_platform/backend/backend/data/notifications.py index d0f49b7021..14cf77d3c4 100644 --- a/autogpt_platform/backend/backend/data/notifications.py +++ b/autogpt_platform/backend/backend/data/notifications.py @@ -111,7 +111,14 @@ class BaseSummaryData(BaseNotificationData): class BaseSummaryParams(BaseModel): - pass + start_date: datetime + end_date: datetime + + @field_validator("start_date", "end_date") + def validate_timezone(cls, value): + if value.tzinfo is None: + raise ValueError("datetime must have timezone information") + return value class DailySummaryParams(BaseSummaryParams): diff --git a/autogpt_platform/backend/backend/data/redis.py b/autogpt_platform/backend/backend/data/redis.py index 36410fe29c..c6225131f2 100644 --- a/autogpt_platform/backend/backend/data/redis.py +++ b/autogpt_platform/backend/backend/data/redis.py @@ -1,6 +1,8 @@ import logging import os +from functools import cache +from autogpt_libs.utils.cache import thread_cached from dotenv import load_dotenv from redis import Redis from redis.asyncio import Redis as AsyncRedis @@ -14,16 +16,10 @@ PORT = int(os.getenv("REDIS_PORT", "6379")) PASSWORD = os.getenv("REDIS_PASSWORD", "password") logger = logging.getLogger(__name__) -connection: Redis | None = None -connection_async: AsyncRedis | None = None @conn_retry("Redis", "Acquiring connection") def connect() -> Redis: - global connection - if connection: - return connection - c = Redis( host=HOST, port=PORT, @@ -31,32 +27,21 @@ def connect() -> Redis: decode_responses=True, ) c.ping() - connection = c - return connection + return c @conn_retry("Redis", "Releasing connection") def disconnect(): - global connection - if connection: - connection.close() - connection = None + get_redis().close() -def get_redis(auto_connect: bool = True) -> Redis: - if connection: - return connection - if auto_connect: - return connect() - raise RuntimeError("Redis connection is not established") +@cache +def get_redis() -> Redis: + return connect() @conn_retry("AsyncRedis", "Acquiring connection") async def connect_async() -> AsyncRedis: - global connection_async - if connection_async: - return connection_async - c = AsyncRedis( host=HOST, port=PORT, @@ -64,21 +49,15 @@ async def connect_async() -> AsyncRedis: decode_responses=True, ) await c.ping() - connection_async = c - return connection_async + return c @conn_retry("AsyncRedis", "Releasing connection") async def disconnect_async(): - global connection_async - if connection_async: - await connection_async.close() - connection_async = None + c = await get_redis_async() + await c.close() -async def get_redis_async(auto_connect: bool = True) -> AsyncRedis: - if connection_async: - return connection_async - if auto_connect: - return await connect_async() - raise RuntimeError("AsyncRedis connection is not established") +@thread_cached +async def get_redis_async() -> AsyncRedis: + return await connect_async() diff --git a/autogpt_platform/backend/backend/executor/__init__.py b/autogpt_platform/backend/backend/executor/__init__.py index a92302a62e..92d8b5dc58 100644 --- a/autogpt_platform/backend/backend/executor/__init__.py +++ b/autogpt_platform/backend/backend/executor/__init__.py @@ -1,10 +1,11 @@ -from .database import DatabaseManager, DatabaseManagerClient +from .database import DatabaseManager, DatabaseManagerAsyncClient, DatabaseManagerClient from .manager import ExecutionManager from .scheduler import Scheduler __all__ = [ "DatabaseManager", "DatabaseManagerClient", + "DatabaseManagerAsyncClient", "ExecutionManager", "Scheduler", ] diff --git a/autogpt_platform/backend/backend/executor/database.py b/autogpt_platform/backend/backend/executor/database.py index e1b4c9c776..81440f3555 100644 --- a/autogpt_platform/backend/backend/executor/database.py +++ b/autogpt_platform/backend/backend/executor/database.py @@ -192,3 +192,26 @@ class DatabaseManagerClient(AppServiceClient): get_user_notification_oldest_message_in_batch = _( d.get_user_notification_oldest_message_in_batch ) + + +class DatabaseManagerAsyncClient(AppServiceClient): + d = DatabaseManager + + @classmethod + def get_service_type(cls): + return DatabaseManager + + create_graph_execution = d.create_graph_execution + get_latest_node_execution = d.get_latest_node_execution + get_graph = d.get_graph + get_node = d.get_node + get_node_execution = d.get_node_execution + get_node_executions = d.get_node_executions + get_user_integrations = d.get_user_integrations + 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 diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index d4bf006473..aabdb94411 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -1,20 +1,18 @@ -import atexit +import asyncio import logging import multiprocessing import os -import signal import sys import threading import time from collections import defaultdict -from concurrent.futures import Future, ProcessPoolExecutor -from contextlib import contextmanager -from multiprocessing.pool import Pool -from typing import TYPE_CHECKING, Optional, TypeVar, cast +from concurrent.futures import CancelledError, Future, ProcessPoolExecutor +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast from pika.adapters.blocking_connection import BlockingChannel from pika.spec import Basic, BasicProperties -from redis.lock import Lock as RedisLock +from redis.asyncio.lock import Lock as RedisLock from backend.blocks.io import AgentOutputBlock from backend.data.model import ( @@ -34,7 +32,7 @@ from backend.notifications.notifications import queue_notification from backend.util.exceptions import InsufficientBalanceError if TYPE_CHECKING: - from backend.executor import DatabaseManagerClient + from backend.executor import DatabaseManagerClient, DatabaseManagerAsyncClient from autogpt_libs.utils.cache import thread_cached from prometheus_client import Gauge, start_http_server @@ -66,6 +64,7 @@ from backend.executor.utils import ( NodeExecutionProgress, block_usage_cost, execution_usage_cost, + get_async_execution_event_bus, get_execution_event_bus, get_execution_queue, parse_execution_output, @@ -73,7 +72,12 @@ from backend.executor.utils import ( ) from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.util import json -from backend.util.decorator import error_logged, time_measured +from backend.util.decorator import ( + async_error_logged, + async_time_measured, + error_logged, + time_measured, +) from backend.util.file import clean_exec_files from backend.util.logging import TruncatedLogger, configure_logging from backend.util.process import AppProcess, set_service_name @@ -128,7 +132,7 @@ class LogMetadata(TruncatedLogger): T = TypeVar("T") -def execute_node( +async def execute_node( node: Node, creds_manager: IntegrationCredentialsManager, data: NodeExecutionEntry, @@ -205,12 +209,14 @@ def execute_node( input_model = cast(type[BlockSchema], node_block.input_schema) for field_name, input_type in input_model.get_credentials_fields().items(): credentials_meta = input_type(**input_data[field_name]) - credentials, creds_lock = creds_manager.acquire(user_id, credentials_meta.id) + credentials, creds_lock = await creds_manager.acquire( + user_id, credentials_meta.id + ) extra_exec_kwargs[field_name] = credentials output_size = 0 try: - for output_name, output_data in node_block.execute( + async for output_name, output_data in node_block.execute( input_data, **extra_exec_kwargs ): output_data = json.convert_pydantic_to_json(output_data) @@ -225,9 +231,9 @@ def execute_node( finally: # Ensure credentials are released even if execution fails - if creds_lock and creds_lock.locked() and creds_lock.owned(): + if creds_lock and (await creds_lock.locked()) and (await creds_lock.owned()): try: - creds_lock.release() + await creds_lock.release() except Exception as e: log_metadata.error(f"Failed to release credentials lock: {e}") @@ -240,8 +246,8 @@ def execute_node( execution_stats.output_size = output_size -def _enqueue_next_nodes( - db_client: "DatabaseManagerClient", +async def _enqueue_next_nodes( + db_client: "DatabaseManagerAsyncClient", node: Node, output: BlockData, user_id: str, @@ -250,10 +256,10 @@ def _enqueue_next_nodes( log_metadata: LogMetadata, node_credentials_input_map: Optional[dict[str, dict[str, CredentialsMetaInput]]], ) -> list[NodeExecutionEntry]: - def add_enqueued_execution( + async def add_enqueued_execution( node_exec_id: str, node_id: str, block_id: str, data: BlockInput ) -> NodeExecutionEntry: - update_node_execution_status( + await async_update_node_execution_status( db_client=db_client, exec_id=node_exec_id, status=ExecutionStatus.QUEUED, @@ -269,14 +275,14 @@ def _enqueue_next_nodes( inputs=data, ) - def register_next_executions(node_link: Link) -> list[NodeExecutionEntry]: + async def register_next_executions(node_link: Link) -> list[NodeExecutionEntry]: try: - return _register_next_executions(node_link) + return await _register_next_executions(node_link) except Exception as e: log_metadata.exception(f"Failed to register next executions: {e}") return [] - def _register_next_executions(node_link: Link) -> list[NodeExecutionEntry]: + async def _register_next_executions(node_link: Link) -> list[NodeExecutionEntry]: enqueued_executions = [] next_output_name = node_link.source_name next_input_name = node_link.sink_name @@ -285,21 +291,20 @@ def _enqueue_next_nodes( next_data = parse_execution_output(output, next_output_name) if next_data is None: return enqueued_executions - - next_node = db_client.get_node(next_node_id) + next_node = await db_client.get_node(next_node_id) # Multiple node can register the same next node, we need this to be atomic # To avoid same execution to be enqueued multiple times, # Or the same input to be consumed multiple times. - with synchronized(f"upsert_input-{next_node_id}-{graph_exec_id}"): + async with synchronized(f"upsert_input-{next_node_id}-{graph_exec_id}"): # Add output data to the earliest incomplete execution, or create a new one. - next_node_exec_id, next_node_input = db_client.upsert_execution_input( + next_node_exec_id, next_node_input = await db_client.upsert_execution_input( node_id=next_node_id, graph_exec_id=graph_exec_id, input_name=next_input_name, input_data=next_data, ) - update_node_execution_status( + await async_update_node_execution_status( db_client=db_client, exec_id=next_node_exec_id, status=ExecutionStatus.INCOMPLETE, @@ -312,7 +317,7 @@ def _enqueue_next_nodes( if link.is_static and link.sink_name not in next_node_input } if static_link_names and ( - latest_execution := db_client.get_latest_node_execution( + latest_execution := await db_client.get_latest_node_execution( next_node_id, graph_exec_id ) ): @@ -340,7 +345,7 @@ def _enqueue_next_nodes( # Input is complete, enqueue the execution. log_metadata.info(f"Enqueued {suffix}") enqueued_executions.append( - add_enqueued_execution( + await add_enqueued_execution( node_exec_id=next_node_exec_id, node_id=next_node_id, block_id=next_node.block_id, @@ -354,7 +359,7 @@ def _enqueue_next_nodes( # If link is static, there could be some incomplete executions waiting for it. # Load and complete the input missing input data, and try to re-enqueue them. - for iexec in db_client.get_node_executions( + for iexec in await db_client.get_node_executions( node_id=next_node_id, graph_exec_id=graph_exec_id, statuses=[ExecutionStatus.INCOMPLETE], @@ -383,7 +388,7 @@ def _enqueue_next_nodes( continue log_metadata.info(f"Enqueueing static-link execution {suffix}") enqueued_executions.append( - add_enqueued_execution( + await add_enqueued_execution( node_exec_id=iexec.node_exec_id, node_id=next_node_id, block_id=next_node.block_id, @@ -395,7 +400,7 @@ def _enqueue_next_nodes( return [ execution for link in node.output_links - for execution in register_next_executions(link) + for execution in await register_next_executions(link) ] @@ -404,11 +409,9 @@ class Executor: This class contains event handlers for the process pool executor events. The main events are: - on_node_executor_start: Initialize the process that executes the node. - on_node_execution: Execution logic for a node. - on_graph_executor_start: Initialize the process that executes the graph. on_graph_execution: Execution logic for a graph. + on_node_execution: Execution logic for a node. The execution flow: 1. Graph execution request is added to the queue. @@ -425,46 +428,11 @@ class Executor: """ @classmethod - @func_retry - def on_node_executor_start(cls): - configure_logging() - set_service_name("NodeExecutor") - redis.connect() - cls.pid = os.getpid() - cls.db_client = get_db_client() - cls.creds_manager = IntegrationCredentialsManager() - - # Set up shutdown handlers - cls.shutdown_lock = threading.Lock() - atexit.register(cls.on_node_executor_stop) - signal.signal(signal.SIGTERM, lambda _, __: cls.on_node_executor_sigterm()) - signal.signal(signal.SIGINT, lambda _, __: cls.on_node_executor_sigterm()) - - @classmethod - def on_node_executor_stop(cls, log=logger.info): - if not cls.shutdown_lock.acquire(blocking=False): - return # already shutting down - - log(f"[on_node_executor_stop {cls.pid}] ⏳ Releasing locks...") - cls.creds_manager.release_all_locks() - log(f"[on_node_executor_stop {cls.pid}] ⏳ Disconnecting Redis...") - redis.disconnect() - log(f"[on_node_executor_stop {cls.pid}] ⏳ Disconnecting DB manager...") - cls.db_client.close() - log(f"[on_node_executor_stop {cls.pid}] ✅ Finished NodeExec cleanup") - sys.exit(0) - - @classmethod - def on_node_executor_sigterm(cls): - llprint(f"[on_node_executor_sigterm {cls.pid}] ⚠️ NodeExec SIGTERM received") - cls.on_node_executor_stop(log=llprint) - - @classmethod - @error_logged - def on_node_execution( + @async_error_logged + async def on_node_execution( cls, - q: ExecutionQueue[ExecutionOutputEntry], node_exec: NodeExecutionEntry, + node_exec_progress: NodeExecutionProgress, node_credentials_input_map: Optional[ dict[str, dict[str, CredentialsMetaInput]] ] = None, @@ -477,13 +445,15 @@ class Executor: node_id=node_exec.node_id, block_name="-", ) - node = cls.db_client.get_node(node_exec.node_id) + db_client = get_db_async_client() + node = await db_client.get_node(node_exec.node_id) execution_stats = NodeExecutionStats() - timing_info, _ = cls._on_node_execution( - q=q, - node_exec=node_exec, + timing_info, _ = await cls._on_node_execution( node=node, + node_exec=node_exec, + node_exec_progress=node_exec_progress, + db_client=db_client, log_metadata=log_metadata, stats=execution_stats, node_credentials_input_map=node_credentials_input_map, @@ -493,19 +463,20 @@ class Executor: if isinstance(execution_stats.error, Exception): execution_stats.error = str(execution_stats.error) - exec_update = cls.db_client.update_node_execution_stats( + exec_update = await db_client.update_node_execution_stats( node_exec.node_exec_id, execution_stats ) - send_execution_update(exec_update) + await send_async_execution_update(exec_update) return execution_stats @classmethod - @time_measured - def _on_node_execution( + @async_time_measured + async def _on_node_execution( cls, - q: ExecutionQueue[ExecutionOutputEntry], - node_exec: NodeExecutionEntry, node: Node, + node_exec: NodeExecutionEntry, + node_exec_progress: NodeExecutionProgress, + db_client: "DatabaseManagerAsyncClient", log_metadata: LogMetadata, stats: NodeExecutionStats | None = None, node_credentials_input_map: Optional[ @@ -514,20 +485,20 @@ class Executor: ): try: log_metadata.info(f"Start node execution {node_exec.node_exec_id}") - update_node_execution_status( - db_client=cls.db_client, + await async_update_node_execution_status( + db_client=db_client, exec_id=node_exec.node_exec_id, status=ExecutionStatus.RUNNING, ) - for output_name, output_data in execute_node( + async for output_name, output_data in execute_node( node=node, creds_manager=cls.creds_manager, data=node_exec, execution_stats=stats, node_credentials_input_map=node_credentials_input_map, ): - q.add( + node_exec_progress.add_output( ExecutionOutputEntry( node=node, node_exec_id=node_exec.node_exec_id, @@ -554,19 +525,19 @@ class Executor: def on_graph_executor_start(cls): configure_logging() set_service_name("GraphExecutor") - - cls.db_client = get_db_client() - cls.pool_size = settings.config.num_node_workers cls.pid = os.getpid() - cls._init_node_executor_pool() - logger.info(f"GraphExec {cls.pid} started with {cls.pool_size} node workers") - - @classmethod - def _init_node_executor_pool(cls): - cls.executor = Pool( - processes=cls.pool_size, - initializer=cls.on_node_executor_start, + 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 ) + cls.node_evaluation_thread = threading.Thread( + target=cls.node_evaluation_loop.run_forever, daemon=True + ) + cls.node_execution_thread.start() + cls.node_evaluation_thread.start() + logger.info(f"[GraphExecutor] {cls.pid} started") @classmethod @error_logged @@ -581,8 +552,9 @@ class Executor: node_eid="*", block_name="-", ) + db_client = get_db_client() - exec_meta = cls.db_client.get_graph_execution_meta( + exec_meta = db_client.get_graph_execution_meta( user_id=graph_exec.user_id, execution_id=graph_exec.graph_exec_id, ) @@ -596,9 +568,7 @@ class Executor: log_metadata.info(f"⚙️ Starting graph execution #{graph_exec.graph_exec_id}") exec_meta.status = ExecutionStatus.RUNNING send_execution_update( - cls.db_client.update_graph_execution_start_time( - graph_exec.graph_exec_id - ) + db_client.update_graph_execution_start_time(graph_exec.graph_exec_id) ) elif exec_meta.status == ExecutionStatus.RUNNING: log_metadata.info( @@ -622,14 +592,14 @@ class Executor: exec_stats.cputime += timing_info.cpu_time exec_stats.error = str(error) if error else exec_stats.error - if graph_exec_result := cls.db_client.update_graph_execution_stats( + 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) - cls._handle_agent_run_notif(graph_exec, exec_stats) + cls._handle_agent_run_notif(db_client, graph_exec, exec_stats) @classmethod def _charge_usage( @@ -638,6 +608,7 @@ class Executor: execution_count: int, execution_stats: GraphExecutionStats, ): + 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.") @@ -647,7 +618,7 @@ class Executor: block=block, input_data=node_exec.inputs ) if cost > 0: - cls.db_client.spend_credits( + db_client.spend_credits( user_id=node_exec.user_id, cost=cost, metadata=UsageTransactionMetadata( @@ -665,7 +636,7 @@ class Executor: cost, usage_count = execution_usage_cost(execution_count) if cost > 0: - cls.db_client.spend_credits( + db_client.spend_credits( user_id=node_exec.user_id, cost=cost, metadata=UsageTransactionMetadata( @@ -695,18 +666,11 @@ class Executor: ExecutionStatus: The final status of the graph execution. Exception | None: The error that occurred during the execution, if any. """ - execution_status = ExecutionStatus.RUNNING - error = None - finished = False + execution_status: ExecutionStatus = ExecutionStatus.RUNNING + error: Exception | None = None + db_client = get_db_client() - def drain_output_queue(): - while output := output_queue.get_or_none(): - log_metadata.debug( - f"Received output for {output.node.id} - {output.node_exec_id}: {output.data}" - ) - running_executions[output.node.id].add_output(output) - - def drain_done_task(node_exec_id: str, result: object): + 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 @@ -718,49 +682,40 @@ class Executor: if (err := result.error) and isinstance(err, Exception): execution_stats.node_error_count += 1 update_node_execution_status( - db_client=cls.db_client, + db_client=db_client, exec_id=node_exec_id, status=ExecutionStatus.FAILED, ) else: update_node_execution_status( - db_client=cls.db_client, + db_client=db_client, exec_id=node_exec_id, status=ExecutionStatus.COMPLETED, ) - if _graph_exec := cls.db_client.update_graph_execution_stats( + if _graph_exec := db_client.update_graph_execution_stats( graph_exec_id=graph_exec.graph_exec_id, status=execution_status, stats=execution_stats, ): send_execution_update(_graph_exec) else: - logger.error( - "Callback for " - f"finished node execution #{node_exec_id} " - "could not update execution stats " + 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}" ) - def cancel_handler(): - nonlocal execution_status - - while not cancel.is_set(): - cancel.wait(1) - if finished: - return - execution_status = ExecutionStatus.TERMINATED - cls.executor.terminate() - log_metadata.info(f"Terminated graph execution {graph_exec.graph_exec_id}") - cls._init_node_executor_pool() - - cancel_thread = threading.Thread(target=cancel_handler) - cancel_thread.start() + # State holders ---------------------------------------------------- + running_node_execution: dict[str, NodeExecutionProgress] = defaultdict( + lambda: NodeExecutionProgress(on_done_task=on_done_task) + ) + running_node_evaluation: dict[str, Future] = {} + execution_queue = ExecutionQueue[NodeExecutionEntry]() try: - if cls.db_client.get_credits(graph_exec.user_id) <= 0: + if db_client.get_credits(graph_exec.user_id) <= 0: raise InsufficientBalanceError( user_id=graph_exec.user_id, message="You have no credits left to run an agent.", @@ -768,21 +723,18 @@ class Executor: amount=1, ) - output_queue = ExecutionQueue[ExecutionOutputEntry]() - execution_queue = ExecutionQueue[NodeExecutionEntry]() - for node_exec in cls.db_client.get_node_executions( + # ------------------------------------------------------------ + # Pre‑populate queue --------------------------------------- + # ------------------------------------------------------------ + for node_exec in db_client.get_node_executions( graph_exec.graph_exec_id, statuses=[ExecutionStatus.RUNNING, ExecutionStatus.QUEUED], ): execution_queue.add(node_exec.to_node_execution_entry()) - running_executions: dict[str, NodeExecutionProgress] = defaultdict( - lambda: NodeExecutionProgress( - drain_output_queue=drain_output_queue, - drain_done_task=drain_done_task, - ) - ) - + # ------------------------------------------------------------ + # Main dispatch / polling loop ----------------------------- + # ------------------------------------------------------------ while not execution_queue.empty(): if cancel.is_set(): execution_status = ExecutionStatus.TERMINATED @@ -795,6 +747,7 @@ class Executor: f"for node {queued_node_exec.node_id}", ) + # Charge usage (may raise) ------------------------------ try: cls._charge_usage( node_exec=queued_node_exec, @@ -803,19 +756,20 @@ class Executor: ) except InsufficientBalanceError as error: node_exec_id = queued_node_exec.node_exec_id - cls.db_client.upsert_execution_output( + db_client.upsert_execution_output( node_exec_id=node_exec_id, output_name="error", output_data=str(error), ) update_node_execution_status( - db_client=cls.db_client, + db_client=db_client, exec_id=node_exec_id, status=ExecutionStatus.FAILED, ) execution_status = ExecutionStatus.FAILED cls._handle_low_balance_notif( + db_client, graph_exec.user_id, graph_exec.graph_id, execution_stats, @@ -823,7 +777,7 @@ class Executor: ) raise - # Add credentials input overrides + # Add credential overrides ----------------------------- node_id = queued_node_exec.node_id if (node_creds_map := graph_exec.node_credentials_input_map) and ( node_field_creds_map := node_creds_map.get(node_id) @@ -835,72 +789,120 @@ class Executor: } ) - # Initiate node execution - running_executions[queued_node_exec.node_id].add_task( - queued_node_exec.node_exec_id, - cls.executor.apply_async( - cls.on_node_execution, - (output_queue, queued_node_exec, node_creds_map), + # Kick off async node execution ------------------------- + node_execution_task = asyncio.run_coroutine_threadsafe( + cls.on_node_execution( + node_exec=queued_node_exec, + node_exec_progress=running_node_execution[node_id], + node_credentials_input_map=node_creds_map, ), + cls.node_execution_loop, + ) + running_node_execution[node_id].add_task( + node_exec_id=queued_node_exec.node_exec_id, + task=node_execution_task, ) - # Avoid terminating graph execution when some nodes are still running. - while execution_queue.empty() and running_executions: - log_metadata.debug( - f"Queue empty; running nodes: {list(running_executions.keys())}" - ) - - # Register next node executions from running_executions. - for node_id, execution in list(running_executions.items()): + # Poll until queue refills or all inflight work done ---- + while execution_queue.empty() and ( + running_node_execution or running_node_evaluation + ): + # -------------------------------------------------- + # 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 - log_metadata.debug(f"Waiting on execution of node {node_id}") - while output := execution.pop_output(): - cls._process_node_output( - output=output, - node_id=node_id, - graph_exec=graph_exec, - log_metadata=log_metadata, - node_creds_map=node_creds_map, - execution_queue=execution_queue, + # node evaluation future ----------------- + if inflight_eval := running_node_evaluation.get(node_id): + try: + inflight_eval.result() + running_node_evaluation.pop(node_id) + except TimeoutError: + continue + + # node execution future --------------------------- + if inflight_exec.is_done(): + running_node_execution.pop(node_id) + continue + + if output := inflight_exec.pop_output(): + node_output_found = True + running_node_evaluation[node_id] = ( + asyncio.run_coroutine_threadsafe( + cls._process_node_output( + output=output, + node_id=node_id, + graph_exec=graph_exec, + log_metadata=log_metadata, + node_creds_map=node_creds_map, + execution_queue=execution_queue, + ), + cls.node_evaluation_loop, + ) ) - if not execution_queue.empty(): - break # Prioritize executing next nodes than enqueuing outputs - - if execution.is_done(): - running_executions.pop(node_id) - - if not execution_queue.empty(): - continue # Make sure each not is checked once - - if execution_queue.empty() and running_executions: - log_metadata.debug( - "No more nodes to execute, waiting for outputs..." - ) + if ( + not node_output_found + and execution_queue.empty() + and (running_node_execution or running_node_evaluation) + ): + # There is nothing to execute, and no output to process, let's relax for a while. time.sleep(0.1) - log_metadata.info(f"Finished graph execution {graph_exec.graph_exec_id}") + # loop done -------------------------------------------------- execution_status = ExecutionStatus.COMPLETED + return execution_stats, execution_status, error - except Exception as e: - error = e + except CancelledError as exc: + execution_status = ExecutionStatus.TERMINATED + error = exc + log_metadata.exception( + f"Cancelled graph execution {graph_exec.graph_exec_id}: {error}" + ) + except Exception as exc: + execution_status = ExecutionStatus.FAILED + error = exc log_metadata.exception( f"Failed graph execution {graph_exec.graph_exec_id}: {error}" ) - execution_status = ExecutionStatus.FAILED - finally: - if not cancel.is_set(): - finished = True - cancel.set() - cancel_thread.join() + 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() + + 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, + ], + ) + 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, + ) + 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 @classmethod - def _process_node_output( + async def _process_node_output( cls, output: ExecutionOutputEntry, node_id: str, @@ -919,19 +921,21 @@ class Executor: node_creds_map: Optional map of node credentials execution_queue: Queue to add next executions to """ + db_client = get_db_async_client() + try: name, data = output.data - cls.db_client.upsert_execution_output( + await db_client.upsert_execution_output( node_exec_id=output.node_exec_id, output_name=name, output_data=data, ) - if exec_update := cls.db_client.get_node_execution(output.node_exec_id): - send_execution_update(exec_update) + 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}") - for next_execution in _enqueue_next_nodes( - db_client=cls.db_client, + for next_execution in await _enqueue_next_nodes( + db_client=db_client, node=output.node, output=output.data, user_id=graph_exec.user_id, @@ -943,13 +947,13 @@ class Executor: execution_queue.add(next_execution) except Exception as e: log_metadata.exception(f"Failed to process node output: {e}") - cls.db_client.upsert_execution_output( + await db_client.upsert_execution_output( node_exec_id=output.node_exec_id, output_name="error", output_data=str(e), ) - update_node_execution_status( - db_client=cls.db_client, + await async_update_node_execution_status( + db_client=db_client, exec_id=output.node_exec_id, status=ExecutionStatus.FAILED, ) @@ -957,13 +961,14 @@ class Executor: @classmethod def _handle_agent_run_notif( cls, + db_client: "DatabaseManagerClient", graph_exec: GraphExecutionEntry, exec_stats: GraphExecutionStats, ): - metadata = cls.db_client.get_graph_metadata( + metadata = db_client.get_graph_metadata( graph_exec.graph_id, graph_exec.graph_version ) - outputs = cls.db_client.get_node_executions( + outputs = db_client.get_node_executions( graph_exec.graph_exec_id, block_ids=[AgentOutputBlock().id], ) @@ -994,13 +999,14 @@ class Executor: @classmethod def _handle_low_balance_notif( cls, + db_client: "DatabaseManagerClient", user_id: str, graph_id: str, exec_stats: GraphExecutionStats, e: InsufficientBalanceError, ): shortfall = e.balance - e.amount - metadata = cls.db_client.get_graph_metadata(graph_id) + metadata = db_client.get_graph_metadata(graph_id) base_url = ( settings.config.frontend_base_url or settings.config.platform_base_url ) @@ -1046,9 +1052,6 @@ class ExecutionManager(AppProcess): initializer=Executor.on_graph_executor_start, ) - logger.info(f"[{self.service_name}] ⏳ Connecting to Redis...") - redis.connect() - threading.Thread( target=lambda: self._consume_execution_cancel(), daemon=True, @@ -1162,19 +1165,21 @@ class ExecutionManager(AppProcess): 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 f.exception(): + if exec_error := f.exception(): logger.error( - f"[{self.service_name}] Execution for {graph_exec_id} failed: {f.exception()}" + f"[{self.service_name}] Execution for {graph_exec_id} failed: {exec_error}" ) channel.connection.add_callback_threadsafe( - lambda: channel.basic_nack(delivery_tag, requeue=False) + lambda: channel.basic_nack(delivery_tag, requeue=True) ) else: channel.connection.add_callback_threadsafe( lambda: channel.basic_ack(delivery_tag) ) - except Exception as e: - logger.error(f"[{self.service_name}] Error acknowledging message: {e}") + except BaseException as e: + logger.exception( + f"[{self.service_name}] Error acknowledging message: {e}" + ) future.add_done_callback(_on_run_done) @@ -1212,35 +1217,68 @@ def get_db_client() -> "DatabaseManagerClient": return get_service_client(DatabaseManagerClient, health_check=False) +@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) + + +async def send_async_execution_update( + entry: GraphExecution | NodeExecutionResult | None, +) -> None: + if entry is None: + return + await get_async_execution_event_bus().publish(entry) + + def send_execution_update(entry: GraphExecution | NodeExecutionResult | None): if entry is None: return return get_execution_event_bus().publish(entry) +async def async_update_node_execution_status( + db_client: "DatabaseManagerAsyncClient", + exec_id: str, + status: ExecutionStatus, + execution_data: BlockInput | None = None, + stats: dict[str, Any] | None = None, +) -> NodeExecutionResult: + """Sets status and fetches+broadcasts the latest state of the node execution""" + exec_update = await db_client.update_node_execution_status( + exec_id, status, execution_data, stats + ) + await send_async_execution_update(exec_update) + return exec_update + + def update_node_execution_status( db_client: "DatabaseManagerClient", exec_id: str, status: ExecutionStatus, execution_data: BlockInput | None = None, + stats: dict[str, Any] | None = None, ) -> NodeExecutionResult: """Sets status and fetches+broadcasts the latest state of the node execution""" exec_update = db_client.update_node_execution_status( - exec_id, status, execution_data + exec_id, status, execution_data, stats ) send_execution_update(exec_update) return exec_update -@contextmanager -def synchronized(key: str, timeout: int = 60): - lock: RedisLock = redis.get_redis().lock(f"lock:{key}", timeout=timeout) +@asynccontextmanager +async def synchronized(key: str, timeout: int = 60): + r = await redis.get_redis_async() + lock: RedisLock = r.lock(f"lock:{key}", timeout=timeout) try: - lock.acquire() + await lock.acquire() yield finally: - if lock.locked() and lock.owned(): - lock.release() + if await lock.locked() and await lock.owned(): + await lock.release() def increment_execution_count(user_id: str) -> int: diff --git a/autogpt_platform/backend/backend/executor/scheduler.py b/autogpt_platform/backend/backend/executor/scheduler.py index 6b9008d4cc..acb3aa481f 100644 --- a/autogpt_platform/backend/backend/executor/scheduler.py +++ b/autogpt_platform/backend/backend/executor/scheduler.py @@ -1,3 +1,4 @@ +import asyncio import logging import os from datetime import datetime, timedelta, timezone @@ -72,14 +73,19 @@ def get_notification_client(): def execute_graph(**kwargs): + asyncio.run(_execute_graph(**kwargs)) + + +async def _execute_graph(**kwargs): args = GraphExecutionJobArgs(**kwargs) try: log(f"Executing recurring job for graph #{args.graph_id}") - execution_utils.add_graph_execution( + await execution_utils.add_graph_execution( graph_id=args.graph_id, inputs=args.input_data, user_id=args.user_id, graph_version=args.graph_version, + use_db_query=False, ) except Exception as e: logger.exception(f"Error executing graph {args.graph_id}: {e}") diff --git a/autogpt_platform/backend/backend/executor/utils.py b/autogpt_platform/backend/backend/executor/utils.py index 2129632c2a..025528909d 100644 --- a/autogpt_platform/backend/backend/executor/utils.py +++ b/autogpt_platform/backend/backend/executor/utils.py @@ -1,6 +1,7 @@ +import asyncio import logging from collections import defaultdict -from multiprocessing.pool import AsyncResult +from concurrent.futures import Future from typing import TYPE_CHECKING, Any, Callable, Optional, cast from autogpt_libs.utils.cache import thread_cached @@ -23,6 +24,7 @@ from backend.data.execution import ( GraphExecutionWithNodes, RedisExecutionEventBus, create_graph_execution, + get_node_executions, update_graph_execution_stats, update_node_execution_status_batch, ) @@ -43,7 +45,7 @@ from backend.util.settings import Config from backend.util.type import convert if TYPE_CHECKING: - from backend.executor import DatabaseManagerClient + from backend.executor import DatabaseManagerAsyncClient, DatabaseManagerClient from backend.integrations.credentials_store import IntegrationCredentialsStore config = Config() @@ -90,6 +92,13 @@ def get_db_client() -> "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 ============ # @@ -422,7 +431,7 @@ def validate_exec( return data, node_block.name -def _validate_node_input_credentials( +async def _validate_node_input_credentials( graph: GraphModel, user_id: str, node_credentials_input_map: Optional[ @@ -459,7 +468,7 @@ def _validate_node_input_credentials( ) # Fetch the corresponding Credentials and perform sanity checks - credentials = get_integration_credentials_store().get_creds_by_id( + credentials = await get_integration_credentials_store().get_creds_by_id( user_id, credentials_meta.id ) if not credentials: @@ -516,7 +525,7 @@ def make_node_credentials_input_map( return result -def construct_node_execution_input( +async def construct_node_execution_input( graph: GraphModel, user_id: str, graph_inputs: BlockInput, @@ -541,7 +550,7 @@ def construct_node_execution_input( the corresponding input data for that node. """ graph.validate_graph(for_run=True) - _validate_node_input_credentials(graph, user_id, node_credentials_input_map) + await _validate_node_input_credentials(graph, user_id, node_credentials_input_map) nodes_input = [] for node in graph.starting_nodes: @@ -642,13 +651,92 @@ def create_execution_queue_config() -> RabbitMQConfig: ) -async def add_graph_execution_async( +async def stop_graph_execution( + graph_exec_id: str, + use_db_query: bool = True, +): + """ + Mechanism: + 1. Set the cancel event + 2. Graph executor's cancel handler thread detects the event, terminates workers, + reinitializes worker pool, and returns. + 3. Update execution statuses in DB and set `error` outputs to `"TERMINATED"`. + """ + queue_client = await get_async_execution_queue() + await queue_client.publish_message( + routing_key="", + message=CancelExecutionEvent(graph_exec_id=graph_exec_id).model_dump_json(), + exchange=GRAPH_EXECUTION_CANCEL_EXCHANGE, + ) + + # Update the status of the graph execution + if use_db_query: + graph_execution = await update_graph_execution_stats( + graph_exec_id, + ExecutionStatus.TERMINATED, + ) + else: + graph_execution = await get_db_async_client().update_graph_execution_stats( + graph_exec_id, + ExecutionStatus.TERMINATED, + ) + + if graph_execution: + await get_async_execution_event_bus().publish(graph_execution) + else: + raise NotFoundError( + f"Graph execution #{graph_exec_id} not found for termination." + ) + + # Update the status of the node executions + if use_db_query: + node_executions = await get_node_executions( + graph_exec_id=graph_exec_id, + statuses=[ + ExecutionStatus.QUEUED, + ExecutionStatus.RUNNING, + ExecutionStatus.INCOMPLETE, + ], + ) + await update_node_execution_status_batch( + [v.node_exec_id for v in node_executions], + ExecutionStatus.TERMINATED, + ) + else: + node_executions = await get_db_async_client().get_node_executions( + graph_exec_id=graph_exec_id, + statuses=[ + ExecutionStatus.QUEUED, + ExecutionStatus.RUNNING, + ExecutionStatus.INCOMPLETE, + ], + ) + await get_db_async_client().update_node_execution_status_batch( + [v.node_exec_id for v in node_executions], + ExecutionStatus.TERMINATED, + ) + + await asyncio.gather( + *[ + get_async_execution_event_bus().publish( + v.model_copy(update={"status": ExecutionStatus.TERMINATED}) + ) + for v in node_executions + ] + ) + + +async def add_graph_execution( graph_id: str, user_id: str, inputs: BlockInput, preset_id: Optional[str] = None, graph_version: Optional[int] = None, graph_credentials_inputs: Optional[dict[str, CredentialsMetaInput]] = None, + node_credentials_input_map: Optional[ + dict[str, dict[str, CredentialsMetaInput]] + ] = None, + use_db_query: bool = True, ) -> GraphExecutionWithNodes: """ Adds a graph execution to the queue and returns the execution entry. @@ -661,38 +749,63 @@ async def add_graph_execution_async( graph_version: The version of the graph to execute. graph_credentials_inputs: Credentials inputs to use in the execution. Keys should map to the keys generated by `GraphModel.aggregate_credentials_inputs`. + node_credentials_input_map: Credentials inputs to use in the execution, mapped to specific nodes. Returns: GraphExecutionEntry: The entry for the graph execution. Raises: ValueError: If the graph is not found or if there are validation errors. """ # noqa - graph: GraphModel | None = await get_graph( - graph_id=graph_id, - user_id=user_id, - version=graph_version, - include_subgraphs=True, - ) + if use_db_query: + graph: GraphModel | None = await get_graph( + graph_id=graph_id, + user_id=user_id, + version=graph_version, + include_subgraphs=True, + ) + else: + graph: GraphModel | None = await get_db_async_client().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.") - node_credentials_input_map = ( + node_credentials_input_map = node_credentials_input_map or ( make_node_credentials_input_map(graph, graph_credentials_inputs) if graph_credentials_inputs else None ) - graph_exec = await create_graph_execution( - user_id=user_id, - graph_id=graph_id, - graph_version=graph.version, - starting_nodes_input=construct_node_execution_input( - graph=graph, + if use_db_query: + graph_exec = await create_graph_execution( user_id=user_id, - graph_inputs=inputs, - node_credentials_input_map=node_credentials_input_map, - ), - preset_id=preset_id, - ) + graph_id=graph_id, + graph_version=graph.version, + starting_nodes_input=await construct_node_execution_input( + graph=graph, + user_id=user_id, + graph_inputs=inputs, + node_credentials_input_map=node_credentials_input_map, + ), + preset_id=preset_id, + ) + else: + graph_exec = await get_db_async_client().create_graph_execution( + user_id=user_id, + graph_id=graph_id, + graph_version=graph.version, + starting_nodes_input=await construct_node_execution_input( + graph=graph, + user_id=user_id, + graph_inputs=inputs, + node_credentials_input_map=node_credentials_input_map, + ), + preset_id=preset_id, + ) + try: queue = await get_async_execution_queue() graph_exec_entry = graph_exec.to_graph_execution_entry() @@ -711,101 +824,27 @@ async def add_graph_execution_async( except Exception as e: logger.error(f"Unable to publish graph #{graph_id} exec #{graph_exec.id}: {e}") - await update_node_execution_status_batch( - [node_exec.node_exec_id for node_exec in graph_exec.node_executions], - ExecutionStatus.FAILED, - ) - await update_graph_execution_stats( - graph_exec_id=graph_exec.id, - status=ExecutionStatus.FAILED, - stats=GraphExecutionStats(error=str(e)), - ) - raise + if use_db_query: + await update_node_execution_status_batch( + [node_exec.node_exec_id for node_exec in graph_exec.node_executions], + ExecutionStatus.FAILED, + ) + await update_graph_execution_stats( + graph_exec_id=graph_exec.id, + status=ExecutionStatus.FAILED, + stats=GraphExecutionStats(error=str(e)), + ) + else: + await get_db_async_client().update_node_execution_status_batch( + [node_exec.node_exec_id for node_exec in graph_exec.node_executions], + ExecutionStatus.FAILED, + ) + await get_db_async_client().update_graph_execution_stats( + graph_exec_id=graph_exec.id, + status=ExecutionStatus.FAILED, + stats=GraphExecutionStats(error=str(e)), + ) - -def add_graph_execution( - graph_id: str, - user_id: str, - inputs: BlockInput, - preset_id: Optional[str] = None, - graph_version: Optional[int] = None, - graph_credentials_inputs: Optional[dict[str, CredentialsMetaInput]] = None, - node_credentials_input_map: Optional[ - dict[str, dict[str, CredentialsMetaInput]] - ] = None, -) -> GraphExecutionWithNodes: - """ - Adds a graph execution to the queue and returns the execution entry. - - Args: - graph_id: The ID of the graph to execute. - user_id: The ID of the user executing the graph. - inputs: The input data for the graph execution. - preset_id: The ID of the preset to use. - graph_version: The version of the graph to execute. - graph_credentials_inputs: Credentials inputs to use in the execution. - Keys should map to the keys generated by `GraphModel.aggregate_credentials_inputs`. - node_credentials_input_map: Credentials inputs to use in the execution, mapped to specific nodes. - Returns: - GraphExecutionEntry: The entry for the graph execution. - Raises: - ValueError: If the graph is not found or if there are validation errors. - """ - db = get_db_client() - graph: GraphModel | None = db.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.") - - node_credentials_input_map = node_credentials_input_map or ( - make_node_credentials_input_map(graph, graph_credentials_inputs) - if graph_credentials_inputs - else None - ) - - graph_exec = db.create_graph_execution( - user_id=user_id, - graph_id=graph_id, - graph_version=graph.version, - starting_nodes_input=construct_node_execution_input( - graph=graph, - user_id=user_id, - graph_inputs=inputs, - node_credentials_input_map=node_credentials_input_map, - ), - preset_id=preset_id, - ) - try: - queue = get_execution_queue() - graph_exec_entry = graph_exec.to_graph_execution_entry() - if node_credentials_input_map: - graph_exec_entry.node_credentials_input_map = node_credentials_input_map - queue.publish_message( - routing_key=GRAPH_EXECUTION_ROUTING_KEY, - message=graph_exec_entry.model_dump_json(), - exchange=GRAPH_EXECUTION_EXCHANGE, - ) - - bus = get_execution_event_bus() - 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}") - - db.update_node_execution_status_batch( - [node_exec.node_exec_id for node_exec in graph_exec.node_executions], - ExecutionStatus.FAILED, - ) - db.update_graph_execution_stats( - graph_exec_id=graph_exec.id, - status=ExecutionStatus.FAILED, - stats=GraphExecutionStats(error=str(e)), - ) raise @@ -821,15 +860,13 @@ class ExecutionOutputEntry(BaseModel): class NodeExecutionProgress: def __init__( self, - drain_output_queue: Callable[[], None], - drain_done_task: Callable[[str, object], None], + on_done_task: Callable[[str, object], None], ): self.output: dict[str, list[ExecutionOutputEntry]] = defaultdict(list) - self.tasks: dict[str, AsyncResult] = {} - self.drain_output_queue = drain_output_queue - self.drain_done_task = drain_done_task + self.tasks: dict[str, Future] = {} + self.on_done_task = on_done_task - def add_task(self, node_exec_id: str, task: AsyncResult): + def add_task(self, node_exec_id: str, task: Future): self.tasks[node_exec_id] = task def add_output(self, output: ExecutionOutputEntry): @@ -859,23 +896,46 @@ class NodeExecutionProgress: if wait_time <= 0: return False - self.tasks[exec_id].wait(wait_time) + try: + self.tasks[exec_id].result(wait_time) + except TimeoutError: + print( + ">>>>>>> -- Timeout, after waiting for", + wait_time, + "seconds for node_id", + exec_id, + ) + pass + return self.is_done(0) + def stop(self) -> list[str]: + """ + Stops all tasks and clears the output. + This is useful for cleaning up when the execution is cancelled or terminated. + Returns a list of execution IDs that were stopped. + """ + cancelled_ids = [] + for task_id, task in self.tasks.items(): + if task.done(): + continue + task.cancel() + cancelled_ids.append(task_id) + return cancelled_ids + def _pop_done_task(self, exec_id: str) -> bool: task = self.tasks.get(exec_id) if not task: return True - if not task.ready(): + if not task.done(): return False - self.drain_output_queue() if self.output[exec_id]: return False if task := self.tasks.pop(exec_id): - self.drain_done_task(exec_id, task.get()) + self.on_done_task(exec_id, task.result()) return True diff --git a/autogpt_platform/backend/backend/integrations/credentials_store.py b/autogpt_platform/backend/backend/integrations/credentials_store.py index a5a88dfbcb..a86cc28e92 100644 --- a/autogpt_platform/backend/backend/integrations/credentials_store.py +++ b/autogpt_platform/backend/backend/integrations/credentials_store.py @@ -6,11 +6,13 @@ from typing import TYPE_CHECKING, Optional from pydantic import SecretStr +from backend.data.redis import get_redis_async + if TYPE_CHECKING: - from backend.executor.database import DatabaseManagerClient + from backend.executor.database import DatabaseManagerAsyncClient from autogpt_libs.utils.cache import thread_cached -from autogpt_libs.utils.synchronize import RedisKeyedMutex +from autogpt_libs.utils.synchronize import AsyncRedisKeyedMutex from backend.data.model import ( APIKeyCredentials, @@ -220,31 +222,36 @@ DEFAULT_CREDENTIALS = [ class IntegrationCredentialsStore: def __init__(self): - from backend.data.redis import get_redis + self._locks = None - self.locks = RedisKeyedMutex(get_redis()) + async def locks(self) -> AsyncRedisKeyedMutex: + if self._locks: + return self._locks + + self._locks = AsyncRedisKeyedMutex(await get_redis_async()) + return self._locks @property @thread_cached - def db_manager(self) -> "DatabaseManagerClient": - from backend.executor.database import DatabaseManagerClient + def db_manager(self) -> "DatabaseManagerAsyncClient": + from backend.executor.database import DatabaseManagerAsyncClient from backend.util.service import get_service_client - return get_service_client(DatabaseManagerClient) + return get_service_client(DatabaseManagerAsyncClient) - def add_creds(self, user_id: str, credentials: Credentials) -> None: - with self.locked_user_integrations(user_id): - if self.get_creds_by_id(user_id, credentials.id): + 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): raise ValueError( f"Can not re-create existing credentials #{credentials.id} " f"for user #{user_id}" ) - self._set_user_integration_creds( - user_id, [*self.get_all_creds(user_id), credentials] + await self._set_user_integration_creds( + user_id, [*(await self.get_all_creds(user_id)), credentials] ) - def get_all_creds(self, user_id: str) -> list[Credentials]: - users_credentials = self._get_user_integrations(user_id).credentials + async def get_all_creds(self, user_id: str) -> list[Credentials]: + users_credentials = (await self._get_user_integrations(user_id)).credentials all_credentials = users_credentials # These will always be added all_credentials.append(ollama_credentials) @@ -294,21 +301,25 @@ class IntegrationCredentialsStore: all_credentials.append(google_maps_credentials) return all_credentials - def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None: - all_credentials = self.get_all_creds(user_id) + async def get_creds_by_id( + self, user_id: str, credentials_id: str + ) -> Credentials | None: + all_credentials = await self.get_all_creds(user_id) return next((c for c in all_credentials if c.id == credentials_id), None) - def get_creds_by_provider(self, user_id: str, provider: str) -> list[Credentials]: - credentials = self.get_all_creds(user_id) + async def get_creds_by_provider( + self, user_id: str, provider: str + ) -> list[Credentials]: + credentials = await self.get_all_creds(user_id) return [c for c in credentials if c.provider == provider] - def get_authorized_providers(self, user_id: str) -> list[str]: - credentials = self.get_all_creds(user_id) + async def get_authorized_providers(self, user_id: str) -> list[str]: + credentials = await self.get_all_creds(user_id) return list(set(c.provider for c in credentials)) - def update_creds(self, user_id: str, updated: Credentials) -> None: - with self.locked_user_integrations(user_id): - current = self.get_creds_by_id(user_id, updated.id) + async def update_creds(self, user_id: str, updated: Credentials) -> None: + async with await self.locked_user_integrations(user_id): + current = await self.get_creds_by_id(user_id, updated.id) if not current: raise ValueError( f"Credentials with ID {updated.id} " @@ -336,18 +347,18 @@ class IntegrationCredentialsStore: # Update the credentials updated_credentials_list = [ updated if c.id == updated.id else c - for c in self.get_all_creds(user_id) + for c in await self.get_all_creds(user_id) ] - self._set_user_integration_creds(user_id, updated_credentials_list) + await self._set_user_integration_creds(user_id, updated_credentials_list) - def delete_creds_by_id(self, user_id: str, credentials_id: str) -> None: - with self.locked_user_integrations(user_id): + async def delete_creds_by_id(self, user_id: str, credentials_id: str) -> None: + async with await self.locked_user_integrations(user_id): filtered_credentials = [ - c for c in self.get_all_creds(user_id) if c.id != credentials_id + c for c in await self.get_all_creds(user_id) if c.id != credentials_id ] - self._set_user_integration_creds(user_id, filtered_credentials) + await self._set_user_integration_creds(user_id, filtered_credentials) - def store_state_token( + async def store_state_token( self, user_id: str, provider: str, scopes: list[str], use_pkce: bool = False ) -> tuple[str, str]: token = secrets.token_urlsafe(32) @@ -363,14 +374,14 @@ class IntegrationCredentialsStore: scopes=scopes, ) - with self.locked_user_integrations(user_id): + async with await self.locked_user_integrations(user_id): - user_integrations = self._get_user_integrations(user_id) + user_integrations = await self._get_user_integrations(user_id) oauth_states = user_integrations.oauth_states oauth_states.append(state) user_integrations.oauth_states = oauth_states - self.db_manager.update_user_integrations( + await self.db_manager.update_user_integrations( user_id=user_id, data=user_integrations ) @@ -386,11 +397,11 @@ class IntegrationCredentialsStore: code_challenge = base64.urlsafe_b64encode(sha256_hash).decode("utf-8") return code_challenge.replace("=", ""), code_verifier - def verify_state_token( + async def verify_state_token( self, user_id: str, token: str, provider: str ) -> Optional[OAuthState]: - with self.locked_user_integrations(user_id): - user_integrations = self._get_user_integrations(user_id) + async with await self.locked_user_integrations(user_id): + user_integrations = await self._get_user_integrations(user_id) oauth_states = user_integrations.oauth_states now = datetime.now(timezone.utc) @@ -409,26 +420,26 @@ class IntegrationCredentialsStore: # Remove the used state oauth_states.remove(valid_state) user_integrations.oauth_states = oauth_states - self.db_manager.update_user_integrations(user_id, user_integrations) + await self.db_manager.update_user_integrations( + user_id, user_integrations + ) return valid_state return None - def _set_user_integration_creds( + async def _set_user_integration_creds( self, user_id: str, credentials: list[Credentials] ) -> None: - integrations = self._get_user_integrations(user_id) + integrations = await self._get_user_integrations(user_id) # Remove default credentials from the list credentials = [c for c in credentials if c not in DEFAULT_CREDENTIALS] integrations.credentials = credentials - self.db_manager.update_user_integrations(user_id, integrations) + await self.db_manager.update_user_integrations(user_id, integrations) - def _get_user_integrations(self, user_id: str) -> UserIntegrations: - integrations: UserIntegrations = self.db_manager.get_user_integrations( - user_id=user_id - ) - return integrations + async def _get_user_integrations(self, user_id: str) -> UserIntegrations: + return await self.db_manager.get_user_integrations(user_id=user_id) - def locked_user_integrations(self, user_id: str): + async def locked_user_integrations(self, user_id: str): key = (f"user:{user_id}", "integrations") - return self.locks.locked(key) + locks = await self.locks() + return locks.locked(key) diff --git a/autogpt_platform/backend/backend/integrations/creds_manager.py b/autogpt_platform/backend/backend/integrations/creds_manager.py index eb5e132503..bacc3a53e9 100644 --- a/autogpt_platform/backend/backend/integrations/creds_manager.py +++ b/autogpt_platform/backend/backend/integrations/creds_manager.py @@ -1,13 +1,13 @@ import logging -from contextlib import contextmanager +from contextlib import asynccontextmanager from datetime import datetime -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable, Coroutine -from autogpt_libs.utils.synchronize import RedisKeyedMutex -from redis.lock import Lock as RedisLock +from autogpt_libs.utils.synchronize import AsyncRedisKeyedMutex +from redis.asyncio.lock import Lock as AsyncRedisLock -from backend.data import redis from backend.data.model import Credentials, OAuth2Credentials +from backend.data.redis import get_redis_async from backend.integrations.credentials_store import IntegrationCredentialsStore from backend.integrations.oauth import HANDLERS_BY_NAME from backend.integrations.providers import ProviderName @@ -54,20 +54,26 @@ class IntegrationCredentialsManager: """ def __init__(self): - redis_conn = redis.get_redis() - self._locks = RedisKeyedMutex(redis_conn) self.store = IntegrationCredentialsStore() + self._locks = None - def create(self, user_id: str, credentials: Credentials) -> None: - return self.store.add_creds(user_id, credentials) + async def locks(self) -> AsyncRedisKeyedMutex: + if self._locks: + return self._locks - def exists(self, user_id: str, credentials_id: str) -> bool: - return self.store.get_creds_by_id(user_id, credentials_id) is not None + self._locks = AsyncRedisKeyedMutex(await get_redis_async()) + return self._locks - def get( + async def create(self, user_id: str, credentials: Credentials) -> None: + return await self.store.add_creds(user_id, credentials) + + async def exists(self, user_id: str, credentials_id: str) -> bool: + return (await self.store.get_creds_by_id(user_id, credentials_id)) is not None + + async def get( self, user_id: str, credentials_id: str, lock: bool = True ) -> Credentials | None: - credentials = self.store.get_creds_by_id(user_id, credentials_id) + credentials = await self.store.get_creds_by_id(user_id, credentials_id) if not credentials: return None @@ -78,15 +84,15 @@ class IntegrationCredentialsManager: f"{datetime.fromtimestamp(credentials.access_token_expires_at)}; " f"current time is {datetime.now()}" ) - credentials = self.refresh_if_needed(user_id, credentials, lock) + credentials = await self.refresh_if_needed(user_id, credentials, lock) else: logger.debug(f"Credentials #{credentials.id} never expire") return credentials - def acquire( + async def acquire( self, user_id: str, credentials_id: str - ) -> tuple[Credentials, RedisLock]: + ) -> tuple[Credentials, AsyncRedisLock]: """ ⚠️ WARNING: this locks credentials system-wide and blocks both acquiring and updating them elsewhere until the lock is released. @@ -94,23 +100,25 @@ class IntegrationCredentialsManager: """ # Use a low-priority (!time_sensitive) locking queue on top of the general lock # to allow priority access for refreshing/updating the tokens. - with self._locked(user_id, credentials_id, "!time_sensitive"): - lock = self._acquire_lock(user_id, credentials_id) - credentials = self.get(user_id, credentials_id, lock=False) + async with self._locked(user_id, credentials_id, "!time_sensitive"): + lock = await self._acquire_lock(user_id, credentials_id) + credentials = await self.get(user_id, credentials_id, lock=False) if not credentials: raise ValueError( f"Credentials #{credentials_id} for user #{user_id} not found" ) return credentials, lock - def cached_getter(self, user_id: str) -> Callable[[str], "Credentials | None"]: + def cached_getter( + self, user_id: str + ) -> Callable[[str], "Coroutine[Any, Any, Credentials | None]"]: all_credentials = None - def get_credentials(creds_id: str) -> "Credentials | None": + async def get_credentials(creds_id: str) -> "Credentials | None": nonlocal all_credentials if not all_credentials: # Fetch credentials on first necessity - all_credentials = self.store.get_all_creds(user_id) + all_credentials = await self.store.get_all_creds(user_id) credential = next((c for c in all_credentials if c.id == creds_id), None) if not credential: @@ -120,15 +128,15 @@ class IntegrationCredentialsManager: return credential # Credential is OAuth2 credential and has expiration timestamp - return self.refresh_if_needed(user_id, credential) + return await self.refresh_if_needed(user_id, credential) return get_credentials - def refresh_if_needed( + async def refresh_if_needed( self, user_id: str, credentials: OAuth2Credentials, lock: bool = True ) -> OAuth2Credentials: - with self._locked(user_id, credentials.id, "refresh"): - oauth_handler = _get_provider_oauth_handler(credentials.provider) + async with self._locked(user_id, credentials.id, "refresh"): + oauth_handler = await _get_provider_oauth_handler(credentials.provider) if oauth_handler.needs_refresh(credentials): logger.debug( f"Refreshing '{credentials.provider}' " @@ -137,50 +145,53 @@ class IntegrationCredentialsManager: _lock = None if lock: # Wait until the credentials are no longer in use anywhere - _lock = self._acquire_lock(user_id, credentials.id) + _lock = await self._acquire_lock(user_id, credentials.id) - fresh_credentials = oauth_handler.refresh_tokens(credentials) - self.store.update_creds(user_id, fresh_credentials) - if _lock and _lock.locked() and _lock.owned(): - _lock.release() + fresh_credentials = await oauth_handler.refresh_tokens(credentials) + await self.store.update_creds(user_id, fresh_credentials) + if _lock and (await _lock.locked()) and (await _lock.owned()): + await _lock.release() credentials = fresh_credentials return credentials - def update(self, user_id: str, updated: Credentials) -> None: - with self._locked(user_id, updated.id): - self.store.update_creds(user_id, updated) + async def update(self, user_id: str, updated: Credentials) -> None: + async with self._locked(user_id, updated.id): + await self.store.update_creds(user_id, updated) - def delete(self, user_id: str, credentials_id: str) -> None: - with self._locked(user_id, credentials_id): - self.store.delete_creds_by_id(user_id, credentials_id) + async def delete(self, user_id: str, credentials_id: str) -> None: + async with self._locked(user_id, credentials_id): + await self.store.delete_creds_by_id(user_id, credentials_id) # -- Locking utilities -- # - def _acquire_lock(self, user_id: str, credentials_id: str, *args: str) -> RedisLock: + async def _acquire_lock( + self, user_id: str, credentials_id: str, *args: str + ) -> AsyncRedisLock: key = ( f"user:{user_id}", f"credentials:{credentials_id}", *args, ) - return self._locks.acquire(key) + locks = await self.locks() + return await locks.acquire(key) - @contextmanager - def _locked(self, user_id: str, credentials_id: str, *args: str): - lock = self._acquire_lock(user_id, credentials_id, *args) + @asynccontextmanager + async def _locked(self, user_id: str, credentials_id: str, *args: str): + lock = await self._acquire_lock(user_id, credentials_id, *args) try: yield finally: - if lock.locked() and lock.owned(): - lock.release() + if (await lock.locked()) and (await lock.owned()): + await lock.release() - def release_all_locks(self): + async def release_all_locks(self): """Call this on process termination to ensure all locks are released""" - self._locks.release_all_locks() - self.store.locks.release_all_locks() + await (await self.locks()).release_all_locks() + await (await self.store.locks()).release_all_locks() -def _get_provider_oauth_handler(provider_name_str: str) -> "BaseOAuthHandler": +async def _get_provider_oauth_handler(provider_name_str: str) -> "BaseOAuthHandler": provider_name = ProviderName(provider_name_str) if provider_name not in HANDLERS_BY_NAME: raise KeyError(f"Unknown provider '{provider_name}'") diff --git a/autogpt_platform/backend/backend/integrations/oauth/base.py b/autogpt_platform/backend/backend/integrations/oauth/base.py index fc6c68c161..b8d08582b2 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/base.py +++ b/autogpt_platform/backend/backend/integrations/oauth/base.py @@ -32,7 +32,7 @@ class BaseOAuthHandler(ABC): @abstractmethod # --8<-- [start:BaseOAuthHandler4] - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: # --8<-- [end:BaseOAuthHandler4] @@ -41,31 +41,33 @@ class BaseOAuthHandler(ABC): @abstractmethod # --8<-- [start:BaseOAuthHandler5] - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: # --8<-- [end:BaseOAuthHandler5] """Implements the token refresh mechanism""" ... @abstractmethod # --8<-- [start:BaseOAuthHandler6] - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: # --8<-- [end:BaseOAuthHandler6] """Revokes the given token at provider, returns False provider does not support it""" ... - def refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: if credentials.provider != self.PROVIDER_NAME: raise ValueError( f"{self.__class__.__name__} can not refresh tokens " f"for other provider '{credentials.provider}'" ) - return self._refresh_tokens(credentials) + return await self._refresh_tokens(credentials) - def get_access_token(self, credentials: OAuth2Credentials) -> str: + async def get_access_token(self, credentials: OAuth2Credentials) -> str: """Returns a valid access token, refreshing it first if needed""" if self.needs_refresh(credentials): - credentials = self.refresh_tokens(credentials) + credentials = await self.refresh_tokens(credentials) return credentials.access_token.get_secret_value() def needs_refresh(self, credentials: OAuth2Credentials) -> bool: diff --git a/autogpt_platform/backend/backend/integrations/oauth/github.py b/autogpt_platform/backend/backend/integrations/oauth/github.py index 3358295170..ebec116660 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/github.py +++ b/autogpt_platform/backend/backend/integrations/oauth/github.py @@ -45,12 +45,14 @@ class GitHubOAuthHandler(BaseOAuthHandler): } return f"{self.auth_base_url}?{urlencode(params)}" - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: - return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri}) + return await self._request_tokens( + {"code": code, "redirect_uri": self.redirect_uri} + ) - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: if not credentials.access_token: raise ValueError("No access token to revoke") @@ -59,7 +61,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): "X-GitHub-Api-Version": "2022-11-28", } - Requests().delete( + await Requests().delete( url=self.revoke_url.format(client_id=self.client_id), auth=(self.client_id, self.client_secret), headers=headers, @@ -67,18 +69,20 @@ class GitHubOAuthHandler(BaseOAuthHandler): ) return True - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: if not credentials.refresh_token: return credentials - return self._request_tokens( + return await self._request_tokens( { "refresh_token": credentials.refresh_token.get_secret_value(), "grant_type": "refresh_token", } ) - def _request_tokens( + async def _request_tokens( self, params: dict[str, str], current_credentials: Optional[OAuth2Credentials] = None, @@ -89,10 +93,12 @@ class GitHubOAuthHandler(BaseOAuthHandler): **params, } headers = {"Accept": "application/json"} - response = Requests().post(self.token_url, data=request_body, headers=headers) + response = await Requests().post( + self.token_url, data=request_body, headers=headers + ) token_data: dict = response.json() - username = self._request_username(token_data["access_token"]) + username = await self._request_username(token_data["access_token"]) now = int(time.time()) new_credentials = OAuth2Credentials( @@ -124,7 +130,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): new_credentials.id = current_credentials.id return new_credentials - def _request_username(self, access_token: str) -> str | None: + async def _request_username(self, access_token: str) -> str | None: url = "https://api.github.com/user" headers = { "Accept": "application/vnd.github+json", @@ -132,13 +138,14 @@ class GitHubOAuthHandler(BaseOAuthHandler): "X-GitHub-Api-Version": "2022-11-28", } - response = Requests().get(url, headers=headers) + response = await Requests().get(url, headers=headers) if not response.ok: return None # Get the login (username) - return response.json().get("login") + resp = response.json() + return resp.get("login") # --8<-- [end:GithubOAuthHandlerExample] diff --git a/autogpt_platform/backend/backend/integrations/oauth/google.py b/autogpt_platform/backend/backend/integrations/oauth/google.py index 4de5c7365a..bba2bc71c5 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/google.py +++ b/autogpt_platform/backend/backend/integrations/oauth/google.py @@ -54,7 +54,7 @@ class GoogleOAuthHandler(BaseOAuthHandler): ) return authorization_url - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: logger.debug(f"Exchanging code for tokens with scopes: {scopes}") @@ -106,7 +106,7 @@ class GoogleOAuthHandler(BaseOAuthHandler): return credentials - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: session = AuthorizedSession(credentials) session.post( self.revoke_uri, @@ -127,7 +127,9 @@ class GoogleOAuthHandler(BaseOAuthHandler): return None return response.json()["email"] - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: # Google credentials should ALWAYS have a refresh token assert credentials.refresh_token diff --git a/autogpt_platform/backend/backend/integrations/oauth/linear.py b/autogpt_platform/backend/backend/integrations/oauth/linear.py index afc84da22e..09e3c6d537 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/linear.py +++ b/autogpt_platform/backend/backend/integrations/oauth/linear.py @@ -40,12 +40,14 @@ class LinearOAuthHandler(BaseOAuthHandler): } return f"{self.auth_base_url}?{urlencode(params)}" - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: - return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri}) + return await self._request_tokens( + {"code": code, "redirect_uri": self.redirect_uri} + ) - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: if not credentials.access_token: raise ValueError("No access token to revoke") @@ -53,7 +55,7 @@ class LinearOAuthHandler(BaseOAuthHandler): "Authorization": f"Bearer {credentials.access_token.get_secret_value()}" } - response = Requests().post(self.revoke_url, headers=headers) + response = await Requests().post(self.revoke_url, headers=headers) if not response.ok: try: error_data = response.json() @@ -61,26 +63,28 @@ class LinearOAuthHandler(BaseOAuthHandler): except json.JSONDecodeError: error_message = response.text raise LinearAPIException( - f"Failed to revoke Linear tokens ({response.status_code}): {error_message}", - response.status_code, + f"Failed to revoke Linear tokens ({response.status}): {error_message}", + response.status, ) return True # Linear doesn't return JSON on successful revoke - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: if not credentials.refresh_token: raise ValueError( "No refresh token available." ) # Linear uses non-expiring tokens - return self._request_tokens( + return await self._request_tokens( { "refresh_token": credentials.refresh_token.get_secret_value(), "grant_type": "refresh_token", } ) - def _request_tokens( + async def _request_tokens( self, params: dict[str, str], current_credentials: Optional[OAuth2Credentials] = None, @@ -95,18 +99,19 @@ class LinearOAuthHandler(BaseOAuthHandler): headers = { "Content-Type": "application/x-www-form-urlencoded" } # Correct header for token request - response = Requests().post(self.token_url, data=request_body, headers=headers) + response = await Requests().post( + self.token_url, data=request_body, headers=headers + ) if not response.ok: try: error_data = response.json() error_message = error_data.get("error", "Unknown error") - except json.JSONDecodeError: error_message = response.text raise LinearAPIException( - f"Failed to fetch Linear tokens ({response.status_code}): {error_message}", - response.status_code, + f"Failed to fetch Linear tokens ({response.status}): {error_message}", + response.status, ) token_data = response.json() @@ -132,13 +137,11 @@ class LinearOAuthHandler(BaseOAuthHandler): new_credentials.id = current_credentials.id return new_credentials - def _request_username(self, access_token: str) -> Optional[str]: - + async def _request_username(self, access_token: str) -> Optional[str]: # Use the LinearClient to fetch user details using GraphQL from backend.blocks.linear._api import LinearClient try: - linear_client = LinearClient( APIKeyCredentials( api_key=SecretStr(access_token), @@ -156,10 +159,9 @@ class LinearOAuthHandler(BaseOAuthHandler): } """ - response = linear_client.query(query) + response = await linear_client.query(query) return response["viewer"]["name"] except Exception as e: # Handle any errors - print(f"Error fetching username: {e}") return None diff --git a/autogpt_platform/backend/backend/integrations/oauth/notion.py b/autogpt_platform/backend/backend/integrations/oauth/notion.py index 10f699d61f..67fc5bcc25 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/notion.py +++ b/autogpt_platform/backend/backend/integrations/oauth/notion.py @@ -39,7 +39,7 @@ class NotionOAuthHandler(BaseOAuthHandler): } return f"{self.auth_base_url}?{urlencode(params)}" - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: request_body = { @@ -52,7 +52,9 @@ class NotionOAuthHandler(BaseOAuthHandler): "Authorization": f"Basic {auth_str}", "Accept": "application/json", } - response = Requests().post(self.token_url, json=request_body, headers=headers) + response = await Requests().post( + self.token_url, json=request_body, headers=headers + ) token_data = response.json() # Email is only available for non-bot users email = ( @@ -80,11 +82,13 @@ class NotionOAuthHandler(BaseOAuthHandler): }, ) - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: # Notion doesn't support token revocation return False - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: # Notion doesn't support token refresh return credentials diff --git a/autogpt_platform/backend/backend/integrations/oauth/todoist.py b/autogpt_platform/backend/backend/integrations/oauth/todoist.py index 014be26332..66d34f95e8 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/todoist.py +++ b/autogpt_platform/backend/backend/integrations/oauth/todoist.py @@ -35,7 +35,7 @@ class TodoistOAuthHandler(BaseOAuthHandler): return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}" - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: """Exchange authorization code for access tokens""" @@ -47,17 +47,14 @@ class TodoistOAuthHandler(BaseOAuthHandler): "redirect_uri": self.redirect_uri, } - response = Requests().post(self.TOKEN_URL, data=data) - response.raise_for_status() - + response = await Requests().post(self.TOKEN_URL, data=data) tokens = response.json() - response = Requests().post( + response = await Requests().post( "https://api.todoist.com/sync/v9/sync", headers={"Authorization": f"Bearer {tokens['access_token']}"}, data={"sync_token": "*", "resource_types": '["user"]'}, ) - response.raise_for_status() user_info = response.json() user_email = user_info["user"].get("email") @@ -72,9 +69,11 @@ class TodoistOAuthHandler(BaseOAuthHandler): scopes=scopes, ) - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: # Todoist does not support token refresh return credentials - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: return False diff --git a/autogpt_platform/backend/backend/integrations/oauth/twitter.py b/autogpt_platform/backend/backend/integrations/oauth/twitter.py index 8645c0814c..486c14a210 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/twitter.py +++ b/autogpt_platform/backend/backend/integrations/oauth/twitter.py @@ -4,7 +4,7 @@ from typing import ClassVar, Optional from backend.data.model import OAuth2Credentials, ProviderName from backend.integrations.oauth.base import BaseOAuthHandler -from backend.util.request import Requests, req +from backend.util.request import Requests class TwitterOAuthHandler(BaseOAuthHandler): @@ -61,7 +61,7 @@ class TwitterOAuthHandler(BaseOAuthHandler): return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}" - def exchange_code_for_tokens( + async def exchange_code_for_tokens( self, code: str, scopes: list[str], code_verifier: Optional[str] ) -> OAuth2Credentials: """Exchange authorization code for access tokens""" @@ -77,14 +77,12 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = Requests().post( + response = await Requests().post( self.TOKEN_URL, headers=headers, data=data, auth=auth ) - response.raise_for_status() - tokens = response.json() - username = self._get_username(tokens["access_token"]) + username = await self._get_username(tokens["access_token"]) return OAuth2Credentials( provider=self.PROVIDER_NAME, @@ -97,20 +95,21 @@ class TwitterOAuthHandler(BaseOAuthHandler): scopes=scopes, ) - def _get_username(self, access_token: str) -> str: + async def _get_username(self, access_token: str) -> str: """Get the username from the access token""" headers = {"Authorization": f"Bearer {access_token}"} params = {"user.fields": "username"} - response = Requests().get( + response = await Requests().get( f"{self.USERNAME_URL}?{urllib.parse.urlencode(params)}", headers=headers ) - response.raise_for_status() return response.json()["data"]["username"] - def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + async def _refresh_tokens( + self, credentials: OAuth2Credentials + ) -> OAuth2Credentials: """Refresh access tokens using refresh token""" if not credentials.refresh_token: raise ValueError("No refresh token available") @@ -123,17 +122,19 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = Requests().post(self.TOKEN_URL, headers=header, data=data, auth=auth) + response = await Requests().post( + self.TOKEN_URL, headers=header, data=data, auth=auth + ) - try: - response.raise_for_status() - except req.exceptions.HTTPError: - print(f"HTTP Error: {response.status_code}") - raise + if not response.ok: + error_text = response.text + print("HTTP Error:", response.status) + print("Response Content:", error_text) + raise ValueError(f"HTTP Error: {response.status} - {error_text}") tokens = response.json() - username = self._get_username(tokens["access_token"]) + username = await self._get_username(tokens["access_token"]) return OAuth2Credentials( id=credentials.id, @@ -147,7 +148,7 @@ class TwitterOAuthHandler(BaseOAuthHandler): refresh_token_expires_at=None, ) - def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + async def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: """Revoke the access token""" header = {"Content-Type": "application/x-www-form-urlencoded"} @@ -159,14 +160,14 @@ class TwitterOAuthHandler(BaseOAuthHandler): auth = (self.client_id, self.client_secret) - response = Requests().post( + response = await Requests().post( self.REVOKE_URL, headers=header, data=data, auth=auth ) - try: - response.raise_for_status() - except req.exceptions.HTTPError: - print(f"HTTP Error: {response.status_code}") - raise + if not response.ok: + error_text = response.text + print("HTTP Error:", response.status) + print("Response Content:", error_text) + raise ValueError(f"HTTP Error: {response.status} - {error_text}") - return response.status_code == 200 + return response.ok diff --git a/autogpt_platform/backend/backend/integrations/webhooks/github.py b/autogpt_platform/backend/backend/integrations/webhooks/github.py index 90cb06d303..5d2977cacc 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/github.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/github.py @@ -8,7 +8,7 @@ from strenum import StrEnum from backend.data import integrations from backend.data.model import Credentials from backend.integrations.providers import ProviderName -from backend.util.request import Requests, req +from backend.util.request import Requests, Response from ._base import BaseWebhooksManager @@ -73,9 +73,9 @@ class GithubWebhooksManager(BaseWebhooksManager): repo, github_hook_id = webhook.resource, webhook.provider_webhook_id ping_url = f"{self.GITHUB_API_URL}/repos/{repo}/hooks/{github_hook_id}/pings" - response = Requests().post(ping_url, headers=headers) + response = await Requests().post(ping_url, headers=headers) - if response.status_code != 204: + if response.status != 204: error_msg = extract_github_error_msg(response) raise ValueError(f"Failed to ping GitHub webhook: {error_msg}") @@ -110,13 +110,13 @@ class GithubWebhooksManager(BaseWebhooksManager): }, } - response = Requests().post( + response = await Requests().post( f"{self.GITHUB_API_URL}/repos/{resource}/hooks", headers=headers, json=webhook_data, ) - if response.status_code != 201: + if response.status != 201: error_msg = extract_github_error_msg(response) if "not found" in error_msg.lower(): error_msg = ( @@ -126,8 +126,9 @@ class GithubWebhooksManager(BaseWebhooksManager): ) raise ValueError(f"Failed to create GitHub webhook: {error_msg}") - webhook_id = response.json()["id"] - config = response.json()["config"] + resp = response.json() + webhook_id = resp["id"] + config = resp["config"] return str(webhook_id), config @@ -153,9 +154,9 @@ class GithubWebhooksManager(BaseWebhooksManager): f"Unsupported webhook type '{webhook.webhook_type}'" ) - response = Requests().delete(delete_url, headers=headers) + response = await Requests().delete(delete_url, headers=headers) - if response.status_code not in [204, 404]: + if response.status not in [204, 404]: # 204 means successful deletion, 404 means the webhook was already deleted error_msg = extract_github_error_msg(response) raise ValueError(f"Failed to delete GitHub webhook: {error_msg}") @@ -166,7 +167,7 @@ class GithubWebhooksManager(BaseWebhooksManager): # --8<-- [end:GithubWebhooksManager] -def extract_github_error_msg(response: req.Response) -> str: +def extract_github_error_msg(response: Response) -> str: error_msgs = [] resp = response.json() if resp.get("message"): 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 898c6772bb..01e7b3a49e 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/graph_lifecycle_hooks.py @@ -37,7 +37,7 @@ async def on_graph_activate(graph: "GraphModel", user_id: str): ) ) and (creds_meta := new_node.input_default.get(creds_field_name)) - and not (node_credentials := get_credentials(creds_meta["id"])) + and not (node_credentials := await get_credentials(creds_meta["id"])) ): raise ValueError( f"Node #{new_node.id} input '{creds_field_name}' updated with " @@ -74,7 +74,7 @@ async def on_graph_deactivate(graph: "GraphModel", user_id: str): ) ) and (creds_meta := node.input_default.get(creds_field_name)) - and not (node_credentials := get_credentials(creds_meta["id"])) + and not (node_credentials := await get_credentials(creds_meta["id"])) ): logger.error( f"Node #{node.id} input '{creds_field_name}' referenced non-existent " diff --git a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py index 7c923435a9..bc0337d4c5 100644 --- a/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py +++ b/autogpt_platform/backend/backend/integrations/webhooks/slant3d.py @@ -39,7 +39,7 @@ class Slant3DWebhooksManager(BaseWebhooksManager): # Slant3D's API doesn't use events list, just register for all order updates payload = {"endPoint": ingress_url} - response = Requests().post( + response = await Requests().post( f"{self.BASE_URL}/customer/webhookSubscribe", headers=headers, json=payload ) diff --git a/autogpt_platform/backend/backend/notifications/notifications.py b/autogpt_platform/backend/backend/notifications/notifications.py index 808a8cb699..dbbd8fe075 100644 --- a/autogpt_platform/backend/backend/notifications/notifications.py +++ b/autogpt_platform/backend/backend/notifications/notifications.py @@ -1,6 +1,6 @@ +import asyncio import logging -import time -from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import ProcessPoolExecutor from datetime import datetime, timedelta, timezone from typing import Callable @@ -39,9 +39,11 @@ from backend.data.user import generate_unsubscribe_link from backend.notifications.email import EmailSender from backend.util.logging import TruncatedLogger from backend.util.metrics import discord_send_alert +from backend.util.retry import continuous_retry from backend.util.service import ( AppService, AppServiceClient, + endpoint_to_sync, expose, get_service_client, ) @@ -55,7 +57,7 @@ NOTIFICATION_EXCHANGE = Exchange(name="notifications", type=ExchangeType.TOPIC) DEAD_LETTER_EXCHANGE = Exchange(name="dead_letter", type=ExchangeType.TOPIC) EXCHANGES = [NOTIFICATION_EXCHANGE, DEAD_LETTER_EXCHANGE] -background_executor = ThreadPoolExecutor(max_workers=2) +background_executor = ProcessPoolExecutor(max_workers=2) def create_notification_config() -> RabbitMQConfig: @@ -231,9 +233,9 @@ class NotificationManager(AppService): @expose def queue_weekly_summary(self): - background_executor.submit(self._queue_weekly_summary) + background_executor.submit(lambda: asyncio.run(self._queue_weekly_summary())) - def _queue_weekly_summary(self): + async def _queue_weekly_summary(self): """Process weekly summary for specified notification types""" try: logger.info("Processing weekly summary queuing operation") @@ -245,8 +247,7 @@ class NotificationManager(AppService): start_time=start_time.isoformat(), ) for user in users: - - self._queue_scheduled_notification( + await self._queue_scheduled_notification( SummaryParamsEventModel( user_id=user, type=NotificationType.WEEKLY_SUMMARY, @@ -387,10 +388,10 @@ class NotificationManager(AppService): } @expose - def discord_system_alert(self, content: str): - discord_send_alert(content) + async def discord_system_alert(self, content: str): + await discord_send_alert(content) - def _queue_scheduled_notification(self, event: SummaryParamsEventModel): + 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=}") @@ -399,12 +400,10 @@ class NotificationManager(AppService): routing_key = get_routing_key(event.type) # Publish to RabbitMQ - self.run_and_wait( - self.rabbit.publish_message( - routing_key=routing_key, - message=event.model_dump_json(), - exchange=next(ex for ex in EXCHANGES if ex.name == exchange), - ) + await self.rabbit.publish_message( + routing_key=routing_key, + message=event.model_dump_json(), + exchange=next(ex for ex in EXCHANGES if ex.name == exchange), ) except Exception as e: @@ -695,7 +694,7 @@ class NotificationManager(AppService): logger.exception(f"Error processing notification for summary queue: {e}") return False - def _run_queue( + async def _run_queue( self, queue: aio_pika.abc.AbstractQueue, process_func: Callable[[str], bool], @@ -704,12 +703,12 @@ class NotificationManager(AppService): message: aio_pika.abc.AbstractMessage | None = None try: # This parameter "no_ack" is named like shit, think of it as "auto_ack" - message = self.run_and_wait(queue.get(timeout=1.0, no_ack=False)) + message = await queue.get(timeout=1.0, no_ack=False) result = process_func(message.body.decode()) if result: - self.run_and_wait(message.ack()) + await message.ack() else: - self.run_and_wait(message.reject(requeue=False)) + await message.reject(requeue=False) except QueueEmpty: logger.debug(f"Queue {error_queue_name} empty") @@ -720,61 +719,58 @@ class NotificationManager(AppService): logger.error( f"Error in notification service loop, message rejected {e}" ) - self.run_and_wait(message.reject(requeue=False)) + 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=}" ) + @continuous_retry() def run_service(self): + self.run_and_wait(self._run_service()) + + async def _run_service(self): logger.info(f"[{self.service_name}] ⏳ Configuring RabbitMQ...") self.rabbitmq_service = rabbitmq.AsyncRabbitMQ(self.rabbitmq_config) - self.run_and_wait(self.rabbitmq_service.connect()) + await self.rabbitmq_service.connect() logger.info(f"[{self.service_name}] Started notification service") # Set up queue consumers - channel = self.run_and_wait(self.rabbit.get_channel()) + channel = await self.rabbit.get_channel() - immediate_queue = self.run_and_wait( - channel.get_queue("immediate_notifications") - ) - batch_queue = self.run_and_wait(channel.get_queue("batch_notifications")) + immediate_queue = await channel.get_queue("immediate_notifications") + batch_queue = await channel.get_queue("batch_notifications") - admin_queue = self.run_and_wait(channel.get_queue("admin_notifications")) + admin_queue = await channel.get_queue("admin_notifications") - summary_queue = self.run_and_wait(channel.get_queue("summary_notifications")) + summary_queue = await channel.get_queue("summary_notifications") while self.running: try: - self._run_queue( + await self._run_queue( queue=immediate_queue, process_func=self._process_immediate, error_queue_name="immediate_notifications", ) - self._run_queue( + await self._run_queue( queue=admin_queue, process_func=self._process_admin_message, error_queue_name="admin_notifications", ) - self._run_queue( + await self._run_queue( queue=batch_queue, process_func=self._process_batch, error_queue_name="batch_notifications", ) - - self._run_queue( + await self._run_queue( queue=summary_queue, process_func=self._process_summary, error_queue_name="summary_notifications", ) - - time.sleep(0.1) - + await asyncio.sleep(0.1) except QueueEmpty as e: logger.debug(f"Queue empty: {e}") - except Exception as e: - logger.error(f"Error in notification service loop: {e}") def cleanup(self): """Cleanup service resources""" @@ -791,4 +787,4 @@ class NotificationManagerClient(AppServiceClient): process_existing_batches = NotificationManager.process_existing_batches queue_weekly_summary = NotificationManager.queue_weekly_summary - discord_system_alert = NotificationManager.discord_system_alert + discord_system_alert = endpoint_to_sync(NotificationManager.discord_system_alert) diff --git a/autogpt_platform/backend/backend/server/external/routes/v1.py b/autogpt_platform/backend/backend/server/external/routes/v1.py index 15fe8dc7ee..baf77c0e75 100644 --- a/autogpt_platform/backend/backend/server/external/routes/v1.py +++ b/autogpt_platform/backend/backend/server/external/routes/v1.py @@ -12,7 +12,7 @@ from backend.data import graph as graph_db from backend.data.api_key import APIKey from backend.data.block import BlockInput, CompletedBlockOutput from backend.data.execution import NodeExecutionResult -from backend.executor.utils import add_graph_execution_async +from backend.executor.utils import add_graph_execution from backend.server.external.middleware import require_permission from backend.util.settings import Settings @@ -71,7 +71,7 @@ def get_graph_blocks() -> Sequence[dict[Any, Any]]: tags=["blocks"], dependencies=[Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK))], ) -def execute_graph_block( +async def execute_graph_block( block_id: str, data: BlockInput, api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_BLOCK)), @@ -81,7 +81,7 @@ def execute_graph_block( raise HTTPException(status_code=404, detail=f"Block #{block_id} not found.") output = defaultdict(list) - for name, data in obj.execute(data): + async for name, data in obj.execute(data): output[name].append(data) return output @@ -97,7 +97,7 @@ async def execute_graph( api_key: APIKey = Depends(require_permission(APIKeyPermission.EXECUTE_GRAPH)), ) -> dict[str, Any]: try: - graph_exec = await add_graph_execution_async( + graph_exec = await add_graph_execution( graph_id=graph_id, user_id=api_key.user_id, inputs=node_input, diff --git a/autogpt_platform/backend/backend/server/integrations/router.py b/autogpt_platform/backend/backend/server/integrations/router.py index 4739bd8417..26f33eba37 100644 --- a/autogpt_platform/backend/backend/server/integrations/router.py +++ b/autogpt_platform/backend/backend/server/integrations/router.py @@ -15,7 +15,7 @@ from backend.data.integrations import ( wait_for_webhook_event, ) from backend.data.model import Credentials, CredentialsType, OAuth2Credentials -from backend.executor.utils import add_graph_execution_async +from backend.executor.utils import add_graph_execution from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.oauth import HANDLERS_BY_NAME from backend.integrations.providers import ProviderName @@ -41,7 +41,7 @@ class LoginResponse(BaseModel): @router.get("/{provider}/login") -def login( +async def login( provider: Annotated[ ProviderName, Path(title="The provider to initiate an OAuth flow for") ], @@ -56,7 +56,7 @@ def login( requested_scopes = scopes.split(",") if scopes else [] # Generate and store a secure random state token along with the scopes - state_token, code_challenge = creds_manager.store.store_state_token( + state_token, code_challenge = await creds_manager.store.store_state_token( user_id, provider, requested_scopes ) login_url = handler.get_login_url( @@ -76,7 +76,7 @@ class CredentialsMetaResponse(BaseModel): @router.post("/{provider}/callback") -def callback( +async def callback( provider: Annotated[ ProviderName, Path(title="The target provider for this OAuth exchange") ], @@ -89,7 +89,9 @@ def callback( handler = _get_provider_oauth_handler(request, provider) # Verify the state token - valid_state = creds_manager.store.verify_state_token(user_id, state_token, provider) + valid_state = await creds_manager.store.verify_state_token( + user_id, state_token, provider + ) if not valid_state: logger.warning(f"Invalid or expired state token for user {user_id}") @@ -100,7 +102,7 @@ def callback( scopes = handler.handle_default_scopes(scopes) - credentials = handler.exchange_code_for_tokens( + credentials = await handler.exchange_code_for_tokens( code, scopes, valid_state.code_verifier ) @@ -134,7 +136,7 @@ def callback( ) # TODO: Allow specifying `title` to set on `credentials` - creds_manager.create(user_id, credentials) + await creds_manager.create(user_id, credentials) logger.debug( f"Successfully processed OAuth callback for user {user_id} " @@ -151,10 +153,10 @@ def callback( @router.get("/credentials") -def list_credentials( +async def list_credentials( user_id: Annotated[str, Depends(get_user_id)], ) -> list[CredentialsMetaResponse]: - credentials = creds_manager.store.get_all_creds(user_id) + credentials = await creds_manager.store.get_all_creds(user_id) return [ CredentialsMetaResponse( id=cred.id, @@ -169,13 +171,13 @@ def list_credentials( @router.get("/{provider}/credentials") -def list_credentials_by_provider( +async def list_credentials_by_provider( provider: Annotated[ ProviderName, Path(title="The provider to list credentials for") ], user_id: Annotated[str, Depends(get_user_id)], ) -> list[CredentialsMetaResponse]: - credentials = creds_manager.store.get_creds_by_provider(user_id, provider) + credentials = await creds_manager.store.get_creds_by_provider(user_id, provider) return [ CredentialsMetaResponse( id=cred.id, @@ -190,14 +192,14 @@ def list_credentials_by_provider( @router.get("/{provider}/credentials/{cred_id}") -def get_credential( +async def get_credential( provider: Annotated[ ProviderName, Path(title="The provider to retrieve credentials for") ], cred_id: Annotated[str, Path(title="The ID of the credentials to retrieve")], user_id: Annotated[str, Depends(get_user_id)], ) -> Credentials: - credential = creds_manager.get(user_id, cred_id) + credential = await creds_manager.get(user_id, cred_id) if not credential: raise HTTPException(status_code=404, detail="Credentials not found") if credential.provider != provider: @@ -208,7 +210,7 @@ def get_credential( @router.post("/{provider}/credentials", status_code=201) -def create_credentials( +async def create_credentials( user_id: Annotated[str, Depends(get_user_id)], provider: Annotated[ ProviderName, Path(title="The provider to create credentials for") @@ -217,7 +219,7 @@ def create_credentials( ) -> Credentials: credentials.provider = provider try: - creds_manager.create(user_id, credentials) + await creds_manager.create(user_id, credentials) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to store credentials: {str(e)}" @@ -252,7 +254,7 @@ async def delete_credentials( bool, Query(title="Whether to proceed if any linked webhooks are still in use") ] = False, ) -> CredentialsDeletionResponse | CredentialsDeletionNeedsConfirmationResponse: - creds = creds_manager.store.get_creds_by_id(user_id, cred_id) + creds = await creds_manager.store.get_creds_by_id(user_id, cred_id) if not creds: raise HTTPException(status_code=404, detail="Credentials not found") if creds.provider != provider: @@ -265,12 +267,12 @@ async def delete_credentials( except NeedConfirmation as e: return CredentialsDeletionNeedsConfirmationResponse(message=str(e)) - creds_manager.delete(user_id, cred_id) + await creds_manager.delete(user_id, cred_id) tokens_revoked = None if isinstance(creds, OAuth2Credentials): handler = _get_provider_oauth_handler(request, provider) - tokens_revoked = handler.revoke_tokens(creds) + tokens_revoked = await handler.revoke_tokens(creds) return CredentialsDeletionResponse(revoked=tokens_revoked) @@ -329,7 +331,7 @@ async def webhook_ingress_generic( continue logger.debug(f"Executing graph #{node.graph_id} node #{node.id}") executions.append( - add_graph_execution_async( + add_graph_execution( user_id=webhook.user_id, graph_id=node.graph_id, graph_version=node.graph_version, @@ -348,7 +350,7 @@ async def webhook_ping( webhook_manager = get_webhook_manager(webhook.provider) credentials = ( - creds_manager.get(user_id, webhook.credentials_id) + await creds_manager.get(user_id, webhook.credentials_id) if webhook.credentials_id else None ) diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 9214c00ab2..7ebbab9c4a 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -320,14 +320,14 @@ class AgentServer(backend.util.service.AppProcess): ) @staticmethod - def test_create_credentials( + async def test_create_credentials( user_id: str, provider: ProviderName, credentials: Credentials, ) -> Credentials: from backend.server.integrations.router import create_credentials - return create_credentials( + return await create_credentials( user_id=user_id, provider=provider, credentials=credentials ) diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index d9d39e4666..1b9d804b54 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -50,7 +50,6 @@ from backend.data.onboarding import ( onboarding_enabled, update_user_onboarding, ) -from backend.data.rabbitmq import AsyncRabbitMQ from backend.data.user import ( get_or_create_user, get_user_notification_preference, @@ -59,7 +58,6 @@ from backend.data.user import ( ) from backend.executor import scheduler from backend.executor import utils as execution_utils -from backend.executor.utils import create_execution_queue_config from backend.integrations.webhooks.graph_lifecycle_hooks import ( on_graph_activate, on_graph_deactivate, @@ -83,13 +81,6 @@ def execution_scheduler_client() -> scheduler.SchedulerClient: return get_service_client(scheduler.SchedulerClient, health_check=False) -@thread_cached -async def execution_queue_client() -> AsyncRabbitMQ: - client = AsyncRabbitMQ(create_execution_queue_config()) - await client.connect() - return client - - @thread_cached def execution_event_bus() -> AsyncRedisExecutionEventBus: return AsyncRedisExecutionEventBus() @@ -224,13 +215,13 @@ def get_graph_blocks() -> Sequence[dict[Any, Any]]: tags=["blocks"], dependencies=[Depends(auth_middleware)], ) -def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput: +async def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput: obj = get_block(block_id) if not obj: raise HTTPException(status_code=404, detail=f"Block #{block_id} not found.") output = defaultdict(list) - for name, data in obj.execute(data): + async for name, data in obj.execute(data): output[name].append(data) return output @@ -582,7 +573,7 @@ async def execute_graph( detail="Insufficient balance to execute the agent. Please top up your account.", ) - graph_exec = await execution_utils.add_graph_execution_async( + graph_exec = await execution_utils.add_graph_execution( graph_id=graph_id, user_id=user_id, inputs=inputs, @@ -606,7 +597,7 @@ async def stop_graph_run( ): raise HTTPException(404, detail=f"Agent execution #{graph_exec_id} not found") - await _cancel_execution(graph_exec_id) + await execution_utils.stop_graph_execution(graph_exec_id) # Retrieve & return canceled graph execution in its final state result = await execution_db.get_graph_execution( @@ -620,52 +611,6 @@ async def stop_graph_run( return result -async def _cancel_execution(graph_exec_id: str): - """ - Mechanism: - 1. Set the cancel event - 2. Graph executor's cancel handler thread detects the event, terminates workers, - reinitializes worker pool, and returns. - 3. Update execution statuses in DB and set `error` outputs to `"TERMINATED"`. - """ - queue_client = await execution_queue_client() - await queue_client.publish_message( - routing_key="", - message=execution_utils.CancelExecutionEvent( - graph_exec_id=graph_exec_id - ).model_dump_json(), - exchange=execution_utils.GRAPH_EXECUTION_CANCEL_EXCHANGE, - ) - - # Update the status of the graph execution - graph_execution = await execution_db.update_graph_execution_stats( - graph_exec_id, - execution_db.ExecutionStatus.TERMINATED, - ) - if graph_execution: - await execution_event_bus().publish(graph_execution) - - # Update the status of the node executions - node_execs = [ - node_exec.model_copy(update={"status": execution_db.ExecutionStatus.TERMINATED}) - for node_exec in await execution_db.get_node_executions( - graph_exec_id=graph_exec_id, - statuses=[ - execution_db.ExecutionStatus.QUEUED, - execution_db.ExecutionStatus.RUNNING, - execution_db.ExecutionStatus.INCOMPLETE, - ], - ) - ] - await execution_db.update_node_execution_status_batch( - [node_exec.node_exec_id for node_exec in node_execs], - execution_db.ExecutionStatus.TERMINATED, - ) - await asyncio.gather( - *[execution_event_bus().publish(node_exec) for node_exec in node_execs] - ) - - @v1_router.get( path="/executions", tags=["graphs"], diff --git a/autogpt_platform/backend/backend/server/routers/v1_test.py b/autogpt_platform/backend/backend/server/routers/v1_test.py index 17c06b5778..cddee98273 100644 --- a/autogpt_platform/backend/backend/server/routers/v1_test.py +++ b/autogpt_platform/backend/backend/server/routers/v1_test.py @@ -137,10 +137,12 @@ def test_execute_graph_block( """Test execute block endpoint""" # Mock block mock_block = Mock() - mock_block.execute.return_value = [ - ("output1", {"data": "result1"}), - ("output2", {"data": "result2"}), - ] + + async def mock_execute(*args, **kwargs): + yield "output1", {"data": "result1"} + yield "output2", {"data": "result2"} + + mock_block.execute = mock_execute mocker.patch( "backend.server.routers.v1.get_block", diff --git a/autogpt_platform/backend/backend/server/v2/library/routes/presets.py b/autogpt_platform/backend/backend/server/v2/library/routes/presets.py index 504f46e088..9df8df3c2b 100644 --- a/autogpt_platform/backend/backend/server/v2/library/routes/presets.py +++ b/autogpt_platform/backend/backend/server/v2/library/routes/presets.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query, status import backend.server.v2.library.db as db import backend.server.v2.library.model as models -from backend.executor.utils import add_graph_execution_async +from backend.executor.utils import add_graph_execution from backend.util.exceptions import NotFoundError logger = logging.getLogger(__name__) @@ -245,7 +245,7 @@ async def execute_preset( # Merge input overrides with preset inputs merged_node_input = preset.inputs | node_input - execution = await add_graph_execution_async( + execution = await add_graph_execution( graph_id=graph_id, user_id=user_id, inputs=merged_node_input, 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 ed1db82244..b75536d3cd 100644 --- a/autogpt_platform/backend/backend/server/v2/store/image_gen.py +++ b/autogpt_platform/backend/backend/server/v2/store/image_gen.py @@ -1,4 +1,3 @@ -import asyncio import io import logging from enum import Enum @@ -20,7 +19,7 @@ from backend.blocks.ideogram import ( from backend.data.graph import Graph from backend.data.model import CredentialsMetaInput, ProviderName from backend.integrations.credentials_store import ideogram_credentials -from backend.util.request import requests +from backend.util.request import Requests from backend.util.settings import Settings logger = logging.getLogger(__name__) @@ -37,12 +36,12 @@ class ImageStyle(str, Enum): async def generate_agent_image(agent: Graph | AgentGraph) -> io.BytesIO: if settings.config.use_agent_image_generation_v2: - return await asyncio.to_thread(generate_agent_image_v2, graph=agent) + return await generate_agent_image_v2(graph=agent) else: return await generate_agent_image_v1(agent=agent) -def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: +async def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: """ Generate an image for an agent using Ideogram model. Returns: @@ -74,7 +73,7 @@ def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: ] # Run the Ideogram model block with the specified parameters - url = IdeogramModelBlock().run_once( + url = await IdeogramModelBlock().run_once( IdeogramModelBlock.Input( credentials=CredentialsMetaInput( id=ideogram_credentials.id, @@ -96,7 +95,8 @@ def generate_agent_image_v2(graph: Graph | AgentGraph) -> io.BytesIO: "result", credentials=ideogram_credentials, ) - return io.BytesIO(requests.get(url).content) + response = await Requests().get(url) + return io.BytesIO(response.content) async def generate_agent_image_v1(agent: Graph | AgentGraph) -> io.BytesIO: @@ -145,13 +145,13 @@ async def generate_agent_image_v1(agent: Graph | AgentGraph) -> io.BytesIO: else: # If it's a URL string, fetch the image bytes result_url = output[0] - response = requests.get(result_url) + response = await Requests().get(result_url) image_bytes = response.content elif isinstance(output, FileOutput): image_bytes = output.read() elif isinstance(output, str): # Output is a URL - response = requests.get(output) + response = await Requests().get(output) image_bytes = response.content else: raise RuntimeError("Unexpected output format from the model.") diff --git a/autogpt_platform/backend/backend/util/decorator.py b/autogpt_platform/backend/backend/util/decorator.py index 9047ea0b77..84f128333f 100644 --- a/autogpt_platform/backend/backend/util/decorator.py +++ b/autogpt_platform/backend/backend/util/decorator.py @@ -2,7 +2,7 @@ import functools import logging import os import time -from typing import Callable, ParamSpec, Tuple, TypeVar +from typing import Any, Awaitable, Callable, Coroutine, ParamSpec, Tuple, TypeVar from pydantic import BaseModel @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) def time_measured(func: Callable[P, T]) -> Callable[P, Tuple[TimingInfo, T]]: """ - Decorator to measure the time taken by a function to execute. + Decorator to measure the time taken by a synchronous function to execute. """ @functools.wraps(func) @@ -50,6 +50,28 @@ def time_measured(func: Callable[P, T]) -> Callable[P, Tuple[TimingInfo, T]]: return wrapper +def async_time_measured( + func: Callable[P, Awaitable[T]], +) -> Callable[P, Awaitable[Tuple[TimingInfo, T]]]: + """ + 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]: + start_wall_time, start_cpu_time = _start_measurement() + try: + result = await func(*args, **kwargs) + finally: + wall_duration, cpu_duration = _end_measurement( + start_wall_time, start_cpu_time + ) + timing_info = TimingInfo(cpu_time=cpu_duration, wall_time=wall_duration) + return timing_info, result + + return async_wrapper + + def error_logged(func: Callable[P, T]) -> Callable[P, T | None]: """ Decorator to suppress and log any exceptions raised by a function. @@ -65,3 +87,22 @@ def error_logged(func: Callable[P, T]) -> Callable[P, T | None]: ) return wrapper + + +def async_error_logged( + func: Callable[P, Coroutine[Any, Any, T]], +) -> Callable[P, Coroutine[Any, Any, T | None]]: + """ + Decorator to suppress and log any exceptions raised by an async function. + """ + + @functools.wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | None: + try: + return await func(*args, **kwargs) + except Exception as e: + logger.exception( + f"Error when calling async function {func.__name__} with arguments {args} {kwargs}: {e}" + ) + + return wrapper diff --git a/autogpt_platform/backend/backend/util/file.py b/autogpt_platform/backend/backend/util/file.py index 5b876a3ec0..27ad4cdd19 100644 --- a/autogpt_platform/backend/backend/util/file.py +++ b/autogpt_platform/backend/backend/util/file.py @@ -7,7 +7,7 @@ import uuid from pathlib import Path from urllib.parse import urlparse -from backend.util.request import requests +from backend.util.request import Requests from backend.util.type import MediaFileType TEMP_DIR = Path(tempfile.gettempdir()).resolve() @@ -29,7 +29,7 @@ def clean_exec_files(graph_exec_id: str, file: str = "") -> None: shutil.rmtree(exec_path) -def store_media_file( +async def store_media_file( graph_exec_id: str, file: MediaFileType, return_content: bool = False ) -> MediaFileType: """ @@ -114,8 +114,7 @@ def store_media_file( target_path = _ensure_inside_base(base_path / filename, base_path) # Download and save - resp = requests.get(file) - resp.raise_for_status() + resp = await Requests().get(file) target_path.write_bytes(resp.content) else: diff --git a/autogpt_platform/backend/backend/util/metrics.py b/autogpt_platform/backend/backend/util/metrics.py index 3e1822fad0..f169887307 100644 --- a/autogpt_platform/backend/backend/util/metrics.py +++ b/autogpt_platform/backend/backend/util/metrics.py @@ -1,4 +1,3 @@ -import asyncio import logging import sentry_sdk @@ -31,7 +30,7 @@ def sentry_capture_error(error: Exception): sentry_sdk.flush() -def discord_send_alert(content: str): +async def discord_send_alert(content: str): from backend.blocks.discord import SendDiscordMessageBlock from backend.data.model import APIKeyCredentials, CredentialsMetaInput, ProviderName from backend.util.settings import Settings @@ -44,13 +43,7 @@ def discord_send_alert(content: str): expires_at=None, ) - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - return SendDiscordMessageBlock().run_once( + return await SendDiscordMessageBlock().run_once( SendDiscordMessageBlock.Input( credentials=CredentialsMetaInput( id=creds.id, diff --git a/autogpt_platform/backend/backend/util/request.py b/autogpt_platform/backend/backend/util/request.py index 4dfd32a12a..a3327c137a 100644 --- a/autogpt_platform/backend/backend/util/request.py +++ b/autogpt_platform/backend/backend/util/request.py @@ -1,17 +1,18 @@ +import asyncio import ipaddress import re import socket import ssl -from typing import Callable, Optional +from io import BytesIO +from typing import Any, Callable, Optional from urllib.parse import ParseResult as URL from urllib.parse import quote, urljoin, urlparse +import aiohttp import idna -import requests as req -from requests.adapters import HTTPAdapter -from urllib3 import PoolManager +from aiohttp import FormData, abc -from backend.util.settings import Config +from backend.util.json import json # List of IP networks to block BLOCKED_IP_NETWORKS = [ @@ -61,26 +62,61 @@ def _remove_insecure_headers(headers: dict, old_url: URL, new_url: URL) -> dict: return headers -class HostSSLAdapter(HTTPAdapter): +class HostResolver(abc.AbstractResolver): """ - A custom adapter that connects to an IP address but still + A custom resolver that connects to specified IP addresses but still sets the TLS SNI to the original host name so the cert can match. """ - def __init__(self, ssl_hostname, *args, **kwargs): + def __init__(self, ssl_hostname: str, ip_addresses: list[str]): self.ssl_hostname = ssl_hostname - super().__init__(*args, **kwargs) + self.ip_addresses = ip_addresses + self._default = aiohttp.AsyncResolver() - def init_poolmanager(self, *args, **kwargs): - self.poolmanager = PoolManager( - *args, - ssl_context=ssl.create_default_context(), - server_hostname=self.ssl_hostname, # This works for urllib3>=2 - **kwargs, - ) + async def resolve(self, host, port=0, family=socket.AF_INET): + if host == self.ssl_hostname: + results = [] + for ip in self.ip_addresses: + results.append( + { + "hostname": self.ssl_hostname, + "host": ip, + "port": port, + "family": family, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + ) + return results + return await self._default.resolve(host, port, family) + + async def close(self): + await self._default.close() -def validate_url(url: str, trusted_origins: list[str]) -> tuple[URL, bool, list[str]]: +async def _resolve_host(hostname: str) -> list[str]: + """ + Resolves the hostname to a list of IP addresses (IPv4 first, then IPv6). + """ + loop = asyncio.get_running_loop() + try: + infos = await loop.getaddrinfo(hostname, None) + except socket.gaierror: + raise ValueError(f"Unable to resolve IP address for hostname {hostname}") + + ip_list = [info[4][0] for info in infos] + ipv4 = [ip for ip in ip_list if ":" not in ip] + ipv6 = [ip for ip in ip_list if ":" in ip] + ip_addresses = ipv4 + ipv6 + + if not ip_addresses: + raise ValueError(f"No IP addresses found for {hostname}") + return ip_addresses + + +async def validate_url( + url: str, trusted_origins: list[str] +) -> tuple[URL, bool, list[str]]: """ Validates the URL to prevent SSRF attacks by ensuring it does not point to a private, link-local, or otherwise blocked IP address — unless @@ -125,7 +161,7 @@ def validate_url(url: str, trusted_origins: list[str]) -> tuple[URL, bool, list[ ip_addresses: list[str] = [] if not is_trusted: # Resolve all IP addresses for the hostname - ip_addresses = _resolve_host(ascii_hostname) + ip_addresses = await _resolve_host(ascii_hostname) # Block any IP address that belongs to a blocked range for ip_str in ip_addresses: @@ -165,7 +201,9 @@ def pin_url(url: URL, ip_addresses: Optional[list[str]] = None) -> URL: if not ip_addresses: # Resolve all IP addresses for the hostname - ip_addresses = _resolve_host(url.hostname) + # (This call is blocking; ensure to call async _resolve_host before if possible) + ip_addresses = [] + # You may choose to raise or call synchronous resolve here; for simplicity, leave empty. # Pin to the first valid IP (for SSRF defense) pinned_ip = ip_addresses[0] @@ -189,23 +227,58 @@ def pin_url(url: URL, ip_addresses: Optional[list[str]] = None) -> URL: ) -def _resolve_host(hostname: str) -> list[str]: - try: - ip_list = [str(res[4][0]) for res in socket.getaddrinfo(hostname, None)] - ipv4 = [ip for ip in ip_list if ":" not in ip] - ipv6 = [ip for ip in ip_list if ":" in ip] - ip_addresses = ipv4 + ipv6 # Prefer IPv4 over IPv6 - except socket.gaierror: - raise ValueError(f"Unable to resolve IP address for hostname {hostname}") +ClientResponse = aiohttp.ClientResponse +ClientResponseError = aiohttp.ClientResponseError - if not ip_addresses: - raise ValueError(f"No IP addresses found for {hostname}") - return ip_addresses + +class Response: + """ + Buffered wrapper around aiohttp.ClientResponse that does *not* require + callers to manage connection or session lifetimes. + """ + + def __init__( + self, + *, + response: ClientResponse, + url: str, + body: bytes, + ): + self.status: int = response.status + self.headers = response.headers + self.reason: str | None = response.reason + self.request_info = response.request_info + self.url: str = url + self.content: bytes = body # raw bytes + + def json(self, encoding: str | None = None, **kwargs) -> dict: + """ + Parse the body as JSON and return the resulting Python object. + """ + return json.loads( + self.content.decode(encoding or "utf-8", errors="replace"), **kwargs + ) + + def text(self, encoding: str | None = None) -> str: + """ + Decode the body to a string. Encoding is guessed from the + Content-Type header if not supplied. + """ + if encoding is None: + # Try to extract charset from headers; fall back to UTF-8 + ctype = self.headers.get("content-type", "") + match = re.search(r"charset=([^\s;]+)", ctype, flags=re.I) + encoding = match.group(1) if match else None + return self.content.decode(encoding or "utf-8", errors="replace") + + @property + def ok(self) -> bool: + return 200 <= self.status < 300 class Requests: """ - A wrapper around the requests library that validates URLs before + A wrapper around an aiohttp ClientSession that validates URLs before making requests, preventing SSRF by blocking private networks and other disallowed address spaces. """ @@ -228,112 +301,161 @@ class Requests: self.extra_url_validator = extra_url_validator self.extra_headers = extra_headers - def request( + async def request( self, - method, - url, - headers=None, - allow_redirects=True, - max_redirects=10, - *args, + method: str, + url: str, + *, + headers: Optional[dict] = None, + files: list[tuple[str, tuple[str, BytesIO, str]]] | None = None, + data: Any | None = None, + json: Any | None = None, + allow_redirects: bool = True, + max_redirects: int = 10, **kwargs, - ) -> req.Response: + ) -> Response: + if files is not None: + if json is not None: + raise ValueError( + "Cannot mix file uploads with JSON body; " + "use 'data' for extra form fields instead." + ) + + form = FormData(quote_fields=False) + # add normal form fields first + if isinstance(data, dict): + for k, v in data.items(): + form.add_field(k, str(v)) + elif data is not None: + raise ValueError( + "When uploading files, 'data' must be a dict of form fields." + ) + + # add the file parts + for field_name, (filename, fh, content_type) in files: + form.add_field( + name=field_name, + value=fh, + filename=filename, + content_type=content_type or "application/octet-stream", + ) + + data = form + # Validate URL and get trust status - url, is_trusted, ip_addresses = validate_url(url, self.trusted_origins) + parsed_url, is_trusted, ip_addresses = await validate_url( + url, self.trusted_origins + ) # Apply any extra user-defined validation/transformation if self.extra_url_validator is not None: - url = self.extra_url_validator(url) + parsed_url = self.extra_url_validator(parsed_url) # Pin the URL if untrusted - hostname = url.hostname - original_url = url.geturl() + hostname = parsed_url.hostname + if hostname is None: + raise ValueError(f"Invalid URL: Unable to determine hostname of {url}") + + original_url = parsed_url.geturl() + connector: Optional[aiohttp.TCPConnector] = None if not is_trusted: - url = pin_url(url, ip_addresses) + # Replace hostname with IP for connection but preserve SNI via resolver + resolver = HostResolver(ssl_hostname=hostname, ip_addresses=ip_addresses) + ssl_context = ssl.create_default_context() + connector = aiohttp.TCPConnector(resolver=resolver, ssl=ssl_context) + session_kwargs = {} + if connector: + session_kwargs["connector"] = connector # Merge any extra headers - headers = dict(headers) if headers else {} + req_headers = dict(headers) if headers else {} if self.extra_headers is not None: - headers.update(self.extra_headers) + req_headers.update(self.extra_headers) - session = req.Session() + # Override Host header if using IP connection + if connector: + req_headers["Host"] = hostname - # If untrusted, the hostname in the URL is replaced with the corresponding - # IP address, and we need to override the Host header with the actual hostname. - if url.hostname != hostname: - headers["Host"] = hostname + # Override data if files are provided - # If hostname was untrusted and we replaced it by (pinned it to) its IP, - # we also need to attach a custom SNI adapter to make SSL work: - adapter = HostSSLAdapter(ssl_hostname=hostname) - session.mount("https://", adapter) - - # Perform the request with redirects disabled for manual handling - response = session.request( - method, - url.geturl(), - headers=headers, - allow_redirects=False, - *args, - **kwargs, - ) - - # Replace response URLs with the original host for clearer error messages - if url.hostname != hostname: - response.url = original_url - if response.request is not None: - response.request.url = original_url - - if self.raise_for_status: - response.raise_for_status() - - # If allowed and a redirect is received, follow the redirect manually - if allow_redirects and response.is_redirect: - if max_redirects <= 0: - raise Exception("Too many redirects.") - - location = response.headers.get("Location") - if not location: - return response - - # The base URL is the pinned_url we just used - # so that relative redirects resolve correctly. - redirect_url = urlparse(urljoin(url.geturl(), location)) - # Carry forward the same headers but update Host - new_headers = _remove_insecure_headers(headers, url, redirect_url) - - return self.request( + async with aiohttp.ClientSession(**session_kwargs) as session: + # Perform the request with redirects disabled for manual handling + async with session.request( method, - redirect_url.geturl(), - headers=new_headers, - allow_redirects=allow_redirects, - max_redirects=max_redirects - 1, - *args, + parsed_url.geturl(), + headers=req_headers, + allow_redirects=False, + data=data, + json=json, **kwargs, - ) + ) as response: - return response + if self.raise_for_status: + response.raise_for_status() - def get(self, url, *args, **kwargs) -> req.Response: - return self.request("GET", url, *args, **kwargs) + # If allowed and a redirect is received, follow the redirect manually + if allow_redirects and response.status in (301, 302, 303, 307, 308): + if max_redirects <= 0: + raise Exception("Too many redirects.") - def post(self, url, *args, **kwargs) -> req.Response: - return self.request("POST", url, *args, **kwargs) + location = response.headers.get("Location") + if not location: + return Response( + response=response, + url=original_url, + body=await response.read(), + ) - def put(self, url, *args, **kwargs) -> req.Response: - return self.request("PUT", url, *args, **kwargs) + # The base URL is the pinned_url we just used + # so that relative redirects resolve correctly. + redirect_url = urlparse(urljoin(parsed_url.geturl(), location)) + # Carry forward the same headers but update Host + new_headers = _remove_insecure_headers( + req_headers, parsed_url, redirect_url + ) - def delete(self, url, *args, **kwargs) -> req.Response: - return self.request("DELETE", url, *args, **kwargs) + return await self.request( + method, + redirect_url.geturl(), + headers=new_headers, + allow_redirects=allow_redirects, + max_redirects=max_redirects - 1, + files=files, + data=data, + json=json, + **kwargs, + ) - def head(self, url, *args, **kwargs) -> req.Response: - return self.request("HEAD", url, *args, **kwargs) + # Reset response URL to original host for clarity + if parsed_url.hostname != hostname: + try: + response.url = original_url # type: ignore + except Exception: + pass - def options(self, url, *args, **kwargs) -> req.Response: - return self.request("OPTIONS", url, *args, **kwargs) + return Response( + response=response, + url=original_url, + body=await response.read(), + ) - def patch(self, url, *args, **kwargs) -> req.Response: - return self.request("PATCH", url, *args, **kwargs) + async def get(self, url: str, *args, **kwargs) -> Response: + return await self.request("GET", url, *args, **kwargs) + async def post(self, url: str, *args, **kwargs) -> Response: + return await self.request("POST", url, *args, **kwargs) -requests = Requests(trusted_origins=Config().trust_endpoints_for_requests) + async def put(self, url: str, *args, **kwargs) -> Response: + return await self.request("PUT", url, *args, **kwargs) + + async def delete(self, url: str, *args, **kwargs) -> Response: + return await self.request("DELETE", url, *args, **kwargs) + + async def head(self, url: str, *args, **kwargs) -> Response: + return await self.request("HEAD", url, *args, **kwargs) + + async def options(self, url: str, *args, **kwargs) -> Response: + return await self.request("OPTIONS", url, *args, **kwargs) + + async def patch(self, url: str, *args, **kwargs) -> Response: + return await self.request("PATCH", url, *args, **kwargs) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index c349f0c804..b2691cfd18 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -59,12 +59,6 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): le=1000, description="Maximum number of workers to use for graph execution.", ) - num_node_workers: int = Field( - default=5, - ge=1, - le=1000, - description="Maximum number of workers to use for node execution within a single graph.", - ) pyro_host: str = Field( default="localhost", description="The default hostname of the Pyro server.", diff --git a/autogpt_platform/backend/backend/util/test.py b/autogpt_platform/backend/backend/util/test.py index 06a1f44f63..72a39d4027 100644 --- a/autogpt_platform/backend/backend/util/test.py +++ b/autogpt_platform/backend/backend/util/test.py @@ -1,3 +1,4 @@ +import inspect import logging import time import uuid @@ -93,7 +94,7 @@ async def wait_execution( assert False, "Execution did not complete in time." -def execute_block_test(block: Block): +async def execute_block_test(block: Block): prefix = f"[Test-{block.name}]" if not block.test_input or not block.test_output: @@ -110,10 +111,25 @@ def execute_block_test(block: Block): for mock_name, mock_obj in (block.test_mock or {}).items(): log.info(f"{prefix} mocking {mock_name}...") - if hasattr(block, mock_name): - setattr(block, mock_name, mock_obj) - else: + # check whether the field mock_name is an async function or not + if not hasattr(block, mock_name): log.info(f"{prefix} mock {mock_name} not found in block") + continue + + fun = getattr(block, mock_name) + is_async = inspect.iscoroutinefunction(fun) or inspect.isasyncgenfunction(fun) + + if is_async: + + async def async_mock( + *args, _mock_name=mock_name, _mock_obj=mock_obj, **kwargs + ): + return _mock_obj(*args, **kwargs) + + setattr(block, mock_name, async_mock) + + else: + setattr(block, mock_name, mock_obj) # Populate credentials argument(s) extra_exec_kwargs: dict = { @@ -141,7 +157,9 @@ def execute_block_test(block: Block): for input_data in block.test_input: log.info(f"{prefix} in: {input_data}") - for output_name, output_data in block.execute(input_data, **extra_exec_kwargs): + async for output_name, output_data in block.execute( + input_data, **extra_exec_kwargs + ): if output_index >= len(block.test_output): raise ValueError( f"{prefix} produced output more than expected {output_index} >= {len(block.test_output)}:\nOutput Expected:\t\t{block.test_output}\nFailed Output Produced:\t('{output_name}', {output_data})\nNote that this may not be the one that was unexpected, but it is the first that triggered the extra output warning" diff --git a/autogpt_platform/backend/poetry.lock b/autogpt_platform/backend/poetry.lock index 842ebe16f2..4ea22878d4 100644 --- a/autogpt_platform/backend/poetry.lock +++ b/autogpt_platform/backend/poetry.lock @@ -17,6 +17,33 @@ aiormq = ">=6.8,<6.9" exceptiongroup = ">=1,<2" yarl = "*" +[[package]] +name = "aiodns" +version = "3.4.0" +description = "Simple DNS resolver for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiodns-3.4.0-py3-none-any.whl", hash = "sha256:4da2b25f7475343f3afbb363a2bfe46afa544f2b318acb9a945065e622f4ed24"}, + {file = "aiodns-3.4.0.tar.gz", hash = "sha256:24b0ae58410530367f21234d0c848e4de52c1f16fbddc111726a4ab536ec1b2f"}, +] + +[package.dependencies] +pycares = ">=4.0.0" + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -3742,6 +3769,93 @@ files = [ [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" +[[package]] +name = "pycares" +version = "4.8.0" +description = "Python interface for c-ares" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pycares-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f40d9f4a8de398b110fdf226cdfadd86e8c7eb71d5298120ec41cf8d94b0012f"}, + {file = "pycares-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:339de06fc849a51015968038d2bbed68fc24047522404af9533f32395ca80d25"}, + {file = "pycares-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372a236c1502b9056b0bea195c64c329603b4efa70b593a33b7ae37fbb7fad00"}, + {file = "pycares-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f66a5e143d102ccc204bd4e29edd70bed28420f707efd2116748241e30cb73"}, + {file = "pycares-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef50504296cd5fc58cfd6318f82e20af24fbe2c83004f6ff16259adb13afdf14"}, + {file = "pycares-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1bc541b627c7951dd36136b18bd185c5244a0fb2af5b1492ffb8acaceec1c5b"}, + {file = "pycares-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:938d188ed6bed696099be67ebdcdf121827b9432b17a9ea9e40dc35fd9d85363"}, + {file = "pycares-4.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:327837ffdc0c7adda09c98e1263c64b2aff814eea51a423f66733c75ccd9a642"}, + {file = "pycares-4.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a6b9b8d08c4508c45bd39e0c74e9e7052736f18ca1d25a289365bb9ac36e5849"}, + {file = "pycares-4.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:feac07d5e6d2d8f031c71237c21c21b8c995b41a1eba64560e8cf1e42ac11bc6"}, + {file = "pycares-4.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5bcdbf37012fd2323ca9f2a1074421a9ccf277d772632f8f0ce8c46ec7564250"}, + {file = "pycares-4.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebb692cb43fcf34fe0d26f2cf9a0ea53fdfb136463845b81fad651277922db"}, + {file = "pycares-4.8.0-cp310-cp310-win32.whl", hash = "sha256:d98447ec0efff3fa868ccc54dcc56e71faff498f8848ecec2004c3108efb4da2"}, + {file = "pycares-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:1abb8f40917960ead3c2771277f0bdee1967393b0fdf68743c225b606787da68"}, + {file = "pycares-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e25db89005ddd8d9c5720293afe6d6dd92e682fc6bc7a632535b84511e2060d"}, + {file = "pycares-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f9665ef116e6ee216c396f5f927756c2164f9f3316aec7ff1a9a1e1e7ec9b2a"}, + {file = "pycares-4.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54a96893133471f6889b577147adcc21a480dbe316f56730871028379c8313f3"}, + {file = "pycares-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51024b3a69762bd3100d94986a29922be15e13f56f991aaefb41f5bcd3d7f0bb"}, + {file = "pycares-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47ff9db50c599e4d965ae3bec99cc30941c1d2b0f078ec816680b70d052dd54a"}, + {file = "pycares-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27ef8ff4e0f60ea6769a60d1c3d1d2aefed1d832e7bb83fc3934884e2dba5cdd"}, + {file = "pycares-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63511af7a3f9663f562fbb6bfa3591a259505d976e2aba1fa2da13dde43c6ca7"}, + {file = "pycares-4.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:73c3219b47616e6a5ad1810de96ed59721c7751f19b70ae7bf24997a8365408f"}, + {file = "pycares-4.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:da42a45207c18f37be5e491c14b6d1063cfe1e46620eb661735d0cedc2b59099"}, + {file = "pycares-4.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8a068e898bb5dd09cd654e19cd2abf20f93d0cc59d5d955135ed48ea0f806aa1"}, + {file = "pycares-4.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:962aed95675bb66c0b785a2fbbd1bb58ce7f009e283e4ef5aaa4a1f2dc00d217"}, + {file = "pycares-4.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce8b1a16c1e4517a82a0ebd7664783a327166a3764d844cf96b1fb7b9dd1e493"}, + {file = "pycares-4.8.0-cp311-cp311-win32.whl", hash = "sha256:b3749ddbcbd216376c3b53d42d8b640b457133f1a12b0e003f3838f953037ae7"}, + {file = "pycares-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:5ce8a4e1b485b2360ab666c4ea1db97f57ede345a3b566d80bfa52b17e616610"}, + {file = "pycares-4.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3273e01a75308ed06d2492d83c7ba476e579a60a24d9f20fe178ce5e9d8d028b"}, + {file = "pycares-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fcedaadea1f452911fd29935749f98d144dae758d6003b7e9b6c5d5bd47d1dff"}, + {file = "pycares-4.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aae6cb33e287e06a4aabcbc57626df682c9a4fa8026207f5b498697f1c2fb562"}, + {file = "pycares-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25038b930e5be82839503fb171385b2aefd6d541bc5b7da0938bdb67780467d2"}, + {file = "pycares-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc8499b6e7dfbe4af65f6938db710ce9acd1debf34af2cbb93b898b1e5da6a5a"}, + {file = "pycares-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4e1c6a68ef56a7622f6176d9946d4e51f3c853327a0123ef35a5380230c84cd"}, + {file = "pycares-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7cc8c3c9114b9c84e4062d25ca9b4bddc80a65d0b074c7cb059275273382f89"}, + {file = "pycares-4.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4404014069d3e362abf404c9932d4335bb9c07ba834cfe7d683c725b92e0f9da"}, + {file = "pycares-4.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ee0a58c32ec2a352cef0e1d20335a7caf9871cd79b73be2ca2896fe70f09c9d7"}, + {file = "pycares-4.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:35f32f52b486b8fede3cbebf088f30b01242d0321b5216887c28e80490595302"}, + {file = "pycares-4.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ecbb506e27a3b3a2abc001c77beeccf265475c84b98629a6b3e61bd9f2987eaa"}, + {file = "pycares-4.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9392b2a34adbf60cb9e38f4a0d363413ecea8d835b5a475122f50f76676d59dd"}, + {file = "pycares-4.8.0-cp312-cp312-win32.whl", hash = "sha256:f0fbefe68403ffcff19c869b8d621c88a6d2cef18d53cf0dab0fa9458a6ca712"}, + {file = "pycares-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa8aab6085a2ddfb1b43a06ddf1b498347117bb47cd620d9b12c43383c9c2737"}, + {file = "pycares-4.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:358a9a2c6fed59f62788e63d88669224955443048a1602016d4358e92aedb365"}, + {file = "pycares-4.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e3e1278967fa8d4a0056be3fcc8fc551b8bad1fc7d0e5172196dccb8ddb036a"}, + {file = "pycares-4.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79befb773e370a8f97de9f16f5ea2c7e7fa0e3c6c74fbea6d332bf58164d7d06"}, + {file = "pycares-4.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b00d3695db64ce98a34e632e1d53f5a1cdb25451489f227bec2a6c03ff87ee8"}, + {file = "pycares-4.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37bdc4f2ff0612d60fc4f7547e12ff02cdcaa9a9e42e827bb64d4748994719f1"}, + {file = "pycares-4.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c44498ec7a6139888b464b28c49f7ba975933689bd67ea8d572b94188404"}, + {file = "pycares-4.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2665a0d810e2bbc41e97f3c3e5ea7950f666b3aa19c5f6c99d6b018ccd2e0052"}, + {file = "pycares-4.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45a629a6470a33478514c566bce50c63f1b17d1c5f2f964c9a6790330dc105fb"}, + {file = "pycares-4.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:47bb378f1773f41cca8e31dcdf009ce4a9b8aff8a30c7267aaff9a099c407ba5"}, + {file = "pycares-4.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fb3feae38458005cc101956e38f16eb3145fff8cd793e35cd4bdef6bf1aa2623"}, + {file = "pycares-4.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:14bc28aeaa66b0f4331ac94455e8043c8a06b3faafd78cc49d4b677bae0d0b08"}, + {file = "pycares-4.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62c82b871470f2864a1febf7b96bb1d108ce9063e6d3d43727e8a46f0028a456"}, + {file = "pycares-4.8.0-cp313-cp313-win32.whl", hash = "sha256:01afa8964c698c8f548b46d726f766aa7817b2d4386735af1f7996903d724920"}, + {file = "pycares-4.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:22f86f81b12ab17b0a7bd0da1e27938caaed11715225c1168763af97f8bb51a7"}, + {file = "pycares-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:61325d13a95255e858f42a7a1a9e482ff47ef2233f95ad9a4f308a3bd8ecf903"}, + {file = "pycares-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfec3a7d42336fa46a1e7e07f67000fd4b97860598c59a894c08f81378629e4e"}, + {file = "pycares-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b65067e4b4f5345688817fff6be06b9b1f4ec3619b0b9ecc639bc681b73f646b"}, + {file = "pycares-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0322ad94bbaa7016139b5bbdcd0de6f6feb9d146d69e03a82aaca342e06830a6"}, + {file = "pycares-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:456c60f170c997f9a43c7afa1085fced8efb7e13ae49dd5656f998ae13c4bdb4"}, + {file = "pycares-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a2c4c9ce423a85b0e0227409dbaf0d478f5e0c31d9e626768e77e1e887d32f"}, + {file = "pycares-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:478d9c479108b7527266864c0affe3d6e863492c9bc269217e36100c8fd89b91"}, + {file = "pycares-4.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aed56bca096990ca0aa9bbf95761fc87e02880e04b0845922b5c12ea9abe523f"}, + {file = "pycares-4.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef265a390928ee2f77f8901c2273c53293157860451ad453ce7f45dd268b72f9"}, + {file = "pycares-4.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a5f17d7a76d8335f1c90a8530c8f1e8bb22e9a1d70a96f686efaed946de1c908"}, + {file = "pycares-4.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:891f981feb2ef34367378f813fc17b3d706ce95b6548eeea0c9fe7705d7e54b1"}, + {file = "pycares-4.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4102f6d9117466cc0a1f527907a1454d109cc9e8551b8074888071ef16050fe3"}, + {file = "pycares-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d6775308659652adc88c82c53eda59b5e86a154aaba5ad1e287bbb3e0be77076"}, + {file = "pycares-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:8bc05462aa44788d48544cca3d2532466fed2cdc5a2f24a43a92b620a61c9d19"}, + {file = "pycares-4.8.0.tar.gz", hash = "sha256:2fc2ebfab960f654b3e3cf08a732486950da99393a657f8b44618ad3ed2d39c1"}, +] + +[package.dependencies] +cffi = ">=1.5.0" + +[package.extras] +idna = ["idna (>=2.1)"] + [[package]] name = "pycodestyle" version = "2.13.0" @@ -6255,4 +6369,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "8d59c154b4ec91c28424c552de7c85c6399efe24ab74a979bfd62275d112fbf0" +content-hash = "6c93e51cf22c06548aa6d0e23ca8ceb4450f5e02d4142715e941aabc1a2cbd6a" diff --git a/autogpt_platform/backend/pyproject.toml b/autogpt_platform/backend/pyproject.toml index b54ba12b23..26c05668c0 100644 --- a/autogpt_platform/backend/pyproject.toml +++ b/autogpt_platform/backend/pyproject.toml @@ -10,6 +10,7 @@ packages = [{ include = "backend", format = "sdist" }] [tool.poetry.dependencies] python = ">=3.10,<3.13" aio-pika = "^9.5.5" +aiodns = "^3.1.1" anthropic = "^0.51.0" apscheduler = "^3.11.0" autogpt-libs = { path = "../autogpt_libs", develop = true } @@ -66,6 +67,7 @@ youtube-transcript-api = "^0.6.2" zerobouncesdk = "^1.1.1" # NOTE: please insert new dependencies in their alphabetical location pytest-snapshot = "^0.9.0" +aiofiles = "^24.1.0" [tool.poetry.group.dev.dependencies] aiohappyeyeballs = "^2.6.1" diff --git a/autogpt_platform/backend/test/block/test_block.py b/autogpt_platform/backend/test/block/test_block.py index 48d2616f61..3f4aedb1e9 100644 --- a/autogpt_platform/backend/test/block/test_block.py +++ b/autogpt_platform/backend/test/block/test_block.py @@ -7,5 +7,5 @@ from backend.util.test import execute_block_test @pytest.mark.parametrize("block", get_blocks().values(), ids=lambda b: b.name) -def test_available_blocks(block: Type[Block]): - execute_block_test(block()) +async def test_available_blocks(block: Type[Block]): + await execute_block_test(block()) diff --git a/autogpt_platform/backend/test/executor/test_tool_use.py b/autogpt_platform/backend/test/executor/test_tool_use.py index d93797633b..54ecfbce0a 100644 --- a/autogpt_platform/backend/test/executor/test_tool_use.py +++ b/autogpt_platform/backend/test/executor/test_tool_use.py @@ -22,11 +22,11 @@ async def create_graph(s: SpinTestServer, g: graph.Graph, u: User) -> graph.Grap return await s.agent_server.test_create_graph(CreateGraph(graph=g), u.id) -def create_credentials(s: SpinTestServer, u: User): +async def create_credentials(s: SpinTestServer, u: User): provider = ProviderName.OPENAI credentials = llm.TEST_CREDENTIALS try: - s.agent_server.test_create_credentials(u.id, provider, credentials) + await s.agent_server.test_create_credentials(u.id, provider, credentials) except Exception: # ValueErrors is raised trying to recreate the same credentials # so hidding the error @@ -65,7 +65,7 @@ async def execute_graph( async def test_graph_validation_with_tool_nodes_correct(server: SpinTestServer): test_user = await create_test_user() test_tool_graph = await create_graph(server, create_test_graph(), test_user) - create_credentials(server, test_user) + await create_credentials(server, test_user) nodes = [ graph.Node( @@ -116,7 +116,7 @@ async def test_graph_validation_with_tool_nodes_raises_error(server: SpinTestSer test_user = await create_test_user() test_tool_graph = await create_graph(server, create_test_graph(), test_user) - create_credentials(server, test_user) + await create_credentials(server, test_user) nodes = [ graph.Node( @@ -176,7 +176,7 @@ async def test_graph_validation_with_tool_nodes_raises_error(server: SpinTestSer async def test_smart_decision_maker_function_signature(server: SpinTestServer): test_user = await create_test_user() test_tool_graph = await create_graph(server, create_test_graph(), test_user) - create_credentials(server, test_user) + await create_credentials(server, test_user) nodes = [ graph.Node( diff --git a/autogpt_platform/backend/test/util/test_request.py b/autogpt_platform/backend/test/util/test_request.py index e973e7dd6e..57717ff77f 100644 --- a/autogpt_platform/backend/test/util/test_request.py +++ b/autogpt_platform/backend/test/util/test_request.py @@ -54,14 +54,14 @@ from backend.util.request import pin_url, validate_url ("example.com?param=äöü", [], "http://example.com?param=äöü", False), ], ) -def test_validate_url_no_dns_rebinding( +async def test_validate_url_no_dns_rebinding( raw_url: str, trusted_origins: list[str], expected_value: str, should_raise: bool ): if should_raise: with pytest.raises(ValueError): - validate_url(raw_url, trusted_origins) + await validate_url(raw_url, trusted_origins) else: - validated_url, _, _ = validate_url(raw_url, trusted_origins) + validated_url, _, _ = await validate_url(raw_url, trusted_origins) assert validated_url.geturl() == expected_value @@ -78,7 +78,7 @@ def test_validate_url_no_dns_rebinding( ("blocked.com", ["127.0.0.1"], True, None), ], ) -def test_dns_rebinding_fix( +async def test_dns_rebinding_fix( monkeypatch, hostname: str, resolved_ips: list[str], @@ -100,10 +100,10 @@ def test_dns_rebinding_fix( if expect_error: # If any IP is blocked, we expect a ValueError with pytest.raises(ValueError): - url, _, ip_addresses = validate_url(hostname, []) + url, _, ip_addresses = await validate_url(hostname, []) pin_url(url, ip_addresses) else: - url, _, ip_addresses = validate_url(hostname, []) + url, _, ip_addresses = await validate_url(hostname, []) pinned_url = pin_url(url, ip_addresses).geturl() # The pinned_url should contain the first valid IP assert pinned_url.startswith("http://") or pinned_url.startswith("https://") diff --git a/autogpt_platform/frontend/src/components/CustomEdge.tsx b/autogpt_platform/frontend/src/components/CustomEdge.tsx index bb620b2e6f..d7ab076481 100644 --- a/autogpt_platform/frontend/src/components/CustomEdge.tsx +++ b/autogpt_platform/frontend/src/components/CustomEdge.tsx @@ -115,9 +115,7 @@ export function CustomEdge({ .map((bead) => ({ ...bead })) .filter((bead, index) => { const beadDown = data?.beadDown!; - - // Remove always one less bead in case of static edge, so it stays at the connection point - const removeCount = beadDown - destroyed - (data?.isStatic ? 1 : 0); + const removeCount = beadDown - destroyed; if (bead.t >= bead.targetT && index < removeCount) { destroyedCount++; return false; @@ -154,9 +152,7 @@ export function CustomEdge({ }) .filter((bead, index) => { const beadDown = data?.beadDown!; - - // Remove always one less bead in case of static edge, so it stays at the connection point - const removeCount = beadDown - destroyed - (data?.isStatic ? 1 : 0); + const removeCount = beadDown - destroyed; if (bead.t >= bead.targetT && index < removeCount) { destroyedCount++; return false; diff --git a/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx b/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx index 5fef2152f2..2f72754205 100644 --- a/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx +++ b/autogpt_platform/frontend/src/components/RunnerUIWrapper.tsx @@ -104,7 +104,10 @@ const RunnerUIWrapper = forwardRef( (node.data.hardcodedValues as any).description || "Output from the agent", }, - result: (node.data.executionResults as any)?.at(-1)?.data?.output, + result: + (node.data.executionResults as any) + ?.map((result: any) => result?.data?.output) + .join("\n--\n") || "No output yet", }) satisfies BlockOutput, ); diff --git a/autogpt_platform/frontend/src/lib/utils.ts b/autogpt_platform/frontend/src/lib/utils.ts index b1ba8dc0dc..1053cff5ee 100644 --- a/autogpt_platform/frontend/src/lib/utils.ts +++ b/autogpt_platform/frontend/src/lib/utils.ts @@ -397,7 +397,7 @@ export function isEmptyOrWhitespace(str: string | undefined | null): boolean { return !str || str.trim().length === 0; } -/** Chech if a value is an object or not */ +/** Check if a value is an object or not */ export function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } From 2269e3593aba27f35940a8f092e2458ae1ddbd63 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Tue, 17 Jun 2025 14:39:28 +0400 Subject: [PATCH 08/29] chore(frontend): document icons on storybook (#10181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ ### Checklist 📋 Screenshot 2025-06-17 at 14 11 55 Document the icons for the new design system. With the design team, it was agreed we will settle on [phosphor icons](https://phosphoricons.com/), so we will need to migrate progressively out of `lucide-react`. ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run Storybook locally - [x] Check the icons story and displays well --- autogpt_platform/frontend/package.json | 1 + autogpt_platform/frontend/pnpm-lock.yaml | 973 +++++++++--------- .../frontend/src/stories/icons.stories.tsx | 462 +++++++++ 3 files changed, 969 insertions(+), 467 deletions(-) create mode 100644 autogpt_platform/frontend/src/stories/icons.stories.tsx diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index ff174e390c..b8abd0baf0 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -27,6 +27,7 @@ "@faker-js/faker": "9.8.0", "@hookform/resolvers": "5.1.1", "@next/third-parties": "15.3.3", + "@phosphor-icons/react": "2.1.10", "@radix-ui/react-alert-dialog": "1.1.14", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.2", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index a258b772eb..e418b8bb87 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@next/third-parties': specifier: 15.3.3 version: 15.3.3(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(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) @@ -76,7 +79,7 @@ importers: 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) '@sentry/nextjs': specifier: 9.27.0 - version: 9.27.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.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + version: 9.27.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.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) '@supabase/ssr': specifier: 0.6.1 version: 0.6.1(@supabase/supabase-js@2.50.0) @@ -230,7 +233,7 @@ importers: version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/nextjs': specifier: 8.6.14 - version: 8.6.14(@swc/core@1.11.31)(esbuild@0.24.2)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(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@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + version: 8.6.14(@swc/core@1.12.1)(esbuild@0.25.5)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(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@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) '@storybook/react': specifier: 8.6.14 version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) @@ -266,7 +269,7 @@ importers: version: 3.16.3 axe-playwright: specifier: 2.1.0 - version: 2.1.0(playwright@1.52.0) + version: 2.1.0(playwright@1.53.0) chromatic: specifier: 11.25.2 version: 11.25.2 @@ -988,152 +991,152 @@ packages: '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1564,8 +1567,8 @@ packages: resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==} engines: {node: '>=18'} - '@napi-rs/wasm-runtime@0.2.10': - resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} '@next/env@15.3.3': resolution: {integrity: sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==} @@ -1840,6 +1843,13 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2897,68 +2907,68 @@ packages: '@supabase/supabase-js@2.50.0': resolution: {integrity: sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==} - '@swc/core-darwin-arm64@1.11.31': - resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==} + '@swc/core-darwin-arm64@1.12.1': + resolution: {integrity: sha512-nUjWVcJ3YS2N40ZbKwYO2RJ4+o2tWYRzNOcIQp05FqW0+aoUCVMdAUUzQinPDynfgwVshDAXCKemY8X7nN5MaA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.11.31': - resolution: {integrity: sha512-THSGaSwT96JwXDwuXQ6yFBbn+xDMdyw7OmBpnweAWsh5DhZmQkALEm1DgdQO3+rrE99MkmzwAfclc0UmYro/OA==} + '@swc/core-darwin-x64@1.12.1': + resolution: {integrity: sha512-OGm4a4d3OeJn+tRt8H/eiHgTFrJbS6r8mi/Ob65tAEXZGHN900T2kR7c5ALr0V2hBOQ8BfhexwPoQlGQP/B95w==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.11.31': - resolution: {integrity: sha512-laKtQFnW7KHgE57Hx32os2SNAogcuIDxYE+3DYIOmDMqD7/1DCfJe6Rln2N9WcOw6HuDbDpyQavIwZNfSAa8vQ==} + '@swc/core-linux-arm-gnueabihf@1.12.1': + resolution: {integrity: sha512-76YeeQKyK0EtNkQiNBZ0nbVGooPf9IucY0WqVXVpaU4wuG7ZyLEE2ZAIgXafIuzODGQoLfetue7I8boMxh1/MA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.11.31': - resolution: {integrity: sha512-T+vGw9aPE1YVyRxRr1n7NAdkbgzBzrXCCJ95xAZc/0+WUwmL77Z+js0J5v1KKTRxw4FvrslNCOXzMWrSLdwPSA==} + '@swc/core-linux-arm64-gnu@1.12.1': + resolution: {integrity: sha512-BxJDIJPq1+aCh9UsaSAN6wo3tuln8UhNXruOrzTI8/ElIig/3sAueDM6Eq7GvZSGGSA7ljhNATMJ0elD7lFatQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.11.31': - resolution: {integrity: sha512-Mztp5NZkyd5MrOAG+kl+QSn0lL4Uawd4CK4J7wm97Hs44N9DHGIG5nOz7Qve1KZo407Y25lTxi/PqzPKHo61zQ==} + '@swc/core-linux-arm64-musl@1.12.1': + resolution: {integrity: sha512-NhLdbffSXvY0/FwUSAl4hKBlpe5GHQGXK8DxTo3HHjLsD9sCPYieo3vG0NQoUYAy4ZUY1WeGjyxeq4qZddJzEQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.11.31': - resolution: {integrity: sha512-DDVE0LZcXOWwOqFU1Xi7gdtiUg3FHA0vbGb3trjWCuI1ZtDZHEQYL4M3/2FjqKZtIwASrDvO96w91okZbXhvMg==} + '@swc/core-linux-x64-gnu@1.12.1': + resolution: {integrity: sha512-CrYnV8SZIgArQ9LKH0xEF95PKXzX9WkRSc5j55arOSBeDCeDUQk1Bg/iKdnDiuj5HC1hZpvzwMzSBJjv+Z70jA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.11.31': - resolution: {integrity: sha512-mJA1MzPPRIfaBUHZi0xJQ4vwL09MNWDeFtxXb0r4Yzpf0v5Lue9ymumcBPmw/h6TKWms+Non4+TDquAsweuKSw==} + '@swc/core-linux-x64-musl@1.12.1': + resolution: {integrity: sha512-BQMl3d0HaGB0/h2xcKlGtjk/cGRn2tnbsaChAKcjFdCepblKBCz1pgO/mL7w5iXq3s57wMDUn++71/a5RAkZOA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.11.31': - resolution: {integrity: sha512-RdtakUkNVAb/FFIMw3LnfNdlH1/ep6KgiPDRlmyUfd0WdIQ3OACmeBegEFNFTzi7gEuzy2Yxg4LWf4IUVk8/bg==} + '@swc/core-win32-arm64-msvc@1.12.1': + resolution: {integrity: sha512-b7NeGnpqTfmIGtUqXBl0KqoSmOnH64nRZoT5l4BAGdvwY7nxitWR94CqZuwyLPty/bLywmyDA9uO12Kvgb3+gg==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.11.31': - resolution: {integrity: sha512-hErXdCGsg7swWdG1fossuL8542I59xV+all751mYlBoZ8kOghLSKObGQTkBbuNvc0sUKWfWg1X0iBuIhAYar+w==} + '@swc/core-win32-ia32-msvc@1.12.1': + resolution: {integrity: sha512-iU/29X2D7cHBp1to62cUg/5Xk8K+lyOJiKIGGW5rdzTW/c2zz3d/ehgpzVP/rqC4NVr88MXspqHU4il5gmDajw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.11.31': - resolution: {integrity: sha512-5t7SGjUBMMhF9b5j17ml/f/498kiBJNf4vZFNM421UGUEETdtjPN9jZIuQrowBkoFGJTCVL/ECM4YRtTH30u/A==} + '@swc/core-win32-x64-msvc@1.12.1': + resolution: {integrity: sha512-+Zh+JKDwiFqV5N9yAd2DhYVGPORGh9cfenu1ptr9yge+eHAf7vZJcC3rnj6QMR1QJh0Y5VC9+YBjRFjZVA7XDw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.11.31': - resolution: {integrity: sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A==} + '@swc/core@1.12.1': + resolution: {integrity: sha512-aKXdDTqxTVFl/bKQZ3EQUjEMBEoF6JBv29moMZq0kbVO43na6u/u+3Vcbhbrh+A2N0X5OL4RaveuWfAjEgOmeA==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -2978,8 +2988,8 @@ packages: peerDependencies: '@swc/core': '*' - '@swc/types@0.1.22': - resolution: {integrity: sha512-D13mY/ZA4PPEFSy6acki9eBT/3WgjMoRqNcdpIvjaYLQ44Xk5BdaL7UkDxAh6Z9UOe7tCCp67BVmZCojYp9owg==} + '@swc/types@0.1.23': + resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} @@ -3090,9 +3100,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -3159,8 +3166,8 @@ packages: '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} '@types/react-dom@18.3.5': resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} @@ -3218,150 +3225,160 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@8.33.1': - resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} + '@typescript-eslint/eslint-plugin@8.34.1': + resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.33.1 + '@typescript-eslint/parser': ^8.34.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.33.1': - resolution: {integrity: sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==} + '@typescript-eslint/parser@8.34.1': + resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==} 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.33.1': - resolution: {integrity: sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==} + '@typescript-eslint/project-service@8.34.1': + resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.33.1': - resolution: {integrity: sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==} + '@typescript-eslint/scope-manager@8.34.1': + resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.1': - resolution: {integrity: sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==} + '@typescript-eslint/tsconfig-utils@8.34.1': + resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.33.1': - resolution: {integrity: sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==} + '@typescript-eslint/type-utils@8.34.1': + resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==} 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.33.1': - resolution: {integrity: sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==} + '@typescript-eslint/types@8.34.1': + resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.33.1': - resolution: {integrity: sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==} + '@typescript-eslint/typescript-estree@8.34.1': + resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.33.1': - resolution: {integrity: sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==} + '@typescript-eslint/utils@8.34.1': + resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==} 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.33.1': - resolution: {integrity: sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==} + '@typescript-eslint/visitor-keys@8.34.1': + resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==} 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-darwin-arm64@1.7.10': - resolution: {integrity: sha512-ABsM3eEiL3yu903G0uxgvGAoIw011XjTzyEk//gGtuVY1PuXP2IJG6novd6DBjm7MaWmRV/CZFY1rWBXSlSVVw==} + '@unrs/resolver-binding-android-arm-eabi@1.9.0': + resolution: {integrity: sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.9.0': + resolution: {integrity: sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.9.0': + resolution: {integrity: sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.7.10': - resolution: {integrity: sha512-lGVWy4FQEDo/PuI1VQXaQCY0XUg4xUJilf3fQ8NY4wtsQTm9lbasbUYf3nkoma+O2/do90jQTqkb02S3meyTDg==} + '@unrs/resolver-binding-darwin-x64@1.9.0': + resolution: {integrity: sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.7.10': - resolution: {integrity: sha512-g9XLCHzNGatY79JJNgxrUH6uAAfBDj2NWIlTnqQN5odwGKjyVfFZ5tFL1OxYPcxTHh384TY5lvTtF+fuEZNvBQ==} + '@unrs/resolver-binding-freebsd-x64@1.9.0': + resolution: {integrity: sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.10': - resolution: {integrity: sha512-zV0ZMNy50sJFJapsjec8onyL9YREQKT88V8KwMoOA+zki/duFUP0oyTlbax1jGKdh8rQnruvW9VYkovGvdBAsw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.0': + resolution: {integrity: sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.7.10': - resolution: {integrity: sha512-jQxgb1DIDI7goyrabh4uvyWWBrFRfF+OOnS9SbF15h52g3Qjn/u8zG7wOQ0NjtcSMftzO75TITu9aHuI7FcqQQ==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.0': + resolution: {integrity: sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.7.10': - resolution: {integrity: sha512-9wVVlO6+aNlm90YWitwSI++HyCyBkzYCwMi7QbuGrTxDFm2pAgtpT0OEliaI7tLS8lAWYuDbzRRCJDgsdm6nwg==} + '@unrs/resolver-binding-linux-arm64-gnu@1.9.0': + resolution: {integrity: sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-arm64-musl@1.7.10': - resolution: {integrity: sha512-FtFweORChdXOes0RAAyTZp6I4PodU2cZiSILAbGaEKDXp378UOumD2vaAkWHNxpsreQUKRxG5O1uq9EoV1NiVQ==} + '@unrs/resolver-binding-linux-arm64-musl@1.9.0': + resolution: {integrity: sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==} cpu: [arm64] os: [linux] - '@unrs/resolver-binding-linux-ppc64-gnu@1.7.10': - resolution: {integrity: sha512-B+hOjpG2ncCR96a9d9ww1dWVuRVC2NChD0bITgrUhEWBhpdv2o/Mu2l8MsB2fzjdV/ku+twaQhr8iLHBoZafZQ==} + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.0': + resolution: {integrity: sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==} cpu: [ppc64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-gnu@1.7.10': - resolution: {integrity: sha512-DS6jFDoQCFsnsdLXlj3z3THakQLBic63B6A0rpQ1kpkyKa3OzEfqhwRNVaywuUuOKP9bX55Jk2uqpvn/hGjKCg==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.0': + resolution: {integrity: sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-riscv64-musl@1.7.10': - resolution: {integrity: sha512-A82SB6yEaA8EhIW2r0I7P+k5lg7zPscFnGs1Gna5rfPwoZjeUAGX76T55+DiyTiy08VFKUi79PGCulXnfjDq0g==} + '@unrs/resolver-binding-linux-riscv64-musl@1.9.0': + resolution: {integrity: sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==} cpu: [riscv64] os: [linux] - '@unrs/resolver-binding-linux-s390x-gnu@1.7.10': - resolution: {integrity: sha512-J+VmOPH16U69QshCp9WS+Zuiuu9GWTISKchKIhLbS/6JSCEfw2A4N02whv2VmrkXE287xxZbhW1p6xlAXNzwqg==} + '@unrs/resolver-binding-linux-s390x-gnu@1.9.0': + resolution: {integrity: sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==} cpu: [s390x] os: [linux] - '@unrs/resolver-binding-linux-x64-gnu@1.7.10': - resolution: {integrity: sha512-bYTdDltcB/V3fEqpx8YDwDw8ta9uEg8TUbJOtek6JM42u9ciJ7R/jBjNeAOs+QbyxGDd2d6xkBaGwty1HzOz3Q==} + '@unrs/resolver-binding-linux-x64-gnu@1.9.0': + resolution: {integrity: sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-linux-x64-musl@1.7.10': - resolution: {integrity: sha512-NYZ1GvSuTokJ28lqcjrMTnGMySoo4dVcNK/nsNCKCXT++1zekZtJaE+N+4jc1kR7EV0fc1OhRrOGcSt7FT9t8w==} + '@unrs/resolver-binding-linux-x64-musl@1.9.0': + resolution: {integrity: sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==} cpu: [x64] os: [linux] - '@unrs/resolver-binding-wasm32-wasi@1.7.10': - resolution: {integrity: sha512-MRjJhTaQzLoX8OtzRBQDJ84OJ8IX1FqpRAUSxp/JtPeak+fyDfhXaEjcA/fhfgrACUnvC+jWC52f/V6MixSKCQ==} + '@unrs/resolver-binding-wasm32-wasi@1.9.0': + resolution: {integrity: sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.7.10': - resolution: {integrity: sha512-Cgw6qhdsfzXJnHb006CzqgaX8mD445x5FGKuueaLeH1ptCxDbzRs8wDm6VieOI7rdbstfYBaFtaYN7zBT5CUPg==} + '@unrs/resolver-binding-win32-arm64-msvc@1.9.0': + resolution: {integrity: sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.7.10': - resolution: {integrity: sha512-Z7oECyIT2/HsrWpJ6wi2b+lVbPmWqQHuW5zeatafoRXizk1+2wUl+aSop1PF58XcyBuwPP2YpEUUpMZ8ILV4fA==} + '@unrs/resolver-binding-win32-ia32-msvc@1.9.0': + resolution: {integrity: sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.7.10': - resolution: {integrity: sha512-DGAOo5asNvDsmFgwkb7xsgxNyN0If6XFYwDIC1QlRE7kEYWIMRChtWJyHDf30XmGovDNOs/37krxhnga/nm/4w==} + '@unrs/resolver-binding-win32-x64-msvc@1.9.0': + resolution: {integrity: sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw==} cpu: [x64] os: [win32] @@ -3457,11 +3474,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -3654,8 +3666,8 @@ packages: peerDependencies: playwright: '>1.0.0' - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -3740,11 +3752,11 @@ packages: boring-avatars@1.11.2: resolution: {integrity: sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -3838,8 +3850,8 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001723: + resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -4050,11 +4062,11 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} - core-js-compat@3.42.0: - resolution: {integrity: sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==} + core-js-compat@3.43.0: + resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} - core-js-pure@3.42.0: - resolution: {integrity: sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==} + core-js-pure@3.43.0: + resolution: {integrity: sha512-i/AgxU2+A+BbJdMxh3v7/vxi2SbFqxiFmg6VsDwYB4jkucrd1BZNA9a9gphC0fYMG5IBSgQcbQnk865VCLe7xA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -4246,8 +4258,8 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decode-named-character-reference@1.1.0: - resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -4395,8 +4407,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.165: - resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.169: + resolution: {integrity: sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==} elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -4494,8 +4506,8 @@ packages: peerDependencies: esbuild: '>=0.12 <1' - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} hasBin: true @@ -4607,8 +4619,8 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@8.57.1: @@ -4730,8 +4742,8 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.5: - resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -5699,8 +5711,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -5912,11 +5924,11 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - motion-dom@12.16.0: - resolution: {integrity: sha512-Z2nGwWrrdH4egLEtgYMCEN4V2qQt1qxlKy/uV7w691ztyA41Q5Rbn0KNGbsNVDZr9E8PD2IOQ3hSccRnB6xWzw==} + motion-dom@12.18.1: + resolution: {integrity: sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==} - motion-utils@12.12.1: - resolution: {integrity: sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==} + motion-utils@12.18.1: + resolution: {integrity: sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6263,11 +6275,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.53.0: + resolution: {integrity: sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==} + engines: {node: '>=18'} + hasBin: true + playwright@1.52.0: resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} engines: {node: '>=18'} hasBin: true + playwright@1.53.0: + resolution: {integrity: sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==} + engines: {node: '>=18'} + hasBin: true + pnp-webpack-plugin@1.7.0: resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} @@ -6544,8 +6566,8 @@ packages: peerDependencies: react: '>=16.8.0' - react-docgen-typescript@2.2.2: - resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} peerDependencies: typescript: '>= 4.3.x' @@ -7131,14 +7153,14 @@ packages: peerDependencies: webpack: ^5.0.0 - style-to-js@1.1.16: - resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} - style-to-object@1.0.8: - resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} - styled-components@6.1.18: - resolution: {integrity: sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==} + styled-components@6.1.19: + resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==} engines: {node: '>= 16'} peerDependencies: react: '>= 16.8.0' @@ -7227,8 +7249,8 @@ packages: uglify-js: optional: true - terser@5.41.0: - resolution: {integrity: sha512-H406eLPXpZbAX14+B8psIuvIr8+3c+2hkuYzpMkoE0ij+NdsVATbA78vb8neA/eqrj7rywa2pIkdmWRsXW6wmw==} + terser@5.42.0: + resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} engines: {node: '>=10'} hasBin: true @@ -7449,8 +7471,8 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} - unrs-resolver@1.7.10: - resolution: {integrity: sha512-CJEMJcz6vuwRK6xxWc+uf8AGi0OyfoVtHs5mExtNecS0HZq3a3Br1JC/InwwTn6uy+qkAdAdK+nJUYO9FPtgZw==} + unrs-resolver@1.9.0: + resolution: {integrity: sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg==} update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} @@ -8481,7 +8503,7 @@ snapshots: babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.27.4) babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.27.4) babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.27.4) - core-js-compat: 3.42.0 + core-js-compat: 3.43.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -8595,79 +8617,79 @@ snapshots: '@emotion/unitless@0.8.1': {} - '@esbuild/aix-ppc64@0.24.2': + '@esbuild/aix-ppc64@0.25.5': optional: true - '@esbuild/android-arm64@0.24.2': + '@esbuild/android-arm64@0.25.5': optional: true - '@esbuild/android-arm@0.24.2': + '@esbuild/android-arm@0.25.5': optional: true - '@esbuild/android-x64@0.24.2': + '@esbuild/android-x64@0.25.5': optional: true - '@esbuild/darwin-arm64@0.24.2': + '@esbuild/darwin-arm64@0.25.5': optional: true - '@esbuild/darwin-x64@0.24.2': + '@esbuild/darwin-x64@0.25.5': optional: true - '@esbuild/freebsd-arm64@0.24.2': + '@esbuild/freebsd-arm64@0.25.5': optional: true - '@esbuild/freebsd-x64@0.24.2': + '@esbuild/freebsd-x64@0.25.5': optional: true - '@esbuild/linux-arm64@0.24.2': + '@esbuild/linux-arm64@0.25.5': optional: true - '@esbuild/linux-arm@0.24.2': + '@esbuild/linux-arm@0.25.5': optional: true - '@esbuild/linux-ia32@0.24.2': + '@esbuild/linux-ia32@0.25.5': optional: true - '@esbuild/linux-loong64@0.24.2': + '@esbuild/linux-loong64@0.25.5': optional: true - '@esbuild/linux-mips64el@0.24.2': + '@esbuild/linux-mips64el@0.25.5': optional: true - '@esbuild/linux-ppc64@0.24.2': + '@esbuild/linux-ppc64@0.25.5': optional: true - '@esbuild/linux-riscv64@0.24.2': + '@esbuild/linux-riscv64@0.25.5': optional: true - '@esbuild/linux-s390x@0.24.2': + '@esbuild/linux-s390x@0.25.5': optional: true - '@esbuild/linux-x64@0.24.2': + '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/netbsd-arm64@0.24.2': + '@esbuild/netbsd-arm64@0.25.5': optional: true - '@esbuild/netbsd-x64@0.24.2': + '@esbuild/netbsd-x64@0.25.5': optional: true - '@esbuild/openbsd-arm64@0.24.2': + '@esbuild/openbsd-arm64@0.25.5': optional: true - '@esbuild/openbsd-x64@0.24.2': + '@esbuild/openbsd-x64@0.25.5': optional: true - '@esbuild/sunos-x64@0.24.2': + '@esbuild/sunos-x64@0.25.5': optional: true - '@esbuild/win32-arm64@0.24.2': + '@esbuild/win32-arm64@0.25.5': optional: true - '@esbuild/win32-ia32@0.24.2': + '@esbuild/win32-ia32@0.25.5': optional: true - '@esbuild/win32-x64@0.24.2': + '@esbuild/win32-x64@0.25.5': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': @@ -9139,7 +9161,7 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/wasm-runtime@0.2.10': + '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 '@emnapi/runtime': 1.4.3 @@ -9447,6 +9469,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@phosphor-icons/react@2.1.10(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) + '@pkgjs/parseargs@0.11.0': optional: true @@ -9454,17 +9481,17 @@ snapshots: dependencies: playwright: 1.52.0 - '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5))': dependencies: ansi-html: 0.0.9 - core-js-pure: 3.42.0 + core-js-pure: 3.43.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(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) optionalDependencies: type-fest: 4.41.0 webpack-hot-middleware: 2.26.1 @@ -10020,7 +10047,7 @@ snapshots: '@rollup/pluginutils': 5.1.4(rollup@4.35.0) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) is-reference: 1.2.1 magic-string: 0.30.17 picomatch: 4.0.2 @@ -10182,7 +10209,7 @@ snapshots: '@sentry/core@9.27.0': {} - '@sentry/nextjs@9.27.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.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2))': + '@sentry/nextjs@9.27.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.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 @@ -10193,7 +10220,7 @@ snapshots: '@sentry/opentelemetry': 9.27.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.27.0(react@18.3.1) '@sentry/vercel-edge': 9.27.0 - '@sentry/webpack-plugin': 3.5.0(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + '@sentry/webpack-plugin': 3.5.0(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) chalk: 3.0.0 next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) resolve: 1.22.8 @@ -10270,12 +10297,12 @@ snapshots: '@opentelemetry/api': 1.9.0 '@sentry/core': 9.27.0 - '@sentry/webpack-plugin@3.5.0(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2))': + '@sentry/webpack-plugin@3.5.0(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5))': dependencies: '@sentry/bundler-plugin-core': 3.5.0 unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) transitivePeerDependencies: - encoding - supports-color @@ -10416,7 +10443,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-webpack5@8.6.14(@swc/core@1.11.31)(esbuild@0.24.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': + '@storybook/builder-webpack5@8.6.14(@swc/core@1.12.1)(esbuild@0.25.5)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@types/semver': 7.7.0 @@ -10424,23 +10451,23 @@ snapshots: case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + css-loader: 6.11.0(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) - html-webpack-plugin: 5.6.3(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) + html-webpack-plugin: 5.6.3(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) magic-string: 0.30.17 path-browserify: 1.0.1 process: 0.11.10 semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) - style-loader: 3.3.4(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) - terser-webpack-plugin: 5.3.14(@swc/core@1.11.31)(esbuild@0.24.2)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + style-loader: 3.3.4(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) + terser-webpack-plugin: 5.3.14(@swc/core@1.12.1)(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) - webpack-dev-middleware: 6.1.3(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) + webpack-dev-middleware: 6.1.3(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -10466,8 +10493,8 @@ snapshots: '@storybook/theming': 8.6.14(storybook@8.6.14(prettier@3.5.3)) better-opn: 3.0.2 browser-assert: 1.2.1 - esbuild: 0.24.2 - esbuild-register: 3.6.0(esbuild@0.24.2) + esbuild: 0.25.5 + esbuild-register: 3.6.0(esbuild@0.25.5) jsdoc-type-pratt-parser: 4.1.0 process: 0.11.10 recast: 0.23.11 @@ -10508,7 +10535,7 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) - '@storybook/nextjs@8.6.14(@swc/core@1.11.31)(esbuild@0.24.2)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(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@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2))': + '@storybook/nextjs@8.6.14(@swc/core@1.12.1)(esbuild@0.25.5)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(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@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) @@ -10523,30 +10550,30 @@ snapshots: '@babel/preset-react': 7.27.1(@babel/core@7.27.4) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) '@babel/runtime': 7.27.6 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) - '@storybook/builder-webpack5': 8.6.14(@swc/core@1.11.31)(esbuild@0.24.2)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) - '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(@swc/core@1.11.31)(esbuild@0.24.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) + '@storybook/builder-webpack5': 8.6.14(@swc/core@1.12.1)(esbuild@0.25.5)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) + '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(@swc/core@1.12.1)(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@types/semver': 7.7.0 - babel-loader: 9.2.1(@babel/core@7.27.4)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) - css-loader: 6.11.0(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + babel-loader: 9.2.1(@babel/core@7.27.4)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) + css-loader: 6.11.0(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) find-up: 5.0.0 image-size: 1.2.1 loader-utils: 3.3.1 next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) pnp-webpack-plugin: 1.7.0(typescript@5.8.3) postcss: 8.5.4 - postcss-loader: 8.1.1(postcss@8.5.4)(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + postcss-loader: 8.1.1(postcss@8.5.4)(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) 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: 14.2.1(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + sass-loader: 14.2.1(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) - style-loader: 3.3.4(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + style-loader: 3.3.4(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) styled-jsx: 5.1.7(@babel/core@7.27.4)(react@18.3.1) ts-dedent: 2.2.0 tsconfig-paths: 4.2.0 @@ -10554,7 +10581,7 @@ snapshots: optionalDependencies: sharp: 0.33.5 typescript: 5.8.3 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -10573,11 +10600,11 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(@swc/core@1.11.31)(esbuild@0.24.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': + '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(@swc/core@1.12.1)(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) '@types/semver': 7.7.0 find-up: 5.0.0 magic-string: 0.30.17 @@ -10588,7 +10615,7 @@ snapshots: semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -10603,17 +10630,17 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5))': 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.2.2(typescript@5.8.3) + react-docgen-typescript: 2.4.0(typescript@5.8.3) tslib: 2.8.1 typescript: 5.8.3 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) transitivePeerDependencies: - supports-color @@ -10646,8 +10673,8 @@ snapshots: '@babel/types': 7.27.6 '@jest/types': 29.6.3 '@storybook/csf': 0.1.13 - '@swc/core': 1.11.31 - '@swc/jest': 0.2.38(@swc/core@1.11.31) + '@swc/core': 1.12.1 + '@swc/jest': 0.2.38(@swc/core@1.12.1) expect-playwright: 0.8.0 jest: 29.7.0(@types/node@22.15.30) jest-circus: 29.7.0 @@ -10658,7 +10685,7 @@ snapshots: jest-serializer-html: 7.1.0 jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@22.15.30)) nyc: 15.1.0 - playwright: 1.52.0 + playwright: 1.53.0 storybook: 8.6.14(prettier@3.5.3) transitivePeerDependencies: - '@swc/helpers' @@ -10731,51 +10758,51 @@ snapshots: - bufferutil - utf-8-validate - '@swc/core-darwin-arm64@1.11.31': + '@swc/core-darwin-arm64@1.12.1': optional: true - '@swc/core-darwin-x64@1.11.31': + '@swc/core-darwin-x64@1.12.1': optional: true - '@swc/core-linux-arm-gnueabihf@1.11.31': + '@swc/core-linux-arm-gnueabihf@1.12.1': optional: true - '@swc/core-linux-arm64-gnu@1.11.31': + '@swc/core-linux-arm64-gnu@1.12.1': optional: true - '@swc/core-linux-arm64-musl@1.11.31': + '@swc/core-linux-arm64-musl@1.12.1': optional: true - '@swc/core-linux-x64-gnu@1.11.31': + '@swc/core-linux-x64-gnu@1.12.1': optional: true - '@swc/core-linux-x64-musl@1.11.31': + '@swc/core-linux-x64-musl@1.12.1': optional: true - '@swc/core-win32-arm64-msvc@1.11.31': + '@swc/core-win32-arm64-msvc@1.12.1': optional: true - '@swc/core-win32-ia32-msvc@1.11.31': + '@swc/core-win32-ia32-msvc@1.12.1': optional: true - '@swc/core-win32-x64-msvc@1.11.31': + '@swc/core-win32-x64-msvc@1.12.1': optional: true - '@swc/core@1.11.31': + '@swc/core@1.12.1': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.22 + '@swc/types': 0.1.23 optionalDependencies: - '@swc/core-darwin-arm64': 1.11.31 - '@swc/core-darwin-x64': 1.11.31 - '@swc/core-linux-arm-gnueabihf': 1.11.31 - '@swc/core-linux-arm64-gnu': 1.11.31 - '@swc/core-linux-arm64-musl': 1.11.31 - '@swc/core-linux-x64-gnu': 1.11.31 - '@swc/core-linux-x64-musl': 1.11.31 - '@swc/core-win32-arm64-msvc': 1.11.31 - '@swc/core-win32-ia32-msvc': 1.11.31 - '@swc/core-win32-x64-msvc': 1.11.31 + '@swc/core-darwin-arm64': 1.12.1 + '@swc/core-darwin-x64': 1.12.1 + '@swc/core-linux-arm-gnueabihf': 1.12.1 + '@swc/core-linux-arm64-gnu': 1.12.1 + '@swc/core-linux-arm64-musl': 1.12.1 + '@swc/core-linux-x64-gnu': 1.12.1 + '@swc/core-linux-x64-musl': 1.12.1 + '@swc/core-win32-arm64-msvc': 1.12.1 + '@swc/core-win32-ia32-msvc': 1.12.1 + '@swc/core-win32-x64-msvc': 1.12.1 '@swc/counter@0.1.3': {} @@ -10783,14 +10810,14 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/jest@0.2.38(@swc/core@1.11.31)': + '@swc/jest@0.2.38(@swc/core@1.12.1)': dependencies: '@jest/create-cache-key-function': 29.7.0 - '@swc/core': 1.11.31 + '@swc/core': 1.12.1 '@swc/counter': 0.1.3 jsonc-parser: 3.3.1 - '@swc/types@0.1.22': + '@swc/types@0.1.23': dependencies: '@swc/counter': 0.1.3 @@ -10920,12 +10947,10 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/graceful-fs@4.1.9': @@ -10990,7 +11015,7 @@ snapshots: '@types/phoenix@1.6.6': {} - '@types/prop-types@15.7.14': {} + '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.5(@types/react@18.3.17)': dependencies: @@ -11002,7 +11027,7 @@ snapshots: '@types/react@18.3.17': dependencies: - '@types/prop-types': 15.7.14 + '@types/prop-types': 15.7.15 csstype: 3.1.3 '@types/resolve@1.20.6': {} @@ -11043,14 +11068,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/type-utils': 8.33.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/parser': 8.34.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/type-utils': 8.34.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 eslint: 8.57.1 graphemer: 1.4.0 ignore: 7.0.5 @@ -11060,40 +11085,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 eslint: 8.57.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.33.1(typescript@5.8.3)': + '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.33.1': + '@typescript-eslint/scope-manager@8.34.1': dependencies: - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 - '@typescript-eslint/tsconfig-utils@8.33.1(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.33.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.1(eslint@8.57.1)(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@8.57.1)(typescript@5.8.3) debug: 4.4.1 eslint: 8.57.1 ts-api-utils: 2.1.0(typescript@5.8.3) @@ -11101,14 +11126,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.33.1': {} + '@typescript-eslint/types@8.34.1': {} - '@typescript-eslint/typescript-estree@8.33.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.33.1(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/visitor-keys': 8.33.1 + '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -11119,75 +11144,81 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.1(eslint@8.57.1)(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.33.1 - '@typescript-eslint/types': 8.33.1 - '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) eslint: 8.57.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.33.1': + '@typescript-eslint/visitor-keys@8.34.1': dependencies: - '@typescript-eslint/types': 8.33.1 - eslint-visitor-keys: 4.2.0 + '@typescript-eslint/types': 8.34.1 + eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-darwin-arm64@1.7.10': + '@unrs/resolver-binding-android-arm-eabi@1.9.0': optional: true - '@unrs/resolver-binding-darwin-x64@1.7.10': + '@unrs/resolver-binding-android-arm64@1.9.0': optional: true - '@unrs/resolver-binding-freebsd-x64@1.7.10': + '@unrs/resolver-binding-darwin-arm64@1.9.0': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.10': + '@unrs/resolver-binding-darwin-x64@1.9.0': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.7.10': + '@unrs/resolver-binding-freebsd-x64@1.9.0': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.7.10': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.9.0': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.7.10': + '@unrs/resolver-binding-linux-arm-musleabihf@1.9.0': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.7.10': + '@unrs/resolver-binding-linux-arm64-gnu@1.9.0': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.7.10': + '@unrs/resolver-binding-linux-arm64-musl@1.9.0': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.7.10': + '@unrs/resolver-binding-linux-ppc64-gnu@1.9.0': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.7.10': + '@unrs/resolver-binding-linux-riscv64-gnu@1.9.0': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.7.10': + '@unrs/resolver-binding-linux-riscv64-musl@1.9.0': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.7.10': + '@unrs/resolver-binding-linux-s390x-gnu@1.9.0': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.7.10': + '@unrs/resolver-binding-linux-x64-gnu@1.9.0': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.9.0': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.9.0': dependencies: - '@napi-rs/wasm-runtime': 0.2.10 + '@napi-rs/wasm-runtime': 0.2.11 optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.7.10': + '@unrs/resolver-binding-win32-arm64-msvc@1.9.0': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.7.10': + '@unrs/resolver-binding-win32-ia32-msvc@1.9.0': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.7.10': + '@unrs/resolver-binding-win32-x64-msvc@1.9.0': optional: true '@vitest/expect@2.0.5': @@ -11213,13 +11244,13 @@ snapshots: dependencies: '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 1.2.0 '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 1.2.0 '@webassemblyjs/ast@1.14.1': @@ -11331,11 +11362,9 @@ snapshots: dependencies: acorn: 8.15.0 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 - - acorn@8.14.1: {} + acorn: 8.15.0 acorn@8.15.0: {} @@ -11543,16 +11572,16 @@ snapshots: axe-core: 4.10.3 mustache: 4.2.0 - axe-playwright@2.1.0(playwright@1.52.0): + axe-playwright@2.1.0(playwright@1.53.0): 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.52.0 + playwright: 1.53.0 - axios@1.9.0: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 form-data: 4.0.3 @@ -11575,12 +11604,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.27.4)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + babel-loader@9.2.1(@babel/core@7.27.4)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: '@babel/core': 7.27.4 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) babel-plugin-istanbul@6.1.1: dependencies: @@ -11612,7 +11641,7 @@ snapshots: dependencies: '@babel/core': 7.27.4 '@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.27.4) - core-js-compat: 3.42.0 + core-js-compat: 3.43.0 transitivePeerDependencies: - supports-color @@ -11670,12 +11699,12 @@ snapshots: boring-avatars@1.11.2: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -11734,8 +11763,8 @@ snapshots: browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.165 + caniuse-lite: 1.0.30001723 + electron-to-chromium: 1.5.169 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) @@ -11797,7 +11826,7 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001723: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -11808,7 +11837,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 + loupe: 3.1.4 pathval: 2.0.0 chalk@2.4.2: @@ -11984,11 +12013,11 @@ snapshots: cookie@1.0.2: {} - core-js-compat@3.42.0: + core-js-compat@3.43.0: dependencies: browserslist: 4.25.0 - core-js-pure@3.42.0: {} + core-js-pure@3.43.0: {} core-util-is@1.0.3: {} @@ -12069,7 +12098,7 @@ snapshots: css-color-keywords@1.0.0: {} - css-loader@6.11.0(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + css-loader@6.11.0(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -12080,7 +12109,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) css-select@4.3.0: dependencies: @@ -12209,7 +12238,7 @@ snapshots: decimal.js-light@2.5.1: {} - decode-named-character-reference@1.1.0: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -12351,7 +12380,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.169: {} elliptic@6.6.1: dependencies: @@ -12513,40 +12542,40 @@ snapshots: es6-error@4.1.1: {} - esbuild-register@3.6.0(esbuild@0.24.2): + esbuild-register@3.6.0(esbuild@0.25.5): dependencies: debug: 4.4.1 - esbuild: 0.24.2 + esbuild: 0.25.5 transitivePeerDependencies: - supports-color - esbuild@0.24.2: + esbuild@0.25.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 escalade@3.2.0: {} @@ -12560,12 +12589,12 @@ snapshots: dependencies: '@next/eslint-plugin-next': 15.3.3 '@rushstack/eslint-patch': 1.11.0 - '@typescript-eslint/eslint-plugin': 8.33.1(@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -12593,24 +12622,24 @@ snapshots: is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 - unrs-resolver: 1.7.10 + unrs-resolver: 1.9.0 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(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.0(@typescript-eslint/parser@8.34.1(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): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.33.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -12621,7 +12650,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(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.0(@typescript-eslint/parser@8.34.1(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) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12633,7 +12662,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.33.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@8.57.1)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -12687,7 +12716,7 @@ snapshots: eslint-plugin-storybook@0.12.0(eslint@8.57.1)(typescript@5.8.3): dependencies: '@storybook/csf': 0.1.13 - '@typescript-eslint/utils': 8.33.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -12706,7 +12735,7 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} eslint@8.57.1: dependencies: @@ -12753,8 +12782,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -12862,7 +12891,7 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.4.5(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -12943,7 +12972,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(@swc/core@1.11.31)(esbuild@0.24.2)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -12958,7 +12987,7 @@ snapshots: semver: 7.7.2 tapable: 2.2.2 typescript: 5.8.3 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) form-data@4.0.3: dependencies: @@ -12972,8 +13001,8 @@ snapshots: framer-motion@12.16.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - motion-dom: 12.16.0 - motion-utils: 12.12.1 + motion-dom: 12.18.1 + motion-utils: 12.18.1 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.2.2 @@ -13162,7 +13191,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/hast': 3.0.4 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 @@ -13174,7 +13203,7 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.16 + style-to-js: 1.1.17 unist-util-position: 5.0.0 vfile-message: 4.0.2 transitivePeerDependencies: @@ -13214,11 +13243,11 @@ snapshots: he: 1.2.0 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.41.0 + terser: 5.42.0 html-url-attributes@3.0.1: {} - html-webpack-plugin@5.6.3(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + html-webpack-plugin@5.6.3(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -13226,7 +13255,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) htmlparser2@3.10.1: dependencies: @@ -13737,7 +13766,7 @@ snapshots: jest-process-manager: 0.4.0 jest-runner: 29.7.0 nyc: 15.1.0 - playwright-core: 1.52.0 + playwright-core: 1.53.0 rimraf: 3.0.2 uuid: 8.3.2 transitivePeerDependencies: @@ -14078,7 +14107,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.3: {} + loupe@3.1.4: {} lower-case@2.0.2: dependencies: @@ -14130,7 +14159,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -14229,7 +14258,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -14304,7 +14333,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -14342,7 +14371,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 debug: 4.4.1 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -14386,15 +14415,15 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@8.0.4: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -14408,11 +14437,11 @@ snapshots: moment@2.30.1: {} - motion-dom@12.16.0: + motion-dom@12.18.1: dependencies: - motion-utils: 12.12.1 + motion-utils: 12.18.1 - motion-utils@12.12.1: {} + motion-utils@12.18.1: {} ms@2.1.3: {} @@ -14475,7 +14504,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001721 + caniuse-lite: 1.0.30001723 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14509,7 +14538,7 @@ snapshots: node-int64@0.4.0: {} - node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -14536,7 +14565,7 @@ snapshots: url: 0.11.4 util: 0.12.5 vm-browserify: 1.1.2 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) node-preload@0.2.1: dependencies: @@ -14736,7 +14765,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -14820,12 +14849,20 @@ snapshots: playwright-core@1.52.0: {} + playwright-core@1.53.0: {} + playwright@1.52.0: dependencies: playwright-core: 1.52.0 optionalDependencies: fsevents: 2.3.2 + playwright@1.53.0: + dependencies: + playwright-core: 1.53.0 + optionalDependencies: + fsevents: 2.3.2 + pnp-webpack-plugin@1.7.0(typescript@5.8.3): dependencies: ts-pnp: 1.2.0(typescript@5.8.3) @@ -14857,14 +14894,14 @@ snapshots: optionalDependencies: postcss: 8.5.4 - postcss-loader@8.1.1(postcss@8.5.4)(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + postcss-loader@8.1.1(postcss@8.5.4)(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 postcss: 8.5.4 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) transitivePeerDependencies: - typescript @@ -15040,7 +15077,7 @@ snapshots: date-fns-jalali: 4.1.0-0 react: 18.3.1 - react-docgen-typescript@2.2.2(typescript@5.8.3): + react-docgen-typescript@2.4.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -15070,7 +15107,7 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-components: 6.1.18(react-dom@18.3.1(react@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.57.0(react@18.3.1): dependencies: @@ -15433,11 +15470,11 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - sass-loader@14.2.1(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + sass-loader@14.2.1(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: neo-async: 2.6.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) scheduler@0.23.2: dependencies: @@ -15804,19 +15841,19 @@ snapshots: strip-json-comments@3.1.1: {} - style-loader@3.3.4(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + style-loader@3.3.4(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) - style-to-js@1.1.16: + style-to-js@1.1.17: dependencies: - style-to-object: 1.0.8 + style-to-object: 1.0.9 - style-to-object@1.0.8: + style-to-object@1.0.9: dependencies: inline-style-parser: 0.2.4 - styled-components@6.1.18(react-dom@18.3.1(react@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): dependencies: '@emotion/is-prop-valid': 1.2.2 '@emotion/unitless': 0.8.1 @@ -15905,19 +15942,19 @@ snapshots: tapable@2.2.2: {} - terser-webpack-plugin@5.3.14(@swc/core@1.11.31)(esbuild@0.24.2)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + terser-webpack-plugin@5.3.14(@swc/core@1.12.1)(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.41.0 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + terser: 5.42.0 + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) optionalDependencies: - '@swc/core': 1.11.31 - esbuild: 0.24.2 + '@swc/core': 1.12.1 + esbuild: 0.25.5 - terser@5.41.0: + terser@5.42.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.15.0 @@ -15950,7 +15987,7 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 tinyrainbow@1.2.0: {} @@ -16141,30 +16178,32 @@ snapshots: unplugin@1.16.1: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 webpack-virtual-modules: 0.6.2 - unrs-resolver@1.7.10: + unrs-resolver@1.9.0: dependencies: napi-postinstall: 0.2.4 optionalDependencies: - '@unrs/resolver-binding-darwin-arm64': 1.7.10 - '@unrs/resolver-binding-darwin-x64': 1.7.10 - '@unrs/resolver-binding-freebsd-x64': 1.7.10 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.10 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.10 - '@unrs/resolver-binding-linux-arm64-gnu': 1.7.10 - '@unrs/resolver-binding-linux-arm64-musl': 1.7.10 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.10 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.10 - '@unrs/resolver-binding-linux-riscv64-musl': 1.7.10 - '@unrs/resolver-binding-linux-s390x-gnu': 1.7.10 - '@unrs/resolver-binding-linux-x64-gnu': 1.7.10 - '@unrs/resolver-binding-linux-x64-musl': 1.7.10 - '@unrs/resolver-binding-wasm32-wasi': 1.7.10 - '@unrs/resolver-binding-win32-arm64-msvc': 1.7.10 - '@unrs/resolver-binding-win32-ia32-msvc': 1.7.10 - '@unrs/resolver-binding-win32-x64-msvc': 1.7.10 + '@unrs/resolver-binding-android-arm-eabi': 1.9.0 + '@unrs/resolver-binding-android-arm64': 1.9.0 + '@unrs/resolver-binding-darwin-arm64': 1.9.0 + '@unrs/resolver-binding-darwin-x64': 1.9.0 + '@unrs/resolver-binding-freebsd-x64': 1.9.0 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.9.0 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.9.0 + '@unrs/resolver-binding-linux-arm64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-arm64-musl': 1.9.0 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-riscv64-musl': 1.9.0 + '@unrs/resolver-binding-linux-s390x-gnu': 1.9.0 + '@unrs/resolver-binding-linux-x64-gnu': 1.9.0 + '@unrs/resolver-binding-linux-x64-musl': 1.9.0 + '@unrs/resolver-binding-wasm32-wasi': 1.9.0 + '@unrs/resolver-binding-win32-arm64-msvc': 1.9.0 + '@unrs/resolver-binding-win32-ia32-msvc': 1.9.0 + '@unrs/resolver-binding-win32-x64-msvc': 1.9.0 update-browserslist-db@1.1.3(browserslist@4.25.0): dependencies: @@ -16260,7 +16299,7 @@ snapshots: wait-on@7.2.0: dependencies: - axios: 1.9.0 + axios: 1.10.0 joi: 17.13.3 lodash: 4.17.21 minimist: 1.2.8 @@ -16291,7 +16330,7 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-dev-middleware@6.1.3(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)): + webpack-dev-middleware@6.1.3(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -16299,7 +16338,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.24.2) + webpack: 5.99.9(@swc/core@1.12.1)(esbuild@0.25.5) webpack-hot-middleware@2.26.1: dependencies: @@ -16313,7 +16352,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2): + webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -16321,7 +16360,7 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 + acorn: 8.15.0 browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 @@ -16336,7 +16375,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.11.31)(esbuild@0.24.2)(webpack@5.99.9(@swc/core@1.11.31)(esbuild@0.24.2)) + terser-webpack-plugin: 5.3.14(@swc/core@1.12.1)(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.1)(esbuild@0.25.5)) watchpack: 2.4.4 webpack-sources: 3.3.2 transitivePeerDependencies: diff --git a/autogpt_platform/frontend/src/stories/icons.stories.tsx b/autogpt_platform/frontend/src/stories/icons.stories.tsx new file mode 100644 index 0000000000..825c782a48 --- /dev/null +++ b/autogpt_platform/frontend/src/stories/icons.stories.tsx @@ -0,0 +1,462 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Text } from "@/components/_new/Text/Text"; +import { StoryCode } from "@/stories/helpers/StoryCode"; +import { SquareArrowOutUpRight } from "lucide-react"; +import { + User, + UserPlus, + Key, + FlowArrow, + Play, + Square, + ArrowClockwise, + FloppyDisk, + ArrowCounterClockwise, + ArrowClockwise as Redo, + Cube, + Warning, + UserCircle, + Package, + Megaphone, + List, + Coin, + PencilSimple, + SignOut, + Gear, + SquaresFour, + CloudArrowUp, + MediumLogo, + YoutubeLogo, + TiktokLogo, + Globe, + Hammer, + Books, + GithubLogo, + LinkedinLogo, + FacebookLogo, + XLogo, + InstagramLogo, + ArrowLeft, + ArrowRight, + Heart, + Star, + Bell, + MagnifyingGlass, + Plus, + X, + Check, + Info, + Download, + Upload, + Calendar, + Clock, + Eye, + EyeSlash, + Copy, + Trash, + DotsThreeVertical, + Alien, +} from "@phosphor-icons/react"; + +const meta: Meta = { + title: "Design System/ Tokens /Icons", + parameters: { + layout: "fullscreen", + controls: { disable: true }, + }, +}; + +export default meta; + +// Icon categories with examples +const iconCategories = [ + { + name: "User & Authentication", + description: "Icons for user-related actions and authentication flows", + icons: [ + { component: User, name: "User", phosphorName: "User" }, + { component: UserPlus, name: "UserPlus", phosphorName: "UserPlus" }, + { component: UserCircle, name: "UserCircle", phosphorName: "UserCircle" }, + { component: Key, name: "Key", phosphorName: "Key" }, + { component: SignOut, name: "SignOut", phosphorName: "SignOut" }, + ], + }, + { + name: "Actions & Controls", + description: "Icons for common user actions and interface controls", + icons: [ + { component: Play, name: "Play", phosphorName: "Play" }, + { + component: ArrowClockwise, + name: "Refresh", + phosphorName: "ArrowClockwise", + }, + { component: FloppyDisk, name: "Save", phosphorName: "FloppyDisk" }, + { + component: ArrowCounterClockwise, + name: "Undo", + phosphorName: "ArrowCounterClockwise", + }, + { component: Redo, name: "Redo", phosphorName: "ArrowClockwise" }, + { component: PencilSimple, name: "Edit", phosphorName: "PencilSimple" }, + { component: Copy, name: "Copy", phosphorName: "Copy" }, + { component: Trash, name: "Delete", phosphorName: "Trash" }, + ], + }, + { + name: "Navigation & Layout", + description: "Icons for navigation, layout, and organizational elements", + icons: [ + { component: List, name: "Menu", phosphorName: "List" }, + { + component: SquaresFour, + name: "Dashboard", + phosphorName: "SquaresFour", + }, + { component: ArrowLeft, name: "ArrowLeft", phosphorName: "ArrowLeft" }, + { component: ArrowRight, name: "ArrowRight", phosphorName: "ArrowRight" }, + { component: Gear, name: "Settings", phosphorName: "Gear" }, + { component: Books, name: "Library", phosphorName: "Books" }, + ], + }, + { + name: "Content & Media", + description: "Icons for content types, media, and file operations", + icons: [ + { component: CloudArrowUp, name: "Upload", phosphorName: "CloudArrowUp" }, + { component: Download, name: "Download", phosphorName: "Download" }, + { component: Package, name: "Package", phosphorName: "Package" }, + { component: Cube, name: "Block", phosphorName: "Cube" }, + { component: FlowArrow, name: "Workflow", phosphorName: "FlowArrow" }, + ], + }, + { + name: "Feedback & Status", + description: "Icons for alerts, notifications, and status indicators", + icons: [ + { component: Warning, name: "Warning", phosphorName: "Warning" }, + { component: Info, name: "Info", phosphorName: "Info" }, + { component: Check, name: "Success", phosphorName: "Check" }, + { component: X, name: "Close", phosphorName: "X" }, + { component: Bell, name: "Notification", phosphorName: "Bell" }, + ], + }, + { + name: "Social & External", + description: "Icons for social media platforms and external links", + icons: [ + { component: GithubLogo, name: "GitHub", phosphorName: "GithubLogo" }, + { + component: LinkedinLogo, + name: "LinkedIn", + phosphorName: "LinkedinLogo", + }, + { component: XLogo, name: "X (Twitter)", phosphorName: "XLogo" }, + { + component: FacebookLogo, + name: "Facebook", + phosphorName: "FacebookLogo", + }, + { + component: InstagramLogo, + name: "Instagram", + phosphorName: "InstagramLogo", + }, + { component: YoutubeLogo, name: "YouTube", phosphorName: "YoutubeLogo" }, + ], + }, +]; + +export function AllVariants() { + return ( +
+ {/* Icons System Documentation */} +
+
+ + Icons System + + + Our icon system uses Phosphor Icons to provide a consistent, modern, + and comprehensive set of icons across all components. Phosphor + offers multiple weights and a cohesive design language that aligns + with our design principles. + +
+ +
+
+ + Phosphor Icons + +
+
+ + Phosphor Icons Library{" "} + + + + A flexible icon family with multiple weights and styles + +
+ @phosphor-icons/react → React components +
+
+
+ + Available Weights + + + Phosphor icons offer multiple weights - use the one specified + in your designs + +
+
regular (default), light, bold, fill, thin, duotone
+
+
+
+
+ +
+ + Usage Guidelines + +
+
+ + ✅ Always Use Phosphor Icons + +
+ + • Import from @phosphor-icons/react + + + • Always match size and weight from Figma designs + + + • Ensure icons have proper semantic meaning + + + • Verify accessibility and color contrast + +
+
+
+ + 🎨 Design Consistency + +
+ + • Follow the exact specifications from design team + + + • Maintain consistency across similar UI elements + + + • Consider accessibility requirements (minimum 16px) + +
+
+
+
+
+
+ + {/* Design Matching */} +
+
+ + Matching Design Specifications + + + When implementing icons, always reference the design specifications + provided by the design team to ensure proper sizing and weight. + +
+ +
+ + 🎨 Always Match Figma Designs + +
+ + • Check the Figma designs for exact icon sizes (16px, 20px, 24px, + etc.) + + + • Match the icon weight specified in designs (regular, bold, fill, + etc.) + + + • Ensure color and opacity match the design specifications + + + • Verify spacing and alignment with surrounding elements + +
+
+ +
+
+ + + 16px + +
+
+ + + 20px + +
+
+ + + 24px + +
+
+ + + 32px + +
+
+
+ + {/* Icon Categories */} +
+
+ + Icon Categories + + + Our curated icon set organized by functional categories. Each icon + is carefully selected to maintain consistency and semantic clarity. + +
+ + {iconCategories.map((category) => ( +
+
+ + {category.name} + + + {category.description} + +
+
+ {category.icons.map((icon) => ( +
+ + + {icon.phosphorName} + +
+ ))} +
+
+ ))} +
+ + {/* Usage Examples */} +
+
+ + Usage Examples + + + How to properly implement Phosphor icons in your React components. + +
+ + + + +// Custom sizes + // Small + // Default + // Large + // Extra large + +// With custom colors + + + +// Different weights + // 1px stroke + // 1.5px stroke + // 2px stroke (default) + // 2.5px stroke + // Filled version + // Two-tone style + +// Interactive states + + +// In buttons + + +// Responsive sizing with Tailwind +`} + /> +
+
+ ); +} + +type Story = StoryObj; From b477d316416d55388dce0f2df086d2f98c5b4fa7 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 17 Jun 2025 12:11:40 +0100 Subject: [PATCH 09/29] fix(backend): Unbreak `add_store_agent_to_library` (#10166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Follow-up fix for #9786 A change to a DB statement introduced in #9786 turns out to be breaking. Apparently `connect` can't just be used for *some* relations: if it is used, it must be used for *all* relations created by the statement. ### Changes 🏗️ - Fix broken DB statement in `add_store_agent_to_library(..)` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Add store agent to library Co-authored-by: Swifty --- .../backend/backend/server/v2/library/db.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/autogpt_platform/backend/backend/server/v2/library/db.py b/autogpt_platform/backend/backend/server/v2/library/db.py index 15d479e588..b0f8f7a755 100644 --- a/autogpt_platform/backend/backend/server/v2/library/db.py +++ b/autogpt_platform/backend/backend/server/v2/library/db.py @@ -466,15 +466,15 @@ async def add_store_agent_to_library( # Create LibraryAgent entry added_agent = await prisma.models.LibraryAgent.prisma().create( - data=prisma.types.LibraryAgentCreateInput( - userId=user_id, - AgentGraph={ + data={ + "User": {"connect": {"id": user_id}}, + "AgentGraph": { "connect": { "graphVersionId": {"id": graph.id, "version": graph.version} } }, - isCreatedByUser=False, - ), + "isCreatedByUser": False, + }, include=library_agent_include(user_id), ) logger.debug( From 86361fc1ae8ec08cecbeccfff5b15f04c3a7449a Mon Sep 17 00:00:00 2001 From: Ubbe Date: Tue, 17 Jun 2025 18:29:21 +0400 Subject: [PATCH 10/29] fix(frontend): fix all lint errors and add next/typescript (#10182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ ### ESLint Config 1. **Disabled `react-hooks/exhaustive-deps`:** - to prevent unnecessary dependency proliferation and rely on code review instead 2. **Added [`next/typescript`](https://nextjs.org/docs/app/api-reference/config/eslint#with-typescript):** - to the ESLint config to make sure we also have TS linting rules 3. **Added custom rule for `@typescript-eslint/no-unused-vars`:** - to allow underscore-prefixed variables (convention for intentionally unused), in some cases helpful From now on, whenever we have unused variables or imports, the `lint` CI will fail 🔴 , thanks to `next/typescript` that adds `typescript-eslint/no-unused-vars` 💆🏽 ### Minor Fixes - Replaced empty interfaces with type aliases to resolve `@typescript-eslint/no-empty-object-type` warnings - Fixed unsafe non-null assertions with proper null checks - Removed `@ts-ignore` comments in favour of proper type casting ( _when possible_ 🙏🏽 ) ### Google Analytics Component - Changed Next.js Script strategy from `beforeInteractive` to `afterInteractive` to resolve Next.js warnings - this make sure loading analytics does not block page render 🙏🏽 ( _better page load time_ ) ### Are these changes safe? As long as the Typescript compiler does not complain ( check the `type-check` job ) we should be save. Most changes are removing unused code, if that code would be used somewhere else the compiler should catch it and tell us 🫶 I also typed some code when possible, or bypassed the linter when I thought it was fair for now. I disabled a couple ESLint rules. Most importantly the `no-explicity-any` one as we have loads of stuff untyped yet ( _this should be improved once API types are generated for us_ ). ### DX Added some settings on `.vscode` folder 📁 so that files will be formatted on save and also ESLint will fix errors on save when able 💯 ### 📈 **Result:** - ✅ All linting errors resolved - ✅ Improved TypeScript strict mode compliance - ✅ Better developer experience with cleaner code ## Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Lint CI job passes - [x] There is not type errors ( _TS will catch issue related to these changes_ ) --- autogpt_platform/frontend/.eslintrc.json | 24 +++++- autogpt_platform/frontend/.prettierignore | 1 + .../onboarding/3-services/page.tsx | 1 - .../app/(no-navbar)/onboarding/5-run/page.tsx | 1 + .../src/app/(platform)/admin/layout.tsx | 3 +- .../(platform)/admin/marketplace/actions.ts | 2 - .../app/(platform)/admin/spending/actions.ts | 1 + .../src/app/(platform)/auth/callback/route.ts | 2 +- .../auth/integrations/oauth_callback/route.ts | 2 +- .../src/app/(platform)/build/actions.ts | 1 - .../marketplace/creator/[creator]/page.tsx | 2 +- .../(platform)/marketplace/search/page.tsx | 5 +- .../profile/(user)/dashboard/page.tsx | 2 +- autogpt_platform/frontend/src/app/layout.tsx | 2 +- .../src/components/ConnectionLine.tsx | 4 +- .../frontend/src/components/CustomEdge.tsx | 6 +- .../frontend/src/components/CustomNode.tsx | 16 +--- .../frontend/src/components/DataTable.tsx | 7 +- .../frontend/src/components/Flow.tsx | 9 +-- .../src/components/OttoChatWidget.tsx | 2 +- .../frontend/src/components/SchemaTooltip.tsx | 2 +- .../src/components/_new/Text/Text.stories.tsx | 2 +- .../admin/marketplace/expandable-row.tsx | 4 +- .../admin/marketplace/search-filter-form.tsx | 1 - .../admin/spending/add-money-button.tsx | 3 - .../admin-grant-history-data-table.tsx | 2 +- .../admin/spending/search-filter-form.tsx | 3 +- .../components/agptui/AgentTable.stories.tsx | 3 +- .../src/components/agptui/AgentTable.tsx | 2 +- .../agptui/AgentTableCard.stories.tsx | 2 +- .../src/components/agptui/AgentTableCard.tsx | 1 - .../src/components/agptui/BreadCrumbs.tsx | 1 - .../agptui/FeaturedStoreCard.stories.tsx | 4 +- .../components/agptui/FilterChips.stories.tsx | 6 +- .../src/components/agptui/Navbar.stories.tsx | 26 ------ .../agptui/ProfilePopoutMenuLogoutButton.tsx | 10 +-- .../components/agptui/StoreCard.stories.tsx | 2 +- .../src/components/agptui/ThemeToggle.tsx | 1 - .../src/components/agptui/WalletRefill.tsx | 1 - .../agptui/composite/APIKeySection.tsx | 6 +- .../composite/FeaturedSection.stories.tsx | 11 ++- .../agptui/composite/FeaturedSection.tsx | 2 +- .../agptui/composite/HeroSection.tsx | 1 - .../composite/PublishAgentPopout.stories.tsx | 2 +- .../agptui/composite/PublishAgentPopout.tsx | 4 +- .../components/analytics/google-analytics.tsx | 19 ++--- .../src/components/auth/AuthFeedback.tsx | 2 +- .../src/components/auth/GoogleOAuthButton.tsx | 1 - .../src/components/auth/Turnstile.tsx | 4 +- .../frontend/src/components/cronScheduler.tsx | 7 +- .../library/library-action-header.tsx | 2 +- .../library/library-notification-card.tsx | 2 +- .../components/library/library-sort-menu.tsx | 2 +- .../src/components/monitor/FlowInfo.tsx | 4 +- .../components/monitor/FlowRunsTimeline.tsx | 2 +- .../src/components/node-input-components.tsx | 23 +----- .../onboarding/onboarding-provider.tsx | 1 - .../profile/settings/SettingsForm.tsx | 5 +- .../components/runner-ui/RunnerOutputUI.tsx | 2 +- .../src/components/type-based-input.tsx | 22 +++--- .../src/components/ui/button.stories.tsx | 1 - .../frontend/src/components/ui/command.tsx | 4 +- .../frontend/src/components/ui/icons.tsx | 3 +- .../frontend/src/components/ui/input.tsx | 3 +- .../src/components/ui/multiselect.stories.tsx | 8 +- .../src/components/ui/popover.stories.tsx | 2 +- .../src/components/ui/render.stories.tsx | 1 - .../frontend/src/components/ui/render.tsx | 1 - .../frontend/src/components/ui/textarea.tsx | 3 +- .../frontend/src/components/ui/use-toast.tsx | 14 ++-- .../frontend/src/hooks/useAgentGraph.tsx | 39 ++++----- .../src/lib/autogpt-server-api/client.ts | 8 +- .../src/lib/autogpt-server-api/types.ts | 1 + .../src/lib/supabase/hooks/useSupabase.ts | 6 +- .../frontend/src/lib/supabase/middleware.ts | 10 +-- .../lib/supabase/server/getServerSupabase.ts | 8 +- .../src/lib/supabase/server/getServerUser.ts | 1 - autogpt_platform/frontend/src/lib/utils.ts | 2 +- .../frontend/src/stories/Spacing.stories.tsx | 4 +- .../src/stories/Typography.stories.tsx | 2 +- .../src/stories/border-radius.stories.tsx | 4 +- .../frontend/src/stories/icons.stories.tsx | 79 +++++++------------ .../frontend/src/tests/build.spec.ts | 2 +- .../src/tests/fixtures/test-user.fixture.ts | 36 --------- .../frontend/src/tests/monitor.spec.ts | 6 +- .../frontend/src/tests/pages/build.page.ts | 12 +-- .../frontend/src/tests/pages/monitor.page.ts | 8 +- .../frontend/src/tests/profile.spec.ts | 7 +- .../frontend/src/tests/util.spec.ts | 1 + 89 files changed, 220 insertions(+), 349 deletions(-) diff --git a/autogpt_platform/frontend/.eslintrc.json b/autogpt_platform/frontend/.eslintrc.json index bb8b1c099d..f5bc19d00b 100644 --- a/autogpt_platform/frontend/.eslintrc.json +++ b/autogpt_platform/frontend/.eslintrc.json @@ -1,3 +1,25 @@ { - "extends": ["next/core-web-vitals", "plugin:storybook/recommended"] + "extends": [ + "next/core-web-vitals", + "next/typescript", + "plugin:storybook/recommended" + ], + "rules": { + // Disabling exhaustive-deps to avoid forcing unnecessary dependencies and useCallback proliferation. + // We rely on code review for proper dependency management instead of mechanical rule following. + // See: https://kentcdodds.com/blog/usememo-and-usecallback + "react-hooks/exhaustive-deps": "off", + // Disable temporarily as we have some `any` in the codebase and we need to got case by case + // and see if they can be fixed. + "@typescript-eslint/no-explicit-any": "off", + // Allow unused vars that start with underscore (convention for intentionally unused) + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] + } } diff --git a/autogpt_platform/frontend/.prettierignore b/autogpt_platform/frontend/.prettierignore index 2f038d1278..9951e9905d 100644 --- a/autogpt_platform/frontend/.prettierignore +++ b/autogpt_platform/frontend/.prettierignore @@ -1,5 +1,6 @@ node_modules pnpm-lock.yaml .next +.auth build public diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/3-services/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/3-services/page.tsx index 92623301d8..a7867aca50 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/3-services/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/3-services/page.tsx @@ -9,7 +9,6 @@ import { OnboardingText } from "@/components/onboarding/OnboardingText"; import { OnboardingGrid } from "@/components/onboarding/OnboardingGrid"; import { useCallback } from "react"; import OnboardingInput from "@/components/onboarding/OnboardingInput"; -import { isEmptyOrWhitespace } from "@/lib/utils"; import { useOnboarding } from "@/components/onboarding/onboarding-provider"; const services = [ diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/page.tsx index 5ea5e3adf2..7769e0c48a 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/5-run/page.tsx @@ -49,6 +49,7 @@ export default function Page() { .getAgentMetaByStoreListingVersionId(state?.selectedStoreListingVersionId) .then((agent) => { setAgent(agent); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const update: { [key: string]: any } = {}; // Set default values from schema Object.entries(agent.input_schema.properties).forEach( diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx index cb72daee73..bfb2b2695d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/admin/layout.tsx @@ -1,6 +1,5 @@ -import { ShoppingBag } from "lucide-react"; import { Sidebar } from "@/components/agptui/Sidebar"; -import { Users, DollarSign, LogOut } from "lucide-react"; +import { Users, DollarSign } from "lucide-react"; import { IconSliders } from "@/components/ui/icons"; diff --git a/autogpt_platform/frontend/src/app/(platform)/admin/marketplace/actions.ts b/autogpt_platform/frontend/src/app/(platform)/admin/marketplace/actions.ts index ad7f48f24c..480e79e669 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/marketplace/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/admin/marketplace/actions.ts @@ -3,9 +3,7 @@ import { revalidatePath } from "next/cache"; import BackendApi from "@/lib/autogpt-server-api"; import { - NotificationPreferenceDTO, StoreListingsWithVersionsResponse, - StoreSubmissionsResponse, SubmissionStatus, } from "@/lib/autogpt-server-api/types"; 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 b6ef75a599..ea4a194acd 100644 --- a/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/admin/spending/actions.ts @@ -29,6 +29,7 @@ export async function getUsersTransactionHistory( search?: string, transactionType?: CreditTransactionType, ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const data: Record = { page, page_size: pageSize, diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts index e0a991138e..62e324761e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts @@ -38,7 +38,7 @@ export async function GET(request: Request) { return NextResponse.redirect(`${origin}/error`); } - const { data, error } = await supabase.auth.exchangeCodeForSession(code); + const { error } = await supabase.auth.exchangeCodeForSession(code); // data.session?.refresh_token is available if you need to store it for later use if (!error) { try { diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/integrations/oauth_callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/integrations/oauth_callback/route.ts index 5d4100d48e..18e74369df 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/integrations/oauth_callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/integrations/oauth_callback/route.ts @@ -5,7 +5,7 @@ import { NextResponse } from "next/server"; // controlled by the CredentialsInput component. The CredentialsInput opens the login // page in a pop-up window, which then redirects to this route to close the loop. export async function GET(request: Request) { - const { searchParams, origin } = new URL(request.url); + const { searchParams } = new URL(request.url); const code = searchParams.get("code"); const state = searchParams.get("state"); diff --git a/autogpt_platform/frontend/src/app/(platform)/build/actions.ts b/autogpt_platform/frontend/src/app/(platform)/build/actions.ts index 16c577d16c..7907d919f4 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/actions.ts @@ -1,6 +1,5 @@ "use server"; -import { revalidatePath } from "next/cache"; import BackendAPI from "@/lib/autogpt-server-api/client"; import { OttoQuery, OttoResponse } from "@/lib/autogpt-server-api/types"; 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 b462cdb704..484022f151 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 @@ -92,7 +92,7 @@ export default async function Page({
); - } catch (error) { + } catch { return (
Creator not found
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx index 88f620f6ab..d9209b2c4a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx @@ -8,6 +8,7 @@ import { Separator } from "@/components/ui/separator"; import { SearchFilterChips } from "@/components/agptui/SearchFilterChips"; import { SortDropdown } from "@/components/agptui/SortDropdown"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import { Creator, StoreAgent } from "@/lib/autogpt-server-api"; type MarketplaceSearchPageSearchParams = { searchTerm?: string; sort?: string }; @@ -33,8 +34,8 @@ function SearchResults({ }): React.ReactElement { const [showAgents, setShowAgents] = useState(true); const [showCreators, setShowCreators] = useState(true); - const [agents, setAgents] = useState([]); - const [creators, setCreators] = useState([]); + const [agents, setAgents] = useState([]); + const [creators, setCreators] = useState([]); const [isLoading, setIsLoading] = useState(true); const api = useBackendAPI(); diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx index e2cface85c..5c86d99f21 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx @@ -14,7 +14,7 @@ import { import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -export default function Page({}: {}) { +export default function Page() { const { supabase } = useSupabase(); const api = useBackendAPI(); const [submissions, setSubmissions] = useState(); diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index 9df9c1b671..df8887eff5 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from "react"; +import React from "react"; import type { Metadata } from "next"; import { fonts } from "@/components/styles/fonts"; diff --git a/autogpt_platform/frontend/src/components/ConnectionLine.tsx b/autogpt_platform/frontend/src/components/ConnectionLine.tsx index 2111a7b253..0a790aedd4 100644 --- a/autogpt_platform/frontend/src/components/ConnectionLine.tsx +++ b/autogpt_platform/frontend/src/components/ConnectionLine.tsx @@ -17,8 +17,8 @@ export default function ConnectionLine({ }: ConnectionLineComponentProps) { const sourceX = fromPosition === Position.Right - ? fromX + (fromHandle?.width! / 2 - 5) - : fromX - (fromHandle?.width! / 2 - 5); + ? fromX + ((fromHandle?.width ?? 0) / 2 - 5) + : fromX - ((fromHandle?.width ?? 0) / 2 - 5); const [path] = getBezierPath({ sourceX: sourceX, diff --git a/autogpt_platform/frontend/src/components/CustomEdge.tsx b/autogpt_platform/frontend/src/components/CustomEdge.tsx index d7ab076481..ecc9249fb2 100644 --- a/autogpt_platform/frontend/src/components/CustomEdge.tsx +++ b/autogpt_platform/frontend/src/components/CustomEdge.tsx @@ -93,7 +93,7 @@ export function CustomEdge({ return; } - const beadUp = data?.beadUp!; + const beadUp: number = data?.beadUp ?? 0; // Add beads setBeads(({ beads, created, destroyed }) => { @@ -114,7 +114,7 @@ export function CustomEdge({ const newBeads = beads .map((bead) => ({ ...bead })) .filter((bead, index) => { - const beadDown = data?.beadDown!; + const beadDown: number = data?.beadDown ?? 0; const removeCount = beadDown - destroyed; if (bead.t >= bead.targetT && index < removeCount) { destroyedCount++; @@ -151,7 +151,7 @@ export function CustomEdge({ }; }) .filter((bead, index) => { - const beadDown = data?.beadDown!; + const beadDown: number = data?.beadDown ?? 0; const removeCount = beadDown - destroyed; if (bead.t >= bead.targetT && index < removeCount) { destroyedCount++; diff --git a/autogpt_platform/frontend/src/components/CustomNode.tsx b/autogpt_platform/frontend/src/components/CustomNode.tsx index 52ed10a867..aa87ba30e2 100644 --- a/autogpt_platform/frontend/src/components/CustomNode.tsx +++ b/autogpt_platform/frontend/src/components/CustomNode.tsx @@ -95,13 +95,7 @@ export type CustomNodeData = { export type CustomNode = XYNode; export const CustomNode = React.memo( - function CustomNode({ - data, - id, - width, - height, - selected, - }: NodeProps) { + function CustomNode({ data, id, height, selected }: NodeProps) { const [isOutputOpen, setIsOutputOpen] = useState( data.isOutputOpen || false, ); @@ -199,10 +193,6 @@ export const CustomNode = React.memo( [id, updateNodeData], ); - const toggleOutput = (checked: boolean) => { - setIsOutputOpen(checked); - }; - const toggleAdvancedSettings = (checked: boolean) => { setIsAdvancedOpen(checked); }; @@ -256,7 +246,7 @@ export const CustomNode = React.memo( nodeType: BlockUIType, ) => { if (!schema?.properties) return null; - let keys = Object.entries(schema.properties); + const keys = Object.entries(schema.properties); switch (nodeType) { case BlockUIType.NOTE: // For NOTE blocks, don't render any input handles @@ -443,7 +433,7 @@ export const CustomNode = React.memo( // For primitive values, use the original string handleInputChange(activeKey, value); } - } catch (error) { + } catch { // If JSON parsing fails, treat as plain text handleInputChange(activeKey, value); } diff --git a/autogpt_platform/frontend/src/components/DataTable.tsx b/autogpt_platform/frontend/src/components/DataTable.tsx index aba62af6c2..b7bb83c80a 100644 --- a/autogpt_platform/frontend/src/components/DataTable.tsx +++ b/autogpt_platform/frontend/src/components/DataTable.tsx @@ -1,6 +1,8 @@ -import React from "react"; import { beautifyString } from "@/lib/utils"; +import { Clipboard } from "lucide-react"; +import React from "react"; import { Button } from "./ui/button"; +import { ContentRenderer } from "./ui/render"; import { Table, TableBody, @@ -9,9 +11,7 @@ import { TableHeader, TableRow, } from "./ui/table"; -import { Clipboard } from "lucide-react"; import { useToast } from "./ui/use-toast"; -import { ContentRenderer } from "./ui/render"; type DataTableProps = { title?: string; @@ -25,7 +25,6 @@ export default function DataTable({ data, }: DataTableProps) { const { toast } = useToast(); - const maxChars = 100; const copyData = (pin: string, data: string) => { navigator.clipboard.writeText(data).then(() => { diff --git a/autogpt_platform/frontend/src/components/Flow.tsx b/autogpt_platform/frontend/src/components/Flow.tsx index ece3239580..345fc60b5a 100644 --- a/autogpt_platform/frontend/src/components/Flow.tsx +++ b/autogpt_platform/frontend/src/components/Flow.tsx @@ -90,9 +90,7 @@ const FlowEditor: React.FC<{ } = useReactFlow(); const [nodeId, setNodeId] = useState(1); const [isAnyModalOpen, setIsAnyModalOpen] = useState(false); - const [visualizeBeads, setVisualizeBeads] = useState< - "no" | "static" | "animate" - >("animate"); + const [visualizeBeads] = useState<"no" | "static" | "animate">("animate"); const [flowExecutionID, setFlowExecutionID] = useState< GraphExecutionID | undefined >(); @@ -366,10 +364,7 @@ const FlowEditor: React.FC<{ replaceEdges = edgeChanges.filter( (change) => change.type === "replace", ), - removedEdges = edgeChanges.filter((change) => change.type === "remove"), - selectedEdges = edgeChanges.filter( - (change) => change.type === "select", - ); + removedEdges = edgeChanges.filter((change) => change.type === "remove"); if (addedEdges.length > 0 || removedEdges.length > 0) { setNodes((nds) => { diff --git a/autogpt_platform/frontend/src/components/OttoChatWidget.tsx b/autogpt_platform/frontend/src/components/OttoChatWidget.tsx index 3e111165a8..3eaf75951d 100644 --- a/autogpt_platform/frontend/src/components/OttoChatWidget.tsx +++ b/autogpt_platform/frontend/src/components/OttoChatWidget.tsx @@ -212,7 +212,7 @@ export default function OttoChatWidget({

{children}

), code(props) { - const { children, className, node, ...rest } = props; + const { children, className, node: _, ...rest } = props; const match = /language-(\w+)/.exec(className || ""); return match ? (
diff --git a/autogpt_platform/frontend/src/components/SchemaTooltip.tsx b/autogpt_platform/frontend/src/components/SchemaTooltip.tsx
index 313b55cb6b..b4010287bc 100644
--- a/autogpt_platform/frontend/src/components/SchemaTooltip.tsx
+++ b/autogpt_platform/frontend/src/components/SchemaTooltip.tsx
@@ -22,7 +22,7 @@ const SchemaTooltip: React.FC<{ description?: string }> = ({ description }) => {
         
            (
+              a: ({ node: _, ...props }) => (
                  = {
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 bc42c43686..569bdc7eaf 100644
--- a/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx
+++ b/autogpt_platform/frontend/src/components/admin/marketplace/expandable-row.tsx
@@ -9,9 +9,8 @@ import {
   TableHead,
   TableBody,
 } from "@/components/ui/table";
-import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
-import { ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
+import { ChevronDown, ChevronRight } from "lucide-react";
 import { formatDistanceToNow } from "date-fns";
 import {
   type StoreListingWithVersions,
@@ -19,7 +18,6 @@ import {
   SubmissionStatus,
 } from "@/lib/autogpt-server-api/types";
 import { ApproveRejectButtons } from "./approve-reject-buttons";
-import { downloadAsAdmin } from "@/app/(platform)/admin/marketplace/actions";
 import { DownloadAgentAdminButton } from "./download-agent-button";
 
 // Moved the getStatusBadge function into the client component
diff --git a/autogpt_platform/frontend/src/components/admin/marketplace/search-filter-form.tsx b/autogpt_platform/frontend/src/components/admin/marketplace/search-filter-form.tsx
index 4d406fab93..73da6da431 100644
--- a/autogpt_platform/frontend/src/components/admin/marketplace/search-filter-form.tsx
+++ b/autogpt_platform/frontend/src/components/admin/marketplace/search-filter-form.tsx
@@ -15,7 +15,6 @@ import {
 import { SubmissionStatus } from "@/lib/autogpt-server-api/types";
 
 export function SearchAndFilterAdminMarketplace({
-  initialStatus,
   initialSearch,
 }: {
   initialStatus?: SubmissionStatus;
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 2f1083eb7d..ee84464df0 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,7 +15,6 @@ 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 useCredits from "@/hooks/useCredits";
 
 export function AdminAddMoneyButton({
   userId,
@@ -36,8 +35,6 @@ export function AdminAddMoneyButton({
     defaultAmount ? Math.abs(defaultAmount / 100).toFixed(2) : "1.00",
   );
 
-  const { formatCredits } = useCredits();
-
   const handleApproveSubmit = async (formData: FormData) => {
     setIsAddMoneyDialogOpen(false);
     try {
diff --git a/autogpt_platform/frontend/src/components/admin/spending/admin-grant-history-data-table.tsx b/autogpt_platform/frontend/src/components/admin/spending/admin-grant-history-data-table.tsx
index 818d385d04..80695b803c 100644
--- a/autogpt_platform/frontend/src/components/admin/spending/admin-grant-history-data-table.tsx
+++ b/autogpt_platform/frontend/src/components/admin/spending/admin-grant-history-data-table.tsx
@@ -48,7 +48,7 @@ export async function AdminUserGrantHistory({
     const isPurchased = type === CreditTransactionType.TOP_UP;
     const isSpent = type === CreditTransactionType.USAGE;
 
-    let displayText = type;
+    const displayText = type;
     let bgColor = "";
 
     if (isGrant) {
diff --git a/autogpt_platform/frontend/src/components/admin/spending/search-filter-form.tsx b/autogpt_platform/frontend/src/components/admin/spending/search-filter-form.tsx
index 5e0160cbdd..f301461071 100644
--- a/autogpt_platform/frontend/src/components/admin/spending/search-filter-form.tsx
+++ b/autogpt_platform/frontend/src/components/admin/spending/search-filter-form.tsx
@@ -15,7 +15,6 @@ import {
 } from "@/components/ui/select";
 
 export function SearchAndFilterAdminSpending({
-  initialStatus,
   initialSearch,
 }: {
   initialStatus?: CreditTransactionType;
@@ -74,7 +73,7 @@ export function SearchAndFilterAdminSpending({