From f109c3e019aea89aca634797f8ed94b49d3e0239 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 24 Sep 2024 18:26:15 +0200 Subject: [PATCH 01/32] ci: Set up customized CodeQL scanning (#8106) This replaces the dynamically configured CodeQL scanning that we had set up through the GitHub settings. --- .github/workflows/codeql.yml | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..31848f3bbb --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,97 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master", "release-*" ] + pull_request: + branches: [ "master", "release-*" ] + schedule: + - cron: '15 4 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + config: | + paths-ignore: + - classic/frontend/build/** + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From b5dd75fad29d29d403c166eed56de292cd1da445 Mon Sep 17 00:00:00 2001 From: Toran Bruce Richards Date: Tue, 24 Sep 2024 20:03:41 +0100 Subject: [PATCH 02/32] Update README with new Tutorial.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9393a5ca06..db3c4ba02f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## How to Get Started -https://github.com/user-attachments/assets/8508f4dc-b362-4cab-900f-644964a96cdf +https://github.com/user-attachments/assets/d04273a5-b36a-4a37-818e-f631ce72d603 ### 🧱 AutoGPT Builder From 769058a8c9ef5499b072dafe27acb21b44086276 Mon Sep 17 00:00:00 2001 From: Kaitlyn Barnard Date: Tue, 24 Sep 2024 12:24:21 -0700 Subject: [PATCH 03/32] Update README.md (#8150) Edits based on AutoGPT Platform launch Co-authored-by: Toran Bruce Richards --- README.md | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index db3c4ba02f..b28bcf9182 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,63 @@ -# AutoGPT: Build & Use AI Agents +# AutoGPT: Build, Deploy, and Run AI Agents [![Discord Follow](https://dcbadge.vercel.app/api/server/autogpt?style=flat)](https://discord.gg/autogpt)   [![Twitter Follow](https://img.shields.io/twitter/follow/Auto_GPT?style=social)](https://twitter.com/Auto_GPT)   [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**AutoGPT** is a powerful tool that lets you create and run intelligent agents. These agents can perform various tasks automatically, making your life easier. +**AutoGPT** is a powerful platform that allows you to create, deploy, and manage continuous AI agents that automate complex workflows. + +## Hosting Options + - Download to self-host + - [Join the Waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta ## How to Get Started https://github.com/user-attachments/assets/d04273a5-b36a-4a37-818e-f631ce72d603 -### 🧱 AutoGPT Builder +### 🧱 AutoGPT Frontend -The AutoGPT Builder is the frontend. It allows you to design agents using an easy flowchart style. You build your agent by connecting blocks, where each block performs a single action. It's simple and intuitive! +The AutoGPT frontend is where users interact with our powerful AI automation platform. It offers multiple ways to engage with and leverage our AI agents. This is the interface where you'll bring your AI automation ideas to life: + + **Agent Builder:** For those who want to customize, our intuitive, low-code interface allows you to design and configure your own AI agents. + + **Workflow Management:** Build, modify, and optimize your automation workflows with ease. You build your agent by connecting blocks, where each block performs a single action. + + **Deployment Controls:** Manage the lifecycle of your agents, from testing to production. + + **Ready-to-Use Agents:** Don't want to build? Simply select from our library of pre-configured agents and put them to work immediately. + + **Agent Interaction:** Whether you've built your own or are using pre-configured agents, easily run and interact with them through our user-friendly interface. + + **Monitoring and Analytics:** Keep track of your agents' performance and gain insights to continually improve your automation processes. [Read this guide](https://docs.agpt.co/server/new_blocks/) to learn how to build your own custom blocks. ### 💽 AutoGPT Server -The AutoGPT Server is the backend. This is where your agents run. Once deployed, agents can be triggered by external sources and can operate continuously. +The AutoGPT Server is the powerhouse of our platform This is where your agents run. Once deployed, agents can be triggered by external sources and can operate continuously. It contains all the essential components that make AutoGPT run smoothly. + + **Source Code:** The core logic that drives our agents and automation processes. + + **Infrastructure:** Robust systems that ensure reliable and scalable performance. + + **Marketplace:** A comprehensive marketplace where you can find and deploy a wide range of pre-built agents. ### 🐙 Example Agents Here are two examples of what you can do with AutoGPT: -1. **Reddit Marketing Agent** - - This agent reads comments on Reddit. - - It looks for people asking about your product. - - It then automatically responds to them. +1. **Generate Viral Videos from Trending Topics** + - This agent reads topics on Reddit. + - It identifies trending topics. + - It then automatically creates a short-form video based on the content. -2. **YouTube Content Repurposing Agent** +2. **Identify Top Quotes from Videos for Social Media** - This agent subscribes to your YouTube channel. - When you post a new video, it transcribes it. - - It uses AI to write a search engine optimized blog post. - - Then, it publishes this blog post to your Medium account. + - It uses AI to identify the most impactful quotes to generate a summary. + - Then, it writes a post to automatically publish to your social media. -These examples show just a glimpse of what you can achieve with AutoGPT! +These examples show just a glimpse of what you can achieve with AutoGPT! You can create customized workflows to build agents for any use case. --- Our mission is to provide the tools, so that you can focus on what matters: From e04beffe62757995ce045d3112567b470e1b31c3 Mon Sep 17 00:00:00 2001 From: Toran Bruce Richards Date: Tue, 24 Sep 2024 20:55:24 +0100 Subject: [PATCH 04/32] Update README.md (#8153) * Update README.md * Update README.md * Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b28bcf9182..c6c603988e 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,15 @@ - Download to self-host - [Join the Waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta -## How to Get Started +## How to Setup for Self-Hosting +> [!NOTE] +> Setting up and hosting the AutoGPT Platform yourself is a technical process. +> If you'd rather something that just works, we recommend [joining the waitlist](https://bit.ly/3ZDijAI) for the cloud-hosted beta. https://github.com/user-attachments/assets/d04273a5-b36a-4a37-818e-f631ce72d603 +This tutorial assumes you have Docker, VSCode, git and npm installed. + ### 🧱 AutoGPT Frontend The AutoGPT frontend is where users interact with our powerful AI automation platform. It offers multiple ways to engage with and leverage our AI agents. This is the interface where you'll bring your AI automation ideas to life: From 00b8d219f2d85c1d12cd3cc1cfe0f5f431d7c19d Mon Sep 17 00:00:00 2001 From: Swifty Date: Tue, 24 Sep 2024 22:01:06 +0200 Subject: [PATCH 05/32] fix(frontend): Remove Sentry Pop-up and add run options (#8138) * modify sentry setup * Update sentry.client.config.ts * remove env vars from dev so it will work on windows still --- autogpt_platform/frontend/package.json | 3 ++- autogpt_platform/frontend/sentry.client.config.ts | 10 +--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 5f9b5cad68..d37c0977b9 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "export NODE_ENV=development && next dev", + "dev": "next dev", + "dev:nosentry": "export NODE_ENV=development && export DISABLE_SENTRY=true && next dev", "dev:test": "export NODE_ENV=test && next dev", "build": "next build", "start": "next start", diff --git a/autogpt_platform/frontend/sentry.client.config.ts b/autogpt_platform/frontend/sentry.client.config.ts index f37d5cda23..bbfe73ea51 100644 --- a/autogpt_platform/frontend/sentry.client.config.ts +++ b/autogpt_platform/frontend/sentry.client.config.ts @@ -7,7 +7,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: "https://fe4e4aa4a283391808a5da396da20159@o4505260022104064.ingest.us.sentry.io/4507946746380288", - enabled: process.env.NODE_ENV !== "development", + enabled: process.env.DISABLE_SENTRY !== "true", // Add optional integrations for additional features integrations: [ @@ -31,14 +31,6 @@ Sentry.init({ /^https:\/\/dev\-builder\.agpt\.co\/api/, ], - beforeSend(event, hint) { - // Check if it is an exception, and if so, show the report dialog - if (event.exception && event.event_id) { - Sentry.showReportDialog({ eventId: event.event_id }); - } - return event; - }, - // Define how likely Replay events are sampled. // This sets the sample rate to be 10%. You may want this to be 100% while // in development and sample at a lower rate in production From 591a2bc43191a02f72a9ec01f1c962bdf123c3d7 Mon Sep 17 00:00:00 2001 From: Toran Bruce Richards Date: Tue, 24 Sep 2024 23:11:57 +0100 Subject: [PATCH 06/32] Add files via upload --- .../Contributor License Agreement (CLA).md | 21 +++ autogpt_platform/LICENCE.txt | 164 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 autogpt_platform/Contributor License Agreement (CLA).md create mode 100644 autogpt_platform/LICENCE.txt diff --git a/autogpt_platform/Contributor License Agreement (CLA).md b/autogpt_platform/Contributor License Agreement (CLA).md new file mode 100644 index 0000000000..b15bd52e56 --- /dev/null +++ b/autogpt_platform/Contributor License Agreement (CLA).md @@ -0,0 +1,21 @@ +**Determinist Ltd** + +**Contributor License Agreement (“Agreement”)** + +Thank you for your interest in the AutoGPT open source project at [https://github.com/Significant-Gravitas/AutoGPT](https://github.com/Significant-Gravitas/AutoGPT) stewarded by Determinist Ltd (“**Determinist**”), with offices at 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom. The form of license below is a document that clarifies the terms under which You, the person listed below, may contribute software code described below (the “**Contribution**”) to the project. We appreciate your participation in our project, and your help in improving our products, so we want you to understand what will be done with the Contributions. This license is for your protection as well as the protection of Determinist and its licensees; it does not change your rights to use your own Contributions for any other purpose. + +By submitting a Pull Request which modifies the content of the “autogpt\_platform” folder at [https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt\_platform](https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform), You hereby agree: + +1\. **You grant us the ability to use the Contributions in any way**. You hereby grant to Determinist a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: (a) open source licenses like the GNU General Public License (GPL), the GNU Lesser General Public License (LGPL), the Common Public License, or the Berkeley Science Division license (BSD); and (b) binary, proprietary, or commercial licenses. + +2\. **Grant of Patent License**. You hereby grant to Determinist a worldwide, non-exclusive, royalty-free, irrevocable, license, under any rights you may have, now or in the future, in any patents or patent applications, to make, have made, use, offer to sell, sell, and import products containing the Contribution or portions of the Contribution. This license extends to patent claims that are infringed by the Contribution alone or by combination of the Contribution with other inventions. + +4\. **Limitations on Licenses**. The licenses granted in this Agreement will continue for the duration of the applicable patent or intellectual property right under which such license is granted. The licenses granted in this Agreement will include the right to grant and authorize sublicenses, so long as the sublicenses are within the scope of the licenses granted in this Agreement. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution. + +5\. **You are able to grant us these rights**. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You are authorized to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions. + +3\. **The Contributions are your original work**. You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license. For example, if you have signed an agreement requiring you to assign the intellectual property rights in the Contributions to an employer or customer, that would conflict with the terms of this license. + +6\. **We determine the code that is in our products**. You understand that the decision to include the Contribution in any product or source repository is entirely that of Determinist, and this agreement does not guarantee that the Contributions will be included in any product. + +7\. **No Implied Warranties.** Determinist acknowledges that, except as explicitly described in this Agreement, the Contribution is provided on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. diff --git a/autogpt_platform/LICENCE.txt b/autogpt_platform/LICENCE.txt new file mode 100644 index 0000000000..3f09b052c0 --- /dev/null +++ b/autogpt_platform/LICENCE.txt @@ -0,0 +1,164 @@ +# PolyForm Shield License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncompete + +Any purpose is a permitted purpose, except for providing any +product that competes with the software or any product the +licensor or any of its affiliates provides using the software. + +## Competition + +Goods and services compete even when they provide functionality +through different kinds of interfaces or for different technical +platforms. Applications can compete with services, libraries +with plugins, frameworks with development tools, and so on, +even if they're written in different programming languages +or for different computer architectures. Goods and services +compete even when provided free of charge. If you market a +product as a practical substitute for the software or another +product, it definitely competes. + +## New Products + +If you are using the software to provide a product that does +not compete, but the licensor or any of its affiliates brings +your product into competition by providing a new version of +the software or another product using the software, you may +continue using versions of the software available under these +terms beforehand to provide your competing product, but not +any later versions. + +## Discontinued Products + +You may begin using the software to compete with a product +or service that the licensor or any of its affiliates has +stopped providing, unless the licensor includes a plain-text +line beginning with `Licensor Line of Business:` with the +software that mentions that line of business. For example: + +> Licensor Line of Business: YoyodyneCMS Content Management +System (http://example.com/cms) + +## Sales of Business + +If the licensor or any of its affiliates sells a line of +business developing the software or using the software +to provide a product, the buyer can also enforce +[Noncompete](#noncompete) for that product. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +A **product** can be a good or service, or a combination +of them. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +its affiliates. + +**Affiliates** means the other organizations than an +organization has control over, is under the control of, or is +under common control with. + +**Control** means ownership of substantially all the assets of +an entity, or the power to direct its management and policies +by vote, contract, or otherwise. Control can be direct or +indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. From 2b0ec123cd9bb10e936a6758660e59c3faae1730 Mon Sep 17 00:00:00 2001 From: Toran Bruce Richards Date: Tue, 24 Sep 2024 23:16:24 +0100 Subject: [PATCH 07/32] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e083c5709..fb29d72d38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,9 @@ Also check out our [🚀 Roadmap][roadmap] for information about our priorities [roadmap]: https://github.com/Significant-Gravitas/AutoGPT/discussions/6971 [kanban board]: https://github.com/orgs/Significant-Gravitas/projects/1 +## Contributing to the AutoGPT Platform Folder +All contributions to [the autogpt_platform folder](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform) will be under our [Contribution License Agreement](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/Contributor%20License%20Agreement%20(CLA).md). By making a pull request contributing to this folder, you agree to the terms of our CLA for your contribution. + ## In short 1. Avoid duplicate work, issues, PRs etc. 2. We encourage you to collaborate with fellow community members on some of our bigger From 6da8007ce02e7ef27096fb97101af7b9fd36a24d Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 24 Sep 2024 17:43:54 -0500 Subject: [PATCH 08/32] fix(platform): Refresh doc setup instruction (#8142) --- autogpt_platform/README.md | 59 ++++++++++----- docs/content/server/setup.md | 142 +++++++++++++++-------------------- 2 files changed, 100 insertions(+), 101 deletions(-) diff --git a/autogpt_platform/README.md b/autogpt_platform/README.md index 4415ce1a3c..db64280932 100644 --- a/autogpt_platform/README.md +++ b/autogpt_platform/README.md @@ -8,39 +8,60 @@ Welcome to the AutoGPT Platform - a powerful system for creating and running AI - Docker - Docker Compose V2 (comes with Docker Desktop, or can be installed separately) +- Node.js & NPM (for running the frontend application) ### Running the System To run the AutoGPT Platform, follow these steps: -1. Clone this repository to your local machine. -2. Navigate to autogpt_platform/supabase +1. Clone this repository to your local machine and navigate to the `autogpt_platform` directory within the repository: + ``` + git clone + cd AutoGPT/autogpt_platform + ``` + +2. Run the following command: + ``` + git submodule update --init --recursive + ``` + This command will initialize and update the submodules in the repository. The `supabase` folder will be cloned to the root directory. + 3. Run the following command: ``` - git submodule update --init --recursive + cp supabase/docker/.env.example .env ``` -4. Navigate back to autogpt_platform (cd ..) -5. Run the following command: - ``` - cp supabase/docker/.env.example .env - ``` -6. Run the following command: + This command will copy the `.env.example` file to `.env` in the `supabase/docker` directory. You can modify the `.env` file to add your own environment variables. +4. Run the following command: ``` docker compose up -d + ``` + This command will start all the necessary backend services defined in the `docker-compose.yml` file in detached mode. +5. Navigate to `frontend` within the `autogpt_platform` directory: + ``` + cd frontend + ``` + You will need to run your frontend application separately on your local machine. + +6. Run the following command: + ``` + cp .env.example .env + ``` + This command will copy the `.env.example` file to `.env` in the `frontend` directory. You can modify the `.env` within this folder to add your own environment variables for the frontend application. + +7. Run the following command: + ``` + npm install + npm run dev + ``` + This command will install the necessary dependencies and start the frontend application in development mode. + If you are using Yarn, you can run the following commands instead: + ``` + yarn install && yarn dev ``` - This command will start all the necessary backend services defined in the `docker-compose.combined.yml` file in detached mode. -7. Navigate to autogpt_platform/frontend. -8. Run the following command: - ``` - cp .env.example .env.local - ``` -9. Run the following command: - ``` - yarn dev - ``` +8. Open your browser and navigate to `http://localhost:3000` to access the AutoGPT Platform frontend. ### Docker Compose Commands diff --git a/docs/content/server/setup.md b/docs/content/server/setup.md index cf850cfae2..3a33fc781f 100644 --- a/docs/content/server/setup.md +++ b/docs/content/server/setup.md @@ -19,121 +19,99 @@ We also offer this in video format. You can check it out [here](https://github.c To setup the server, you need to have the following installed: - [Node.js](https://nodejs.org/en/) -- [Python 3.10](https://www.python.org/downloads/) +- [Docker](https://docs.docker.com/get-docker/) -### Checking if you have Node.js and Python installed +### Checking if you have Node.js & NPM installed -You can check if you have Node.js installed by running the following command: +We use Node.js to run our frontend application. + +If you need assistance installing Node.js: +https://nodejs.org/en/download/ + +NPM is included with Node.js, but if you need assistance installing NPM: +https://docs.npmjs.com/downloading-and-installing-node-js-and-npm + +You can check if you have Node.js & NPM installed by running the following command: ```bash node -v +npm -v ``` -You can check if you have Python installed by running the following command: +Once you have Node.js installed, you can proceed to the next step. -```bash -python --version -``` - -Once you have node and python installed, you can proceed to the next step. - -### Installing the package managers - -In order to install the dependencies, you need to have the appropriate package managers installed. - -- Installing Yarn - -Yarn is a package manager for Node.js. You can install it by running the following command: - -```bash -npm install -g yarn -``` - -- Installing Poetry - -Poetry is a package manager for Python. You can install it by running the following command: - -```bash -pip install poetry -``` -- Installing Docker and Docker Compose +### Checking if you have Docker & Docker Compose installed Docker containerizes applications, while Docker Compose orchestrates multi-container Docker applications. -You can follow the steps here: - If you need assistance installing docker: https://docs.docker.com/desktop/ -If you need assistance installing docker compose: + +Docker-compose is included in Docker Desktop, but if you need assistance installing docker compose: https://docs.docker.com/compose/install/ -### Installing the dependencies - -Once you have installed Yarn and Poetry, you can run the following command to install the dependencies: +You can check if you have Docker installed by running the following command: ```bash -cd autogpt_platform/backend -cp .env.example .env -poetry install +docker -v +docker-compose -v ``` -**In another terminal**, run the following command to install the dependencies for the frontend: +Once you have Docker and Docker Compose installed, you can proceed to the next step. -```bash -cd autogpt_platform/frontend -yarn install -``` +## Running the backend services -Once you have installed the dependencies, you can proceed to the next step. +To run the backend services, follow these steps: -### Setting up the database +* Within the repository, clone the submodules and navigate to the `autogpt_platform` directory: + ```bash + git submodule update --init --recursive + cd autogpt_platform + ``` + This command will initialize and update the submodules in the repository. The `supabase` folder will be cloned to the root directory. -In order to setup the database, you need to run the following commands, in the same terminal you ran the `poetry install` command: +* Copy the `.env.example` file available in the `supabase/docker` directory to `.env` in `autogpt_platform`: + ``` + cp supabase/docker/.env.example .env + ``` + This command will copy the `.env.example` file to `.env` in the `supabase/docker` directory. You can modify the `.env` file to add your own environment variables. - ```sh - docker compose up postgres redis -d - poetry run prisma migrate dev - ``` -After deploying the migration, to ensure that the database schema is correctly mapped to your codebase, allowing the application to interact with the database properly, you need to generate the Prisma database model: +* Run the backend services: + ``` + docker compose up -d + ``` + This command will start all the necessary backend services defined in the `docker-compose.combined.yml` file in detached mode. -```bash -poetry run prisma generate -``` -Without running this command, the necessary Python modules (prisma.models) won't be available, leading to a `ModuleNotFoundError`. +## Running the frontend application -### Get access to Supabase +To run the frontend application, follow these steps: -Navigate to rnd/supabase -Run the following command: +* Navigate to `frontend` folder within the `autogpt_platform` directory: + ``` + cd frontend + ``` -```bash - git submodule update --init --recursive -``` -### Running the server +* Copy the `.env.example` file available in the `frontend` directory to `.env` in the same directory: + ``` + cp .env.example .env + ``` + You can modify the `.env` within this folder to add your own environment variables for the frontend application. -To run the server, navigate back to rnd (cd..) and run the following commands in the same terminal you ran the `poetry install` command: +* Run the following command: + ``` + npm install + npm run dev + ``` + This command will install the necessary dependencies and start the frontend application in development mode. -```bash -cp supabase/docker/.env.example .env -docker compose build -docker compose up -d -``` - -In the other terminal from frontend, you can run the following command to start the frontend: - -```bash -cp .env.example .env -yarn dev -``` - -### Checking if the server is running +## Checking if the application is running You can check if the server is running by visiting [http://localhost:3000](http://localhost:3000) in your browser. ### Notes: -By default the daemons for different services run on the following ports: +By default the application for different services run on the following ports: -Execution Manager Daemon: 8002 -Execution Scheduler Daemon: 8003 -Rest Server Daemon: 8004 +Frontend UI Server: 3000 +Backend Websocket Server: 8001 +Execution API Rest Server: 8006 From 81d1be73cd8ba746988cb19d95ac76f47aca87e8 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 24 Sep 2024 18:11:15 -0500 Subject: [PATCH 09/32] feat(platform): Add OpenAI reasoning models (#8152) --- .../backend/backend/blocks/llm.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index e2bdfd8c24..ead18b2dbf 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -30,6 +30,8 @@ class ModelMetadata(NamedTuple): class LlmModel(str, Enum): # OpenAI models + O1_PREVIEW = "o1-preview" + O1_MINI = "o1-mini" GPT4O_MINI = "gpt-4o-mini" GPT4O = "gpt-4o" GPT4_TURBO = "gpt-4-turbo" @@ -57,6 +59,8 @@ class LlmModel(str, Enum): MODEL_METADATA = { + LlmModel.O1_PREVIEW: ModelMetadata("openai", 32000, cost_factor=60), + LlmModel.O1_MINI: ModelMetadata("openai", 62000, cost_factor=30), LlmModel.GPT4O_MINI: ModelMetadata("openai", 128000, cost_factor=10), LlmModel.GPT4O: ModelMetadata("openai", 128000, cost_factor=12), LlmModel.GPT4_TURBO: ModelMetadata("openai", 128000, cost_factor=11), @@ -84,7 +88,10 @@ for model in LlmModel: class AIStructuredResponseGeneratorBlock(Block): class Input(BlockSchema): prompt: str - expected_format: dict[str, str] + expected_format: dict[str, str] = SchemaField( + description="Expected format of the response. If provided, the response will be validated against this format. " + "The keys should be the expected fields in the response, and the values should be the description of the field.", + ) model: LlmModel = LlmModel.GPT4_TURBO api_key: BlockSecret = SecretField(value="") sys_prompt: str = "" @@ -132,7 +139,18 @@ class AIStructuredResponseGeneratorBlock(Block): if provider == "openai": openai.api_key = api_key - response_format = {"type": "json_object"} if json_format else None + response_format = None + + if model in [LlmModel.O1_MINI, LlmModel.O1_PREVIEW]: + sys_messages = [p["content"] for p in prompt if p["role"] == "system"] + usr_messages = [p["content"] for p in prompt if p["role"] != "system"] + prompt = [ + {"role": "user", "content": "\n".join(sys_messages)}, + {"role": "user", "content": "\n".join(usr_messages)}, + ] + elif json_format: + response_format = {"type": "json_object"} + response = openai.chat.completions.create( model=model.value, messages=prompt, # type: ignore @@ -207,11 +225,11 @@ class AIStructuredResponseGeneratorBlock(Block): format_prompt = ",\n ".join(expected_format) sys_prompt = trim_prompt( f""" - |Reply in json format: - |{{ - | {format_prompt} - |}} - """ + |Reply strictly only in the following JSON format: + |{{ + | {format_prompt} + |}} + """ ) prompt.append({"role": "system", "content": sys_prompt}) From b78c43111f72a779aeb4b26d68f4e5d9b04ea91b Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Tue, 24 Sep 2024 18:15:45 -0500 Subject: [PATCH 10/32] feat(platform): Make REST & WS server host configurable (#8143) --- autogpt_platform/backend/backend/server/rest_api.py | 7 ++++++- autogpt_platform/backend/backend/server/ws_api.py | 6 +++++- autogpt_platform/backend/backend/util/settings.py | 10 ++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 9f3afd2fba..9ddaa5bdc0 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -251,7 +251,12 @@ class AgentServer(AppService): app.include_router(api_router) - uvicorn.run(app, host="0.0.0.0", port=Config().agent_api_port, log_config=None) + uvicorn.run( + app, + host=Config().agent_api_host, + port=Config().agent_api_port, + log_config=None, + ) def set_test_dependency_overrides(self, overrides: dict): self._test_dependency_overrides = overrides diff --git a/autogpt_platform/backend/backend/server/ws_api.py b/autogpt_platform/backend/backend/server/ws_api.py index cbfe00e439..0bf8231eeb 100644 --- a/autogpt_platform/backend/backend/server/ws_api.py +++ b/autogpt_platform/backend/backend/server/ws_api.py @@ -174,4 +174,8 @@ async def websocket_router( class WebsocketServer(AppProcess): def run(self): - uvicorn.run(app, host="0.0.0.0", port=Config().websocket_server_port) + uvicorn.run( + app, + host=Config().websocket_server_host, + port=Config().websocket_server_port, + ) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 230fa14f52..c81ac09bd8 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -80,6 +80,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): extra="allow", ) + websocket_server_host: str = Field( + default="0.0.0.0", + description="The host for the websocket server to run on", + ) + websocket_server_port: int = Field( default=8001, description="The port for the websocket server to run on", @@ -100,6 +105,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): description="The port for agent server daemon to run on", ) + agent_api_host: str = Field( + default="0.0.0.0", + description="The host for agent server API to run on", + ) + agent_api_port: int = Field( default=8006, description="The port for agent server API to run on", From 03b8f5ec6ed986c7dcb57913ceaf1879de9d558e Mon Sep 17 00:00:00 2001 From: Swifty Date: Wed, 25 Sep 2024 01:19:50 +0200 Subject: [PATCH 11/32] feat(marketplace): Added a list of keywords to describe agents (#8146) * Added more keywords * formatting --- .../src/app/marketplace/submit/page.tsx | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/autogpt_platform/frontend/src/app/marketplace/submit/page.tsx b/autogpt_platform/frontend/src/app/marketplace/submit/page.tsx index b6fdeae9a1..a489e9087b 100644 --- a/autogpt_platform/frontend/src/app/marketplace/submit/page.tsx +++ b/autogpt_platform/frontend/src/app/marketplace/submit/page.tsx @@ -37,6 +37,49 @@ type FormData = { selectedAgentId: string; }; +const keywords = [ + "Automation", + "AI Workflows", + "Integration", + "Task Automation", + "Data Processing", + "Workflow Management", + "Real-time Analytics", + "Custom Triggers", + "Event-driven", + "API Integration", + "Data Transformation", + "Multi-step Workflows", + "Collaboration Tools", + "Business Process Automation", + "No-code Solutions", + "AI-Powered", + "Smart Notifications", + "Data Syncing", + "User Engagement", + "Reporting Automation", + "Lead Generation", + "Customer Support Automation", + "E-commerce Automation", + "Social Media Management", + "Email Marketing Automation", + "Document Management", + "Data Enrichment", + "Performance Tracking", + "Predictive Analytics", + "Resource Allocation", + "Chatbot", + "Virtual Assistant", + "Workflow Automation", + "Social Media Manager", + "Email Optimizer", + "Content Generator", + "Data Analyzer", + "Task Scheduler", + "Customer Service Bot", + "Personalization Engine", +]; + const SubmitPage: React.FC = () => { const router = useRouter(); const { @@ -292,12 +335,11 @@ const SubmitPage: React.FC = () => { - - Keyword 1 - - - Keyword 2 - + {keywords.map((keyword) => ( + + {keyword} + + ))} {/* Add more predefined keywords as needed */} From 46b8f9af0ad0c2522ef4354b33eb425389180aee Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Tue, 24 Sep 2024 18:31:38 -0500 Subject: [PATCH 12/32] feat(builder): scaffold playwright (#8109) --- .github/workflows/platform-frontend-ci.yml | 82 ++++++++++++++----- autogpt_platform/frontend/.gitignore | 5 ++ autogpt_platform/frontend/package.json | 6 +- .../frontend/playwright.config.ts | 81 ++++++++++++++++++ .../frontend/src/tests/title.spec.ts | 8 ++ autogpt_platform/frontend/yarn.lock | 26 ++++++ 6 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 autogpt_platform/frontend/playwright.config.ts create mode 100644 autogpt_platform/frontend/src/tests/title.spec.ts diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index cbd07ea6bc..72c3888c1d 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -2,14 +2,14 @@ name: AutoGPT Platform - Frontend CI on: push: - branches: [ master ] + branches: [master] paths: - - '.github/workflows/platform-frontend-ci.yml' - - 'autogpt_platform/frontend/**' + - ".github/workflows/platform-frontend-ci.yml" + - "autogpt_platform/frontend/**" pull_request: paths: - - '.github/workflows/platform-frontend-ci.yml' - - 'autogpt_platform/frontend/**' + - ".github/workflows/platform-frontend-ci.yml" + - "autogpt_platform/frontend/**" defaults: run: @@ -17,25 +17,67 @@ defaults: working-directory: autogpt_platform/frontend jobs: - lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '21' + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" - - name: Install dependencies - run: | - npm install + - name: Install dependencies + run: | + npm install - - name: Check formatting with Prettier - run: | - npx prettier --check . + - name: Check formatting with Prettier + run: | + npx prettier --check . - - name: Run lint - run: | - npm run lint + - name: Run lint + run: | + npm run lint + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" + + - name: Copy default supabase .env + run: | + cp ../supabase/docker/.env.example ../.env + + - name: Run docker compose + run: | + docker compose -f ../docker-compose.yml up -d + + - name: Install dependencies + run: | + npm install + + - name: Setup Builder .env + run: | + cp .env.example .env + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run tests + run: | + npm run test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/autogpt_platform/frontend/.gitignore b/autogpt_platform/frontend/.gitignore index 1dd45b2022..cfe0cde0bb 100644 --- a/autogpt_platform/frontend/.gitignore +++ b/autogpt_platform/frontend/.gitignore @@ -37,3 +37,8 @@ next-env.d.ts # Sentry Config File .env.sentry-build-plugin +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index d37c0977b9..cc4c01649a 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -9,7 +9,10 @@ "build": "next build", "start": "next start", "lint": "next lint", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "playwright test", + "test-ui": "playwright test --ui", + "gentests": "playwright codegen http://localhost:3000" }, "dependencies": { "@hookform/resolvers": "^3.9.0", @@ -59,6 +62,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.47.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/autogpt_platform/frontend/playwright.config.ts b/autogpt_platform/frontend/playwright.config.ts new file mode 100644 index 0000000000..75c9f68d4c --- /dev/null +++ b/autogpt_platform/frontend/playwright.config.ts @@ -0,0 +1,81 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./src/tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000/", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + bypassCSP: true, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + { + name: "Microsoft Edge", + use: { ...devices["Desktop Edge"], channel: "msedge" }, + }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run build && npm run start", + url: "http://localhost:3000/", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/autogpt_platform/frontend/src/tests/title.spec.ts b/autogpt_platform/frontend/src/tests/title.spec.ts new file mode 100644 index 0000000000..e7e95c949b --- /dev/null +++ b/autogpt_platform/frontend/src/tests/title.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("/"); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/NextGen AutoGPT/); +}); diff --git a/autogpt_platform/frontend/yarn.lock b/autogpt_platform/frontend/yarn.lock index 4efddaff79..5318bb4280 100644 --- a/autogpt_platform/frontend/yarn.lock +++ b/autogpt_platform/frontend/yarn.lock @@ -655,6 +655,13 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@^1.47.1": + version "1.47.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.47.1.tgz#568a46229a5aef54b74977297a7946bb5ac4b67b" + integrity sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q== + dependencies: + playwright "1.47.1" + "@prisma/instrumentation@5.19.1": version "5.19.1" resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.19.1.tgz#146319cf85f22b7a43296f0f40cfeac55516e66e" @@ -3173,6 +3180,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -4604,6 +4616,20 @@ pirates@^4.0.1: resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== +playwright-core@1.47.1: + version "1.47.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.1.tgz#bb45bdfb0d48412c535501aa3805867282857df8" + integrity sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ== + +playwright@1.47.1: + version "1.47.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.1.tgz#cdc1116f5265b8d2ff7be0d8942d49900634dc6c" + integrity sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw== + dependencies: + playwright-core "1.47.1" + optionalDependencies: + fsevents "2.3.2" + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz" From 9f79e70b0f9bed30b1457450cdb502e7e8fceb61 Mon Sep 17 00:00:00 2001 From: Swifty Date: Wed, 25 Sep 2024 13:06:47 +0200 Subject: [PATCH 13/32] fix(platform): Fix REST API CORS issue + UI build for Firefox (#8140) --- autogpt_platform/backend/.env.example | 2 ++ .../backend/backend/server/rest_api.py | 8 ++++- .../backend/backend/server/ws_api.py | 7 ++-- .../backend/backend/util/settings.py | 33 +++++++++++++++++-- autogpt_platform/docker-compose.platform.yml | 4 +++ autogpt_platform/frontend/package.json | 3 ++ .../helm/autogpt-builder/values.dev.yaml | 2 +- .../infra/helm/autogpt-market/values.dev.yaml | 3 +- .../infra/helm/autogpt-server/values.dev.yaml | 1 + .../autogpt-websocket-server/values.dev.yaml | 3 +- autogpt_platform/market/market/app.py | 12 +++---- 11 files changed, 60 insertions(+), 18 deletions(-) diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index de15de840a..e74c5887e6 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -5,6 +5,8 @@ DB_PORT=5432 DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=platform" PRISMA_SCHEMA="postgres/schema.prisma" +BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000" + REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=password diff --git a/autogpt_platform/backend/backend/server/rest_api.py b/autogpt_platform/backend/backend/server/rest_api.py index 9ddaa5bdc0..59dbd286dd 100644 --- a/autogpt_platform/backend/backend/server/rest_api.py +++ b/autogpt_platform/backend/backend/server/rest_api.py @@ -1,4 +1,5 @@ import inspect +import logging from collections import defaultdict from contextlib import asynccontextmanager from functools import wraps @@ -27,6 +28,7 @@ from backend.util.settings import Config, Settings from .utils import get_user_id settings = Settings() +logger = logging.getLogger(__name__) class AgentServer(AppService): @@ -65,9 +67,13 @@ class AgentServer(AppService): if self._test_dependency_overrides: app.dependency_overrides.update(self._test_dependency_overrides) + logger.debug( + f"FastAPI CORS allow origins: {Config().backend_cors_allow_origins}" + ) + app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Allows all origins + allow_origins=Config().backend_cors_allow_origins, allow_credentials=True, allow_methods=["*"], # Allows all methods allow_headers=["*"], # Allows all headers diff --git a/autogpt_platform/backend/backend/server/ws_api.py b/autogpt_platform/backend/backend/server/ws_api.py index 0bf8231eeb..da941233d0 100644 --- a/autogpt_platform/backend/backend/server/ws_api.py +++ b/autogpt_platform/backend/backend/server/ws_api.py @@ -20,13 +20,10 @@ app = FastAPI() event_queue = AsyncRedisEventQueue() _connection_manager = None +logger.info(f"CORS allow origins: {settings.config.backend_cors_allow_origins}") app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://localhost:3000", - "http://127.0.0.1:3000", - "https://dev-builder.agpt.co", - ], + allow_origins=settings.config.backend_cors_allow_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index c81ac09bd8..54a4240513 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -1,8 +1,8 @@ import json import os -from typing import Any, Dict, Generic, Set, Tuple, Type, TypeVar +from typing import Any, Dict, Generic, List, Set, Tuple, Type, TypeVar -from pydantic import BaseModel, Field, PrivateAttr +from pydantic import BaseModel, Field, PrivateAttr, field_validator from pydantic_settings import ( BaseSettings, JsonConfigSettingsSource, @@ -115,6 +115,35 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): description="The port for agent server API to run on", ) + backend_cors_allow_origins: List[str] = Field(default_factory=list) + + @field_validator("backend_cors_allow_origins") + @classmethod + def validate_cors_allow_origins(cls, v: List[str]) -> List[str]: + out = [] + port = None + has_localhost = False + has_127_0_0_1 = False + for url in v: + url = url.strip() + if url.startswith(("http://", "https://")): + if "localhost" in url: + port = url.split(":")[2] + has_localhost = True + if "127.0.0.1" in url: + port = url.split(":")[2] + has_127_0_0_1 = True + out.append(url) + else: + raise ValueError(f"Invalid URL: {url}") + + if has_127_0_0_1 and not has_localhost: + out.append(f"http://localhost:{port}") + if has_localhost and not has_127_0_0_1: + out.append(f"http://127.0.0.1:{port}") + + return out + @classmethod def settings_customise_sources( cls, diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml index a154d364a0..68fc94bb21 100644 --- a/autogpt_platform/docker-compose.platform.yml +++ b/autogpt_platform/docker-compose.platform.yml @@ -66,6 +66,7 @@ services: - ENABLE_AUTH=true - PYRO_HOST=0.0.0.0 - EXECUTIONMANAGER_HOST=executor + - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] ports: - "8006:8006" - "8003:8003" # execution scheduler @@ -134,6 +135,8 @@ services: - REDIS_PASSWORD=password - ENABLE_AUTH=true - PYRO_HOST=0.0.0.0 + - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] + ports: - "8001:8001" networks: @@ -158,6 +161,7 @@ services: - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market + - BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000" ports: - "8015:8015" networks: diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index cc4c01649a..967674e78a 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -14,6 +14,9 @@ "test-ui": "playwright test --ui", "gentests": "playwright codegen http://localhost:3000" }, + "browserslist": [ + "defaults" + ], "dependencies": { "@hookform/resolvers": "^3.9.0", "@next/third-parties": "^14.2.5", diff --git a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml index e41d3fd23b..d1ddec587f 100644 --- a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml @@ -56,7 +56,7 @@ domain: "dev-builder.agpt.co" env: APP_ENV: "dev" - NEXT_PUBLIC_AGPT_SERVER_URL: "http://agpt-server:8000/api" + NEXT_PUBLIC_AGPT_SERVER_URL: ["http://agpt-server:8000/api"] GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" NEXT_PUBLIC_SUPABASE_URL: "" diff --git a/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml index bd5b7c05bb..27fe4b2bd4 100644 --- a/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-market/values.dev.yaml @@ -99,4 +99,5 @@ env: SUPABASE_JWT_SECRET: "" SUPABASE_ANON_KEY: "" SUPABASE_URL: "" - DATABASE_URL: "" \ No newline at end of file + DATABASE_URL: "" + BACKEND_CORS_ALLOW_ORIGINS: "https://dev-builder.agpt.co" \ No newline at end of file diff --git a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml index 2d292a8a03..ae26c71515 100644 --- a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml @@ -85,3 +85,4 @@ env: NUM_NODE_WORKERS: 5 REDIS_HOST: "redis-dev-master.redis-dev.svc.cluster.local" REDIS_PORT: "6379" + BACKEND_CORS_ALLOW_ORIGINS: ["https://dev-builder.agpt.co"] diff --git a/autogpt_platform/infra/helm/autogpt-websocket-server/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-websocket-server/values.dev.yaml index aba1ab6b9c..529dc6f179 100644 --- a/autogpt_platform/infra/helm/autogpt-websocket-server/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-websocket-server/values.dev.yaml @@ -60,4 +60,5 @@ livenessProbe: env: REDIS_HOST: "redis-dev-master.redis-dev.svc.cluster.local" REDIS_PORT: "6379" - REDIS_PASSWORD: "password" \ No newline at end of file + REDIS_PASSWORD: "password" + BACKEND_CORS_ALLOW_ORIGINS: "https://dev-builder.agpt.co" \ No newline at end of file diff --git a/autogpt_platform/market/market/app.py b/autogpt_platform/market/market/app.py index b03071ab90..785f3d9854 100644 --- a/autogpt_platform/market/market/app.py +++ b/autogpt_platform/market/market/app.py @@ -16,9 +16,9 @@ import sentry_sdk.integrations.starlette import market.config import market.routes.admin import market.routes.agents +import market.routes.analytics import market.routes.search import market.routes.submissions -import market.routes.analytics dotenv.load_dotenv() @@ -62,12 +62,9 @@ app = fastapi.FastAPI( app.add_middleware(fastapi.middleware.gzip.GZipMiddleware, minimum_size=1000) app.add_middleware( middleware_class=fastapi.middleware.cors.CORSMiddleware, - allow_origins=[ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://127.0.0.1:3000", - "https://dev-builder.agpt.co", - ], + allow_origins=os.environ.get( + "BACKEND_CORS_ALLOW_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000" + ).split(","), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -87,6 +84,7 @@ def health(): content="

Marketplace API

", status_code=200 ) + @app.get("/") def default(): return fastapi.responses.HTMLResponse( From bd39d5da0b9593a603554af9c8cf252e7591acdd Mon Sep 17 00:00:00 2001 From: Swifty Date: Wed, 25 Sep 2024 13:53:11 +0200 Subject: [PATCH 14/32] fix(platform): Update Backend .env.example BACKEND_CORS_ALLOW_ORIGINS to be a list (#8163) updated to list --- autogpt_platform/backend/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index e74c5887e6..6b704e77fe 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -5,7 +5,7 @@ DB_PORT=5432 DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=platform" PRISMA_SCHEMA="postgres/schema.prisma" -BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000" +BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] REDIS_HOST=localhost REDIS_PORT=6379 From d2205628064a489177fb01c89dca226a658e57c7 Mon Sep 17 00:00:00 2001 From: Aarushi <50577581+aarushik93@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:18:52 +0100 Subject: [PATCH 15/32] tweak(frontend): Add msg for custom error (#8157) add waitlist msg --- autogpt_platform/frontend/src/app/login/actions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autogpt_platform/frontend/src/app/login/actions.ts b/autogpt_platform/frontend/src/app/login/actions.ts index b304cf8ba3..b0f4a59bf1 100644 --- a/autogpt_platform/frontend/src/app/login/actions.ts +++ b/autogpt_platform/frontend/src/app/login/actions.ts @@ -50,6 +50,9 @@ export async function signup(values: z.infer) { const { data, error } = await supabase.auth.signUp(values); if (error) { + if (error.message.includes("P0001")) { + return "Please join our waitlist for your turn: https://agpt.co/waitlist"; + } return error.message; } From 3a1574e4bd977c8745e3e97c0c9f7a4b88481c9a Mon Sep 17 00:00:00 2001 From: Swifty Date: Wed, 25 Sep 2024 22:00:24 +0200 Subject: [PATCH 16/32] tweak(platform): Add primary action buttons (#8161) --- .../frontend/src/components/Flow.tsx | 44 ++++++----- .../src/components/PrimaryActionButton.tsx | 75 +++++++++++++++++++ .../frontend/src/components/ui/button.tsx | 1 + 3 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 autogpt_platform/frontend/src/components/PrimaryActionButton.tsx diff --git a/autogpt_platform/frontend/src/components/Flow.tsx b/autogpt_platform/frontend/src/components/Flow.tsx index dc5606de17..bbffd80192 100644 --- a/autogpt_platform/frontend/src/components/Flow.tsx +++ b/autogpt_platform/frontend/src/components/Flow.tsx @@ -39,7 +39,6 @@ import { IconUndo2, IconRedo2, IconSquare, - IconOutput, } from "@/components/ui/icons"; import { startTutorial } from "./tutorial"; import useAgentGraph from "@/hooks/useAgentGraph"; @@ -49,6 +48,7 @@ import { LogOut } from "lucide-react"; import RunnerUIWrapper, { RunnerUIWrapperRef, } from "@/components/RunnerUIWrapper"; +import PrimaryActionBar from "@/components/PrimaryActionButton"; // This is for the history, this is the minimum distance a block must move before it is logged // It helps to prevent spamming the history with small movements especially when pressing on a input in a block @@ -557,23 +557,6 @@ const FlowEditor: React.FC<{ icon: , onClick: handleRedo, }, - { - label: !savedAgent - ? "Please save the agent to run" - : !isRunning - ? "Run" - : "Stop", - icon: !isRunning ? : , - onClick: !isRunning - ? () => runnerUIRef.current?.runOrOpenInput() - : requestStopRun, - disabled: !savedAgent, - }, - { - label: "Runner Output", - icon: , - onClick: () => runnerUIRef.current?.openRunnerOutput(), - }, ]; return ( @@ -614,6 +597,31 @@ const FlowEditor: React.FC<{ onNameChange={setAgentName} /> + runnerUIRef.current?.openRunnerOutput()} + onClickRunAgent={() => { + if (!savedAgent) { + alert( + "Please save the agent to run, by clicking the save button in the left sidebar.", + ); + return; + } + if (!isRunning) { + runnerUIRef.current?.runOrOpenInput(); + } else { + requestStopRun(); + } + }} + isRunning={isRunning} + requestStopRun={requestStopRun} + runAgentTooltip={ + !savedAgent + ? "Please save the agent to run" + : !isRunning + ? "Run Agent" + : "Stop Agent" + } + /> void; + onClickRunAgent: () => void; + isRunning: boolean; + requestStopRun: () => void; + runAgentTooltip: string; +} + +const PrimaryActionBar: React.FC = ({ + onClickAgentOutputs, + onClickRunAgent, + isRunning, + requestStopRun, + runAgentTooltip, +}) => { + const runButtonLabel = !isRunning ? "Run" : "Stop"; + + const runButtonIcon = !isRunning ? : ; + + const runButtonOnClick = !isRunning ? onClickRunAgent : requestStopRun; + + return ( +
+
+ + + + + +

View agent outputs

+
+
+ + + + + +

{runAgentTooltip}

+
+
+
+
+ ); +}; + +export default PrimaryActionBar; diff --git a/autogpt_platform/frontend/src/components/ui/button.tsx b/autogpt_platform/frontend/src/components/ui/button.tsx index 245a21b074..467f58b2d8 100644 --- a/autogpt_platform/frontend/src/components/ui/button.tsx +++ b/autogpt_platform/frontend/src/components/ui/button.tsx @@ -25,6 +25,7 @@ const buttonVariants = cva( default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", + primary: "h-14 w-44 rounded-2xl", icon: "h-9 w-9", }, }, From 5e2874c31567bcffbf9cf4f687e87fcb9da1e1df Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Thu, 26 Sep 2024 00:36:29 +0200 Subject: [PATCH 17/32] feat(platform): OAuth support + API key management + GitHub blocks (#8044) ## Config - For Supabase, the back end needs `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `SUPABASE_JWT_SECRET` - For the GitHub integration to work, the back end needs `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` - For integrations OAuth flows to work in local development, the back end needs `FRONTEND_BASE_URL` to generate login URLs with accurate redirect URLs ## REST API - Tweak output of OAuth `/login` endpoint: add `state_token` separately in response - Add `POST /integrations/{provider}/credentials` (for API keys) - Add `DELETE /integrations/{provider}/credentials/{cred_id}` ## Back end - Add Supabase support to `AppService` - Add `FRONTEND_BASE_URL` config option, mainly for local development use ### `autogpt_libs.supabase_integration_credentials_store` - Add `CredentialsType` alias - Add `.bearer()` helper methods to `APIKeyCredentials` and `OAuth2Credentials` ### Blocks - Add `CredentialsField(..) -> CredentialsMetaInput` ## Front end ### UI components - `CredentialsInput` for use on `CustomNode`: allows user to add/select credentials for a service. - `APIKeyCredentialsModal`: a dialog for creating API keys - `OAuth2FlowWaitingModal`: a dialog to indicate that the application is waiting for the user to log in to the 3rd party service in the provided pop-up window - `NodeCredentialsInput`: wrapper for `CredentialsInput` with the "usual" interface of node input components - New icons: `IconKey`, `IconKeyPlus`, `IconUser`, `IconUserPlus` ### Data model - `CredentialsProvider`: introduces the app-level `CredentialsProvidersContext`, which acts as an application-wide store and cache for credentials metadata. - `useCredentials` for use on `CustomNode`: uses `CredentialsProvidersContext` and provides node-specific credential data and provider-specific data/functions - `/auth/integrations/oauth_callback` route to close the loop to the `CredentialsInput` after a user completes sign-in to the external service - Add `BlockIOCredentialsSubSchema` ### API client - Add `isAuthenticated` method - Add methods for integration OAuth flow: `oAuthLogin`, `oAuthCallback` - Add CRD methods for credentials: `createAPIKeyCredentials`, `listCredentials`, `getCredentials`, `deleteCredentials` - Add mirrored types `CredentialsMetaResponse`, `CredentialsMetaInput`, `OAuth2Credentials`, `APIKeyCredentials` - Add GitHub blocks + "DEVELOPER_TOOLS" category - Add `**kwargs` to `Block.run(..)` signature to support additional kwargs - Add support for loading blocks from nested modules (e.g. `blocks/github/issues.py`) #### Executor - Add strict support for `credentials` fields on blocks - Fetch credentials for graph execution and pass them down through to the node execution --- .github/workflows/platform-backend-ci.yml | 2 +- .../autogpt_libs/auth/middleware.py | 5 +- .../types.py | 9 + autogpt_platform/backend/.env.example | 23 +- .../backend/backend/blocks/__init__.py | 15 +- .../backend/backend/blocks/basic.py | 16 +- .../backend/backend/blocks/block.py | 2 +- .../backend/backend/blocks/branching.py | 2 +- .../backend/backend/blocks/csv.py | 2 +- .../backend/backend/blocks/discord.py | 6 +- .../backend/backend/blocks/email_block.py | 2 +- .../backend/backend/blocks/github/_auth.py | 54 ++ .../backend/backend/blocks/github/issues.py | 683 +++++++++++++++ .../backend/blocks/github/pull_requests.py | 596 +++++++++++++ .../backend/backend/blocks/github/repo.py | 786 ++++++++++++++++++ .../backend/backend/blocks/http.py | 2 +- .../backend/backend/blocks/iteration.py | 2 +- .../backend/backend/blocks/llm.py | 8 +- .../backend/backend/blocks/maths.py | 4 +- .../backend/backend/blocks/medium.py | 2 +- .../backend/backend/blocks/reddit.py | 4 +- .../backend/backend/blocks/rss.py | 2 +- .../backend/backend/blocks/sampling.py | 2 +- .../backend/backend/blocks/search.py | 8 +- .../backend/backend/blocks/talking_head.py | 2 +- .../backend/backend/blocks/text.py | 8 +- .../backend/backend/blocks/time_blocks.py | 8 +- .../backend/backend/blocks/youtube.py | 2 +- .../backend/backend/data/block.py | 70 +- .../backend/backend/data/execution.py | 2 + .../backend/backend/data/model.py | 48 +- .../backend/backend/executor/manager.py | 93 ++- .../backend/server/routers/integrations.py | 75 +- .../backend/backend/server/utils.py | 4 +- .../backend/backend/util/service.py | 10 +- .../backend/backend/util/settings.py | 10 +- autogpt_platform/backend/backend/util/test.py | 12 +- .../backend/test/block/test_block.py | 10 +- autogpt_platform/docker-compose.platform.yml | 9 +- .../auth/integrations/oauth_callback/route.ts | 38 + .../frontend/src/app/providers.tsx | 5 +- .../frontend/src/components/CustomNode.tsx | 20 +- .../integrations/credentials-input.tsx | 419 ++++++++++ .../integrations/credentials-provider.tsx | 164 ++++ .../src/components/node-input-components.tsx | 36 + .../frontend/src/components/ui/icons.tsx | 144 ++++ .../frontend/src/hooks/useCredentials.ts | 77 ++ .../src/lib/autogpt-server-api/baseClient.ts | 99 ++- .../src/lib/autogpt-server-api/types.ts | 54 ++ .../infra/helm/autogpt-server/values.dev.yaml | 6 + docs/content/server/new_blocks.md | 144 +++- 51 files changed, 3689 insertions(+), 117 deletions(-) create mode 100644 autogpt_platform/backend/backend/blocks/github/_auth.py create mode 100644 autogpt_platform/backend/backend/blocks/github/issues.py create mode 100644 autogpt_platform/backend/backend/blocks/github/pull_requests.py create mode 100644 autogpt_platform/backend/backend/blocks/github/repo.py create mode 100644 autogpt_platform/frontend/src/app/auth/integrations/oauth_callback/route.ts create mode 100644 autogpt_platform/frontend/src/components/integrations/credentials-input.tsx create mode 100644 autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx create mode 100644 autogpt_platform/frontend/src/hooks/useCredentials.ts diff --git a/.github/workflows/platform-backend-ci.yml b/.github/workflows/platform-backend-ci.yml index 20a64892fe..eb5a3481c7 100644 --- a/.github/workflows/platform-backend-ci.yml +++ b/.github/workflows/platform-backend-ci.yml @@ -105,7 +105,7 @@ jobs: LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }} DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} SUPABASE_URL: ${{ steps.supabase.outputs.API_URL }} - SUPABASE_SERVICE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }} + SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }} SUPABASE_JWT_SECRET: ${{ steps.supabase.outputs.JWT_SECRET }} env: CI: true diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py index 71a2268fa0..783e1b35be 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/auth/middleware.py @@ -7,12 +7,13 @@ from .config import settings from .jwt_utils import parse_jwt_token security = HTTPBearer() +logger = logging.getLogger(__name__) async def auth_middleware(request: Request): if not settings.ENABLE_AUTH: # If authentication is disabled, allow the request to proceed - logging.warn("Auth disabled") + logger.warn("Auth disabled") return {} security = HTTPBearer() @@ -24,7 +25,7 @@ async def auth_middleware(request: Request): try: payload = parse_jwt_token(credentials.credentials) request.state.user = payload - logging.info("Token decoded successfully") + logger.debug("Token decoded successfully") except ValueError as e: raise HTTPException(status_code=401, detail=str(e)) return payload diff --git a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py index 500dfdb442..da39f6a842 100644 --- a/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py +++ b/autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py @@ -29,6 +29,9 @@ class OAuth2Credentials(_BaseCredentials): scopes: list[str] metadata: dict[str, Any] = Field(default_factory=dict) + def bearer(self) -> str: + return f"Bearer {self.access_token.get_secret_value()}" + class APIKeyCredentials(_BaseCredentials): type: Literal["api_key"] = "api_key" @@ -36,6 +39,9 @@ class APIKeyCredentials(_BaseCredentials): expires_at: Optional[int] """Unix timestamp (seconds) indicating when the API key expires (if at all)""" + def bearer(self) -> str: + return f"Bearer {self.api_key.get_secret_value()}" + Credentials = Annotated[ OAuth2Credentials | APIKeyCredentials, @@ -43,6 +49,9 @@ Credentials = Annotated[ ] +CredentialsType = Literal["api_key", "oauth2"] + + class OAuthState(BaseModel): token: str provider: str diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index 6b704e77fe..1e1cb8bd92 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -11,13 +11,30 @@ REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=password -ENABLE_AUTH=false ENABLE_CREDIT=false APP_ENV="local" PYRO_HOST=localhost SENTRY_DSN= -# This is needed when ENABLE_AUTH is true -SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long + +## User auth with Supabase is required for any of the 3rd party integrations with auth to work. +ENABLE_AUTH=false +SUPABASE_URL= +SUPABASE_SERVICE_ROLE_KEY= +SUPABASE_JWT_SECRET= + +# For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow for integrations to work. +# FRONTEND_BASE_URL=http://localhost:3000 + +## == INTEGRATION CREDENTIALS == ## +# Each set of server side credentials is required for the corresponding 3rd party +# integration to work. + +# For the OAuth callback URL, use /auth/integrations/oauth_callback, +# e.g. http://localhost:3000/auth/integrations/oauth_callback + +# GitHub OAuth App server credentials - https://github.com/settings/developers +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= ## ===== OPTIONAL API KEYS ===== ## diff --git a/autogpt_platform/backend/backend/blocks/__init__.py b/autogpt_platform/backend/backend/blocks/__init__.py index 84364bdae3..d090aa41be 100644 --- a/autogpt_platform/backend/backend/blocks/__init__.py +++ b/autogpt_platform/backend/backend/blocks/__init__.py @@ -1,4 +1,3 @@ -import glob import importlib import os import re @@ -8,17 +7,17 @@ from backend.data.block import Block # Dynamically load all modules under backend.blocks AVAILABLE_MODULES = [] -current_dir = os.path.dirname(__file__) -modules = glob.glob(os.path.join(current_dir, "*.py")) +current_dir = Path(__file__).parent modules = [ - Path(f).stem - for f in modules - if os.path.isfile(f) and f.endswith(".py") and not f.endswith("__init__.py") + str(f.relative_to(current_dir))[:-3].replace(os.path.sep, ".") + for f in current_dir.rglob("*.py") + if f.is_file() and f.name != "__init__.py" ] for module in modules: - if not re.match("^[a-z_]+$", module): + if not re.match("^[a-z_.]+$", module): raise ValueError( - f"Block module {module} error: module name must be lowercase, separated by underscores, and contain only alphabet characters" + f"Block module {module} error: module name must be lowercase, " + "separated by underscores, and contain only alphabet characters" ) importlib.import_module(f".{module}", package=__name__) diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 5d3c144ace..a38f8f1628 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -57,7 +57,7 @@ class StoreValueBlock(Block): static_output=True, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.data or input_data.input @@ -79,7 +79,7 @@ class PrintToConsoleBlock(Block): test_output=("status", "printed"), ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: print(">>>>> Print: ", input_data.text) yield "status", "printed" @@ -118,7 +118,7 @@ class FindInDictionaryBlock(Block): categories={BlockCategory.BASIC}, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: obj = input_data.input key = input_data.key @@ -200,7 +200,7 @@ class AgentInputBlock(Block): ui_type=BlockUIType.INPUT, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "result", input_data.value @@ -283,7 +283,7 @@ class AgentOutputBlock(Block): ui_type=BlockUIType.OUTPUT, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **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. @@ -343,7 +343,7 @@ class AddToDictionaryBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # If no dictionary is provided, create a new one if input_data.dictionary is None: @@ -414,7 +414,7 @@ class AddToListBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # If no list is provided, create a new one if input_data.list is None: @@ -455,5 +455,5 @@ class NoteBlock(Block): ui_type=BlockUIType.NOTE, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "output", input_data.text diff --git a/autogpt_platform/backend/backend/blocks/block.py b/autogpt_platform/backend/backend/blocks/block.py index d6c2cca9bb..a4bea7aee7 100644 --- a/autogpt_platform/backend/backend/blocks/block.py +++ b/autogpt_platform/backend/backend/blocks/block.py @@ -31,7 +31,7 @@ class BlockInstallationBlock(Block): disabled=True, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: code = input_data.code if search := re.search(r"class (\w+)\(Block\):", code): diff --git a/autogpt_platform/backend/backend/blocks/branching.py b/autogpt_platform/backend/backend/blocks/branching.py index 58bc9b6b57..d651b595ce 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) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: value1 = input_data.value1 operator = input_data.operator value2 = input_data.value2 diff --git a/autogpt_platform/backend/backend/blocks/csv.py b/autogpt_platform/backend/backend/blocks/csv.py index f41d4198f7..ccac4e556e 100644 --- a/autogpt_platform/backend/backend/blocks/csv.py +++ b/autogpt_platform/backend/backend/blocks/csv.py @@ -40,7 +40,7 @@ class ReadCsvBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: import csv from io import StringIO diff --git a/autogpt_platform/backend/backend/blocks/discord.py b/autogpt_platform/backend/backend/blocks/discord.py index 75e901d9f2..bebe54d906 100644 --- a/autogpt_platform/backend/backend/blocks/discord.py +++ b/autogpt_platform/backend/backend/blocks/discord.py @@ -81,14 +81,14 @@ class ReadDiscordMessagesBlock(Block): await client.start(token) - def run(self, input_data: "ReadDiscordMessagesBlock.Input") -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: while True: for output_name, output_value in self.__run(input_data): yield output_name, output_value if not input_data.continuous_read: break - def __run(self, input_data: "ReadDiscordMessagesBlock.Input") -> BlockOutput: + def __run(self, input_data: Input) -> BlockOutput: try: loop = asyncio.get_event_loop() future = self.run_bot(input_data.discord_bot_token.get_secret_value()) @@ -187,7 +187,7 @@ 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(self, input_data: "SendDiscordMessageBlock.Input") -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: loop = asyncio.get_event_loop() future = self.send_message( diff --git a/autogpt_platform/backend/backend/blocks/email_block.py b/autogpt_platform/backend/backend/blocks/email_block.py index 043e58a217..edfb2f391b 100644 --- a/autogpt_platform/backend/backend/blocks/email_block.py +++ b/autogpt_platform/backend/backend/blocks/email_block.py @@ -88,7 +88,7 @@ class SendEmailBlock(Block): except Exception as e: return f"Failed to send email: {str(e)}" - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: status = self.send_email( input_data.creds, input_data.to_email, diff --git a/autogpt_platform/backend/backend/blocks/github/_auth.py b/autogpt_platform/backend/backend/blocks/github/_auth.py new file mode 100644 index 0000000000..4ea31e98be --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/_auth.py @@ -0,0 +1,54 @@ +from typing import Literal + +from autogpt_libs.supabase_integration_credentials_store.types import ( + APIKeyCredentials, + OAuth2Credentials, +) +from pydantic import SecretStr + +from backend.data.model import CredentialsField, CredentialsMetaInput +from backend.util.settings import Secrets + +secrets = Secrets() +GITHUB_OAUTH_IS_CONFIGURED = bool( + secrets.github_client_id and secrets.github_client_secret +) + +GithubCredentials = APIKeyCredentials | OAuth2Credentials +GithubCredentialsInput = CredentialsMetaInput[ + Literal["github"], + Literal["api_key", "oauth2"] if GITHUB_OAUTH_IS_CONFIGURED else Literal["api_key"], +] + + +def GithubCredentialsField(scope: str) -> GithubCredentialsInput: + """ + Creates a GitHub credentials input on a block. + + Params: + scope: The authorization scope needed for the block to work. ([list of available scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes)) + """ # noqa + return CredentialsField( + provider="github", + supported_credential_types=( + {"api_key", "oauth2"} if GITHUB_OAUTH_IS_CONFIGURED else {"api_key"} + ), + required_scopes={scope}, + description="The GitHub integration can be used with OAuth, " + "or any API key with sufficient permissions for the blocks it is used on.", + ) + + +TEST_CREDENTIALS = APIKeyCredentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="github", + api_key=SecretStr("mock-github-api-key"), + title="Mock GitHub API key", + expires_at=None, +) +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.type, +} diff --git a/autogpt_platform/backend/backend/blocks/github/issues.py b/autogpt_platform/backend/backend/blocks/github/issues.py new file mode 100644 index 0000000000..97a4694340 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/issues.py @@ -0,0 +1,683 @@ +import requests +from typing_extensions import TypedDict + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +from ._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + GithubCredentials, + GithubCredentialsField, + GithubCredentialsInput, +) + + +class GithubCommentBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue or pull request", + placeholder="https://github.com/owner/repo/issues/1", + ) + comment: str = SchemaField( + description="Comment to post on the issue or pull request", + placeholder="Enter your comment", + ) + + class Output(BlockSchema): + id: int = SchemaField(description="ID of the created comment") + url: str = SchemaField(description="URL to the comment on GitHub") + error: str = SchemaField( + description="Error message if the comment posting failed" + ) + + def __init__(self): + super().__init__( + id="a8db4d8d-db1c-4a25-a1b0-416a8c33602b", + description="This block posts a comment on a specified GitHub issue or pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubCommentBlock.Input, + output_schema=GithubCommentBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "comment": "This is a test comment.", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", 1337), + ("url", "https://github.com/owner/repo/issues/1#issuecomment-1337"), + ], + test_mock={ + "post_comment": lambda *args, **kwargs: ( + 1337, + "https://github.com/owner/repo/issues/1#issuecomment-1337", + ) + }, + ) + + @staticmethod + def post_comment( + credentials: GithubCredentials, issue_url: str, body_text: str + ) -> tuple[int, str]: + if "/pull/" in issue_url: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/issues/" + ) + + "/comments" + ) + else: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos") + "/comments" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"body": body_text} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + comment = response.json() + return comment["id"], comment["html_url"] + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + id, url = self.post_comment( + credentials, + input_data.issue_url, + input_data.comment, + ) + yield "id", id + yield "url", url + except Exception as e: + yield "error", f"Failed to post comment: {str(e)}" + + +class GithubMakeIssueBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + title: str = SchemaField( + description="Title of the issue", placeholder="Enter the issue title" + ) + body: str = SchemaField( + description="Body of the issue", placeholder="Enter the issue body" + ) + + class Output(BlockSchema): + number: int = SchemaField(description="Number of the created issue") + url: str = SchemaField(description="URL of the created issue") + error: str = SchemaField( + description="Error message if the issue creation failed" + ) + + def __init__(self): + super().__init__( + id="691dad47-f494-44c3-a1e8-05b7990f2dab", + description="This block creates a new issue on a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubMakeIssueBlock.Input, + output_schema=GithubMakeIssueBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "title": "Test Issue", + "body": "This is a test issue.", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("number", 1), + ("url", "https://github.com/owner/repo/issues/1"), + ], + test_mock={ + "create_issue": lambda *args, **kwargs: ( + 1, + "https://github.com/owner/repo/issues/1", + ) + }, + ) + + @staticmethod + def create_issue( + credentials: GithubCredentials, repo_url: str, title: str, body: str + ) -> tuple[int, str]: + api_url = repo_url.replace("github.com", "api.github.com/repos") + "/issues" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"title": title, "body": body} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + issue = response.json() + return issue["number"], issue["html_url"] + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + number, url = self.create_issue( + credentials, + input_data.repo_url, + input_data.title, + input_data.body, + ) + yield "number", number + yield "url", url + except Exception as e: + yield "error", f"Failed to create issue: {str(e)}" + + +class GithubReadIssueBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue", + placeholder="https://github.com/owner/repo/issues/1", + ) + + class Output(BlockSchema): + title: str = SchemaField(description="Title of the issue") + body: str = SchemaField(description="Body of the issue") + user: str = SchemaField(description="User who created the issue") + error: str = SchemaField( + description="Error message if reading the issue failed" + ) + + def __init__(self): + super().__init__( + id="6443c75d-032a-4772-9c08-230c707c8acc", + description="This block reads the body, title, and user of a specified GitHub issue.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubReadIssueBlock.Input, + output_schema=GithubReadIssueBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("title", "Title of the issue"), + ("body", "This is the body of the issue."), + ("user", "username"), + ], + test_mock={ + "read_issue": lambda *args, **kwargs: ( + "Title of the issue", + "This is the body of the issue.", + "username", + ) + }, + ) + + @staticmethod + def read_issue( + credentials: GithubCredentials, issue_url: str + ) -> tuple[str, str, str]: + api_url = issue_url.replace("github.com", "api.github.com/repos") + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + 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( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + title, body, user = self.read_issue( + credentials, + input_data.issue_url, + ) + yield "title", title + yield "body", body + yield "user", user + except Exception as e: + yield "error", f"Failed to read issue: {str(e)}" + + +class GithubListIssuesBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + + class Output(BlockSchema): + class IssueItem(TypedDict): + title: str + url: str + + issue: IssueItem = SchemaField( + title="Issue", description="Issues with their title and URL" + ) + error: str = SchemaField(description="Error message if listing issues failed") + + def __init__(self): + super().__init__( + id="c215bfd7-0e57-4573-8f8c-f7d4963dcd74", + description="This block lists all issues for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListIssuesBlock.Input, + output_schema=GithubListIssuesBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "issue", + { + "title": "Issue 1", + "url": "https://github.com/owner/repo/issues/1", + }, + ) + ], + test_mock={ + "list_issues": lambda *args, **kwargs: [ + { + "title": "Issue 1", + "url": "https://github.com/owner/repo/issues/1", + } + ] + }, + ) + + @staticmethod + def list_issues( + credentials: GithubCredentials, repo_url: str + ) -> list[Output.IssueItem]: + api_url = repo_url.replace("github.com", "api.github.com/repos") + "/issues" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + data = response.json() + issues: list[GithubListIssuesBlock.Output.IssueItem] = [ + {"title": issue["title"], "url": issue["html_url"]} for issue in data + ] + + return issues + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + issues = self.list_issues( + credentials, + input_data.repo_url, + ) + yield from (("issue", issue) for issue in issues) + except Exception as e: + yield "error", f"Failed to list issues: {str(e)}" + + +class GithubAddLabelBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue or pull request", + placeholder="https://github.com/owner/repo/issues/1", + ) + label: str = SchemaField( + description="Label to add to the issue or pull request", + placeholder="Enter the label", + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Status of the label addition operation") + error: str = SchemaField( + description="Error message if the label addition failed" + ) + + def __init__(self): + super().__init__( + id="98bd6b77-9506-43d5-b669-6b9733c4b1f1", + description="This block adds a label to a specified GitHub issue or pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubAddLabelBlock.Input, + output_schema=GithubAddLabelBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "label": "bug", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Label added successfully")], + test_mock={"add_label": lambda *args, **kwargs: "Label added successfully"}, + ) + + @staticmethod + def add_label(credentials: GithubCredentials, issue_url: str, label: str) -> str: + # Convert the provided GitHub URL to the API URL + if "/pull/" in issue_url: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/issues/" + ) + + "/labels" + ) + else: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos") + "/labels" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"labels": [label]} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + return "Label added successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.add_label( + credentials, + input_data.issue_url, + input_data.label, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to add label: {str(e)}" + + +class GithubRemoveLabelBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue or pull request", + placeholder="https://github.com/owner/repo/issues/1", + ) + label: str = SchemaField( + description="Label to remove from the issue or pull request", + placeholder="Enter the label", + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Status of the label removal operation") + error: str = SchemaField( + description="Error message if the label removal failed" + ) + + def __init__(self): + super().__init__( + id="78f050c5-3e3a-48c0-9e5b-ef1ceca5589c", + description="This block removes a label from a specified GitHub issue or pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubRemoveLabelBlock.Input, + output_schema=GithubRemoveLabelBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "label": "bug", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Label removed successfully")], + test_mock={ + "remove_label": lambda *args, **kwargs: "Label removed successfully" + }, + ) + + @staticmethod + def remove_label(credentials: GithubCredentials, issue_url: str, label: str) -> str: + # Convert the provided GitHub URL to the API URL + if "/pull/" in issue_url: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/issues/" + ) + + f"/labels/{label}" + ) + else: + api_url = ( + issue_url.replace("github.com", "api.github.com/repos") + + f"/labels/{label}" + ) + + # Log the constructed API URL for debugging + print(f"Constructed API URL: {api_url}") + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.delete(api_url, headers=headers) + response.raise_for_status() + + return "Label removed successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.remove_label( + credentials, + input_data.issue_url, + input_data.label, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to remove label: {str(e)}" + + +class GithubAssignIssueBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue", + placeholder="https://github.com/owner/repo/issues/1", + ) + assignee: str = SchemaField( + description="Username to assign to the issue", + placeholder="Enter the username", + ) + + class Output(BlockSchema): + status: str = SchemaField( + description="Status of the issue assignment operation" + ) + error: str = SchemaField( + description="Error message if the issue assignment failed" + ) + + def __init__(self): + super().__init__( + id="90507c72-b0ff-413a-886a-23bbbd66f542", + description="This block assigns a user to a specified GitHub issue.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubAssignIssueBlock.Input, + output_schema=GithubAssignIssueBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "assignee": "username1", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Issue assigned successfully")], + test_mock={ + "assign_issue": lambda *args, **kwargs: "Issue assigned successfully" + }, + ) + + @staticmethod + def assign_issue( + credentials: GithubCredentials, + issue_url: str, + assignee: str, + ) -> str: + # Extracting repo path and issue number from the issue URL + repo_path, issue_number = issue_url.replace("https://github.com/", "").split( + "/issues/" + ) + api_url = ( + f"https://api.github.com/repos/{repo_path}/issues/{issue_number}/assignees" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"assignees": [assignee]} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + return "Issue assigned successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.assign_issue( + credentials, + input_data.issue_url, + input_data.assignee, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to assign issue: {str(e)}" + + +class GithubUnassignIssueBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + issue_url: str = SchemaField( + description="URL of the GitHub issue", + placeholder="https://github.com/owner/repo/issues/1", + ) + assignee: str = SchemaField( + description="Username to unassign from the issue", + placeholder="Enter the username", + ) + + class Output(BlockSchema): + status: str = SchemaField( + description="Status of the issue unassignment operation" + ) + error: str = SchemaField( + description="Error message if the issue unassignment failed" + ) + + def __init__(self): + super().__init__( + id="d154002a-38f4-46c2-962d-2488f2b05ece", + description="This block unassigns a user from a specified GitHub issue.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubUnassignIssueBlock.Input, + output_schema=GithubUnassignIssueBlock.Output, + test_input={ + "issue_url": "https://github.com/owner/repo/issues/1", + "assignee": "username1", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Issue unassigned successfully")], + test_mock={ + "unassign_issue": lambda *args, **kwargs: "Issue unassigned successfully" + }, + ) + + @staticmethod + def unassign_issue( + credentials: GithubCredentials, + issue_url: str, + assignee: str, + ) -> str: + # Extracting repo path and issue number from the issue URL + repo_path, issue_number = issue_url.replace("https://github.com/", "").split( + "/issues/" + ) + api_url = ( + f"https://api.github.com/repos/{repo_path}/issues/{issue_number}/assignees" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"assignees": [assignee]} + + response = requests.delete(api_url, headers=headers, json=data) + response.raise_for_status() + + return "Issue unassigned successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.unassign_issue( + credentials, + input_data.issue_url, + input_data.assignee, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to unassign issue: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/github/pull_requests.py b/autogpt_platform/backend/backend/blocks/github/pull_requests.py new file mode 100644 index 0000000000..87540b66df --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/pull_requests.py @@ -0,0 +1,596 @@ +import requests +from typing_extensions import TypedDict + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +from ._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + GithubCredentials, + GithubCredentialsField, + GithubCredentialsInput, +) + + +class GithubListPullRequestsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + + class Output(BlockSchema): + class PRItem(TypedDict): + title: str + url: str + + pull_request: PRItem = SchemaField( + title="Pull Request", description="PRs with their title and URL" + ) + error: str = SchemaField(description="Error message if listing issues failed") + + def __init__(self): + super().__init__( + id="ffef3c4c-6cd0-48dd-817d-459f975219f4", + description="This block lists all pull requests for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListPullRequestsBlock.Input, + output_schema=GithubListPullRequestsBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "pull_request", + { + "title": "Pull request 1", + "url": "https://github.com/owner/repo/pull/1", + }, + ) + ], + test_mock={ + "list_prs": lambda *args, **kwargs: [ + { + "title": "Pull request 1", + "url": "https://github.com/owner/repo/pull/1", + } + ] + }, + ) + + @staticmethod + def list_prs(credentials: GithubCredentials, repo_url: str) -> list[Output.PRItem]: + api_url = repo_url.replace("github.com", "api.github.com/repos") + "/pulls" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + 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( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + pull_requests = self.list_prs( + credentials, + input_data.repo_url, + ) + yield from (("pull_request", pr) for pr in pull_requests) + except Exception as e: + yield "error", f"Failed to list pull requests: {str(e)}" + + +class GithubMakePullRequestBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + title: str = SchemaField( + description="Title of the pull request", + placeholder="Enter the pull request title", + ) + body: str = SchemaField( + description="Body of the pull request", + placeholder="Enter the pull request body", + ) + head: str = SchemaField( + description="The name of the branch where your changes are implemented. For cross-repository pull requests in the same network, namespace head with a user like this: username:branch.", + placeholder="Enter the head branch", + ) + base: str = SchemaField( + description="The name of the branch you want the changes pulled into.", + placeholder="Enter the base branch", + ) + + class Output(BlockSchema): + number: int = SchemaField(description="Number of the created pull request") + url: str = SchemaField(description="URL of the created pull request") + error: str = SchemaField( + description="Error message if the pull request creation failed" + ) + + def __init__(self): + super().__init__( + id="dfb987f8-f197-4b2e-bf19-111812afd692", + description="This block creates a new pull request on a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubMakePullRequestBlock.Input, + output_schema=GithubMakePullRequestBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "title": "Test Pull Request", + "body": "This is a test pull request.", + "head": "feature-branch", + "base": "main", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("number", 1), + ("url", "https://github.com/owner/repo/pull/1"), + ], + test_mock={ + "create_pr": lambda *args, **kwargs: ( + 1, + "https://github.com/owner/repo/pull/1", + ) + }, + ) + + @staticmethod + def create_pr( + credentials: GithubCredentials, + repo_url: str, + title: str, + body: str, + head: str, + base: str, + ) -> tuple[int, str]: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/pulls" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"title": title, "body": body, "head": head, "base": base} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + pr_data = response.json() + return pr_data["number"], pr_data["html_url"] + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + number, url = self.create_pr( + credentials, + input_data.repo_url, + input_data.title, + input_data.body, + input_data.head, + input_data.base, + ) + yield "number", number + yield "url", url + except requests.exceptions.HTTPError as http_err: + if http_err.response.status_code == 422: + error_details = http_err.response.json() + error_message = error_details.get("message", "Unknown error") + else: + error_message = str(http_err) + yield "error", f"Failed to create pull request: {error_message}" + except Exception as e: + yield "error", f"Failed to create pull request: {str(e)}" + + +class GithubReadPullRequestBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + pr_url: str = SchemaField( + description="URL of the GitHub pull request", + placeholder="https://github.com/owner/repo/pull/1", + ) + include_pr_changes: bool = SchemaField( + description="Whether to include the changes made in the pull request", + default=False, + ) + + class Output(BlockSchema): + title: str = SchemaField(description="Title of the pull request") + body: str = SchemaField(description="Body of the pull request") + author: str = SchemaField(description="User who created the pull request") + changes: str = SchemaField(description="Changes made in the pull request") + error: str = SchemaField( + description="Error message if reading the pull request failed" + ) + + def __init__(self): + super().__init__( + id="bf94b2a4-1a30-4600-a783-a8a44ee31301", + description="This block reads the body, title, user, and changes of a specified GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubReadPullRequestBlock.Input, + output_schema=GithubReadPullRequestBlock.Output, + test_input={ + "pr_url": "https://github.com/owner/repo/pull/1", + "include_pr_changes": True, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("title", "Title of the pull request"), + ("body", "This is the body of the pull request."), + ("author", "username"), + ("changes", "List of changes made in the pull request."), + ], + test_mock={ + "read_pr": lambda *args, **kwargs: ( + "Title of the pull request", + "This is the body of the pull request.", + "username", + ), + "read_pr_changes": lambda *args, **kwargs: "List of changes made in the pull request.", + }, + ) + + @staticmethod + def read_pr(credentials: GithubCredentials, pr_url: str) -> tuple[str, str, str]: + api_url = pr_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/issues/" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + 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") + + return title, body, author + + @staticmethod + def read_pr_changes(credentials: GithubCredentials, pr_url: str) -> str: + api_url = ( + pr_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/pulls/" + ) + + "/files" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + 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) + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + title, body, author = self.read_pr( + credentials, + input_data.pr_url, + ) + yield "title", title + yield "body", body + yield "author", author + + if input_data.include_pr_changes: + changes = self.read_pr_changes( + credentials, + input_data.pr_url, + ) + yield "changes", changes + except Exception as e: + yield "error", f"Failed to read pull request: {str(e)}" + + +class GithubAssignPRReviewerBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + pr_url: str = SchemaField( + description="URL of the GitHub pull request", + placeholder="https://github.com/owner/repo/pull/1", + ) + reviewer: str = SchemaField( + description="Username of the reviewer to assign", + placeholder="Enter the reviewer's username", + ) + + class Output(BlockSchema): + status: str = SchemaField( + description="Status of the reviewer assignment operation" + ) + error: str = SchemaField( + description="Error message if the reviewer assignment failed" + ) + + def __init__(self): + super().__init__( + id="c0d22c5e-e688-43e3-ba43-d5faba7927fd", + description="This block assigns a reviewer to a specified GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubAssignPRReviewerBlock.Input, + output_schema=GithubAssignPRReviewerBlock.Output, + test_input={ + "pr_url": "https://github.com/owner/repo/pull/1", + "reviewer": "reviewer_username", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Reviewer assigned successfully")], + test_mock={ + "assign_reviewer": lambda *args, **kwargs: "Reviewer assigned successfully" + }, + ) + + @staticmethod + def assign_reviewer( + credentials: GithubCredentials, pr_url: str, reviewer: str + ) -> str: + # Convert the PR URL to the appropriate API endpoint + api_url = ( + pr_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/pulls/" + ) + + "/requested_reviewers" + ) + + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"reviewers": [reviewer]} + + response = requests.post(api_url, headers=headers, json=data) + response.raise_for_status() + + return "Reviewer assigned successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.assign_reviewer( + credentials, + input_data.pr_url, + input_data.reviewer, + ) + yield "status", status + except requests.exceptions.HTTPError as http_err: + if http_err.response.status_code == 422: + error_msg = ( + "Failed to assign reviewer: " + f"The reviewer '{input_data.reviewer}' may not have permission " + "or the pull request is not in a valid state. " + f"Detailed error: {http_err.response.text}" + ) + else: + error_msg = f"HTTP error: {http_err} - {http_err.response.text}" + yield "error", error_msg + except Exception as e: + yield "error", f"Failed to assign reviewer: {str(e)}" + + +class GithubUnassignPRReviewerBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + pr_url: str = SchemaField( + description="URL of the GitHub pull request", + placeholder="https://github.com/owner/repo/pull/1", + ) + reviewer: str = SchemaField( + description="Username of the reviewer to unassign", + placeholder="Enter the reviewer's username", + ) + + class Output(BlockSchema): + status: str = SchemaField( + description="Status of the reviewer unassignment operation" + ) + error: str = SchemaField( + description="Error message if the reviewer unassignment failed" + ) + + def __init__(self): + super().__init__( + id="9637945d-c602-4875-899a-9c22f8fd30de", + description="This block unassigns a reviewer from a specified GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubUnassignPRReviewerBlock.Input, + output_schema=GithubUnassignPRReviewerBlock.Output, + test_input={ + "pr_url": "https://github.com/owner/repo/pull/1", + "reviewer": "reviewer_username", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Reviewer unassigned successfully")], + test_mock={ + "unassign_reviewer": lambda *args, **kwargs: "Reviewer unassigned successfully" + }, + ) + + @staticmethod + def unassign_reviewer( + credentials: GithubCredentials, pr_url: str, reviewer: str + ) -> str: + api_url = ( + pr_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/pulls/" + ) + + "/requested_reviewers" + ) + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + data = {"reviewers": [reviewer]} + + response = requests.delete(api_url, headers=headers, json=data) + response.raise_for_status() + + return "Reviewer unassigned successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.unassign_reviewer( + credentials, + input_data.pr_url, + input_data.reviewer, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to unassign reviewer: {str(e)}" + + +class GithubListPRReviewersBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + pr_url: str = SchemaField( + description="URL of the GitHub pull request", + placeholder="https://github.com/owner/repo/pull/1", + ) + + class Output(BlockSchema): + class ReviewerItem(TypedDict): + username: str + url: str + + reviewer: ReviewerItem = SchemaField( + title="Reviewer", + description="Reviewers with their username and profile URL", + ) + error: str = SchemaField( + description="Error message if listing reviewers failed" + ) + + def __init__(self): + super().__init__( + id="2646956e-96d5-4754-a3df-034017e7ed96", + description="This block lists all reviewers for a specified GitHub pull request.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListPRReviewersBlock.Input, + output_schema=GithubListPRReviewersBlock.Output, + test_input={ + "pr_url": "https://github.com/owner/repo/pull/1", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "reviewer", + { + "username": "reviewer1", + "url": "https://github.com/reviewer1", + }, + ) + ], + test_mock={ + "list_reviewers": lambda *args, **kwargs: [ + { + "username": "reviewer1", + "url": "https://github.com/reviewer1", + } + ] + }, + ) + + @staticmethod + def list_reviewers( + credentials: GithubCredentials, pr_url: str + ) -> list[Output.ReviewerItem]: + api_url = ( + pr_url.replace("github.com", "api.github.com/repos").replace( + "/pull/", "/pulls/" + ) + + "/requested_reviewers" + ) + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + data = response.json() + reviewers: list[GithubListPRReviewersBlock.Output.ReviewerItem] = [ + {"username": reviewer["login"], "url": reviewer["html_url"]} + for reviewer in data.get("users", []) + ] + + return reviewers + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + reviewers = self.list_reviewers( + credentials, + input_data.pr_url, + ) + yield from (("reviewer", reviewer) for reviewer in reviewers) + except Exception as e: + yield "error", f"Failed to list reviewers: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/github/repo.py b/autogpt_platform/backend/backend/blocks/github/repo.py new file mode 100644 index 0000000000..63dcc7e1a1 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/github/repo.py @@ -0,0 +1,786 @@ +import base64 + +import requests +from typing_extensions import TypedDict + +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + +from ._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + GithubCredentials, + GithubCredentialsField, + GithubCredentialsInput, +) + + +class GithubListTagsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + + class Output(BlockSchema): + class TagItem(TypedDict): + name: str + url: str + + tag: TagItem = SchemaField( + title="Tag", description="Tags with their name and file tree browser URL" + ) + error: str = SchemaField(description="Error message if listing tags failed") + + def __init__(self): + super().__init__( + id="358924e7-9a11-4d1a-a0f2-13c67fe59e2e", + description="This block lists all tags for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListTagsBlock.Input, + output_schema=GithubListTagsBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "tag", + { + "name": "v1.0.0", + "url": "https://github.com/owner/repo/tree/v1.0.0", + }, + ) + ], + test_mock={ + "list_tags": lambda *args, **kwargs: [ + { + "name": "v1.0.0", + "url": "https://github.com/owner/repo/tree/v1.0.0", + } + ] + }, + ) + + @staticmethod + def list_tags( + credentials: GithubCredentials, repo_url: str + ) -> list[Output.TagItem]: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/tags" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + data = response.json() + tags: list[GithubListTagsBlock.Output.TagItem] = [ + { + "name": tag["name"], + "url": f"https://github.com/{repo_path}/tree/{tag['name']}", + } + for tag in data + ] + + return tags + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + tags = self.list_tags( + credentials, + input_data.repo_url, + ) + yield from (("tag", tag) for tag in tags) + except Exception as e: + yield "error", f"Failed to list tags: {str(e)}" + + +class GithubListBranchesBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + + class Output(BlockSchema): + class BranchItem(TypedDict): + name: str + url: str + + branch: BranchItem = SchemaField( + title="Branch", + description="Branches with their name and file tree browser URL", + ) + error: str = SchemaField(description="Error message if listing branches failed") + + def __init__(self): + super().__init__( + id="74243e49-2bec-4916-8bf4-db43d44aead5", + description="This block lists all branches for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListBranchesBlock.Input, + output_schema=GithubListBranchesBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "branch", + { + "name": "main", + "url": "https://github.com/owner/repo/tree/main", + }, + ) + ], + test_mock={ + "list_branches": lambda *args, **kwargs: [ + { + "name": "main", + "url": "https://github.com/owner/repo/tree/main", + } + ] + }, + ) + + @staticmethod + def list_branches( + credentials: GithubCredentials, repo_url: str + ) -> list[Output.BranchItem]: + api_url = repo_url.replace("github.com", "api.github.com/repos") + "/branches" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + data = response.json() + branches: list[GithubListBranchesBlock.Output.BranchItem] = [ + {"name": branch["name"], "url": branch["commit"]["url"]} for branch in data + ] + + return branches + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + branches = self.list_branches( + credentials, + input_data.repo_url, + ) + yield from (("branch", branch) for branch in branches) + except Exception as e: + yield "error", f"Failed to list branches: {str(e)}" + + +class GithubListDiscussionsBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + num_discussions: int = SchemaField( + description="Number of discussions to fetch", default=5 + ) + + class Output(BlockSchema): + class DiscussionItem(TypedDict): + title: str + url: str + + discussion: DiscussionItem = SchemaField( + title="Discussion", description="Discussions with their title and URL" + ) + error: str = SchemaField( + description="Error message if listing discussions failed" + ) + + def __init__(self): + super().__init__( + id="3ef1a419-3d76-4e07-b761-de9dad4d51d7", + description="This block lists recent discussions for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListDiscussionsBlock.Input, + output_schema=GithubListDiscussionsBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "num_discussions": 3, + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "discussion", + { + "title": "Discussion 1", + "url": "https://github.com/owner/repo/discussions/1", + }, + ) + ], + test_mock={ + "list_discussions": lambda *args, **kwargs: [ + { + "title": "Discussion 1", + "url": "https://github.com/owner/repo/discussions/1", + } + ] + }, + ) + + @staticmethod + def list_discussions( + credentials: GithubCredentials, repo_url: str, num_discussions: int + ) -> list[Output.DiscussionItem]: + repo_path = repo_url.replace("https://github.com/", "") + owner, repo = repo_path.split("/") + query = """ + query($owner: String!, $repo: String!, $num: Int!) { + repository(owner: $owner, name: $repo) { + discussions(first: $num) { + nodes { + title + url + } + } + } + } + """ + variables = {"owner": owner, "repo": repo, "num": num_discussions} + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.post( + "https://api.github.com/graphql", + json={"query": query, "variables": variables}, + headers=headers, + ) + response.raise_for_status() + + data = response.json() + discussions: list[GithubListDiscussionsBlock.Output.DiscussionItem] = [ + {"title": discussion["title"], "url": discussion["url"]} + for discussion in data["data"]["repository"]["discussions"]["nodes"] + ] + + return discussions + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + discussions = self.list_discussions( + credentials, input_data.repo_url, input_data.num_discussions + ) + yield from (("discussion", discussion) for discussion in discussions) + except Exception as e: + yield "error", f"Failed to list discussions: {str(e)}" + + +class GithubListReleasesBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + + class Output(BlockSchema): + class ReleaseItem(TypedDict): + name: str + url: str + + release: ReleaseItem = SchemaField( + title="Release", + description="Releases with their name and file tree browser URL", + ) + error: str = SchemaField(description="Error message if listing releases failed") + + def __init__(self): + super().__init__( + id="3460367a-6ba7-4645-8ce6-47b05d040b92", + description="This block lists all releases for a specified GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubListReleasesBlock.Input, + output_schema=GithubListReleasesBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "release", + { + "name": "v1.0.0", + "url": "https://github.com/owner/repo/releases/tag/v1.0.0", + }, + ) + ], + test_mock={ + "list_releases": lambda *args, **kwargs: [ + { + "name": "v1.0.0", + "url": "https://github.com/owner/repo/releases/tag/v1.0.0", + } + ] + }, + ) + + @staticmethod + def list_releases( + credentials: GithubCredentials, repo_url: str + ) -> list[Output.ReleaseItem]: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/releases" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + data = response.json() + releases: list[GithubListReleasesBlock.Output.ReleaseItem] = [ + {"name": release["name"], "url": release["html_url"]} for release in data + ] + + return releases + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + releases = self.list_releases( + credentials, + input_data.repo_url, + ) + yield from (("release", release) for release in releases) + except Exception as e: + yield "error", f"Failed to list releases: {str(e)}" + + +class GithubReadFileBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + file_path: str = SchemaField( + description="Path to the file in the repository", + placeholder="path/to/file", + ) + branch: str = SchemaField( + description="Branch to read from", + placeholder="branch_name", + default="master", + ) + + class Output(BlockSchema): + text_content: str = SchemaField( + description="Content of the file (decoded as UTF-8 text)" + ) + raw_content: str = SchemaField( + description="Raw base64-encoded content of the file" + ) + size: int = SchemaField(description="The size of the file (in bytes)") + error: str = SchemaField(description="Error message if the file reading failed") + + def __init__(self): + super().__init__( + id="87ce6c27-5752-4bbc-8e26-6da40a3dcfd3", + description="This block reads the content of a specified file from a GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubReadFileBlock.Input, + output_schema=GithubReadFileBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "file_path": "path/to/file", + "branch": "master", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("raw_content", "RmlsZSBjb250ZW50"), + ("text_content", "File content"), + ("size", 13), + ], + test_mock={"read_file": lambda *args, **kwargs: ("RmlsZSBjb250ZW50", 13)}, + ) + + @staticmethod + def read_file( + credentials: GithubCredentials, repo_url: str, file_path: str, branch: str + ) -> tuple[str, int]: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/contents/{file_path}?ref={branch}" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + content = response.json() + + if isinstance(content, list): + # Multiple entries of different types exist at this path + if not (file := next((f for f in content if f["type"] == "file"), None)): + raise TypeError("Not a file") + content = file + + if content["type"] != "file": + raise TypeError("Not a file") + + return content["content"], content["size"] + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + raw_content, size = self.read_file( + credentials, + input_data.repo_url, + input_data.file_path.lstrip("/"), + input_data.branch, + ) + yield "raw_content", raw_content + yield "text_content", base64.b64decode(raw_content).decode("utf-8") + yield "size", size + except Exception as e: + yield "error", f"Failed to read file: {str(e)}" + + +class GithubReadFolderBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + folder_path: str = SchemaField( + description="Path to the folder in the repository", + placeholder="path/to/folder", + ) + branch: str = SchemaField( + description="Branch name to read from (defaults to master)", + placeholder="branch_name", + default="master", + ) + + class Output(BlockSchema): + class DirEntry(TypedDict): + name: str + path: str + + class FileEntry(TypedDict): + name: str + path: str + size: int + + file: FileEntry = SchemaField(description="Files in the folder") + dir: DirEntry = SchemaField(description="Directories in the folder") + error: str = SchemaField( + description="Error message if reading the folder failed" + ) + + def __init__(self): + super().__init__( + id="1355f863-2db3-4d75-9fba-f91e8a8ca400", + description="This block reads the content of a specified folder from a GitHub repository.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubReadFolderBlock.Input, + output_schema=GithubReadFolderBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "folder_path": "path/to/folder", + "branch": "master", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "file", + { + "name": "file1.txt", + "path": "path/to/folder/file1.txt", + "size": 1337, + }, + ), + ("dir", {"name": "dir2", "path": "path/to/folder/dir2"}), + ], + test_mock={ + "read_folder": lambda *args, **kwargs: ( + [ + { + "name": "file1.txt", + "path": "path/to/folder/file1.txt", + "size": 1337, + } + ], + [{"name": "dir2", "path": "path/to/folder/dir2"}], + ) + }, + ) + + @staticmethod + def read_folder( + credentials: GithubCredentials, repo_url: str, folder_path: str, branch: str + ) -> tuple[list[Output.FileEntry], list[Output.DirEntry]]: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/contents/{folder_path}?ref={branch}" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + content = response.json() + + if isinstance(content, list): + # Multiple entries of different types exist at this path + if not (dir := next((d for d in content if d["type"] == "dir"), None)): + raise TypeError("Not a folder") + content = dir + + if content["type"] != "dir": + raise TypeError("Not a folder") + + return ( + [ + GithubReadFolderBlock.Output.FileEntry( + name=entry["name"], + path=entry["path"], + size=entry["size"], + ) + for entry in content["entries"] + if entry["type"] == "file" + ], + [ + GithubReadFolderBlock.Output.DirEntry( + name=entry["name"], + path=entry["path"], + ) + for entry in content["entries"] + if entry["type"] == "dir" + ], + ) + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + files, dirs = 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) + except Exception as e: + yield "error", f"Failed to read folder: {str(e)}" + + +class GithubMakeBranchBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + new_branch: str = SchemaField( + description="Name of the new branch", + placeholder="new_branch_name", + ) + source_branch: str = SchemaField( + description="Name of the source branch", + placeholder="source_branch_name", + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Status of the branch creation operation") + error: str = SchemaField( + description="Error message if the branch creation failed" + ) + + def __init__(self): + super().__init__( + id="944cc076-95e7-4d1b-b6b6-b15d8ee5448d", + description="This block creates a new branch from a specified source branch.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubMakeBranchBlock.Input, + output_schema=GithubMakeBranchBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "new_branch": "new_branch_name", + "source_branch": "source_branch_name", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Branch created successfully")], + test_mock={ + "create_branch": lambda *args, **kwargs: "Branch created successfully" + }, + ) + + @staticmethod + def create_branch( + credentials: GithubCredentials, + repo_url: str, + new_branch: str, + source_branch: str, + ) -> str: + repo_path = repo_url.replace("https://github.com/", "") + ref_api_url = ( + f"https://api.github.com/repos/{repo_path}/git/refs/heads/{source_branch}" + ) + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(ref_api_url, headers=headers) + response.raise_for_status() + + sha = response.json()["object"]["sha"] + + create_branch_api_url = f"https://api.github.com/repos/{repo_path}/git/refs" + data = {"ref": f"refs/heads/{new_branch}", "sha": sha} + + response = requests.post(create_branch_api_url, headers=headers, json=data) + response.raise_for_status() + + return "Branch created successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.create_branch( + credentials, + input_data.repo_url, + input_data.new_branch, + input_data.source_branch, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to create branch: {str(e)}" + + +class GithubDeleteBranchBlock(Block): + class Input(BlockSchema): + credentials: GithubCredentialsInput = GithubCredentialsField("repo") + repo_url: str = SchemaField( + description="URL of the GitHub repository", + placeholder="https://github.com/owner/repo", + ) + branch: str = SchemaField( + description="Name of the branch to delete", + placeholder="branch_name", + ) + + class Output(BlockSchema): + status: str = SchemaField(description="Status of the branch deletion operation") + error: str = SchemaField( + description="Error message if the branch deletion failed" + ) + + def __init__(self): + super().__init__( + id="0d4130f7-e0ab-4d55-adc3-0a40225e80f4", + description="This block deletes a specified branch.", + categories={BlockCategory.DEVELOPER_TOOLS}, + input_schema=GithubDeleteBranchBlock.Input, + output_schema=GithubDeleteBranchBlock.Output, + test_input={ + "repo_url": "https://github.com/owner/repo", + "branch": "branch_name", + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("status", "Branch deleted successfully")], + test_mock={ + "delete_branch": lambda *args, **kwargs: "Branch deleted successfully" + }, + ) + + @staticmethod + def delete_branch( + credentials: GithubCredentials, repo_url: str, branch: str + ) -> str: + repo_path = repo_url.replace("https://github.com/", "") + api_url = f"https://api.github.com/repos/{repo_path}/git/refs/heads/{branch}" + headers = { + "Authorization": credentials.bearer(), + "Accept": "application/vnd.github.v3+json", + } + + response = requests.delete(api_url, headers=headers) + response.raise_for_status() + + return "Branch deleted successfully" + + def run( + self, + input_data: Input, + *, + credentials: GithubCredentials, + **kwargs, + ) -> BlockOutput: + try: + status = self.delete_branch( + credentials, + input_data.repo_url, + input_data.branch, + ) + yield "status", status + except Exception as e: + yield "error", f"Failed to delete branch: {str(e)}" diff --git a/autogpt_platform/backend/backend/blocks/http.py b/autogpt_platform/backend/backend/blocks/http.py index e257b7f361..04d893f847 100644 --- a/autogpt_platform/backend/backend/blocks/http.py +++ b/autogpt_platform/backend/backend/blocks/http.py @@ -37,7 +37,7 @@ class SendWebRequestBlock(Block): output_schema=SendWebRequestBlock.Output, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: if isinstance(input_data.body, str): input_data.body = json.loads(input_data.body) diff --git a/autogpt_platform/backend/backend/blocks/iteration.py b/autogpt_platform/backend/backend/blocks/iteration.py index 7356fe7421..7da5b7703c 100644 --- a/autogpt_platform/backend/backend/blocks/iteration.py +++ b/autogpt_platform/backend/backend/blocks/iteration.py @@ -31,6 +31,6 @@ class ListIteratorBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: for index, item in enumerate(input_data.items): yield "item", (index, item) diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index ead18b2dbf..09cf8af3f9 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -203,7 +203,7 @@ class AIStructuredResponseGeneratorBlock(Block): else: raise ValueError(f"Unsupported LLM provider: {provider}") - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: prompt = [] def trim_prompt(s: str) -> str: @@ -341,7 +341,7 @@ class AITextGeneratorBlock(Block): raise RuntimeError(output_data) raise ValueError("Failed to get a response from the LLM.") - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: object_input_data = AIStructuredResponseGeneratorBlock.Input( **{attr: getattr(input_data, attr) for attr in input_data.model_fields}, @@ -383,7 +383,7 @@ class AITextSummarizerBlock(Block): }, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: for output in self._run(input_data): yield output @@ -582,7 +582,7 @@ class AIConversationBlock(Block): else: raise ValueError(f"Unsupported LLM provider: {provider}") - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: api_key = ( input_data.api_key.get_secret_value() diff --git a/autogpt_platform/backend/backend/blocks/maths.py b/autogpt_platform/backend/backend/blocks/maths.py index f5e1e1fe83..4a1301c9a6 100644 --- a/autogpt_platform/backend/backend/blocks/maths.py +++ b/autogpt_platform/backend/backend/blocks/maths.py @@ -51,7 +51,7 @@ class CalculatorBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: operation = input_data.operation a = input_data.a b = input_data.b @@ -105,7 +105,7 @@ class CountItemsBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: collection = input_data.collection try: diff --git a/autogpt_platform/backend/backend/blocks/medium.py b/autogpt_platform/backend/backend/blocks/medium.py index 941e6c0906..034f3cfa4b 100644 --- a/autogpt_platform/backend/backend/blocks/medium.py +++ b/autogpt_platform/backend/backend/blocks/medium.py @@ -136,7 +136,7 @@ class PublishToMediumBlock(Block): return response.json() - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: response = self.create_post( input_data.api_key.get_secret_value(), diff --git a/autogpt_platform/backend/backend/blocks/reddit.py b/autogpt_platform/backend/backend/blocks/reddit.py index be067e1a3e..065436ae73 100644 --- a/autogpt_platform/backend/backend/blocks/reddit.py +++ b/autogpt_platform/backend/backend/blocks/reddit.py @@ -116,7 +116,7 @@ class GetRedditPostsBlock(Block): subreddit = client.subreddit(input_data.subreddit) return subreddit.new(limit=input_data.post_limit) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: current_time = datetime.now(tz=timezone.utc) for post in self.get_posts(input_data): if input_data.last_minutes: @@ -167,5 +167,5 @@ class PostRedditCommentBlock(Block): comment = submission.reply(comment.comment) return comment.id # type: ignore - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: yield "comment_id", self.reply_post(input_data.creds, input_data.data) diff --git a/autogpt_platform/backend/backend/blocks/rss.py b/autogpt_platform/backend/backend/blocks/rss.py index 02f5139d0c..74184acc0d 100644 --- a/autogpt_platform/backend/backend/blocks/rss.py +++ b/autogpt_platform/backend/backend/blocks/rss.py @@ -86,7 +86,7 @@ class ReadRSSFeedBlock(Block): def parse_feed(url: str) -> dict[str, Any]: return feedparser.parse(url) # type: ignore - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: keep_going = True start_time = datetime.now(timezone.utc) - timedelta( minutes=input_data.time_period diff --git a/autogpt_platform/backend/backend/blocks/sampling.py b/autogpt_platform/backend/backend/blocks/sampling.py index 144abe1d83..d2257db06f 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) -> BlockOutput: + 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/search.py b/autogpt_platform/backend/backend/blocks/search.py index a919a22c16..1a1ec098e4 100644 --- a/autogpt_platform/backend/backend/blocks/search.py +++ b/autogpt_platform/backend/backend/blocks/search.py @@ -35,7 +35,7 @@ class GetWikipediaSummaryBlock(Block, GetRequest): test_mock={"get_request": lambda url, json: {"extract": "summary content"}}, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: topic = input_data.topic url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}" @@ -72,7 +72,7 @@ class SearchTheWebBlock(Block, GetRequest): test_mock={"get_request": lambda url, json: "search content"}, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # Encode the search query encoded_query = quote(input_data.query) @@ -113,7 +113,7 @@ class ExtractWebsiteContentBlock(Block, GetRequest): test_mock={"get_request": lambda url, json: "scraped content"}, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # Prepend the Jina-ai Reader URL to the input URL jina_url = f"https://r.jina.ai/{input_data.url}" @@ -166,7 +166,7 @@ class GetWeatherInformationBlock(Block, GetRequest): }, ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: units = "metric" if input_data.use_celsius else "imperial" api_key = input_data.api_key.get_secret_value() diff --git a/autogpt_platform/backend/backend/blocks/talking_head.py b/autogpt_platform/backend/backend/blocks/talking_head.py index 4cf207e908..e1851ae030 100644 --- a/autogpt_platform/backend/backend/blocks/talking_head.py +++ b/autogpt_platform/backend/backend/blocks/talking_head.py @@ -105,7 +105,7 @@ class CreateTalkingAvatarVideoBlock(Block): response.raise_for_status() return response.json() - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: # Create the clip payload = { diff --git a/autogpt_platform/backend/backend/blocks/text.py b/autogpt_platform/backend/backend/blocks/text.py index 6abbb28036..12215de61f 100644 --- a/autogpt_platform/backend/backend/blocks/text.py +++ b/autogpt_platform/backend/backend/blocks/text.py @@ -45,7 +45,7 @@ class MatchTextPatternBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: output = input_data.data or input_data.text flags = 0 if not input_data.case_sensitive: @@ -97,7 +97,7 @@ class ExtractTextInformationBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: flags = 0 if not input_data.case_sensitive: flags = flags | re.IGNORECASE @@ -147,7 +147,7 @@ class FillTextTemplateBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: # For python.format compatibility: replace all {...} with {{..}}. # But avoid replacing {{...}} to {{{...}}}. fmt = re.sub(r"(?}", input_data.format) @@ -180,6 +180,6 @@ class CombineTextsBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: combined_text = input_data.delimiter.join(input_data.input) yield "output", combined_text diff --git a/autogpt_platform/backend/backend/blocks/time_blocks.py b/autogpt_platform/backend/backend/blocks/time_blocks.py index ad59a54f2a..338ee88a42 100644 --- a/autogpt_platform/backend/backend/blocks/time_blocks.py +++ b/autogpt_platform/backend/backend/blocks/time_blocks.py @@ -27,7 +27,7 @@ class GetCurrentTimeBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: current_time = time.strftime("%H:%M:%S") yield "time", current_time @@ -59,7 +59,7 @@ class GetCurrentDateBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: offset = int(input_data.offset) except ValueError: @@ -96,7 +96,7 @@ class GetCurrentDateAndTimeBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: current_date_time = time.strftime("%Y-%m-%d %H:%M:%S") yield "date_time", current_date_time @@ -129,7 +129,7 @@ class CountdownTimerBlock(Block): ], ) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: seconds = int(input_data.seconds) minutes = int(input_data.minutes) hours = int(input_data.hours) diff --git a/autogpt_platform/backend/backend/blocks/youtube.py b/autogpt_platform/backend/backend/blocks/youtube.py index 08872d2740..a5d48e5a30 100644 --- a/autogpt_platform/backend/backend/blocks/youtube.py +++ b/autogpt_platform/backend/backend/blocks/youtube.py @@ -62,7 +62,7 @@ class TranscribeYouTubeVideoBlock(Block): def get_transcript(video_id: str): return YouTubeTranscriptApi.get_transcript(video_id) - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: video_id = self.extract_video_id(input_data.youtube_url) yield "video_id", video_id diff --git a/autogpt_platform/backend/backend/data/block.py b/autogpt_platform/backend/backend/data/block.py index 46af5ee487..f4d05d648a 100644 --- a/autogpt_platform/backend/backend/data/block.py +++ b/autogpt_platform/backend/backend/data/block.py @@ -1,15 +1,28 @@ +import inspect from abc import ABC, abstractmethod from enum import Enum -from typing import Any, ClassVar, Generator, Generic, Type, TypeVar, cast +from typing import ( + Any, + ClassVar, + Generator, + Generic, + Optional, + Type, + TypeVar, + cast, + get_origin, +) import jsonref import jsonschema +from autogpt_libs.supabase_integration_credentials_store.types import Credentials from prisma.models import AgentBlock from pydantic import BaseModel -from backend.data.model import ContributorDetails from backend.util import json +from .model import CREDENTIALS_FIELD_NAME, ContributorDetails, CredentialsMetaInput + 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. @@ -36,6 +49,7 @@ class BlockCategory(Enum): INPUT = "Block that interacts with input of the graph." OUTPUT = "Block that interacts with output of the graph." LOGIC = "Programming logic to control the flow of your agent" + DEVELOPER_TOOLS = "Developer tools such as GitHub blocks." def dict(self) -> dict[str, str]: return {"category": self.name, "description": self.value} @@ -49,7 +63,7 @@ class BlockSchema(BaseModel): if cls.cached_jsonschema: return cls.cached_jsonschema - model = jsonref.replace_refs(cls.model_json_schema()) + model = jsonref.replace_refs(cls.model_json_schema(), merge_props=True) def ref_to_dict(obj): if isinstance(obj, dict): @@ -122,6 +136,46 @@ class BlockSchema(BaseModel): if field_info.is_required() } + @classmethod + def __pydantic_init_subclass__(cls, **kwargs): + """Validates the schema definition. Rules: + - Only one `CredentialsMetaInput` field may be present. + - This field MUST be called `credentials`. + - A field that is called `credentials` MUST be a `CredentialsMetaInput`. + """ + super().__pydantic_init_subclass__(**kwargs) + credentials_fields = [ + field_name + for field_name, info in cls.model_fields.items() + if ( + inspect.isclass(info.annotation) + and issubclass( + get_origin(info.annotation) or info.annotation, + CredentialsMetaInput, + ) + ) + ] + if len(credentials_fields) > 1: + raise ValueError( + f"{cls.__qualname__} can only have one CredentialsMetaInput field" + ) + elif ( + len(credentials_fields) == 1 + and credentials_fields[0] != CREDENTIALS_FIELD_NAME + ): + raise ValueError( + f"CredentialsMetaInput field on {cls.__qualname__} " + "must be named 'credentials'" + ) + elif ( + len(credentials_fields) == 0 + and CREDENTIALS_FIELD_NAME in cls.model_fields.keys() + ): + raise TypeError( + f"Field 'credentials' on {cls.__qualname__} " + f"must be of type {CredentialsMetaInput.__name__}" + ) + BlockSchemaInputType = TypeVar("BlockSchemaInputType", bound=BlockSchema) BlockSchemaOutputType = TypeVar("BlockSchemaOutputType", bound=BlockSchema) @@ -143,6 +197,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): test_input: BlockInput | list[BlockInput] | None = None, test_output: BlockData | list[BlockData] | None = None, test_mock: dict[str, Any] | None = None, + test_credentials: Optional[Credentials] = None, disabled: bool = False, static_output: bool = False, ui_type: BlockUIType = BlockUIType.STANDARD, @@ -170,6 +225,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): self.test_input = test_input self.test_output = test_output self.test_mock = test_mock + self.test_credentials = test_credentials self.description = description self.categories = categories or set() self.contributors = contributors or set() @@ -178,7 +234,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): self.ui_type = ui_type @abstractmethod - def run(self, input_data: BlockSchemaInputType) -> BlockOutput: + def run(self, input_data: BlockSchemaInputType, **kwargs) -> BlockOutput: """ Run the block with the given input data. Args: @@ -209,13 +265,15 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): "uiType": self.ui_type.value, } - def execute(self, input_data: BlockInput) -> BlockOutput: + 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(self.input_schema(**input_data)): + for output_name, output_data in self.run( + self.input_schema(**input_data), **kwargs + ): if error := self.output_schema.validate_field(output_name, output_data): raise ValueError(f"Block produced an invalid output data: {error}") yield output_name, output_data diff --git a/autogpt_platform/backend/backend/data/execution.py b/autogpt_platform/backend/backend/data/execution.py index 8e3449dd5c..962bbfa293 100644 --- a/autogpt_platform/backend/backend/data/execution.py +++ b/autogpt_platform/backend/backend/data/execution.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from multiprocessing import Manager from typing import Any, Generic, TypeVar +from autogpt_libs.supabase_integration_credentials_store.types import Credentials from prisma.enums import AgentExecutionStatus from prisma.models import ( AgentGraphExecution, @@ -25,6 +26,7 @@ class GraphExecution(BaseModel): graph_exec_id: str graph_id: str start_node_execs: list["NodeExecution"] + node_input_credentials: dict[str, Credentials] # dict[node_id, Credentials] class NodeExecution(BaseModel): diff --git a/autogpt_platform/backend/backend/data/model.py b/autogpt_platform/backend/backend/data/model.py index 31f3a70345..0ca9d699c1 100644 --- a/autogpt_platform/backend/backend/data/model.py +++ b/autogpt_platform/backend/backend/data/model.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging -from typing import Any, Callable, ClassVar, Optional, TypeVar +from typing import Any, Callable, ClassVar, Generic, Optional, TypeVar +from autogpt_libs.supabase_integration_credentials_store.types import CredentialsType from pydantic import BaseModel, Field, GetCoreSchemaHandler from pydantic_core import ( CoreSchema, @@ -136,5 +137,50 @@ def SchemaField( ) +CP = TypeVar("CP", bound=str) +CT = TypeVar("CT", bound=CredentialsType) + + +CREDENTIALS_FIELD_NAME = "credentials" + + +class CredentialsMetaInput(BaseModel, Generic[CP, CT]): + id: str + title: Optional[str] = None + provider: CP + type: CT + + +def CredentialsField( + provider: CP, + supported_credential_types: set[CT], + required_scopes: set[str] = set(), + *, + title: Optional[str] = None, + description: Optional[str] = None, + **kwargs, +) -> CredentialsMetaInput[CP, CT]: + """ + `CredentialsField` must and can only be used on fields named `credentials`. + This is enforced by the `BlockSchema` base class. + """ + json_extra = { + k: v + for k, v in { + "credentials_provider": provider, + "credentials_scopes": list(required_scopes) or None, # omit if empty + "credentials_types": list(supported_credential_types), + }.items() + if v is not None + } + + return Field( + title=title, + description=description, + json_schema_extra=json_extra, + **kwargs, + ) + + class ContributorDetails(BaseModel): name: str = Field(title="Name", description="The name of the contributor.") diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index c52086de6b..33d7d74da3 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -9,7 +9,10 @@ import threading from concurrent.futures import Future, ProcessPoolExecutor from contextlib import contextmanager from multiprocessing.pool import AsyncResult, Pool -from typing import TYPE_CHECKING, Any, Coroutine, Generator, TypeVar +from typing import TYPE_CHECKING, Any, Coroutine, Generator, TypeVar, cast + +from autogpt_libs.supabase_integration_credentials_store.types import Credentials +from pydantic import BaseModel if TYPE_CHECKING: from backend.server.rest_api import AgentServer @@ -37,6 +40,7 @@ from backend.data.execution import ( upsert_execution_output, ) from backend.data.graph import Graph, Link, Node, get_graph, get_node +from backend.data.model import CREDENTIALS_FIELD_NAME, CredentialsMetaInput from backend.util import json from backend.util.decorator import error_logged, time_measured from backend.util.logging import configure_logging @@ -100,6 +104,7 @@ def execute_node( loop: asyncio.AbstractEventLoop, api_client: "AgentServer", data: NodeExecution, + input_credentials: Credentials | None = None, execution_stats: dict[str, Any] | None = None, ) -> ExecutionStream: """ @@ -159,13 +164,19 @@ def execute_node( update_execution(ExecutionStatus.RUNNING) user_credit = get_user_credit_model() + extra_exec_kwargs = {} + if input_credentials: + extra_exec_kwargs["credentials"] = input_credentials + output_size = 0 try: credit = wait(user_credit.get_or_refill_credit(user_id)) if credit < 0: raise ValueError(f"Insufficient credit: {credit}") - for output_name, output_data in node_block.execute(input_data): + for output_name, output_data in node_block.execute( + input_data, **extra_exec_kwargs + ): output_size += len(json.dumps(output_data)) log_metadata.info("Node produced output", output_name=output_data) wait(upsert_execution_output(node_exec_id, output_name, output_data)) @@ -460,7 +471,10 @@ class Executor: @classmethod @error_logged def on_node_execution( - cls, q: ExecutionQueue[NodeExecution], node_exec: NodeExecution + cls, + q: ExecutionQueue[NodeExecution], + node_exec: NodeExecution, + input_credentials: Credentials | None, ): log_metadata = LogMetadata( user_id=node_exec.user_id, @@ -473,7 +487,7 @@ class Executor: execution_stats = {} timing_info, _ = cls._on_node_execution( - q, node_exec, log_metadata, execution_stats + q, node_exec, input_credentials, log_metadata, execution_stats ) execution_stats["walltime"] = timing_info.wall_time execution_stats["cputime"] = timing_info.cpu_time @@ -488,13 +502,14 @@ class Executor: cls, q: ExecutionQueue[NodeExecution], node_exec: NodeExecution, + input_credentials: Credentials | None, log_metadata: LogMetadata, stats: dict[str, Any] | None = None, ): try: log_metadata.info(f"Start node execution {node_exec.node_exec_id}") for execution in execute_node( - cls.loop, cls.agent_server_client, node_exec, stats + cls.loop, cls.agent_server_client, node_exec, input_credentials, stats ): q.add(execution) log_metadata.info(f"Finished node execution {node_exec.node_exec_id}") @@ -624,7 +639,11 @@ class Executor: ) running_executions[exec_data.node_id] = cls.executor.apply_async( cls.on_node_execution, - (queue, exec_data), + ( + queue, + exec_data, + graph_exec.node_input_credentials.get(exec_data.node_id), + ), callback=make_exec_callback(exec_data), ) @@ -660,11 +679,17 @@ class ExecutionManager(AppService): def __init__(self): super().__init__(port=Config().execution_manager_port) self.use_db = True + self.use_supabase = True self.pool_size = Config().num_graph_workers self.queue = ExecutionQueue[GraphExecution]() self.active_graph_runs: dict[str, tuple[Future, threading.Event]] = {} def run_service(self): + from autogpt_libs.supabase_integration_credentials_store import ( + SupabaseIntegrationCredentialsStore, + ) + + self.credentials_store = SupabaseIntegrationCredentialsStore(self.supabase) self.executor = ProcessPoolExecutor( max_workers=self.pool_size, initializer=Executor.on_graph_executor_start, @@ -705,7 +730,10 @@ class ExecutionManager(AppService): graph: Graph | None = self.run_and_wait(get_graph(graph_id, user_id=user_id)) if not graph: raise Exception(f"Graph #{graph_id} not found.") + graph.validate_graph(for_run=True) + node_input_credentials = self._get_node_input_credentials(graph, user_id) + nodes_input = [] for node in graph.starting_nodes: input_data = {} @@ -753,6 +781,7 @@ class ExecutionManager(AppService): graph_id=graph_id, graph_exec_id=graph_exec_id, start_node_execs=starting_node_execs, + node_input_credentials=node_input_credentials, ) self.queue.add(graph_exec) @@ -799,6 +828,58 @@ class ExecutionManager(AppService): ) self.agent_server_client.send_execution_update(exec_update.model_dump()) + def _get_node_input_credentials( + self, graph: Graph, user_id: str + ) -> dict[str, Credentials]: + """Gets all credentials for all nodes of the graph""" + + node_credentials: dict[str, Credentials] = {} + + for node in graph.nodes: + block = get_block(node.block_id) + if not block: + raise ValueError(f"Unknown block {node.block_id} for node #{node.id}") + + # Find any fields of type CredentialsMetaInput + model_fields = cast(type[BaseModel], block.input_schema).model_fields + if CREDENTIALS_FIELD_NAME not in model_fields: + continue + + field = model_fields[CREDENTIALS_FIELD_NAME] + + # The BlockSchema class enforces that a `credentials` field is always a + # `CredentialsMetaInput`, so we can safely assume this here. + credentials_meta_type = cast(CredentialsMetaInput, field.annotation) + credentials_meta = credentials_meta_type.model_validate( + node.input_default[CREDENTIALS_FIELD_NAME] + ) + # Fetch the corresponding Credentials and perform sanity checks + credentials = self.credentials_store.get_creds_by_id( + user_id, credentials_meta.id + ) + if not credentials: + raise ValueError( + f"Unknown credentials #{credentials_meta.id} " + f"for node #{node.id}" + ) + if ( + credentials.provider != credentials_meta.provider + or credentials.type != credentials_meta.type + ): + logger.warning( + f"Invalid credentials #{credentials.id} for node #{node.id}: " + "type/provider mismatch: " + f"{credentials_meta.type}<>{credentials.type};" + f"{credentials_meta.provider}<>{credentials.provider}" + ) + raise ValueError( + f"Invalid credentials #{credentials.id} for node #{node.id}: " + "type/provider mismatch" + ) + node_credentials[node.id] = credentials + + return node_credentials + def llprint(message: str): """ diff --git a/autogpt_platform/backend/backend/server/routers/integrations.py b/autogpt_platform/backend/backend/server/routers/integrations.py index 424772a02b..5f0fa411f0 100644 --- a/autogpt_platform/backend/backend/server/routers/integrations.py +++ b/autogpt_platform/backend/backend/server/routers/integrations.py @@ -1,15 +1,26 @@ import logging -from typing import Annotated, Literal +from typing import Annotated from autogpt_libs.supabase_integration_credentials_store import ( SupabaseIntegrationCredentialsStore, ) from autogpt_libs.supabase_integration_credentials_store.types import ( + APIKeyCredentials, Credentials, + CredentialsType, OAuth2Credentials, ) -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request -from pydantic import BaseModel +from fastapi import ( + APIRouter, + Body, + Depends, + HTTPException, + Path, + Query, + Request, + Response, +) +from pydantic import BaseModel, SecretStr from supabase import Client from backend.integrations.oauth import HANDLERS_BY_NAME, BaseOAuthHandler @@ -28,6 +39,7 @@ def get_store(supabase: Client = Depends(get_supabase)): class LoginResponse(BaseModel): login_url: str + state_token: str @router.get("/{provider}/login") @@ -43,17 +55,17 @@ async def login( handler = _get_provider_oauth_handler(request, provider) # Generate and store a secure random state token - state = await store.store_state_token(user_id, provider) + state_token = await store.store_state_token(user_id, provider) requested_scopes = scopes.split(",") if scopes else [] - login_url = handler.get_login_url(requested_scopes, state) + login_url = handler.get_login_url(requested_scopes, state_token) - return LoginResponse(login_url=login_url) + return LoginResponse(login_url=login_url, state_token=state_token) class CredentialsMetaResponse(BaseModel): id: str - type: Literal["oauth2", "api_key"] + type: CredentialsType title: str | None scopes: list[str] | None username: str | None @@ -127,6 +139,52 @@ async def get_credential( return credential +@router.post("/{provider}/credentials", status_code=201) +async def create_api_key_credentials( + store: Annotated[SupabaseIntegrationCredentialsStore, Depends(get_store)], + user_id: Annotated[str, Depends(get_user_id)], + provider: Annotated[str, Path(title="The provider to create credentials for")], + api_key: Annotated[str, Body(title="The API key to store")], + title: Annotated[str, Body(title="Optional title for the credentials")], + expires_at: Annotated[ + int | None, Body(title="Unix timestamp when the key expires") + ] = None, +) -> APIKeyCredentials: + new_credentials = APIKeyCredentials( + provider=provider, + api_key=SecretStr(api_key), + title=title, + expires_at=expires_at, + ) + + try: + store.add_creds(user_id, new_credentials) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to store credentials: {str(e)}" + ) + return new_credentials + + +@router.delete("/{provider}/credentials/{cred_id}", status_code=204) +async def delete_credential( + provider: Annotated[str, Path(title="The provider to delete credentials for")], + cred_id: Annotated[str, Path(title="The ID of the credentials to delete")], + user_id: Annotated[str, Depends(get_user_id)], + store: Annotated[SupabaseIntegrationCredentialsStore, Depends(get_store)], +): + creds = 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: + raise HTTPException( + status_code=404, detail="Credentials do not match the specified provider" + ) + + store.delete_creds_by_id(user_id, cred_id) + return Response(status_code=204) + + # -------- UTILITIES --------- # @@ -145,8 +203,9 @@ def _get_provider_oauth_handler(req: Request, provider_name: str) -> BaseOAuthHa ) handler_class = HANDLERS_BY_NAME[provider_name] + frontend_base_url = settings.config.frontend_base_url or str(req.base_url) return handler_class( client_id=client_id, client_secret=client_secret, - redirect_uri=str(req.url_for("callback", provider=provider_name)), + redirect_uri=f"{frontend_base_url}/auth/integrations/oauth_callback", ) diff --git a/autogpt_platform/backend/backend/server/utils.py b/autogpt_platform/backend/backend/server/utils.py index fc6e9c9c5d..5e01bb0518 100644 --- a/autogpt_platform/backend/backend/server/utils.py +++ b/autogpt_platform/backend/backend/server/utils.py @@ -20,4 +20,6 @@ def get_user_id(payload: dict = Depends(auth_middleware)) -> str: def get_supabase() -> Client: - return create_client(settings.secrets.supabase_url, settings.secrets.supabase_key) + return create_client( + settings.secrets.supabase_url, settings.secrets.supabase_service_role_key + ) diff --git a/autogpt_platform/backend/backend/util/service.py b/autogpt_platform/backend/backend/util/service.py index fefb7d495b..238121663f 100644 --- a/autogpt_platform/backend/backend/util/service.py +++ b/autogpt_platform/backend/backend/util/service.py @@ -13,7 +13,7 @@ from backend.data import db from backend.data.queue import AsyncEventQueue, AsyncRedisEventQueue from backend.util.process import AppProcess from backend.util.retry import conn_retry -from backend.util.settings import Config +from backend.util.settings import Config, Secrets logger = logging.getLogger(__name__) T = TypeVar("T") @@ -48,6 +48,7 @@ class AppService(AppProcess): event_queue: AsyncEventQueue = AsyncRedisEventQueue() use_db: bool = False use_redis: bool = False + use_supabase: bool = False def __init__(self, port): self.port = port @@ -76,6 +77,13 @@ class AppService(AppProcess): self.shared_event_loop.run_until_complete(db.connect()) if self.use_redis: self.shared_event_loop.run_until_complete(self.event_queue.connect()) + if self.use_supabase: + from supabase import create_client + + secrets = Secrets() + self.supabase = create_client( + secrets.supabase_url, secrets.supabase_service_role_key + ) # Initialize the async loop. async_thread = threading.Thread(target=self.__start_async_loop) diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 54a4240513..83f82ee10f 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -115,6 +115,12 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): description="The port for agent server API to run on", ) + frontend_base_url: str = Field( + default="", + description="Can be used to explicitly set the base URL for the frontend. " + "This value is then used to generate redirect URLs for OAuth flows.", + ) + backend_cors_allow_origins: List[str] = Field(default_factory=list) @field_validator("backend_cors_allow_origins") @@ -166,7 +172,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): """Secrets for the server.""" supabase_url: str = Field(default="", description="Supabase URL") - supabase_key: str = Field(default="", description="Supabase key") + supabase_service_role_key: str = Field( + default="", description="Supabase service role key" + ) # OAuth server credentials for integrations github_client_id: str = Field(default="", description="GitHub OAuth client ID") diff --git a/autogpt_platform/backend/backend/util/test.py b/autogpt_platform/backend/backend/util/test.py index d63d94c9b6..b95e035c9b 100644 --- a/autogpt_platform/backend/backend/util/test.py +++ b/autogpt_platform/backend/backend/util/test.py @@ -4,6 +4,7 @@ import time from backend.data import db from backend.data.block import Block, initialize_blocks from backend.data.execution import ExecutionResult, ExecutionStatus +from backend.data.model import CREDENTIALS_FIELD_NAME from backend.data.queue import AsyncEventQueue from backend.data.user import create_default_user from backend.executor import ExecutionManager, ExecutionScheduler @@ -130,10 +131,19 @@ def execute_block_test(block: Block): else: log(f"{prefix} mock {mock_name} not found in block") + extra_exec_kwargs = {} + + if CREDENTIALS_FIELD_NAME in block.input_schema.model_fields: + if not block.test_credentials: + raise ValueError( + f"{prefix} requires credentials but has no test_credentials" + ) + extra_exec_kwargs[CREDENTIALS_FIELD_NAME] = block.test_credentials + for input_data in block.test_input: log(f"{prefix} in: {input_data}") - for output_name, output_data in block.execute(input_data): + 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") ex_output_name, ex_output_data = block.test_output[output_index] diff --git a/autogpt_platform/backend/test/block/test_block.py b/autogpt_platform/backend/test/block/test_block.py index 43b5871b7b..be16a0b1a7 100644 --- a/autogpt_platform/backend/test/block/test_block.py +++ b/autogpt_platform/backend/test/block/test_block.py @@ -1,7 +1,9 @@ -from backend.data.block import get_blocks +import pytest + +from backend.data.block import Block, get_blocks from backend.util.test import execute_block_test -def test_available_blocks(): - for block in get_blocks().values(): - execute_block_test(type(block)()) +@pytest.mark.parametrize("block", get_blocks().values(), ids=lambda b: b.name) +def test_available_blocks(block: Block): + execute_block_test(type(block)()) diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml index 68fc94bb21..8f0fda2aff 100644 --- a/autogpt_platform/docker-compose.platform.yml +++ b/autogpt_platform/docker-compose.platform.yml @@ -58,7 +58,7 @@ services: environment: - SUPABASE_URL=http://kong:8000 - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - REDIS_HOST=redis - REDIS_PORT=6379 @@ -66,6 +66,7 @@ services: - ENABLE_AUTH=true - PYRO_HOST=0.0.0.0 - EXECUTIONMANAGER_HOST=executor + - FRONTEND_BASE_URL=http://localhost:3000 - BACKEND_CORS_ALLOW_ORIGINS=["http://localhost:3000"] ports: - "8006:8006" @@ -92,9 +93,9 @@ services: migrate: condition: service_completed_successfully environment: - - NEXT_PUBLIC_SUPABASE_URL=http://kong:8000 + - SUPABASE_URL=http://kong:8000 - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE + - SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - REDIS_HOST=redis - REDIS_PORT=6379 @@ -126,9 +127,7 @@ services: migrate: condition: service_completed_successfully environment: - - SUPABASE_URL=http://kong:8000 - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long - - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=platform - REDIS_HOST=redis - REDIS_PORT=6379 diff --git a/autogpt_platform/frontend/src/app/auth/integrations/oauth_callback/route.ts b/autogpt_platform/frontend/src/app/auth/integrations/oauth_callback/route.ts new file mode 100644 index 0000000000..6ec06a46ef --- /dev/null +++ b/autogpt_platform/frontend/src/app/auth/integrations/oauth_callback/route.ts @@ -0,0 +1,38 @@ +import { OAuthPopupResultMessage } from "@/components/integrations/credentials-input"; +import { NextResponse } from "next/server"; + +// This route is intended to be used as the callback for integration OAuth flows, +// 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 code = searchParams.get("code"); + const state = searchParams.get("state"); + + // Send message from popup window to host window + const message: OAuthPopupResultMessage = + code && state + ? { message_type: "oauth_popup_result", success: true, code, state } + : { + message_type: "oauth_popup_result", + success: false, + message: `Incomplete query: ${searchParams.toString()}`, + }; + + // Return a response with the message as JSON and a script to close the window + return new NextResponse( + ` + + + + + + `, + { + headers: { "Content-Type": "text/html" }, + }, + ); +} diff --git a/autogpt_platform/frontend/src/app/providers.tsx b/autogpt_platform/frontend/src/app/providers.tsx index ce27312275..93db52de71 100644 --- a/autogpt_platform/frontend/src/app/providers.tsx +++ b/autogpt_platform/frontend/src/app/providers.tsx @@ -5,12 +5,15 @@ import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProviderProps } from "next-themes/dist/types"; import { TooltipProvider } from "@/components/ui/tooltip"; import SupabaseProvider from "@/components/SupabaseProvider"; +import CredentialsProvider from "@/components/integrations/credentials-provider"; export function Providers({ children, ...props }: ThemeProviderProps) { return ( - {children} + + {children} + ); diff --git a/autogpt_platform/frontend/src/components/CustomNode.tsx b/autogpt_platform/frontend/src/components/CustomNode.tsx index 778de86be5..cf1e10db0b 100644 --- a/autogpt_platform/frontend/src/components/CustomNode.tsx +++ b/autogpt_platform/frontend/src/components/CustomNode.tsx @@ -255,13 +255,19 @@ export function CustomNode({ data, id, width, height }: NodeProps) { return ( (isRequired || isAdvancedOpen || isConnected || !isAdvanced) && (
{}}> - + {"credentials_provider" in propSchema ? ( + + Credentials + + ) : ( + + )} {!isConnected && ( > = { + github: FaGithub, + google: FaGoogle, + notion: NotionLogoIcon, +}; + +export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & ( + | { + success: true; + code: string; + state: string; + } + | { + success: false; + message: string; + } +); + +export const CredentialsInput: FC<{ + className?: string; + selectedCredentials?: CredentialsMetaInput; + onSelectCredentials: (newValue: CredentialsMetaInput) => void; +}> = ({ className, selectedCredentials, onSelectCredentials }) => { + const api = useMemo(() => new AutoGPTServerAPI(), []); + const credentials = useCredentials(); + const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] = + useState(false); + const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false); + const [oAuthPopupController, setOAuthPopupController] = + useState(null); + + if (!credentials) { + return null; + } + + if (credentials.isLoading) { + return
Loading...
; + } + + const { + schema, + provider, + providerName, + supportsApiKey, + supportsOAuth2, + savedApiKeys, + savedOAuthCredentials, + oAuthCallback, + } = credentials; + + async function handleOAuthLogin() { + const { login_url, state_token } = await api.oAuthLogin( + provider, + schema.credentials_scopes, + ); + setOAuth2FlowInProgress(true); + const popup = window.open(login_url, "_blank", "popup=true"); + + const controller = new AbortController(); + setOAuthPopupController(controller); + controller.signal.onabort = () => { + setOAuth2FlowInProgress(false); + popup?.close(); + }; + popup?.addEventListener( + "message", + async (e: MessageEvent) => { + if ( + typeof e.data != "object" || + !( + "message_type" in e.data && + e.data.message_type == "oauth_popup_result" + ) + ) + return; + + if (!e.data.success) { + console.error("OAuth flow failed:", e.data.message); + return; + } + + if (e.data.state !== state_token) return; + + const credentials = await oAuthCallback(e.data.code, e.data.state); + onSelectCredentials({ + id: credentials.id, + type: "oauth2", + title: credentials.title, + provider, + }); + controller.abort("success"); + }, + { signal: controller.signal }, + ); + + setTimeout( + () => { + controller.abort("timeout"); + }, + 5 * 60 * 1000, + ); + } + + const ProviderIcon = providerIcons[provider]; + const modals = ( + <> + {supportsApiKey && ( + setAPICredentialsModalOpen(false)} + onCredentialsCreate={(credsMeta) => { + onSelectCredentials(credsMeta); + setAPICredentialsModalOpen(false); + }} + /> + )} + {supportsOAuth2 && ( + oAuthPopupController?.abort("canceled")} + providerName={providerName} + /> + )} + + ); + + // No saved credentials yet + if (savedApiKeys.length === 0 && savedOAuthCredentials.length === 0) { + return ( + <> +
+ {supportsOAuth2 && ( + + )} + {supportsApiKey && ( + + )} +
+ {modals} + + ); + } + + function handleValueChange(newValue: string) { + if (newValue === "sign-in") { + // Trigger OAuth2 sign in flow + handleOAuthLogin(); + } else if (newValue === "add-api-key") { + // Open API key dialog + setAPICredentialsModalOpen(true); + } else { + const selectedCreds = savedApiKeys + .concat(savedOAuthCredentials) + .find((c) => c.id == newValue)!; + + onSelectCredentials({ + id: selectedCreds.id, + type: selectedCreds.type, + provider: schema.credentials_provider, + // title: customTitle, // TODO: add input for title + }); + } + } + + // Saved credentials exist + return ( + <> + + {modals} + + ); +}; + +export const APIKeyCredentialsModal: FC<{ + open: boolean; + onClose: () => void; + onCredentialsCreate: (creds: CredentialsMetaInput) => void; +}> = ({ open, onClose, onCredentialsCreate }) => { + const credentials = useCredentials(); + + const formSchema = z.object({ + apiKey: z.string().min(1, "API Key is required"), + title: z.string().min(1, "Name is required"), + expiresAt: z.string().optional(), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + apiKey: "", + title: "", + expiresAt: "", + }, + }); + + if (!credentials || credentials.isLoading || !credentials.supportsApiKey) { + return null; + } + + const { schema, provider, providerName, createAPIKeyCredentials } = + credentials; + + async function onSubmit(values: z.infer) { + const expiresAt = values.expiresAt + ? new Date(values.expiresAt).getTime() / 1000 + : undefined; + const newCredentials = await createAPIKeyCredentials({ + api_key: values.apiKey, + title: values.title, + expires_at: expiresAt, + }); + onCredentialsCreate({ + provider, + id: newCredentials.id, + type: "api_key", + title: newCredentials.title, + }); + } + + return ( + { + if (!open) onClose(); + }} + > + + + Add new API key for {providerName} + {schema.description && ( + {schema.description} + )} + + +
+ + ( + + API Key + {schema.credentials_scopes && ( + + Required scope(s) for this block:{" "} + {schema.credentials_scopes?.map((s, i, a) => ( + + {s} + {i < a.length - 1 && ", "} + + ))} + + )} + + + + + + )} + /> + ( + + Name + + + + + + )} + /> + ( + + Expiration Date (Optional) + + + + + + )} + /> + + + +
+
+ ); +}; + +export const OAuth2FlowWaitingModal: FC<{ + open: boolean; + onClose: () => void; + providerName: string; +}> = ({ open, onClose, providerName }) => { + return ( + { + if (!open) onClose(); + }} + > + + + + Waiting on {providerName} sign-in process... + + + Complete the sign-in process in the pop-up window. +
+ Closing this dialog will cancel the sign-in process. +
+
+
+
+ ); +}; diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx new file mode 100644 index 0000000000..96a37e40cd --- /dev/null +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -0,0 +1,164 @@ +import AutoGPTServerAPI, { + APIKeyCredentials, + CredentialsMetaResponse, +} from "@/lib/autogpt-server-api"; +import { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; + +const CREDENTIALS_PROVIDER_NAMES = ["github", "google", "notion"] as const; + +type CredentialsProviderName = (typeof CREDENTIALS_PROVIDER_NAMES)[number]; + +const providerDisplayNames: Record = { + github: "GitHub", + google: "Google", + notion: "Notion", +}; + +type APIKeyCredentialsCreatable = Omit< + APIKeyCredentials, + "id" | "provider" | "type" +>; + +export type CredentialsProviderData = { + provider: string; + providerName: string; + savedApiKeys: CredentialsMetaResponse[]; + savedOAuthCredentials: CredentialsMetaResponse[]; + oAuthCallback: ( + code: string, + state_token: string, + ) => Promise; + createAPIKeyCredentials: ( + credentials: APIKeyCredentialsCreatable, + ) => Promise; +}; + +export type CredentialsProvidersContextType = { + [key in CredentialsProviderName]?: CredentialsProviderData; +}; + +export const CredentialsProvidersContext = + createContext(null); + +export default function CredentialsProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [providers, setProviders] = + useState(null); + const api = useMemo(() => new AutoGPTServerAPI(), []); + + const addCredentials = useCallback( + ( + provider: CredentialsProviderName, + credentials: CredentialsMetaResponse, + ) => { + setProviders((prev) => { + if (!prev || !prev[provider]) return prev; + + const updatedProvider = { ...prev[provider] }; + + if (credentials.type === "api_key") { + updatedProvider.savedApiKeys = [ + ...updatedProvider.savedApiKeys, + credentials, + ]; + } else if (credentials.type === "oauth2") { + updatedProvider.savedOAuthCredentials = [ + ...updatedProvider.savedOAuthCredentials, + credentials, + ]; + } + + return { + ...prev, + [provider]: updatedProvider, + }; + }); + }, + [setProviders], + ); + + /** Wraps `AutoGPTServerAPI.oAuthCallback`, and adds the result to the internal credentials store. */ + const oAuthCallback = useCallback( + async ( + provider: CredentialsProviderName, + code: string, + state_token: string, + ): Promise => { + const credsMeta = await api.oAuthCallback(provider, code, state_token); + addCredentials(provider, credsMeta); + return credsMeta; + }, + [api, addCredentials], + ); + + /** Wraps `AutoGPTServerAPI.createAPIKeyCredentials`, and adds the result to the internal credentials store. */ + const createAPIKeyCredentials = useCallback( + async ( + provider: CredentialsProviderName, + credentials: APIKeyCredentialsCreatable, + ): Promise => { + const credsMeta = await api.createAPIKeyCredentials({ + provider, + ...credentials, + }); + addCredentials(provider, credsMeta); + return credsMeta; + }, + [api, addCredentials], + ); + + useEffect(() => { + api.isAuthenticated().then((isAuthenticated) => { + if (!isAuthenticated) return; + + CREDENTIALS_PROVIDER_NAMES.forEach((provider) => { + api.listCredentials(provider).then((response) => { + const { oauthCreds, apiKeys } = response.reduce<{ + oauthCreds: CredentialsMetaResponse[]; + apiKeys: CredentialsMetaResponse[]; + }>( + (acc, cred) => { + if (cred.type === "oauth2") { + acc.oauthCreds.push(cred); + } else if (cred.type === "api_key") { + acc.apiKeys.push(cred); + } + return acc; + }, + { oauthCreds: [], apiKeys: [] }, + ); + + setProviders((prev) => ({ + ...prev, + [provider]: { + provider, + providerName: providerDisplayNames[provider], + savedApiKeys: apiKeys, + savedOAuthCredentials: oauthCreds, + oAuthCallback: (code: string, state_token: string) => + oAuthCallback(provider, code, state_token), + createAPIKeyCredentials: ( + credentials: APIKeyCredentialsCreatable, + ) => createAPIKeyCredentials(provider, credentials), + }, + })); + }); + }); + }); + }, [api, createAPIKeyCredentials, oAuthCallback]); + + return ( + + {children} + + ); +} diff --git a/autogpt_platform/frontend/src/components/node-input-components.tsx b/autogpt_platform/frontend/src/components/node-input-components.tsx index 9799876756..4841e3358a 100644 --- a/autogpt_platform/frontend/src/components/node-input-components.tsx +++ b/autogpt_platform/frontend/src/components/node-input-components.tsx @@ -9,6 +9,7 @@ import { BlockIOStringSubSchema, BlockIONumberSubSchema, BlockIOBooleanSubSchema, + BlockIOCredentialsSubSchema, } from "@/lib/autogpt-server-api/types"; import React, { FC, useCallback, useEffect, useState } from "react"; import { Button } from "./ui/button"; @@ -23,6 +24,7 @@ import { import { Input } from "./ui/input"; import NodeHandle from "./NodeHandle"; import { ConnectionData } from "./CustomNode"; +import { CredentialsInput } from "./integrations/credentials-input"; type NodeObjectInputTreeProps = { selfKey?: string; @@ -114,6 +116,18 @@ export const NodeGenericInputField: FC<{ console.warn(`Unsupported 'allOf' in schema for '${propKey}'!`, propSchema); } + if ("credentials_provider" in propSchema) { + return ( + + ); + } + if ("properties" in propSchema) { return ( = ({ selfKey, value, errors, handleInputChange, className }) => { + return ( +
+ + handleInputChange(selfKey, credsMeta) + } + selectedCredentials={value} + /> + {errors[selfKey] && ( + {errors[selfKey]} + )} +
+ ); +}; + const NodeKeyValueInput: FC<{ selfKey: string; schema: BlockIOKVSubSchema; diff --git a/autogpt_platform/frontend/src/components/ui/icons.tsx b/autogpt_platform/frontend/src/components/ui/icons.tsx index 46a6ca44a9..9f8687e34f 100644 --- a/autogpt_platform/frontend/src/components/ui/icons.tsx +++ b/autogpt_platform/frontend/src/components/ui/icons.tsx @@ -575,4 +575,148 @@ export const IconMegaphone = createIcon((props) => ( )); +/** + * Key icon component. + * + * @component IconKey + * @param {IconProps} props - The props object containing additional attributes and event handlers for the icon. + * @returns {JSX.Element} - The key icon. + * + * @example + * // Default usage + * + * + * @example + * // With custom color and size + * + * + * @example + * // With custom size and onClick handler + * + */ +export const IconKey = createIcon((props) => ( + + + + +)); + +/** + * Key(+) icon component. + * + * @component IconKeyPlus + * @param {IconProps} props - The props object containing additional attributes and event handlers for the icon. + * @returns {JSX.Element} - The key(+) icon. + * + * @example + * // Default usage + * + * + * @example + * // With custom color and size + * + * + * @example + * // With custom size and onClick handler + * + */ +export const IconKeyPlus = createIcon((props) => ( + + + {/* */} + + + +)); + +/** + * User icon component. + * + * @component IconUser + * @param {IconProps} props - The props object containing additional attributes and event handlers for the icon. + * @returns {JSX.Element} - The user icon. + * + * @example + * // Default usage + * + * + * @example + * // With custom color and size + * + * + * @example + * // With custom size and onClick handler + * + */ +export const IconUser = createIcon((props) => ( + + + + +)); + +/** + * User(+) icon component. + * + * @component IconUserPlus + * @param {IconProps} props - The props object containing additional attributes and event handlers for the icon. + * @returns {JSX.Element} - The user plus icon. + * + * @example + * // Default usage + * + * + * @example + * // With custom color and size + * + * + * @example + * // With custom size and onClick handler + * + */ +export const IconUserPlus = createIcon((props) => ( + + + + + + +)); + export { iconVariants }; diff --git a/autogpt_platform/frontend/src/hooks/useCredentials.ts b/autogpt_platform/frontend/src/hooks/useCredentials.ts new file mode 100644 index 0000000000..ac0720d6cd --- /dev/null +++ b/autogpt_platform/frontend/src/hooks/useCredentials.ts @@ -0,0 +1,77 @@ +import { useContext } from "react"; +import { CustomNodeData } from "@/components/CustomNode"; +import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api"; +import { Node, useNodeId, useNodesData } from "@xyflow/react"; +import { + CredentialsProviderData, + CredentialsProvidersContext, +} from "@/components/integrations/credentials-provider"; + +export type CredentialsData = + | { + provider: string; + schema: BlockIOCredentialsSubSchema; + supportsApiKey: boolean; + supportsOAuth2: boolean; + isLoading: true; + } + | (CredentialsProviderData & { + schema: BlockIOCredentialsSubSchema; + supportsApiKey: boolean; + supportsOAuth2: boolean; + isLoading: false; + }); + +export default function useCredentials(): CredentialsData | null { + const nodeId = useNodeId(); + const allProviders = useContext(CredentialsProvidersContext); + + if (!nodeId) { + throw new Error("useCredentials must be within a CustomNode"); + } + + const data = useNodesData>(nodeId)!.data; + const credentialsSchema = data.inputSchema.properties + .credentials as BlockIOCredentialsSubSchema; + + // If block input schema doesn't have credentials, return null + if (!credentialsSchema) { + return null; + } + + const provider = allProviders + ? allProviders[credentialsSchema?.credentials_provider] + : null; + + const supportsApiKey = + credentialsSchema.credentials_types.includes("api_key"); + const supportsOAuth2 = credentialsSchema.credentials_types.includes("oauth2"); + + // No provider means maybe it's still loading + if (!provider) { + return { + provider: credentialsSchema.credentials_provider, + schema: credentialsSchema, + supportsApiKey, + supportsOAuth2, + isLoading: true, + }; + } + + // Filter by OAuth credentials that have sufficient scopes for this block + const requiredScopes = credentialsSchema.credentials_scopes; + const savedOAuthCredentials = requiredScopes + ? provider.savedOAuthCredentials.filter((c) => + new Set(c.scopes).isSupersetOf(new Set(requiredScopes)), + ) + : provider.savedOAuthCredentials; + + return { + ...provider, + schema: credentialsSchema, + supportsApiKey, + supportsOAuth2, + savedOAuthCredentials, + isLoading: false, + }; +} diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts index ae83c97450..6ce83ed7b9 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/baseClient.ts @@ -1,6 +1,10 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { + AnalyticsMetrics, + AnalyticsDetails, + APIKeyCredentials, Block, + CredentialsMetaResponse, Graph, GraphCreatable, GraphUpdateable, @@ -9,9 +13,8 @@ import { GraphExecuteResponse, ExecutionMeta, NodeExecutionResult, + OAuth2Credentials, User, - AnalyticsMetrics, - AnalyticsDetails, } from "./types"; export default class BaseAutoGPTServerAPI { @@ -34,6 +37,14 @@ export default class BaseAutoGPTServerAPI { this.supabaseClient = supabaseClient; } + async isAuthenticated(): Promise { + if (!this.supabaseClient) return false; + const { + data: { session }, + } = await this.supabaseClient?.auth.getSession(); + return session != null; + } + async createUser(): Promise { return this._request("POST", "/auth/user", {}); } @@ -156,6 +167,53 @@ export default class BaseAutoGPTServerAPI { ).map(parseNodeExecutionResultTimestamps); } + async oAuthLogin( + provider: string, + scopes?: string[], + ): Promise<{ login_url: string; state_token: string }> { + const query = scopes ? { scopes: scopes.join(",") } : undefined; + return await this._get(`/integrations/${provider}/login`, query); + } + + async oAuthCallback( + provider: string, + code: string, + state_token: string, + ): Promise { + return this._request("POST", `/integrations/${provider}/callback`, { + code, + state_token, + }); + } + + async createAPIKeyCredentials( + credentials: Omit, + ): Promise { + return this._request( + "POST", + `/integrations/${credentials.provider}/credentials`, + credentials, + ); + } + + async listCredentials(provider: string): Promise { + return this._get(`/integrations/${provider}/credentials`); + } + + async getCredentials( + provider: string, + id: string, + ): Promise { + return this._get(`/integrations/${provider}/credentials/${id}`); + } + + async deleteCredentials(provider: string, id: string): Promise { + return this._request( + "DELETE", + `/integrations/${provider}/credentials/${id}`, + ); + } + async logMetric(metric: AnalyticsMetrics) { return this._request("POST", "/analytics/log_raw_metric", metric); } @@ -164,14 +222,14 @@ export default class BaseAutoGPTServerAPI { return this._request("POST", "/analytics/log_raw_analytics", analytic); } - private async _get(path: string) { - return this._request("GET", path); + private async _get(path: string, query?: Record) { + return this._request("GET", path, query); } private async _request( - method: "GET" | "POST" | "PUT" | "PATCH", + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", path: string, - payload?: { [key: string]: any }, + payload?: Record, ) { if (method != "GET") { console.debug(`${method} ${path} payload:`, payload); @@ -181,18 +239,25 @@ export default class BaseAutoGPTServerAPI { (await this.supabaseClient?.auth.getSession())?.data.session ?.access_token || ""; - const response = await fetch(this.baseUrl + path, { + let url = this.baseUrl + path; + if (method === "GET" && payload) { + // For GET requests, use payload as query + const queryParams = new URLSearchParams(payload); + url += `?${queryParams.toString()}`; + } + + const hasRequestBody = method !== "GET" && payload !== undefined; + const response = await fetch(url, { method, - headers: - method != "GET" - ? { - "Content-Type": "application/json", - Authorization: token ? `Bearer ${token}` : "", - } - : { - Authorization: token ? `Bearer ${token}` : "", - }, - body: JSON.stringify(payload), + headers: hasRequestBody + ? { + "Content-Type": "application/json", + Authorization: token ? `Bearer ${token}` : "", + } + : { + Authorization: token ? `Bearer ${token}` : "", + }, + body: hasRequestBody ? JSON.stringify(payload) : undefined, }); const response_data = await response.json(); diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index df9f691ceb..03a14f64c5 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -41,6 +41,7 @@ export type BlockIOSubSchema = type BlockIOSimpleTypeSubSchema = | BlockIOObjectSubSchema + | BlockIOCredentialsSubSchema | BlockIOKVSubSchema | BlockIOArraySubSchema | BlockIOStringSubSchema @@ -91,6 +92,14 @@ export type BlockIOBooleanSubSchema = BlockIOSubSchemaMeta & { default?: boolean; }; +export type CredentialsType = "api_key" | "oauth2"; + +export type BlockIOCredentialsSubSchema = BlockIOSubSchemaMeta & { + credentials_provider: "github" | "google" | "notion"; + credentials_scopes?: string[]; + credentials_types: Array; +}; + export type BlockIONullSubSchema = BlockIOSubSchemaMeta & { type: "null"; }; @@ -205,6 +214,51 @@ export type NodeExecutionResult = { end_time?: Date; }; +/* Mirror of backend/server/integrations.py:CredentialsMetaResponse */ +export type CredentialsMetaResponse = { + id: string; + type: CredentialsType; + title?: string; + scopes?: Array; + username?: string; +}; + +/* Mirror of backend/data/model.py:CredentialsMetaInput */ +export type CredentialsMetaInput = { + id: string; + type: CredentialsType; + title?: string; + provider: string; +}; + +/* Mirror of autogpt_libs/supabase_integration_credentials_store/types.py:_BaseCredentials */ +type BaseCredentials = { + id: string; + type: CredentialsType; + title?: string; + provider: string; +}; + +/* Mirror of autogpt_libs/supabase_integration_credentials_store/types.py:OAuth2Credentials */ +export type OAuth2Credentials = BaseCredentials & { + type: "oauth2"; + scopes: string[]; + username?: string; + access_token: string; + access_token_expires_at?: number; + refresh_token?: string; + refresh_token_expires_at?: number; + metadata: Record; +}; + +/* Mirror of autogpt_libs/supabase_integration_credentials_store/types.py:APIKeyCredentials */ +export type APIKeyCredentials = BaseCredentials & { + type: "api_key"; + title: string; + api_key: string; + expires_at?: number; +}; + export type User = { id: string; email: string; diff --git a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml index ae26c71515..8d80932ae7 100644 --- a/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-server/values.dev.yaml @@ -86,3 +86,9 @@ env: REDIS_HOST: "redis-dev-master.redis-dev.svc.cluster.local" REDIS_PORT: "6379" BACKEND_CORS_ALLOW_ORIGINS: ["https://dev-builder.agpt.co"] + SUPABASE_SERVICE_ROLE_KEY: "" + GITHUB_CLIENT_ID: "" + GITHUB_CLIENT_SECRET: "" + FRONTEND_BASE_URL: "" + SUPABASE_URL: "" + SUPABASE_JWT_SECRET: "" diff --git a/docs/content/server/new_blocks.md b/docs/content/server/new_blocks.md index 0b46dee5d8..956d241ed7 100644 --- a/docs/content/server/new_blocks.md +++ b/docs/content/server/new_blocks.md @@ -84,7 +84,7 @@ Follow these steps to create and test a new block: 5. **Implement the `run` method with error handling:**, this should contain the main logic of the block: ```python - def run(self, input_data: Input) -> BlockOutput: + def run(self, input_data: Input, **kwargs) -> BlockOutput: try: topic = input_data.topic url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}" @@ -105,6 +105,145 @@ Follow these steps to create and test a new block: - **Error handling**: Handle various exceptions that might occur during the API request and data processing. - **Yield**: Use `yield` to output the results. +### Blocks with authentication + +Our system supports auth offloading for API keys and OAuth2 authorization flows. +Adding a block with API key authentication is straight-forward, as is adding a block +for a service that we already have OAuth2 support for. + +Implementing the block itself is relatively simple. On top of the instructions above, +you're going to add a `credentials` parameter to the `Input` model and the `run` method: +```python +from autogpt_libs.supabase_integration_credentials_store.types import ( + APIKeyCredentials, + OAuth2Credentials, + Credentials, +) + +from backend.data.block import Block, BlockOutput, BlockSchema +from backend.data.model import CredentialsField + + +# API Key auth: +class BlockWithAPIKeyAuth(Block): + class Input(BlockSchema): + credentials = CredentialsField( + provider="github", + supported_credential_types={"api_key"}, + required_scopes={"repo"}, + description="The GitHub integration can be used with " + "any API key with sufficient permissions for the blocks it is used on.", + ) + + # ... + + def run( + self, + input_data: Input, + *, + credentials: APIKeyCredentials, + **kwargs, + ) -> BlockOutput: + ... + +# OAuth: +class BlockWithOAuth(Block): + class Input(BlockSchema): + credentials = CredentialsField( + provider="github", + supported_credential_types={"oauth2"}, + required_scopes={"repo"}, + description="The GitHub integration can be used with OAuth.", + ) + + # ... + + def run( + self, + input_data: Input, + *, + credentials: OAuth2Credentials, + **kwargs, + ) -> BlockOutput: + ... + +# API Key auth + OAuth: +class BlockWithAPIKeyAndOAuth(Block): + class Input(BlockSchema): + credentials = CredentialsField( + provider="github", + supported_credential_types={"api_key", "oauth2"}, + required_scopes={"repo"}, + description="The GitHub integration can be used with OAuth, " + "or any API key with sufficient permissions for the blocks it is used on.", + ) + + # ... + + def run( + self, + input_data: Input, + *, + credentials: Credentials, + **kwargs, + ) -> BlockOutput: + ... +``` +The credentials will be automagically injected by the executor in the back end. + +The `APIKeyCredentials` and `OAuth2Credentials` models are defined [here](https://github.com/Significant-Gravitas/AutoGPT/blob/master/rnd/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/types.py). +To use them in e.g. an API request, you can either access the token directly: +```python +# credentials: APIKeyCredentials +response = requests.post( + url, + headers={ + "Authorization": f"Bearer {credentials.api_key.get_secret_value()})", + }, +) + +# credentials: OAuth2Credentials +response = requests.post( + url, + headers={ + "Authorization": f"Bearer {credentials.access_token.get_secret_value()})", + }, +) +``` +or use the shortcut `credentials.bearer()`: +```python +# credentials: APIKeyCredentials | OAuth2Credentials +response = requests.post( + url, + headers={"Authorization": credentials.bearer()}, +) +``` + +#### Adding an OAuth2 service integration + +To add support for a new OAuth2-authenticated service, you'll need to add an `OAuthHandler`. +All our existing handlers and the base class can be found [here][OAuth2 handlers]. + +Every handler must implement the following parts of the [`BaseOAuthHandler`] interface: +- `PROVIDER_NAME` +- `__init__(client_id, client_secret, redirect_uri)` +- `get_login_url(scopes, state)` +- `exchange_code_for_tokens(code)` +- `_refresh_tokens(credentials)` + +As you can see, this is modeled after the standard OAuth2 flow. + +Aside from implementing the `OAuthHandler` itself, adding a handler into the system requires two more things: +- Adding the handler class to `HANDLERS_BY_NAME` [here](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/__init__.py) +- Adding `{provider}_client_id` and `{provider}_client_secret` to the application's `Secrets` [here](https://github.com/Significant-Gravitas/AutoGPT/blob/e3f35d79c7e9fc6ee0cabefcb73e0fad15a0ce2d/autogpt_platform/backend/backend/util/settings.py#L132) + +[OAuth2 handlers]: https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/integrations/oauth +[`BaseOAuthHandler`]: https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/base.py + +#### Example: GitHub integration +- GitHub blocks with API key + OAuth2 support: [`blocks/github`](https://github.com/Significant-Gravitas/AutoGPT/tree/master/autogpt_platform/backend/backend/blocks/github/) +- GitHub OAuth2 handler: [`integrations/oauth/github.py`](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/backend/backend/integrations/oauth/github.py) + ## Key Points to Remember - **Unique ID**: Give your block a unique ID in the **init** method. @@ -117,7 +256,8 @@ Follow these steps to create and test a new block: The testing of blocks is handled by `test_block.py`, which does the following: -1. It calls the block with the provided `test_input`. +1. It calls the block with the provided `test_input`. + If the block has a `credentials` field, `test_credentials` is passed in as well. 2. If a `test_mock` is provided, it temporarily replaces the specified methods with the mock functions. 3. It then asserts that the output matches the `test_output`. From aec715cb2b02ce0ff73f190c47a442e5a6fc2e39 Mon Sep 17 00:00:00 2001 From: Aarushi <50577581+aarushik93@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:44:54 +0100 Subject: [PATCH 18/32] tweak(platform) Add frontendconfig for redirect (#8160) * add frontendconfig for redirect * add the files --- .../helm/autogpt-builder/templates/frontendconfig.yaml | 8 ++++++++ .../infra/helm/autogpt-builder/values.dev.yaml | 1 + 2 files changed, 9 insertions(+) create mode 100644 autogpt_platform/infra/helm/autogpt-builder/templates/frontendconfig.yaml diff --git a/autogpt_platform/infra/helm/autogpt-builder/templates/frontendconfig.yaml b/autogpt_platform/infra/helm/autogpt-builder/templates/frontendconfig.yaml new file mode 100644 index 0000000000..ee9ae97034 --- /dev/null +++ b/autogpt_platform/infra/helm/autogpt-builder/templates/frontendconfig.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.gke.io/v1beta1 +kind: FrontendConfig +metadata: + name: {{ include "autogpt-builder.fullname" . }}-frontend-config +spec: + redirectToHttps: + enabled: true + responseCodeName: 301 \ No newline at end of file diff --git a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml index d1ddec587f..696d79f0e7 100644 --- a/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml +++ b/autogpt_platform/infra/helm/autogpt-builder/values.dev.yaml @@ -25,6 +25,7 @@ ingress: kubernetes.io/ingress.global-static-ip-name: "agpt-dev-agpt-builder-ip" networking.gke.io/managed-certificates: "autogpt-builder-cert" kubernetes.io/ingress.allow-http: "true" + networking.gke.io/v1beta1.FrontendConfig: "autogpt-builder-frontend-config" hosts: - host: dev-builder.agpt.co paths: From ef9308bed4e8091e5705ae34468de33ce0ce0352 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 25 Sep 2024 23:02:24 -0500 Subject: [PATCH 19/32] fix(platform): Add missing mandatory environment variable (#8183) --- autogpt_platform/backend/.env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index 1e1cb8bd92..fed9a309d2 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -18,9 +18,9 @@ SENTRY_DSN= ## User auth with Supabase is required for any of the 3rd party integrations with auth to work. ENABLE_AUTH=false -SUPABASE_URL= -SUPABASE_SERVICE_ROLE_KEY= -SUPABASE_JWT_SECRET= +SUPABASE_URL=http://localhost:8000 +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q +SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long # For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow for integrations to work. # FRONTEND_BASE_URL=http://localhost:3000 From 7aea24285ab60e175277e311bb7a0257d71e45b6 Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Wed, 25 Sep 2024 23:13:22 -0500 Subject: [PATCH 20/32] fix(market): the database url schema should be market (#8180) --- autogpt_platform/market/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/market/.env.example b/autogpt_platform/market/.env.example index 4b882ea674..21ade826ff 100644 --- a/autogpt_platform/market/.env.example +++ b/autogpt_platform/market/.env.example @@ -2,7 +2,7 @@ DB_USER=postgres DB_PASS=your-super-secret-and-long-postgres-password DB_NAME=postgres DB_PORT=5432 -DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=marketplace" +DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=market" SENTRY_DSN=https://11d0640fef35640e0eb9f022eb7d7626@o4505260022104064.ingest.us.sentry.io/4507890252447744 ENABLE_AUTH=true From 1d2e7b89ea0b137a334c3db173e3bb0382a39b7c Mon Sep 17 00:00:00 2001 From: Swifty Date: Thu, 26 Sep 2024 09:27:06 +0200 Subject: [PATCH 21/32] tweak(platform): Update block descriptions and tweak block card ui (#8147) * Update block descriptions and tweak block card ui * updated string * updated block descriptions * updated output description * fmt --- .../backend/backend/blocks/basic.py | 12 ++------ .../backend/backend/blocks/csv.py | 1 + .../backend/backend/blocks/discord.py | 2 ++ .../backend/backend/blocks/iteration.py | 1 + .../backend/backend/blocks/maths.py | 2 ++ .../backend/backend/blocks/medium.py | 1 + .../backend/backend/blocks/rss.py | 1 + .../backend/backend/blocks/search.py | 1 + .../backend/backend/blocks/text.py | 4 +-- .../backend/backend/blocks/youtube.py | 1 + .../components/edit/control/BlocksControl.tsx | 28 +++++++++---------- .../frontend/src/components/ui/card.tsx | 2 +- 12 files changed, 27 insertions(+), 29 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index a38f8f1628..463bd73129 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -41,8 +41,7 @@ class StoreValueBlock(Block): def __init__(self): super().__init__( id="1ff065e9-88e8-4358-9d82-8dc91f622ba9", - description="This block forwards the `input` pin to `output` pin. " - "This block output will be static, the output can be consumed many times.", + description="This block forwards an input value as output, allowing reuse without change.", categories={BlockCategory.BASIC}, input_schema=StoreValueBlock.Input, output_schema=StoreValueBlock.Output, @@ -244,14 +243,7 @@ class AgentOutputBlock(Block): def __init__(self): super().__init__( id="363ae599-353e-4804-937e-b2ee3cef3da4", - description=( - "This block records the graph output. It takes a value to record, " - "with a name, description, and optional format string. If a format " - "string is given, it tries to format the recorded value. The " - "formatted (or raw, if formatting fails) value is then output. " - "This block is key for capturing and presenting final results or " - "important intermediate outputs of the graph execution." - ), + description=("Stores the output of the graph for users to see."), input_schema=AgentOutputBlock.Input, output_schema=AgentOutputBlock.Output, test_input=[ diff --git a/autogpt_platform/backend/backend/blocks/csv.py b/autogpt_platform/backend/backend/blocks/csv.py index ccac4e556e..b19a65f24e 100644 --- a/autogpt_platform/backend/backend/blocks/csv.py +++ b/autogpt_platform/backend/backend/blocks/csv.py @@ -22,6 +22,7 @@ class ReadCsvBlock(Block): id="acf7625e-d2cb-4941-bfeb-2819fc6fc015", input_schema=ReadCsvBlock.Input, output_schema=ReadCsvBlock.Output, + description="Reads a CSV file and outputs the data as a list of dictionaries and individual rows via rows.", contributors=[ContributorDetails(name="Nicholas Tindle")], categories={BlockCategory.TEXT}, test_input={ diff --git a/autogpt_platform/backend/backend/blocks/discord.py b/autogpt_platform/backend/backend/blocks/discord.py index bebe54d906..8565684a07 100644 --- a/autogpt_platform/backend/backend/blocks/discord.py +++ b/autogpt_platform/backend/backend/blocks/discord.py @@ -31,6 +31,7 @@ class ReadDiscordMessagesBlock(Block): id="d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t", # Unique ID for the node input_schema=ReadDiscordMessagesBlock.Input, # Assign input schema output_schema=ReadDiscordMessagesBlock.Output, # Assign output schema + description="Reads messages from a Discord channel using a bot token.", categories={BlockCategory.SOCIAL}, test_input={"discord_bot_token": "test_token", "continuous_read": False}, test_output=[ @@ -148,6 +149,7 @@ class SendDiscordMessageBlock(Block): id="h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6", # Unique ID for the node input_schema=SendDiscordMessageBlock.Input, # Assign input schema output_schema=SendDiscordMessageBlock.Output, # Assign output schema + description="Sends a message to a Discord channel using a bot token.", categories={BlockCategory.SOCIAL}, test_input={ "discord_bot_token": "YOUR_DISCORD_BOT_TOKEN", diff --git a/autogpt_platform/backend/backend/blocks/iteration.py b/autogpt_platform/backend/backend/blocks/iteration.py index 7da5b7703c..f863521c83 100644 --- a/autogpt_platform/backend/backend/blocks/iteration.py +++ b/autogpt_platform/backend/backend/blocks/iteration.py @@ -21,6 +21,7 @@ class ListIteratorBlock(Block): id="f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l", input_schema=ListIteratorBlock.Input, output_schema=ListIteratorBlock.Output, + description="Iterates over a list of items and outputs each item with its index.", categories={BlockCategory.LOGIC}, test_input={"items": [1, "two", {"three": 3}, [4, 5]]}, test_output=[ diff --git a/autogpt_platform/backend/backend/blocks/maths.py b/autogpt_platform/backend/backend/blocks/maths.py index 4a1301c9a6..cb65de1c09 100644 --- a/autogpt_platform/backend/backend/blocks/maths.py +++ b/autogpt_platform/backend/backend/blocks/maths.py @@ -39,6 +39,7 @@ class CalculatorBlock(Block): id="b1ab9b19-67a6-406d-abf5-2dba76d00c79", input_schema=CalculatorBlock.Input, output_schema=CalculatorBlock.Output, + description="Performs a mathematical operation on two numbers.", categories={BlockCategory.LOGIC}, test_input={ "operation": Operation.ADD.value, @@ -98,6 +99,7 @@ class CountItemsBlock(Block): id="3c9c2f42-b0c3-435f-ba35-05f7a25c772a", input_schema=CountItemsBlock.Input, output_schema=CountItemsBlock.Output, + description="Counts the number of items in a collection.", categories={BlockCategory.LOGIC}, test_input={"collection": [1, 2, 3, 4, 5]}, test_output=[ diff --git a/autogpt_platform/backend/backend/blocks/medium.py b/autogpt_platform/backend/backend/blocks/medium.py index 034f3cfa4b..9ca9b41bf4 100644 --- a/autogpt_platform/backend/backend/blocks/medium.py +++ b/autogpt_platform/backend/backend/blocks/medium.py @@ -69,6 +69,7 @@ class PublishToMediumBlock(Block): id="3f7b2dcb-4a78-4e3f-b0f1-88132e1b89df", input_schema=PublishToMediumBlock.Input, output_schema=PublishToMediumBlock.Output, + description="Publishes a post to Medium.", categories={BlockCategory.SOCIAL}, test_input={ "author_id": "1234567890abcdef", diff --git a/autogpt_platform/backend/backend/blocks/rss.py b/autogpt_platform/backend/backend/blocks/rss.py index 74184acc0d..3dd570385b 100644 --- a/autogpt_platform/backend/backend/blocks/rss.py +++ b/autogpt_platform/backend/backend/blocks/rss.py @@ -46,6 +46,7 @@ class ReadRSSFeedBlock(Block): id="c6731acb-4105-4zp1-bc9b-03d0036h370g", input_schema=ReadRSSFeedBlock.Input, output_schema=ReadRSSFeedBlock.Output, + description="Reads RSS feed entries from a given URL.", categories={BlockCategory.INPUT}, test_input={ "rss_url": "https://example.com/rss", diff --git a/autogpt_platform/backend/backend/blocks/search.py b/autogpt_platform/backend/backend/blocks/search.py index 1a1ec098e4..7414ca2f8a 100644 --- a/autogpt_platform/backend/backend/blocks/search.py +++ b/autogpt_platform/backend/backend/blocks/search.py @@ -148,6 +148,7 @@ class GetWeatherInformationBlock(Block, GetRequest): id="f7a8b2c3-6d4e-5f8b-9e7f-6d4e5f8b9e7f", input_schema=GetWeatherInformationBlock.Input, output_schema=GetWeatherInformationBlock.Output, + description="Retrieves weather information for a specified location using OpenWeatherMap API.", test_input={ "location": "New York", "api_key": "YOUR_API_KEY", diff --git a/autogpt_platform/backend/backend/blocks/text.py b/autogpt_platform/backend/backend/blocks/text.py index 12215de61f..da287b94fa 100644 --- a/autogpt_platform/backend/backend/blocks/text.py +++ b/autogpt_platform/backend/backend/blocks/text.py @@ -25,9 +25,7 @@ class MatchTextPatternBlock(Block): def __init__(self): super().__init__( id="3060088f-6ed9-4928-9ba7-9c92823a7ccd", - description="This block matches the given text with the pattern (regex) and" - " forwards the provided data to positive (if matching) or" - " negative (if not matching) output.", + description="Matches text against a regex pattern and forwards data to positive or negative output based on the match.", categories={BlockCategory.TEXT}, input_schema=MatchTextPatternBlock.Input, output_schema=MatchTextPatternBlock.Output, diff --git a/autogpt_platform/backend/backend/blocks/youtube.py b/autogpt_platform/backend/backend/blocks/youtube.py index a5d48e5a30..e299b121aa 100644 --- a/autogpt_platform/backend/backend/blocks/youtube.py +++ b/autogpt_platform/backend/backend/blocks/youtube.py @@ -26,6 +26,7 @@ class TranscribeYouTubeVideoBlock(Block): id="f3a8f7e1-4b1d-4e5f-9f2a-7c3d5a2e6b4c", input_schema=TranscribeYouTubeVideoBlock.Input, output_schema=TranscribeYouTubeVideoBlock.Output, + description="Transcribes a YouTube video.", categories={BlockCategory.SOCIAL}, test_input={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, test_output=[ diff --git a/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx b/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx index 4bf19c94d8..3163fa81f1 100644 --- a/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx +++ b/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx @@ -132,33 +132,31 @@ export const BlocksControl: React.FC = ({ {filteredBlocks.map((block) => ( addBlock(block.id, block.name)} > -
+ {/* This div needs to be 10px wide and the same height as the card and be the primary color showing up on top of the card with matching rounded corners */} +
+ +
{beautifyString(block.name)} + + {block.description} +
-
- -
+ >
))} diff --git a/autogpt_platform/frontend/src/components/ui/card.tsx b/autogpt_platform/frontend/src/components/ui/card.tsx index d4ecf66baa..769ad50215 100644 --- a/autogpt_platform/frontend/src/components/ui/card.tsx +++ b/autogpt_platform/frontend/src/components/ui/card.tsx @@ -9,7 +9,7 @@ const Card = React.forwardRef<
Date: Thu, 26 Sep 2024 09:27:26 +0200 Subject: [PATCH 22/32] tweak(platform): Update Nav Menu styling (#8148) * easy nav ui changes * teaked mobile nav menu --- autogpt_platform/frontend/package.json | 2 +- .../frontend/src/components/CreditButton.tsx | 6 +- .../frontend/src/components/NavBar.tsx | 77 ++-- autogpt_platform/frontend/yarn.lock | 334 +++++++++--------- 4 files changed, 200 insertions(+), 219 deletions(-) diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 967674e78a..0945f7fb8f 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -54,7 +54,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18", "react-hook-form": "^7.52.1", - "react-icons": "^5.2.1", + "react-icons": "^5.3.0", "react-markdown": "^9.0.1", "react-modal": "^3.16.1", "react-shepherd": "^6.1.1", diff --git a/autogpt_platform/frontend/src/components/CreditButton.tsx b/autogpt_platform/frontend/src/components/CreditButton.tsx index 9663c7bde0..d806153982 100644 --- a/autogpt_platform/frontend/src/components/CreditButton.tsx +++ b/autogpt_platform/frontend/src/components/CreditButton.tsx @@ -22,10 +22,10 @@ export default function CreditButton() { diff --git a/autogpt_platform/frontend/src/components/NavBar.tsx b/autogpt_platform/frontend/src/components/NavBar.tsx index e023a82abe..2ae259d880 100644 --- a/autogpt_platform/frontend/src/components/NavBar.tsx +++ b/autogpt_platform/frontend/src/components/NavBar.tsx @@ -15,6 +15,9 @@ import { } from "@/components/ui/icons"; import AutoGPTServerAPI from "@/lib/autogpt-server-api"; import CreditButton from "@/components/CreditButton"; +import { BsBoxes } from "react-icons/bs"; +import { LuLaptop } from "react-icons/lu"; +import { LuShoppingCart } from "react-icons/lu"; export async function NavBar() { const isAvailable = Boolean( @@ -24,7 +27,7 @@ export async function NavBar() { const { user } = await getServerUser(); return ( -
+
@@ -40,64 +43,58 @@ export async function NavBar() { -
-
{isAvailable && user && } diff --git a/autogpt_platform/frontend/yarn.lock b/autogpt_platform/frontend/yarn.lock index 5318bb4280..0be4d073a5 100644 --- a/autogpt_platform/frontend/yarn.lock +++ b/autogpt_platform/frontend/yarn.lock @@ -9,7 +9,7 @@ "@ampproject/remapping@^2.2.0": version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -17,7 +17,7 @@ "@babel/code-frame@^7.24.7": version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== dependencies: "@babel/highlight" "^7.24.7" @@ -25,12 +25,12 @@ "@babel/compat-data@^7.25.2": version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz" integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== "@babel/core@^7.18.5": version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz" integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== dependencies: "@ampproject/remapping" "^2.2.0" @@ -51,7 +51,7 @@ "@babel/generator@^7.25.0", "@babel/generator@^7.25.6": version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz" integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== dependencies: "@babel/types" "^7.25.6" @@ -61,7 +61,7 @@ "@babel/helper-compilation-targets@^7.25.2": version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz" integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== dependencies: "@babel/compat-data" "^7.25.2" @@ -72,7 +72,7 @@ "@babel/helper-module-imports@^7.24.7": version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz" integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== dependencies: "@babel/traverse" "^7.24.7" @@ -80,7 +80,7 @@ "@babel/helper-module-transforms@^7.25.2": version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz" integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== dependencies: "@babel/helper-module-imports" "^7.24.7" @@ -90,7 +90,7 @@ "@babel/helper-simple-access@^7.24.7": version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz" integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== dependencies: "@babel/traverse" "^7.24.7" @@ -98,22 +98,22 @@ "@babel/helper-string-parser@^7.24.8": version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz" integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== "@babel/helper-validator-option@^7.24.8": version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz" integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== "@babel/helpers@^7.25.0": version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.6.tgz#57ee60141829ba2e102f30711ffe3afab357cc60" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz" integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q== dependencies: "@babel/template" "^7.25.0" @@ -121,7 +121,7 @@ "@babel/highlight@^7.24.7": version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz" integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== dependencies: "@babel/helper-validator-identifier" "^7.24.7" @@ -131,7 +131,7 @@ "@babel/parser@^7.25.0", "@babel/parser@^7.25.6": version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz" integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== dependencies: "@babel/types" "^7.25.6" @@ -145,7 +145,7 @@ "@babel/template@^7.25.0": version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz" integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== dependencies: "@babel/code-frame" "^7.24.7" @@ -154,7 +154,7 @@ "@babel/traverse@^7.24.7", "@babel/traverse@^7.25.2": version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz" integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== dependencies: "@babel/code-frame" "^7.24.7" @@ -167,7 +167,7 @@ "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6": version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz" integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== dependencies: "@babel/helper-string-parser" "^7.24.8" @@ -315,7 +315,7 @@ "@next/swc-darwin-arm64@14.2.4": version "14.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz#da9f04c34a3d5f0b8401ed745768420e4a604036" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz" integrity sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg== "@next/swc-darwin-x64@14.2.4": @@ -355,7 +355,7 @@ "@next/swc-win32-x64-msvc@14.2.4": version "14.2.4" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz#e65a1c6539a671f97bb86d5183d6e3a1733c29c7" integrity sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg== "@next/third-parties@^14.2.5": @@ -388,38 +388,38 @@ "@opentelemetry/api-logs@0.52.1": version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" + resolved "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz" integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A== dependencies: "@opentelemetry/api" "^1.0.0" "@opentelemetry/api-logs@0.53.0": version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be" + resolved "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz" integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw== dependencies: "@opentelemetry/api" "^1.0.0" "@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": version "1.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== "@opentelemetry/context-async-hooks@^1.25.1": version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz#fa92f722cf685685334bba95f258d3ef9fce60f6" + resolved "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz" integrity sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg== "@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.26.0.tgz#7d84265aaa850ed0ca5813f97d831155be42b328" + resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz" integrity sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ== dependencies: "@opentelemetry/semantic-conventions" "1.27.0" "@opentelemetry/instrumentation-connect@0.39.0": version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz#32bdbaac464cba061c95df6c850ee81efdd86f8b" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz" integrity sha512-pGBiKevLq7NNglMgqzmeKczF4XQMTOUOTkK8afRHMZMnrK3fcETyTH7lVaSozwiOM3Ws+SuEmXZT7DYrrhxGlg== dependencies: "@opentelemetry/core" "^1.8.0" @@ -429,7 +429,7 @@ "@opentelemetry/instrumentation-express@0.42.0": version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.42.0.tgz#279f195aa66baee2b98623a16666c6229c8e7564" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.42.0.tgz" integrity sha512-YNcy7ZfGnLsVEqGXQPT+S0G1AE46N21ORY7i7yUQyfhGAL4RBjnZUqefMI0NwqIl6nGbr1IpF0rZGoN8Q7x12Q== dependencies: "@opentelemetry/core" "^1.8.0" @@ -438,7 +438,7 @@ "@opentelemetry/instrumentation-fastify@0.39.0": version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.39.0.tgz#96a040e4944daf77c53a8fe5a128bc3b2568e4aa" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.39.0.tgz" integrity sha512-SS9uSlKcsWZabhBp2szErkeuuBDgxOUlllwkS92dVaWRnMmwysPhcEgHKB8rUe3BHg/GnZC1eo1hbTZv4YhfoA== dependencies: "@opentelemetry/core" "^1.8.0" @@ -447,7 +447,7 @@ "@opentelemetry/instrumentation-fs@0.15.0": version "0.15.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz#41658507860f39fee5209bca961cea8d24ca2a83" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz" integrity sha512-JWVKdNLpu1skqZQA//jKOcKdJC66TWKqa2FUFq70rKohvaSq47pmXlnabNO+B/BvLfmidfiaN35XakT5RyMl2Q== dependencies: "@opentelemetry/core" "^1.8.0" @@ -455,21 +455,21 @@ "@opentelemetry/instrumentation-generic-pool@0.39.0": version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz#2b9af16ad82d5cbe67125c0125753cecd162a728" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz" integrity sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw== dependencies: "@opentelemetry/instrumentation" "^0.53.0" "@opentelemetry/instrumentation-graphql@0.43.0": version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.43.0.tgz#71bb94ea775c70dbd388c739b397ec1418f3f170" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.43.0.tgz" integrity sha512-aI3YMmC2McGd8KW5du1a2gBA0iOMOGLqg4s9YjzwbjFwjlmMNFSK1P3AIg374GWg823RPUGfVTIgZ/juk9CVOA== dependencies: "@opentelemetry/instrumentation" "^0.53.0" "@opentelemetry/instrumentation-hapi@0.41.0": version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz#de8711907256d8fae1b5faf71fc825cef4a7ddbb" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz" integrity sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ== dependencies: "@opentelemetry/core" "^1.8.0" @@ -478,7 +478,7 @@ "@opentelemetry/instrumentation-http@0.53.0": version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz#0d806adf1b3aba036bc46e16162e3c0dbb8a6b60" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz" integrity sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ== dependencies: "@opentelemetry/core" "1.26.0" @@ -488,7 +488,7 @@ "@opentelemetry/instrumentation-ioredis@0.43.0": version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz#dbadabaeefc4cb47c406f878444f1bcac774fa89" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz" integrity sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -497,7 +497,7 @@ "@opentelemetry/instrumentation-kafkajs@0.3.0": version "0.3.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz#6687bce4dac8b90ef8ccbf1b662d5d1e95a34414" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz" integrity sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -505,7 +505,7 @@ "@opentelemetry/instrumentation-koa@0.43.0": version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz#963fd192a1b5f6cbae5dabf4ec82e3105cbb23b1" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz" integrity sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ== dependencies: "@opentelemetry/core" "^1.8.0" @@ -514,7 +514,7 @@ "@opentelemetry/instrumentation-mongodb@0.47.0": version "0.47.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz#f8107d878281433905e717f223fb4c0f10356a7b" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz" integrity sha512-yqyXRx2SulEURjgOQyJzhCECSh5i1uM49NUaq9TqLd6fA7g26OahyJfsr9NE38HFqGRHpi4loyrnfYGdrsoVjQ== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -523,7 +523,7 @@ "@opentelemetry/instrumentation-mongoose@0.42.0": version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz#375afd21adfcd897a8f521c1ffd2d91e6a428705" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz" integrity sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg== dependencies: "@opentelemetry/core" "^1.8.0" @@ -532,7 +532,7 @@ "@opentelemetry/instrumentation-mysql2@0.41.0": version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz#6377b6e2d2487fd88e1d79aa03658db6c8d51651" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz" integrity sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -541,7 +541,7 @@ "@opentelemetry/instrumentation-mysql@0.41.0": version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz#2d50691ead5219774bd36d66c35d5b4681485dd7" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz" integrity sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -550,7 +550,7 @@ "@opentelemetry/instrumentation-nestjs-core@0.40.0": version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz#2c0e6405b56caaec32747d55c57ff9a034668ea8" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz" integrity sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -558,7 +558,7 @@ "@opentelemetry/instrumentation-pg@0.44.0": version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz#1e97a0aeb2dca068ee23ce75884a0a0063a7ce3f" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz" integrity sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -569,7 +569,7 @@ "@opentelemetry/instrumentation-redis-4@0.42.0": version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz#fc01104cfe884c7546385eaae03c57a47edd19d1" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz" integrity sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg== dependencies: "@opentelemetry/instrumentation" "^0.53.0" @@ -578,7 +578,7 @@ "@opentelemetry/instrumentation-undici@0.6.0": version "0.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz#9436ee155c8dcb0b760b66947c0e0f347688a5ef" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz" integrity sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ== dependencies: "@opentelemetry/core" "^1.8.0" @@ -586,7 +586,7 @@ "@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz" integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A== dependencies: "@opentelemetry/api-logs" "0.53.0" @@ -598,7 +598,7 @@ "@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0": version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" + resolved "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz" integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== dependencies: "@opentelemetry/api-logs" "0.52.1" @@ -610,12 +610,12 @@ "@opentelemetry/redis-common@^0.36.2": version "0.36.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47" + resolved "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz" integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g== "@opentelemetry/resources@1.26.0", "@opentelemetry/resources@^1.26.0": version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.26.0.tgz#da4c7366018bd8add1f3aa9c91c6ac59fd503cef" + resolved "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz" integrity sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw== dependencies: "@opentelemetry/core" "1.26.0" @@ -623,7 +623,7 @@ "@opentelemetry/sdk-metrics@^1.9.1": version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz#37bb0afb1d4447f50aab9cdd05db6f2d8b86103e" + resolved "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz" integrity sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ== dependencies: "@opentelemetry/core" "1.26.0" @@ -631,7 +631,7 @@ "@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.26.0": version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz#0c913bc6d2cfafd901de330e4540952269ae579c" + resolved "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz" integrity sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw== dependencies: "@opentelemetry/core" "1.26.0" @@ -640,12 +640,12 @@ "@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.27.0": version "1.27.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" + resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz" integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz#93fbc48d8017449f5b3c3274f2268a08af2b83b6" + resolved "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz" integrity sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg== dependencies: "@opentelemetry/core" "^1.1.0" @@ -664,7 +664,7 @@ "@prisma/instrumentation@5.19.1": version "5.19.1" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.19.1.tgz#146319cf85f22b7a43296f0f40cfeac55516e66e" + resolved "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.19.1.tgz" integrity sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w== dependencies: "@opentelemetry/api" "^1.8" @@ -1110,7 +1110,7 @@ "@radix-ui/react-toast@^1.2.1": version "1.2.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.1.tgz#4bde231ed27d007dcd0455a446565ca619f92a2d" + resolved "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz" integrity sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg== dependencies: "@radix-ui/primitive" "1.1.0" @@ -1231,7 +1231,7 @@ "@rollup/plugin-commonjs@26.0.1": version "26.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz#16d4d6e54fa63021249a292b50f27c0b0f1a30d8" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz" integrity sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ== dependencies: "@rollup/pluginutils" "^5.0.1" @@ -1243,7 +1243,7 @@ "@rollup/pluginutils@^5.0.1": version "5.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz" integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== dependencies: "@types/estree" "^1.0.0" @@ -1251,9 +1251,9 @@ picomatch "^2.3.1" "@rollup/rollup-linux-x64-gnu@^4.9.5": - version "4.21.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz#790ae96118cc892464e9f10da358c0c8a6b9acdd" - integrity sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w== + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz#70116ae6c577fe367f58559e2cffb5641a1dd9d0" + integrity sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg== "@rushstack/eslint-patch@^1.3.3": version "1.10.4" @@ -1267,7 +1267,7 @@ "@sentry-internal/browser-utils@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.30.0.tgz#eb68c79556ffb864eb5924a53affde52f2b77362" + resolved "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.30.0.tgz" integrity sha512-pwX+awNWaxSOAsBLVLqc1+Hw+Fm1Nci9mbKFA6Ed5YzCG049PnBVQwugpmx2dcyyCqJpORhcIqb9jHdCkYmCiA== dependencies: "@sentry/core" "8.30.0" @@ -1276,7 +1276,7 @@ "@sentry-internal/feedback@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.30.0.tgz#6f78a245298502e4cc5ce77313dde6965abfecfe" + resolved "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.30.0.tgz" integrity sha512-ParFRxQY6helxkwUDmro77Wc5uSIC6rZos88jYMrYwFmoTJaNWf4lDzPyECfdSiSYyzSMZk4dorSUN85Ul7DCg== dependencies: "@sentry/core" "8.30.0" @@ -1285,7 +1285,7 @@ "@sentry-internal/replay-canvas@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.30.0.tgz#3630eec14d23b1fd368d8c331ee695aa5bb41425" + resolved "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.30.0.tgz" integrity sha512-y/QqcvchhtMlVA6eOZicIfTxtZarazQZJuFW0018ynPxBTiuuWSxMCLqduulXUYsFejfD8/eKHb3BpCIFdDYjg== dependencies: "@sentry-internal/replay" "8.30.0" @@ -1295,7 +1295,7 @@ "@sentry-internal/replay@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.30.0.tgz#6a4a8bd551a16ea5f77f913acbccd88061868c84" + resolved "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.30.0.tgz" integrity sha512-/KFre+BrovPCiovgAu5N1ErJtkDVzkJA5hV3Jw011AlxRWxrmPwu6+9sV9/rn3tqYAGyq6IggYqeIOHhLh1Ihg== dependencies: "@sentry-internal/browser-utils" "8.30.0" @@ -1305,12 +1305,12 @@ "@sentry/babel-plugin-component-annotate@2.22.3": version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.3.tgz#de4970d51a54ef52b21f0d6ec49bd06bf37753c1" + resolved "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.3.tgz" integrity sha512-OlHA+i+vnQHRIdry4glpiS/xTOtgjmpXOt6IBOUqynx5Jd/iK1+fj+t8CckqOx9wRacO/hru2wfW/jFq0iViLg== "@sentry/browser@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.30.0.tgz#3c6d5ef62d7daca2873b47f59b136c33941b56de" + resolved "https://registry.npmjs.org/@sentry/browser/-/browser-8.30.0.tgz" integrity sha512-M+tKqawH9S3CqlAIcqdZcHbcsNQkEa9MrPqPCYvXco3C4LRpNizJP2XwBiGQY2yK+fOSvbaWpPtlI938/wuRZQ== dependencies: "@sentry-internal/browser-utils" "8.30.0" @@ -1323,7 +1323,7 @@ "@sentry/bundler-plugin-core@2.22.3": version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.3.tgz#f8c0a25321216ae9777749c1a4b9d982ae1ec2e1" + resolved "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.3.tgz" integrity sha512-DeoUl0WffcqZZRl5Wy9aHvX4WfZbbWt0QbJ7NJrcEViq+dRAI2FQTYECFLwdZi5Gtb3oyqZICO+P7k8wDnzsjQ== dependencies: "@babel/core" "^7.18.5" @@ -1337,7 +1337,7 @@ "@sentry/cli-darwin@2.36.1": version "2.36.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.36.1.tgz#786adf6984dbe3c6fb7dac51b625c314117b807d" + resolved "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.36.1.tgz" integrity sha512-JOHQjVD8Kqxm1jUKioAP5ohLOITf+Dh6+DBz4gQjCNdotsvNW5m63TKROwq2oB811p+Jzv5304ujmN4cAqW1Ww== "@sentry/cli-linux-arm64@2.36.1": @@ -1372,7 +1372,7 @@ "@sentry/cli@^2.33.1": version "2.36.1" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.36.1.tgz#a9146b798cb6d2f782a7a48d74633ddcd88dc8d3" + resolved "https://registry.npmjs.org/@sentry/cli/-/cli-2.36.1.tgz" integrity sha512-gzK5uQKDWKhyH5udoB5+oaUNrS//urWyaXgKvHKz4gFfl744HuKY9dpLPP2nMnf0zLGmGM6xJnMXLqILq0mtxw== dependencies: https-proxy-agent "^5.0.0" @@ -1391,7 +1391,7 @@ "@sentry/core@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.30.0.tgz#f929e42e9a537bfa3eb6024082714e9ab98d822b" + resolved "https://registry.npmjs.org/@sentry/core/-/core-8.30.0.tgz" integrity sha512-CJ/FuWLw0QEKGKXGL/nm9eaOdajEcmPekLuHAuOCxID7N07R9l9laz3vFbAkUZ97GGDv3sYrJZgywfY3Moropg== dependencies: "@sentry/types" "8.30.0" @@ -1399,7 +1399,7 @@ "@sentry/nextjs@^8": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.30.0.tgz#8b532ce34a5ecb832e9fc74f652b6272baa3be51" + resolved "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-8.30.0.tgz" integrity sha512-835H7/ERIGvxE2m9cYqB5Mmd4PfLPlVQdyJAEL3br7fpl5mp3A3JtA3AiZ98smpVW/rUkLHGOWziRb6uTgJSXA== dependencies: "@opentelemetry/instrumentation-http" "0.53.0" @@ -1420,7 +1420,7 @@ "@sentry/node@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.30.0.tgz#730461be3a3382ab17e2c3f95c08e4e85a207429" + resolved "https://registry.npmjs.org/@sentry/node/-/node-8.30.0.tgz" integrity sha512-Tog0Ag7sU3lNj4cPUZy1KRJXyYXZlWiwlk34KYNNxAk0vDiK6W0bF8mvS+aaUukgb7FO5A0eu9l+VApdBJOr3Q== dependencies: "@opentelemetry/api" "^1.9.0" @@ -1458,7 +1458,7 @@ "@sentry/opentelemetry@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.30.0.tgz#b80417d44e4d20f03a7ecf5173a8a4ed4f317a57" + resolved "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-8.30.0.tgz" integrity sha512-6mCIP2zvxAiEsNEoF8kv+UUD4XGWSKJU6RY5BF1U26HLitXv1fNPtzaTR96Ehv9h0zktjLfqfpVUZ7DGkdBvLA== dependencies: "@sentry/core" "8.30.0" @@ -1467,7 +1467,7 @@ "@sentry/react@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.30.0.tgz#fe24964d3f5e963749d8a11b45332cea85bd9ac4" + resolved "https://registry.npmjs.org/@sentry/react/-/react-8.30.0.tgz" integrity sha512-ktQjXs87jdsxW0YrHci3sb6zcSzhMECWnrTVU/KGZF8UoDsk4P4xRCknijd2SSmDIjSkwzUAANR43UkCi4BTQg== dependencies: "@sentry/browser" "8.30.0" @@ -1478,19 +1478,19 @@ "@sentry/types@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.30.0.tgz#5f5011f5b16bafd30a039ca5e8c337e948c703fb" + resolved "https://registry.npmjs.org/@sentry/types/-/types-8.30.0.tgz" integrity sha512-kgWW2BCjBmVlSQRG32GonHEVyeDbys74xf9mLPvynwHTgw3+NUlNAlEdu05xnb2ow4bCTHfbkS5G1zRgyv5k4Q== "@sentry/utils@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.30.0.tgz#2343dd8593ea83890b3e0d792ed3fa257955a26b" + resolved "https://registry.npmjs.org/@sentry/utils/-/utils-8.30.0.tgz" integrity sha512-wZxU2HWlzsnu8214Xy7S7cRIuD6h8Z5DnnkojJfX0i0NLooepZQk2824el1Q13AakLb7/S8CHSHXOMnCtoSduw== dependencies: "@sentry/types" "8.30.0" "@sentry/vercel-edge@8.30.0": version "8.30.0" - resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.30.0.tgz#f3e7167b8183b58ec704934078f4add14f59101a" + resolved "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-8.30.0.tgz" integrity sha512-0R0HWmPZwIOunj4JQoOT9FexhesAVNpMkM2mezP7QlVGGBQA39s9j8+o658Ymh8xzKo1ZTg4pHjoySLPhrN1JA== dependencies: "@sentry/core" "8.30.0" @@ -1499,7 +1499,7 @@ "@sentry/webpack-plugin@2.22.3": version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.3.tgz#a9eeb4689c062eb6dc50671c09f06ec6875b9b02" + resolved "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-2.22.3.tgz" integrity sha512-Sq1S6bL3nuoTP5typkj+HPjQ13dqftIE8kACAq4tKkXOpWO9bf6HtqcruEQCxMekbWDTdljsrknQ17ZBx2q66Q== dependencies: "@sentry/bundler-plugin-core" "2.22.3" @@ -1587,19 +1587,19 @@ "@tanstack/react-table@^8.20.5": version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8" + resolved "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz" integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA== dependencies: "@tanstack/table-core" "8.20.5" "@tanstack/table-core@8.20.5": version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" + resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz" integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== "@types/connect@3.4.36": version "3.4.36" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz" integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w== dependencies: "@types/node" "*" @@ -1727,7 +1727,7 @@ "@types/mysql@2.15.26": version "2.15.26" - resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.26.tgz#f0de1484b9e2354d587e7d2bd17a873cc8300836" + resolved "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz" integrity sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ== dependencies: "@types/node" "*" @@ -1741,14 +1741,14 @@ "@types/pg-pool@2.0.6": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.6.tgz#1376d9dc5aec4bb2ec67ce28d7e9858227403c77" + resolved "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz" integrity sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ== dependencies: "@types/pg" "*" "@types/pg@*": version "8.11.9" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.9.tgz#3e92f7edbe4df9de9397f5308d7fe80c31faefe8" + resolved "https://registry.npmjs.org/@types/pg/-/pg-8.11.9.tgz" integrity sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg== dependencies: "@types/node" "*" @@ -1757,7 +1757,7 @@ "@types/pg@8.6.1": version "8.6.1" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.6.1.tgz#099450b8dc977e8197a44f5229cedef95c8747f9" + resolved "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz" integrity sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w== dependencies: "@types/node" "*" @@ -1798,7 +1798,7 @@ "@types/shimmer@^1.0.2", "@types/shimmer@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" + resolved "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz" integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== "@types/unist@*", "@types/unist@^3.0.0": @@ -1893,7 +1893,7 @@ acorn-import-attributes@^1.9.5: version "1.9.5" - resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + resolved "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz" integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: @@ -1908,7 +1908,7 @@ acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" @@ -1945,7 +1945,7 @@ ansi-regex@^6.0.1: ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" @@ -2156,7 +2156,7 @@ braces@^3.0.3, braces@~3.0.2: browserslist@^4.23.1: version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz" integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: caniuse-lite "^1.0.30001646" @@ -2199,7 +2199,7 @@ caniuse-lite@^1.0.30001579: caniuse-lite@^1.0.30001646: version "1.0.30001659" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz#f370c311ffbc19c4965d8ec0064a3625c8aaa7af" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001659.tgz" integrity sha512-Qxxyfv3RdHAfJcXelgf0hU4DFUVXBGTjqrBUZLUh8AtlGnsDo+CnncYtTd95+ZKfnANUOzxyIQCuU/UeBZBYoA== ccount@^2.0.0: @@ -2209,7 +2209,7 @@ ccount@^2.0.0: chalk@3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== dependencies: ansi-styles "^4.1.0" @@ -2217,7 +2217,7 @@ chalk@3.0.0: chalk@^2.4.2: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -2269,7 +2269,7 @@ chokidar@^3.5.3: cjs-module-lexer@^1.2.2: version "1.4.1" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== class-variance-authority@^0.7.0: @@ -2309,7 +2309,7 @@ cmdk@1.0.0: color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" @@ -2323,7 +2323,7 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: @@ -2343,7 +2343,7 @@ commander@^4.0.0: commondir@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== concat-map@0.0.1: @@ -2353,7 +2353,7 @@ concat-map@0.0.1: convert-source-map@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie@^0.6.0: @@ -2530,7 +2530,7 @@ date-fns@^3.6.0: debug@4, debug@^4.1.0, debug@^4.3.5: version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: ms "^2.1.3" @@ -2681,7 +2681,7 @@ eastasianwidth@^0.2.0: electron-to-chromium@^1.5.4: version "1.5.18" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz#5fe62b9d21efbcfa26571066502d94f3ed97e495" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz" integrity sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ== emoji-regex@^8.0.0: @@ -2835,12 +2835,12 @@ es-to-primitive@^1.2.1: escalade@^3.1.2: version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: @@ -3058,7 +3058,7 @@ estree-util-is-identifier-name@^3.0.0: estree-walker@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== esutils@^2.0.2: @@ -3187,7 +3187,7 @@ fsevents@2.3.2: fsevents@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: @@ -3212,7 +3212,7 @@ functions-have-names@^1.2.3: gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: @@ -3274,7 +3274,7 @@ glob@10.3.10, glob@^10.3.10: glob@^10.4.1: version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: foreground-child "^3.1.0" @@ -3298,7 +3298,7 @@ glob@^7.1.3: glob@^9.3.2: version "9.3.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + resolved "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz" integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== dependencies: fs.realpath "^1.0.0" @@ -3308,7 +3308,7 @@ glob@^9.3.2: globals@^11.1.0: version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: @@ -3362,7 +3362,7 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: @@ -3431,7 +3431,7 @@ hast-util-whitespace@^3.0.0: hoist-non-react-statics@^3.3.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" @@ -3443,7 +3443,7 @@ html-url-attributes@^3.0.0: https-proxy-agent@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -3464,7 +3464,7 @@ import-fresh@^3.2.1: import-in-the-middle@^1.11.0, import-in-the-middle@^1.8.1: version "1.11.0" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" + resolved "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz" integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== dependencies: acorn "^8.8.2" @@ -3675,7 +3675,7 @@ is-plain-obj@^4.0.0: is-reference@1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== dependencies: "@types/estree" "*" @@ -3773,7 +3773,7 @@ jackspeak@^2.3.5: jackspeak@^3.1.2: version "3.4.3" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: "@isaacs/cliui" "^8.0.2" @@ -3799,7 +3799,7 @@ js-yaml@^4.1.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-buffer@3.0.1: @@ -3831,7 +3831,7 @@ json5@^1.0.2: json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: @@ -3922,7 +3922,7 @@ lru-cache@^10.2.0: lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" @@ -3934,14 +3934,14 @@ lucide-react@^0.407.0: magic-string@0.30.8: version "0.30.8" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz" integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" magic-string@^0.30.3: version "0.30.11" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz" integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" @@ -4274,7 +4274,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: minimatch@^8.0.2: version "8.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz" integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== dependencies: brace-expansion "^2.0.1" @@ -4293,7 +4293,7 @@ minimist@^1.2.0, minimist@^1.2.6: minipass@^4.2.4: version "4.2.8" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + resolved "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz" integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: @@ -4303,7 +4303,7 @@ minipass@^4.2.4: module-details-from-path@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + resolved "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz" integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== moment@^2.30.1: @@ -4318,7 +4318,7 @@ ms@2.1.2, ms@^2.1.1: ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== mz@^2.7.0: @@ -4370,14 +4370,14 @@ next@14.2.4: node-fetch@^2.6.7: version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" node-releases@^2.0.18: version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-path@^3.0.0, normalize-path@~3.0.0: @@ -4462,7 +4462,7 @@ object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: obuf@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== once@^1.3.0: @@ -4500,7 +4500,7 @@ p-locate@^5.0.0: package-json-from-dist@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz" integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== parent-module@^1.0.0: @@ -4559,22 +4559,22 @@ path-type@^4.0.0: pg-int8@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== pg-numeric@1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + resolved "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== pg-protocol@*: version "1.6.1" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz" integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== pg-types@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz" integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== dependencies: pg-int8 "1.0.1" @@ -4585,7 +4585,7 @@ pg-types@^2.2.0: pg-types@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz" integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== dependencies: pg-int8 "1.0.1" @@ -4699,51 +4699,51 @@ postcss@^8, postcss@^8.4.23: postgres-array@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz" integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== postgres-array@~3.0.1: version "3.0.2" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz" integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== postgres-bytea@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz" integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== postgres-bytea@~3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz" integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== dependencies: obuf "~1.1.2" postgres-date@~1.0.4: version "1.0.7" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== postgres-date@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz" integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== postgres-interval@^1.1.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz" integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== dependencies: xtend "^4.0.0" postgres-interval@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz" integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== postgres-range@^1.1.1: version "1.1.4" - resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + resolved "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz" integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== prelude-ls@^1.2.1: @@ -4763,7 +4763,7 @@ prettier@^3.3.3: progress@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: @@ -4782,7 +4782,7 @@ property-information@^6.0.0: proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== punycode@^2.1.0: @@ -4813,7 +4813,7 @@ react-hook-form@^7.52.1: resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.2.tgz" integrity sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A== -react-icons@^5.2.1: +react-icons@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz" integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg== @@ -5017,7 +5017,7 @@ require-from-string@^2.0.2: require-in-the-middle@^7.1.1: version "7.4.0" - resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz#606977820d4b5f9be75e5a108ce34cfed25b3bb4" + resolved "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz" integrity sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ== dependencies: debug "^4.3.5" @@ -5066,7 +5066,7 @@ rimraf@^3.0.2: rollup@3.29.4: version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + resolved "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz" integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== optionalDependencies: fsevents "~2.3.2" @@ -5159,7 +5159,7 @@ shepherd.js@13.0.3: shimmer@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + resolved "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== side-channel@^1.0.4, side-channel@^1.0.6: @@ -5194,7 +5194,7 @@ space-separated-tokens@^2.0.0: stacktrace-parser@^0.1.10: version "0.1.10" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" + resolved "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz" integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== dependencies: type-fest "^0.7.1" @@ -5211,16 +5211,7 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5308,14 +5299,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5368,7 +5352,7 @@ sucrase@^3.32.0: supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" @@ -5459,7 +5443,7 @@ tiny-invariant@^1.3.1: to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: @@ -5523,7 +5507,7 @@ type-fest@^0.20.2: type-fest@^0.7.1: version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== typed-array-buffer@^1.0.2: @@ -5651,7 +5635,7 @@ unist-util-visit@^5.0.0: unplugin@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" + resolved "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz" integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== dependencies: acorn "^8.8.1" @@ -5661,7 +5645,7 @@ unplugin@1.0.1: update-browserslist-db@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz" integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: escalade "^3.1.2" @@ -5706,7 +5690,7 @@ uuid@^10.0.0: uuid@^9.0.0: version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== vfile-message@^4.0.0: @@ -5760,12 +5744,12 @@ webidl-conversions@^3.0.0: webpack-sources@^3.2.3: version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack-virtual-modules@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz" integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== whatwg-url@^5.0.0: @@ -5868,12 +5852,12 @@ ws@^8.14.2: xtend@^4.0.0: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^2.3.4: From 8f980c43c5a14e0b3655560d82bbeaa682ef3cd7 Mon Sep 17 00:00:00 2001 From: Swifty Date: Thu, 26 Sep 2024 09:32:01 +0200 Subject: [PATCH 23/32] fix(platform): Reset Block Control filters on popover close (#8156) Reset Block Control filters on popover close Co-authored-by: Zamil Majdy --- .../components/edit/control/BlocksControl.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx b/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx index 3163fa81f1..e09017fefd 100644 --- a/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx +++ b/autogpt_platform/frontend/src/components/edit/control/BlocksControl.tsx @@ -45,6 +45,13 @@ export const BlocksControl: React.FC = ({ }) => { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState(null); + const [filteredBlocks, setFilteredBlocks] = useState(blocks); + + const resetFilters = React.useCallback(() => { + setSearchQuery(""); + setSelectedCategory(null); + setFilteredBlocks(blocks); + }, [blocks]); // Extract unique categories from blocks const categories = Array.from( @@ -53,18 +60,29 @@ export const BlocksControl: React.FC = ({ ), ); - const filteredBlocks = blocks.filter( - (block: Block) => - (block.name.toLowerCase().includes(searchQuery.toLowerCase()) || - beautifyString(block.name) - .toLowerCase() - .includes(searchQuery.toLowerCase())) && - (!selectedCategory || - block.categories.some((cat) => cat.category === selectedCategory)), - ); + React.useEffect(() => { + setFilteredBlocks( + blocks.filter( + (block: Block) => + (block.name.toLowerCase().includes(searchQuery.toLowerCase()) || + beautifyString(block.name) + .toLowerCase() + .includes(searchQuery.toLowerCase())) && + (!selectedCategory || + block.categories.some((cat) => cat.category === selectedCategory)), + ), + ); + }, [blocks, searchQuery, selectedCategory]); return ( - + { + if (!open) { + resetFilters(); + } + }} + > From 53a0ee2523197772a938fa158a2cb4b5e8bb6bb6 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Thu, 26 Sep 2024 02:38:58 -0500 Subject: [PATCH 24/32] fix(platform): UI fixes; Fix default value on input fields & fix enum custom fields (#8182) * fix(platform): Fix default value on input fields & fix enum custom fields * fix(platform): Fix default value on input fields & fix enum custom fields --- .../backend/backend/blocks/llm.py | 21 ++++++++++++++++--- .../src/components/node-input-components.tsx | 13 +++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index 09cf8af3f9..bafbaba0fb 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -92,7 +92,12 @@ class AIStructuredResponseGeneratorBlock(Block): description="Expected format of the response. If provided, the response will be validated against this format. " "The keys should be the expected fields in the response, and the values should be the description of the field.", ) - model: LlmModel = LlmModel.GPT4_TURBO + model: LlmModel = SchemaField( + title="LLM Model", + default=LlmModel.GPT4_TURBO, + description="The language model to use for answering the prompt.", + advanced=False, + ) api_key: BlockSecret = SecretField(value="") sys_prompt: str = "" retry: int = 3 @@ -307,7 +312,12 @@ class AIStructuredResponseGeneratorBlock(Block): class AITextGeneratorBlock(Block): class Input(BlockSchema): prompt: str - model: LlmModel = LlmModel.GPT4_TURBO + model: LlmModel = SchemaField( + title="LLM Model", + default=LlmModel.GPT4_TURBO, + description="The language model to use for answering the prompt.", + advanced=False, + ) api_key: BlockSecret = SecretField(value="") sys_prompt: str = "" retry: int = 3 @@ -355,7 +365,11 @@ class AITextGeneratorBlock(Block): class AITextSummarizerBlock(Block): class Input(BlockSchema): text: str - model: LlmModel = LlmModel.GPT4_TURBO + model: LlmModel = SchemaField( + title="LLM Model", + default=LlmModel.GPT4_TURBO, + description="The language model to use for summarizing the text.", + ) api_key: BlockSecret = SecretField(value="") # TODO: Make this dynamic max_tokens: int = 4000 # Adjust based on the model's context window @@ -492,6 +506,7 @@ class AIConversationBlock(Block): description="List of messages in the conversation.", min_length=1 ) model: LlmModel = SchemaField( + title="LLM Model", default=LlmModel.GPT4_TURBO, description="The language model to use for the conversation.", ) diff --git a/autogpt_platform/frontend/src/components/node-input-components.tsx b/autogpt_platform/frontend/src/components/node-input-components.tsx index 4841e3358a..cec4ee1e50 100644 --- a/autogpt_platform/frontend/src/components/node-input-components.tsx +++ b/autogpt_platform/frontend/src/components/node-input-components.tsx @@ -49,7 +49,7 @@ const NodeObjectInputTree: FC = ({ className, displayName, }) => { - object ??= ("default" in schema ? schema.default : null) ?? {}; + object ||= ("default" in schema ? schema.default : null) ?? {}; return (
{displayName && {displayName}} @@ -105,7 +105,7 @@ export const NodeGenericInputField: FC<{ className, displayName, }) => { - displayName ??= propSchema.title || beautifyString(propKey); + displayName ||= propSchema.title || beautifyString(propKey); if ("allOf" in propSchema) { // If this happens, that is because Pydantic wraps $refs in an allOf if the @@ -573,6 +573,7 @@ const NodeStringInput: FC<{ className, displayName, }) => { + value ||= schema.default || ""; return (
{schema.enum ? ( @@ -642,6 +643,7 @@ export const NodeTextBoxInput: FC<{ className, displayName, }) => { + value ||= schema.default || ""; return (
{ - value ??= schema.default; - displayName ??= schema.title || beautifyString(selfKey); + value ||= schema.default; + displayName ||= schema.title || beautifyString(selfKey); return (
@@ -723,7 +725,7 @@ const NodeBooleanInput: FC<{ className, displayName, }) => { - value ??= schema.default ?? false; + value ||= schema.default ?? false; return (
@@ -757,6 +759,7 @@ const NodeFallbackInput: FC<{ className, displayName, }) => { + value ||= (schema as BlockIOStringSubSchema)?.default; return ( Date: Thu, 26 Sep 2024 02:39:20 -0500 Subject: [PATCH 25/32] fix(platform): UI fixes; Fix disabled Run/Stop button (#8171) * fix(platform): UI fixes; Fix disabled Run/Stop button * fix(platform): UI fixes; Fix disabled Run/Stop button --------- Co-authored-by: Swifty --- .../frontend/src/components/Flow.tsx | 29 +++++++------------ .../src/components/PrimaryActionButton.tsx | 4 ++- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/autogpt_platform/frontend/src/components/Flow.tsx b/autogpt_platform/frontend/src/components/Flow.tsx index bbffd80192..a55fd5fc4e 100644 --- a/autogpt_platform/frontend/src/components/Flow.tsx +++ b/autogpt_platform/frontend/src/components/Flow.tsx @@ -34,21 +34,16 @@ import ConnectionLine from "./ConnectionLine"; import { Control, ControlPanel } from "@/components/edit/control/ControlPanel"; import { SaveControl } from "@/components/edit/control/SaveControl"; import { BlocksControl } from "@/components/edit/control/BlocksControl"; -import { - IconPlay, - IconUndo2, - IconRedo2, - IconSquare, -} from "@/components/ui/icons"; +import { IconUndo2, IconRedo2 } from "@/components/ui/icons"; import { startTutorial } from "./tutorial"; import useAgentGraph from "@/hooks/useAgentGraph"; import { v4 as uuidv4 } from "uuid"; -import { useRouter, usePathname, useSearchParams } from "next/navigation"; -import { LogOut } from "lucide-react"; +import { useRouter, usePathname } from "next/navigation"; import RunnerUIWrapper, { RunnerUIWrapperRef, } from "@/components/RunnerUIWrapper"; import PrimaryActionBar from "@/components/PrimaryActionButton"; +import { useToast } from "@/components/ui/use-toast"; // This is for the history, this is the minimum distance a block must move before it is logged // It helps to prevent spamming the history with small movements especially when pressing on a input in a block @@ -108,6 +103,8 @@ const FlowEditor: React.FC<{ const runnerUIRef = useRef(null); + const { toast } = useToast(); + useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -601,9 +598,10 @@ const FlowEditor: React.FC<{ onClickAgentOutputs={() => runnerUIRef.current?.openRunnerOutput()} onClickRunAgent={() => { if (!savedAgent) { - alert( - "Please save the agent to run, by clicking the save button in the left sidebar.", - ); + toast({ + title: `Please save the agent using the button in the left sidebar before running it.`, + duration: 2000, + }); return; } if (!isRunning) { @@ -612,15 +610,10 @@ const FlowEditor: React.FC<{ requestStopRun(); } }} + isDisabled={!savedAgent} isRunning={isRunning} requestStopRun={requestStopRun} - runAgentTooltip={ - !savedAgent - ? "Please save the agent to run" - : !isRunning - ? "Run Agent" - : "Stop Agent" - } + runAgentTooltip={!isRunning ? "Run Agent" : "Stop Agent"} />
diff --git a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx index 5b7af85322..739ad10cdf 100644 --- a/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx +++ b/autogpt_platform/frontend/src/components/PrimaryActionButton.tsx @@ -12,6 +12,7 @@ interface PrimaryActionBarProps { onClickAgentOutputs: () => void; onClickRunAgent: () => void; isRunning: boolean; + isDisabled: boolean; requestStopRun: () => void; runAgentTooltip: string; } @@ -20,6 +21,7 @@ const PrimaryActionBar: React.FC = ({ onClickAgentOutputs, onClickRunAgent, isRunning, + isDisabled, requestStopRun, runAgentTooltip, }) => { @@ -56,7 +58,7 @@ const PrimaryActionBar: React.FC = ({ size="primary" style={{ background: isRunning ? "#FFB3BA" : "#7544DF", - opacity: 1, + opacity: isDisabled ? 0.5 : 1, }} > {runButtonIcon} From b4097f3a51ff17bd4bbd0da69893bd122e4d25ac Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Thu, 26 Sep 2024 02:40:06 -0500 Subject: [PATCH 26/32] fix(platform): UI fixes; Fix block note UI displayed as normal block;Json-prettify execution output (#8169) * fix(platform): UI fixes; Fix block note UI displayed as normal block; Json-prettify execution output * fix(platform): UI fixes; Fix block note UI displayed as normal block; Json-prettify execution output --------- Co-authored-by: Nicholas Tindle --- autogpt_platform/backend/backend/blocks/basic.py | 14 ++++---------- autogpt_platform/backend/backend/data/block.py | 12 ++++-------- .../backend/backend/executor/manager.py | 12 +++++++++--- .../frontend/src/components/CustomNode.tsx | 1 - .../frontend/src/components/DataTable.tsx | 8 +++++--- .../frontend/src/components/RunnerUIWrapper.tsx | 6 +++--- .../frontend/src/hooks/useAgentGraph.ts | 1 + 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/basic.py b/autogpt_platform/backend/backend/blocks/basic.py index 463bd73129..b86d8d872b 100644 --- a/autogpt_platform/backend/backend/blocks/basic.py +++ b/autogpt_platform/backend/backend/blocks/basic.py @@ -4,13 +4,7 @@ from typing import Any, List from jinja2 import BaseLoader, Environment from pydantic import Field -from backend.data.block import ( - Block, - BlockCategory, - BlockOutput, - BlockSchema, - BlockUIType, -) +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType from backend.data.model import SchemaField from backend.util.mock import MockObject @@ -196,7 +190,7 @@ class AgentInputBlock(Block): ("result", "Hello, World!"), ], categories={BlockCategory.INPUT, BlockCategory.BASIC}, - ui_type=BlockUIType.INPUT, + block_type=BlockType.INPUT, ) def run(self, input_data: Input, **kwargs) -> BlockOutput: @@ -272,7 +266,7 @@ class AgentOutputBlock(Block): ("output", MockObject(value="!!", key="key")), ], categories={BlockCategory.OUTPUT, BlockCategory.BASIC}, - ui_type=BlockUIType.OUTPUT, + block_type=BlockType.OUTPUT, ) def run(self, input_data: Input, **kwargs) -> BlockOutput: @@ -444,7 +438,7 @@ class NoteBlock(Block): test_output=[ ("output", "Hello, World!"), ], - ui_type=BlockUIType.NOTE, + block_type=BlockType.NOTE, ) def run(self, input_data: Input, **kwargs) -> BlockOutput: diff --git a/autogpt_platform/backend/backend/data/block.py b/autogpt_platform/backend/backend/data/block.py index f4d05d648a..a3b89cc6f9 100644 --- a/autogpt_platform/backend/backend/data/block.py +++ b/autogpt_platform/backend/backend/data/block.py @@ -29,11 +29,7 @@ BlockOutput = Generator[BlockData, None, None] # Output: 1 output pin produces CompletedBlockOutput = dict[str, list[Any]] # Completed stream, collected as a dict. -class BlockUIType(Enum): - """ - The type of Node UI to be displayed in the builder for this block. - """ - +class BlockType(Enum): STANDARD = "Standard" INPUT = "Input" OUTPUT = "Output" @@ -200,7 +196,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): test_credentials: Optional[Credentials] = None, disabled: bool = False, static_output: bool = False, - ui_type: BlockUIType = BlockUIType.STANDARD, + block_type: BlockType = BlockType.STANDARD, ): """ Initialize the block with the given schema. @@ -231,7 +227,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): self.contributors = contributors or set() self.disabled = disabled self.static_output = static_output - self.ui_type = ui_type + self.block_type = block_type @abstractmethod def run(self, input_data: BlockSchemaInputType, **kwargs) -> BlockOutput: @@ -262,7 +258,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]): contributor.model_dump() for contributor in self.contributors ], "staticOutput": self.static_output, - "uiType": self.ui_type.value, + "uiType": self.block_type.value, } def execute(self, input_data: BlockInput, **kwargs) -> BlockOutput: diff --git a/autogpt_platform/backend/backend/executor/manager.py b/autogpt_platform/backend/backend/executor/manager.py index 33d7d74da3..544c59f8b4 100644 --- a/autogpt_platform/backend/backend/executor/manager.py +++ b/autogpt_platform/backend/backend/executor/manager.py @@ -17,9 +17,8 @@ from pydantic import BaseModel if TYPE_CHECKING: from backend.server.rest_api import AgentServer -from backend.blocks.basic import AgentInputBlock from backend.data import db -from backend.data.block import Block, BlockData, BlockInput, get_block +from backend.data.block import Block, BlockData, BlockInput, BlockType, get_block from backend.data.credit import get_user_credit_model from backend.data.execution import ( ExecutionQueue, @@ -737,7 +736,14 @@ class ExecutionManager(AppService): nodes_input = [] for node in graph.starting_nodes: input_data = {} - if isinstance(get_block(node.block_id), AgentInputBlock): + block = get_block(node.block_id) + + # Invalid block & Note block should never be executed. + if not block or block.block_type == BlockType.NOTE: + continue + + # Extract request input data, and assign it to the input pin. + if block.block_type == BlockType.INPUT: name = node.input_default.get("name") if name and name in data: input_data = {"value": data[name]} diff --git a/autogpt_platform/frontend/src/components/CustomNode.tsx b/autogpt_platform/frontend/src/components/CustomNode.tsx index cf1e10db0b..8c434b9366 100644 --- a/autogpt_platform/frontend/src/components/CustomNode.tsx +++ b/autogpt_platform/frontend/src/components/CustomNode.tsx @@ -540,7 +540,6 @@ export function CustomNode({ data, id, width, height }: NodeProps) { value === inputValues[key] || (!value && !inputValues[key]), ), ); - console.debug(`Block cost ${inputValues}|${data.blockCosts}=${blockCost}`); return (
-
+